2023-01-29 17:18:02 +08:00

720 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require("path")
const shelljs = require("shelljs")
const fs = require("fs-extra")
const semver = require('semver')
/**
*
* 匹配指定路径或文件名称
*
* const matcher = fileMatcher([
* "<pattern>", // 匹配正则表达式字符串
* "!<pattern>", // 以!开头代表否定匹配
* /正则表达式/
* ],{
* basePath:"<指定一个基准目录,所有不是以此开头的均视为不匹配>",
* defaultPatterns:["<默认排除的模式>","<默认排除的模式>","<默认排除的模式>"],
* debug:<true/>false,是否输出调试信息,当=true时.test()方法返回[<true/false>,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(/(?<!\.)\*/g,"[^\/]*")
try{
// 以!开头的表示排除
if(pattern.startsWith("!")){
finalPatterns.unshift([new RegExp(pattern.substring(1),"g"),true])
}else{
finalPatterns.push([new RegExp(pattern,"g"),false])
}
}catch(e){
if(debug){
console.error(`${pattern} is not a valid pattern`)
}
}
}else{
finalPatterns.push([pattern,false])
}
})
return {
patterns:finalPatterns,
basePath,
test: (filename) => {
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}(?:((?<namespace>\w+)::))?(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
// 匹配t('xxxx')的正则表达式
const TranslateRegex =/(?<=\bt\(\s*("|'){1})(?<text>[^\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"){
// 优先在<script setup></script>中导入
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
if(setupScriptRegex.test(code)){
code = code.replace(setupScriptRegex,`$1\nimport { t } from '${importSource}';`)
}else{// 如果没有<script setup>则在<script></script>中导入
code = code.replace(/(^\s*\<script.*\>)/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 // 返回当前工程已安装的包信息,主要是版本号
}