const path = require("path") const shelljs = require("shelljs") const fs = require("fs-extra") const semver = require('semver') /** * * 匹配指定路径或文件名称 * * const matcher = fileMatcher([ * "", // 匹配正则表达式字符串 * "!", // 以!开头代表否定匹配 * /正则表达式/ * ],{ * basePath:"<指定一个基准目录,所有不是以此开头的均视为不匹配>", * defaultPatterns:["<默认排除的模式>","<默认排除的模式>","<默认排除的模式>"], * debug:false,是否输出调试信息,当=true时,.test()方法返回[,pattern] * * }) * * matcher.test("<文件名称>") 返回true/false * * * * @param {*} patterns * @param {*} basePath 如果指定basePath,则所有不是以basePath开头的文件都排除 * @param {*} defaultPatterns 默认的匹配模式 * @param {*} debug 是否输出调试信息 */ function fileMatcher(patterns,{basePath,defaultPatterns=[],debug=true}={}) { if(basePath) { basePath = path.normalize(basePath) } //[[pattern,exclude],[pattern,false],[pattern,true]] let finalPatterns = [] let inputPatterns = Array.isArray(patterns) ? patterns : (patterns ? [patterns] : []) // 默认排除模式 if(defaultPatterns.length===0){ finalPatterns.push([/__test__\/.*/,true]) finalPatterns.push([/.*\/.*\.test\.js$/,true]) finalPatterns.push([/node_modules\/.*/,true]) finalPatterns.push([/.*\/node_modules\/.*/,true]) finalPatterns.push([/.*\/languages\/.*/,true]) // 默认排除语言文件 finalPatterns.push([/\.babelrc/,true]) finalPatterns.push([/babel\.config\.js/,true]) finalPatterns.push([/package\.json$/,true]) finalPatterns.push([/vite\.config\.js$/,true]) finalPatterns.push([/^plugin-vue:.*/,true]) } inputPatterns.forEach(pattern=>{ if(typeof pattern === "string"){ pattern = pattern.replaceAll("**",".*") .replaceAll("?","[^\/]?") .replaceAll(/(? { let isMatched = false let file = filename // 如果指定basePath,则文件名称必须是以basePath开头 if(basePath){ if(path.isAbsolute(file)){ if(!path.normalize(file).startsWith(basePath)){ return debug ? [false,`!^${basePath}`] : false }else{ isMatched = true } } } if(finalPatterns.length===0){ return debug ? [true,"*"] : true }else{ for(const pattern of finalPatterns){ pattern[0].lastIndex = 0 if(pattern[1]===true){ if(pattern[0].test(file)) return debug ? [false,pattern[0].toString()] : false }else{ if(pattern[0].test(file)) return debug ? [true,pattern[0].toString()] : true } } } return debug ? [isMatched,"*"] : isMatched } } } /** * 以floder为基准向上查找文件package.json,并返回package.json所在的文件夹 * @param {*} folder 起始文件夹,如果没有指定,则取当前文件夹 * @param {*} exclueCurrent 如果=true,则folder的父文件夹开始查找 * @returns */ function getProjectRootFolder(folder="./",exclueCurrent=false){ if(!path.isAbsolute(folder)){ folder = path.join(process.cwd(),folder) } try{ const pkgFile =exclueCurrent ? path.join(folder, "..", "package.json") : path.join(folder, "package.json") if(fs.existsSync(pkgFile)){ return path.dirname(pkgFile) } const parent = path.dirname(folder) if(parent===folder) return null return getProjectRootFolder(parent,false) }catch(e){ return process.cwd() } } function fileIsExists(filename){ try{ fs.statSync(filename) return true }catch(e){ return false } } /** * 自动获取当前项目的languages * * @param {*} location */ function getProjectLanguageFolder(location="./"){ // 绝对路径 if(!path.isAbsolute(location)){ location = path.join(process.cwd(),location) } // 发现当前项目根目录 const projectRoot = getProjectRootFolder(location) const searchFolders = [ path.join(location,"src","languages"), path.join(location,"languages") ] for(let folder of searchFolders){ if(fs.existsSync(folder)){ return folder } } return null } /** * 根据当前输入的文件夹位置自动确定源码文件夹位置 * * - 如果没有指定,则取当前文件夹 * - 如果指定是非绝对路径,则以当前文件夹作为base * - 查找pack * - 如果该文件夹中存在src,则取src下的文件夹 * - * * @param {*} location * @returns */ function getProjectSourceFolder(location){ if(!location) { location = process.cwd() }else{ if(!path.isAbsolute(location)){ location = path.join(process.cwd(),location) } } let projectRoot = getProjectRootFolder(location) // 如果当前工程存在src文件夹,则自动使用该文件夹作为源文件夹 if(fs.existsSync(path.join(projectRoot,"src"))){ projectRoot = path.join(projectRoot,"src") } return projectRoot } /** * 读取指定文件夹的package.json文件,如果当前文件夹没有package.json文件,则向上查找 * @param {*} folder * @param {*} exclueCurrent = true 排除folder,从folder的父级开始查找 * @returns */ function getCurrentPackageJson(folder,exclueCurrent=true){ let projectFolder = getProjectRootFolder(folder,exclueCurrent) if(projectFolder){ return fs.readJSONSync(path.join(projectFolder,"package.json")) } } /** * 判断当前是否是Typescript工程 * * */ function isTypeScriptProject(){ let projectFolder = getProjectRootFolder(process.cwd(),false) if(projectFolder){ return fileIsExists(path.join(projectFolder,"tsconfig.json")) || fileIsExists(path.join(projectFolder,"Src","tsconfig.json")) } } /** * * 返回当前项目的模块类型 * * 从当前文件夹开始向上查找package.json文件,并解析出语言包的类型 * * @param {*} folder */ function findModuleType(folder){ let packageJson = getCurrentPackageJson(folder) try{ return packageJson.type || "commonjs" }catch(e){ return "esm" } } /** * 判断是否已经安装了依赖 * * isInstallDependent("@voerkai18n/runtime") * */ function isInstallDependent(url){ try{ // 简单判断是否存在该文件夹node_modules/@voerkai18n/runtime let projectFolder = getProjectRootFolder(process.cwd()) if(fs.existsSync(path.join(projectFolder,"node_modules","@voerkai18n/runtime"))){ return true } // 如果不存在,则尝试require require(url) }catch(e){ return false } } /** * 获取当前包的版本号 */ function getInstalledPackages(){ const packages = { "@voerkai18n/runtime":"未安装", "@voerkai18n/babel":"未安装", "@voerkai18n/vue":"未安装", "@voerkai18n/react":"未安装", "@voerkai18n/vite":"未安装", "@voerkai18n/formatters":"未安装" } for(let package of Object.keys(packages)){ try{ require(package) }catch{ packages[package] = "已安装" } } } /** * 判断是否是JSON对象 * @param {*} obj * @returns */ function isPlainObject(obj){ if (typeof obj !== 'object' || obj === null) return false; var proto = Object.getPrototypeOf(obj); if (proto === null) return true; var baseProto = proto; while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto); } return proto === baseProto; } /** * 判断值是否是一个数字 * @param {*} value * @returns */ function isNumber(value){ if(!value) return false if(typeof(value)=='number') return true if(typeof(value)!='string') return false try{ if(value.includes(".")){ let v = parseFloat(value) if(value.endsWith(".")){ return !isNaN(v) && String(v).length===value.length-1 }else{ return !isNaN(v) && String(v).length===value.length } }else{ let v = parseInt(value) return !isNaN(v) && String(v).length===value.length } }catch{ return false } } /** * 检测当前工程是否是git工程 */ function isGitRepo(){ return shelljs.exec("git status", {silent: true}).code === 0; } /** * 简单进行对象合并 * * options={ * array:0 , // 数组合并策略,0-替换,1-合并,2-去重合并 * } * * @param {*} toObj * @param {*} formObj * @returns 合并后的对象 */ function deepMerge(toObj,formObj,options={}){ let results = Object.assign({},toObj) Object.entries(formObj).forEach(([key,value])=>{ if(key in results){ if(typeof value === "object" && value !== null){ if(Array.isArray(value)){ if(options.array === 0){ results[key] = value }else if(options.array === 1){ results[key] = [...results[key],...value] }else if(options.array === 2){ results[key] = [...new Set([...results[key],...value])] } }else{ results[key] = deepMerge(results[key],value,options) } }else{ results[key] = value } }else{ results[key] = value } }) return results } /** * 获取指定变量类型名称 * getDataTypeName(1) == Number * getDataTypeName("") == String * getDataTypeName(null) == Null * getDataTypeName(undefined) == Undefined * getDataTypeName(new Date()) == Date * getDataTypeName(new Error()) == Error * * @param {*} v * @returns */ function getDataTypeName(v){ if (v === null) return 'Null' if (v === undefined) return 'Undefined' if(typeof(v)==="function") return "Function" return v.constructor && v.constructor.name; }; /** * 在当前工程自动安装@voerkai18n/runtime * @param {*} srcPath * @param {*} opts */ function installVoerkai18nRuntime(srcPath){ const projectFolder = getProjectRootFolder(srcPath || process.cwd()) if(fs.existsSync("pnpm-lock.yaml")){ shelljs.exec("pnpm add @voerkai18n/runtime") }else if(fs.existsSync("yarn.lock")){ shelljs.exec("yarn add @voerkai18n/runtime") }else{ shelljs.exec("npm install @voerkai18n/runtime") } } function getPackageTool(){ const projectFolder = getProjectRootFolder(process.cwd()) if(fs.existsSync(path.join(projectFolder,"pnpm-lock.yaml"))){ return 'pnpm' }else if(fs.existsSync(path.join(projectFolder,"yarn.lock"))){ return 'yarn' }else{ return 'npm' } } /** * 异步执行脚本并返回输出结果 * @param {*} script * @param {*} options * @returns */ async function asyncExecShellScript(script,options={}){ const { silent=true} = options return new Promise((resolve,reject)=>{ shelljs.exec(script,{silent,...options,async:true},(code,stdout)=>{ if(code>0){ reject(new Error(`执行<${script}>失败: ${stdout.trim()}`)) }else{ resolve(stdout.trim()) } }) }) } /** * 从NPM获取包最近发布的版本信息 * { tags: { latest: '1.1.30' }, license: 'MIT', author: 'wxzhang', version: '1.1.30-latest', latestVersion: '1.1.30', firstCreated: '2022-03-24T09:32:51.748Z', lastPublish: '2023-01-28T08:49:33.139Z', size: 888125 } * @param {*} packageName */ async function getPackageReleaseInfo(packageName) { try{ let results = await asyncExecShellScript.call(this,`npm info ${packageName} --json`,{silent:true}) const info = JSON.parse(results) const distTags = info["dist-tags"] // 取得最新版本的版本号,不是latest let lastVersion = Object.entries(distTags).reduce((result,[tag,value])=>{ if(semver.gt(value, result.value)){ result = {tag,value} } return result },{tag:'latest',value:info["version"]}) return { tags : distTags, license : info["license"], author : info["author"], version : `${lastVersion.value}-${lastVersion.tag}`, latestVersion: info["version"], firstCreated : info.time["created"], lastPublish : info.time["modified"], size : info.dist["unpackedSize"] } }catch(e){ console.error(`ERROR: 执行npm info ${packageName}出错: ${e.stack}`) return null; } } /** * 在当前工程升级@voerkai18n/runtime * @param {*} srcPath * @param {*} opts */ function updateVoerkai18nRuntime(srcPath){ const projectFolder = getProjectRootFolder(srcPath || process.cwd()) if(fs.existsSync(path.join(projectFolder,"pnpm-lock.yaml"))){ shelljs.exec("pnpm upgrade --latest @voerkai18n/runtime") }else if(fs.existsSync(path.join(projectFolder,"yarn.lock"))){ shelljs.exec("yarn upgrade @voerkai18n/runtime") }else{ shelljs.exec("npm update --save @voerkai18n/runtime") } } /** * 在指定文件夹下创建package.json文件 * @param {*} targetPath * @param {*} moduleType * @returns */ function createPackageJsonFile(targetPath,moduleType="auto"){ if(moduleType==="auto"){ moduleType = findModuleType(targetPath) } const packageJsonFile = path.join(targetPath, "package.json") if(["esm","es","module"].includes(moduleType)){ fs.writeFileSync(packageJsonFile,JSON.stringify({type:"module",license:"MIT"},null,4)) if(moduleType==="module"){ moduleType = "esm" } }else{ fs.writeFileSync(packageJsonFile,JSON.stringify({license:"MIT"},null,4)) } return moduleType } function installPackage(packageName){ const packageTool = getPackageTool() try{ if(packageTool=='pnpm'){ shelljs.exec(`pnpm add ${packageName}`) }else if(packageTool=='yarn'){ shelljs.exec(`yarn add ${packageName}`) }else{ shelljs.exec(`npm install ${packageName}`) } }catch{ shelljs.exec(`npm install ${packageName}`) } } /** * 读取当前工程下languages/idMap.(js|ts)文件 * * @param {*} location 项目根文件夹或者当前项目下的任意一个文件夹 * @returns */ function readIdMapFile(location="./"){ let searchIdMapFiles = [] if(!path.isAbsolute(location)){ location = path.join(process.cwd(),location) } searchIdMapFiles.push(path.join(location,"src","languages/idMap.js")) searchIdMapFiles.push(path.join(location,"languages/idMap.js")) searchIdMapFiles.push(path.join(location,"idMap.js")) searchIdMapFiles.push(path.join(location,"src","languages/idMap.ts")) searchIdMapFiles.push(path.join(location,"languages/idMap.ts")) searchIdMapFiles.push(path.join(location,"idMap.ts")) let projectRoot = getProjectRootFolder(location) searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.js")) searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.js")) searchIdMapFiles.push(path.join(projectRoot,"idMap.js")) searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.ts")) searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.ts")) searchIdMapFiles.push(path.join(projectRoot,"idMap.ts")) let idMapFile for( idMapFile of searchIdMapFiles){ // 如果不存在idMap文件,则尝试从location/languages/中导入 if(fs.existsSync(idMapFile)){ try{ // 由于idMap.js可能是esm或cjs,并且babel插件不支持异步 // 当require(idMap.js)失败时,对esm模块尝试采用直接读取的方式 return require(idMapFile) }catch(e){ // 出错原因可能是因为无效require esm模块,由于idMap.js文件格式相对简单,因此尝试直接读取解析 try{ let idMapContent = fs.readFileSync(idMapFile).toString() idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"") return JSON.parse(idMapContent) }catch{ } } } } throw new Error(`${idMapFile}文件不存在,无法对翻译文本进行转换。`) return {} } //const TranslateRegex = /\bt\(\s*("|'){1}(?:((?\w+)::))?(?[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm // 匹配t('xxxx')的正则表达式 const TranslateRegex =/(?<=\bt\(\s*("|'){1})(?[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm /** * 将code中的t("xxxx")使用idMap进行映射为t("1"),t("2")的形式 * * @param {*} code * @param {*} idmap */ function replaceTranslateText(code, idmap) { return code.replaceAll(TranslateRegex, (message) => { if(message in idmap) { return idmap[message] }else{ let result // 为什么要decodeURIComponent/unescape? 一些vite插件会将中文编码转义导致无法进行替换,所以要解码一下 try{ result = decodeURIComponent(message.replaceAll("\\u","%u")) return result in idmap ? idmap[result] : message }catch{ return message } } }) // decodeURI 或 decodeURIComponent 对特殊字符进行转义序列编码和解码。 } // 匹配 import {t } from 的正则表达式 const importTRegex = /^[^\w\r\n\s]*import\s*\{(.*)\bt\b(.*)\}\s*from/gm /** * 判定代码中是否导入了Translate函数 * @param {*} code * @returns */ function hasImportTranslateFunction(code){ return importTRegex.test(code) } function importTranslateFunction(code,sourceFile,langPath){ let importSource = path.relative(path.dirname(sourceFile),langPath) if(!importSource.startsWith(".")){ importSource = "./" + importSource } importSource= importSource.replaceAll("\\","/") const extName = path.extname(sourceFile) // Vue文件 if(extName==".vue"){ // 优先在中导入 const setupScriptRegex = /(^\s*\)/gmi if(setupScriptRegex.test(code)){ code = code.replace(setupScriptRegex,`$1\nimport { t } from '${importSource}';`) }else{// 如果没有中导入 code = code.replace(/(^\s*\)/gmi,`$1\nimport { t } from '${importSource}';`) } }else if(['.jsx','.js','.ts','.tsx'].includes(extName)){ // 普通js/ts文件直接添加到最前面 code = code = `import { t } from '${importSource}';\n${code}` } return code } /** * 检测当前环境是否已经安装了指定的包 * 如果已安装则返回 * { * version:"<版本号>", * path:"<安装路径>" * } * 如果未安装则返回null * @param {*} packageName */ function getInstalledPackageInfo(packageName,fields=[]){ try{ const packagePath = path.dirname(require.resolve(packageName)) const pkgInfo = fs.readJSONSync(path.join(packagePath,"package.json")) let results = { version: pkgInfo.version, path: packagePath, } for(let field in fields){ if(field in pkgInfo){ results[field] = pkgInfo[field] }else{ results[field] = null } } return results }catch(e){ return null // if(e instanceof Error && e.code=="MODULE_NOT_FOUND"){ // return null; // }else{ // } } } module.exports = { fileMatcher, // 文件名称匹配器 getProjectRootFolder, // 查找获取项目根目录 createPackageJsonFile, // 创建package.json文件 getProjectSourceFolder, // 获取项目源码目录 getCurrentPackageJson, // 查找获取当前项目package.json getProjectLanguageFolder, // 获取当前项目的languages目录 findModuleType, // 获取当前项目的模块类型 isInstallDependent, // 判断是否已经安装了依赖 installVoerkai18nRuntime, // 在当前工程自动安装@voerkai18n/runtime updateVoerkai18nRuntime, // 在当前工程升级@voerkai18n/runtime isPlainObject, // 判断是否是普通对象 isNumber, // 判断是否是数字 deepMerge, // 深度合并对象 getDataTypeName, // 获取指定变量类型名称 isGitRepo, // 判断当前工程是否是git工程 fileIsExists, // 检查文件是否存在 isTypeScriptProject, // 当前是否是TypeScript工程 getPackageTool, // 获取当前工程使用的包工具,如pnpm,yarn,npm installPackage, // 安装指定的包 readIdMapFile, // 读取当前工程下的idMap文件 replaceTranslateText, // hasImportTranslateFunction, // 检测代码中是否具有import { t } from "xxxx" importTranslateFunction, // 在代码中导入t函数 asyncExecShellScript, // 异步执行一段脚本并返回结果 getPackageReleaseInfo, // 从npm上读取指定包的信息 getInstalledPackageInfo // 返回当前工程已安装的包信息,主要是版本号 }