import chalk from 'chalk'
import fs from 'fs-extra'
import { execSync } from 'node:child_process'
import path from 'node:path'
import semver from 'semver'
import prettier from 'prettier'
import { fileURLToPath } from 'node:url'
import { searchForWorkspaceRoot } from 'vite'
const workspaceRoot = searchForWorkspaceRoot(process.cwd())
const pathFromWorkspaceRoot = (...args: string[]) => path.resolve(workspaceRoot, ...args)
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
* 根据运行上下文获取路径(运行时打包用)
* @returns 文件绝对路径
*/
const resolveCwd = (...args: any[]) => {
return path.join(process.cwd(), ...args)
}
* 获取模板替换路径(动态)
* @param {String} posixPath 路径
* @returns 文件绝对路径
*/
const assetsPath = (posixPath: string) => {
return path.posix.join('static', posixPath)
}
* 根据运行上下文获取,当前运行组件的名称
* @returns 当前运行组件目录名称
*/
const getComponentName = () => {
return process.cwd().split(path.sep).pop()
}
* 获取当前上下文的路径
* @returns 文件绝对路径
*/
const pathJoin = (...args: string[]) => {
return path.join(dirname, ...args)
}
* 获取用户输入命令参数
* @returns 参数数组
*/
const getInputCmd = () => {
const args: string[] = []
const argv: string[] = process.argv || []
argv.forEach((item) => {
if (!item.includes(path.sep)) {
args.push(item)
}
})
return args
}
* 获取当前执行 cli 命令的工具:node\npm\yarn
* @returns node\npm\yarn
*/
const getCurrentCliTool = () => {
const npmExecpaths = process.env.npm_execpaths
if (!npmExecpaths) return 'node'
return npmExecpaths.substring(npmExecpaths.lastIndexOf(path.sep) + 1).replace(/.js|-cli/g, '')
}
* 执行 node 命令
* @param {String} cmdStr 命令字符串
*/
const execCmd = (cmdStr: string) => {
cmdStr && execSync(cmdStr, { stdio: 'inherit' })
}
* 首字母大写
* @param {String} str 字符串
* @returns 字符串
*/
const capitalize = (str: string) => {
return typeof str === 'string' ? str.slice(0, 1).toUpperCase() + str.slice(1) : str
}
* 首字母大写
* @param {String} str 字符串
* @returns 字符串
*/
const capitalizeKebabCase = (str: string, splitChar = '-') => {
return typeof str === 'string' ? str.split(splitChar).map(capitalize).join('') : str
}
* @description 将驼峰字符串转化为以指定字符分割的小写字符串
* @example kebabCase({ str : 'ImgPreviewItem' } )
* @example 输出结果:img-preview-item
*
* @param str 字符串
* @param splitChar 分隔符
*/
const kebabCase = ({ str, splitChar = '-' }: { str: string; splitChar?: string }) => {
if (!str || typeof str !== 'string') return str
return str
.split('')
.map((char, index) => {
const charCod = char.charCodeAt(0)
if (charCod < 65 || charCod > 122) return char
return charCod >= 65 && charCod <= 90 ? (index !== 0 ? splitChar : '') + char.toLowerCase() : char
})
.join('')
}
* 采用 prettier 美化字符串
* @param {String} str 格式字符
* @param {Object} options 格式字符
*/
const prettierFormat = ({ str, options = {} }: { str: string; options?: object }):string => {
return prettier.format(
str,
Object.assign(
{
printWidth: 120,
tabWidth: 2,
useTabs: false,
singleQuote: true,
semi: false,
trailingComma: 'none',
bracketSpacing: true,
quoteProps: 'preserve',
parser: 'typescript'
} as any,
options
)
) as unknown as string
}
* 扫描指定目录下面的组件目录
* @param {String} dirPath 绝对路径
* @param {Boolean} isDeep 是否深度遍历
* @param {Function} fileFilter 文件筛选拦截函数
* @param {Function} callback 遍历回调
*/
const walkFileTree = ({
dirPath,
isDeep = false,
fileFilter,
callback
}: {
dirPath: string
isDeep: boolean
fileFilter?: Function
callback: Function
}) => {
if (!dirPath || typeof callback !== 'function') {
return
}
const dirs = fs.readdirSync(path.isAbsolute(dirPath) ? dirPath : path.join(dirname, dirPath))
if (Array.isArray(dirs) && dirs.length > 0) {
dirs.forEach((file) => {
let isFind = true
const subPath = path.join(dirPath, file)
const isDirectory = fs.statSync(subPath).isDirectory()
if (typeof fileFilter === 'function') {
isFind = fileFilter({ file, subPath, dirs, isDirectory }) === true
}
if (isFind && isDirectory) {
if (isDeep) {
walkFileTree({ isDeep, dirPath: subPath, fileFilter, callback })
return
}
}
callback({ file, subPath, dirs, isDirectory })
})
}
}
* 获取指定包名的版本号
* @param {String} name NPM 包名
* @param {String} context 上下文
* @param {Boolean} 是否为 vue2 环境
* @returns 版本号
*/
const getVersion = ({ name, context, isVue2 }: { name: string; context: string; isVue2: boolean }) => {
return getComponentVersion({ name, context, dir: 'node_modules', isVue2 })
}
* 获取指定包名的版本号
* @param {String} name NPM 包名
* @param {String} context 上下文
* @param {String} dir 文件目录
* @param {Boolean} 是否为 vue2 环境
* @returns 版本号
*/
const getComponentVersion = ({
name,
context = '..',
dir = 'packages',
isOrigin = false,
isVue2
}: {
name: string
context?: string
dir?: string
isOrigin?: boolean
isVue2: boolean
}) => {
let version: string
const packageJSONPath = pathJoin(context, dir, name, 'package.json')
if (fs.existsSync(packageJSONPath)) {
version = fs.readJsonSync(packageJSONPath).version
} else {
const packageJSON = getopentinyVersion({ key: 'full' })
version = (packageJSON.devDependencies || packageJSON.dependencies || {})[name] || packageJSON.version
}
version = isOrigin ? version : '?v=' + version.replace(/[\^|~]/g, '')
return getPublichVersion({ version, isVue2 })
}
* 获取发布的实际版本号
*
* @param {String} version 版本号
* @param {Boolean} 是否为 vue2 环境
* @returns
*/
const getPublichVersion = ({ version, isVue2 }: { version: string; isVue2: boolean }) => {
if (isVue2) {
return version.replace(/^4/, '3').replace('?v=4', '?v=3')
}
return version
}
* 获取指定包的版本号
* @param {String} name 文件名|字段名
* @param {Boolean} isRoot
* @param {Boolean} 是否为 vue2 环境
* @returns 版本号
*/
const getPackageVersion = ({
name,
isRoot = false,
isVue2 = false
}: {
name: string
isRoot: boolean
isVue2: boolean
}) => {
let version = isRoot ? getopentinyVersion({ key: name }) : getComponentVersion({ name, isOrigin: true, isVue2 })
return getMinorVersion(version)
}
const getMinorVersion = (version) => {
return `~${semver.major(version)}.${semver.minor(version)}.0`
}
export const getPatchVersion = (version) => {
const versionItems = version.split('.')
versionItems.pop()
return `~${versionItems.join('.')}.0`
}
* 获取组件的版本号
* @param {String} name 文件名|字段名
* @param {Boolean} 是否为 vue2 环境
* @returns 版本号
*/
const getComponentOriginVersion = ({ name, isVue2 }: { name: string; isVue2: boolean }) => {
return getComponentVersion({ name, isOrigin: true, isVue2 })
}
* 获取根目录 package.json 中的 version
* @param {String} 对象的 Key
* @param {Boolean} 是否为 vue2 环境
* @returns 版本号
*/
const getopentinyVersion = ({ key = 'version' }: { key: string }) => {
const packageJson = fs.readJsonSync(pathFromWorkspaceRoot('packages/vue/package.json'))
const packageJsonOption = packageJson[key] || packageJson
return packageJsonOption
}
* 获取指定依赖的版本号
*/
const getDependentVersion = ({ libraryName, packageJSON }) => {
const packageJson = packageJSON || getopentinyVersion({ key: 'all' })
packageJson.dependencies = packageJson.dependencies || {}
packageJson.devDependencies = packageJson.devDependencies || {}
return packageJson.dependencies[libraryName] || packageJson.devDependencies[libraryName]
}
* 获取当前环境下 theme、theme-mobile、renderless
*/
const getInnerDependenciesVersion = ({ isTestEnv, tag, version }) => {
let themeVersion
let renderlessVersion
let themeMobileVersion
const themeCommonVersion = version.replace(/(4)(\.\d+\.\d+)/, '3$2')
if (isTestEnv) {
renderlessVersion = tag
themeVersion = tag
themeMobileVersion = tag
} else {
themeVersion = themeCommonVersion
themeMobileVersion = themeCommonVersion
renderlessVersion = version.replace('3.22.', '4.22.')
}
return {
themeVersion,
themeMobileVersion,
renderlessVersion
}
}
* 在控制台显示绿色提示
* @param {String} 提示内容
*/
const logGreen = (str: string) => {
console.log(chalk.green('### ' + str))
}
* 在控制台显示黄色提示
* @param {String} 提示内容
*/
const logYellow = (str: string) => {
console.log(chalk.yellow('### ' + str))
}
* 在控制台显示青色提示
* @param {String} 提示内容
*/
const logCyan = (str: string) => {
console.log(chalk.cyan('### ' + str))
}
* 在控制台显示红色提示
* @param {String} 提示内容
*/
const logRed = (str: string) => {
console.log(chalk.red('### ' + str))
}
* 根据运行上下文或者环境变量判断是否为 Vue2 环境
*/
const isBuildForVue2 = () => {
return (process.env.VUE_VERSION || process.env.npm_lifecycle_event || 'vue2').includes('vue2')
}
const getVueVersion = () => {
return parseFloat((process.env.VUE_VERSION || 'vue2').replace('vue', ''))
}
* 打包 Vue3 版本时 packageJson 文件 name dependencies 更改 @opentiny/vue 改为 @opentiny/vue3
*/
const changePackageName = ({ packageJSON, isVue2 }) => {
if (!isVue2) {
packageJSON.name = packageJSON.name.replace(/^@opentiny\/vue(?!3)/, '@opentiny/vue3')
for (const key in packageJSON.dependencies) {
if (/^@opentiny\/vue(?!3)/.test(key)) {
const version = packageJSON.dependencies[key]
delete packageJSON.dependencies[key]
packageJSON.dependencies[key.replace(/^@opentiny\/vue(?!3)/, '@opentiny/vue3')] = version
}
}
}
}
* 获取 opentiny 分组下指定包名、tag 名称的版本号
*/
const getLiblaryTagVersion = ({ tag, name }) => {
const version = execSync(`npm v @opentiny/${name}@${tag} version`).toString('utf-8').replace(/\n/, '')
if (tag === 'latest') {
return getMinorVersion(version)
}
return version.includes(tag) ? version : ''
}
* 获取发布 npm 包版本支持的 tag 类别数组
*/
const getNpmTagArray = () => {
return ['alpha', 'beta', 'latest', 'next', 'echarts5', 'icsl', 'saas', 'mf']
}
const getBuildTag = ({ version }) => {
const tags = getNpmTagArray()
const inputArgs = (version || '').split('.')
let isTestEnv = false
let tag = 'latest'
if (inputArgs.length) {
const versions = (inputArgs[2] || '').split('-')
if (tags.includes(versions[1])) {
isTestEnv = true
tag = versions[1]
}
}
return { isTestEnv, tag }
}
* 获取指定文件夹下所有文件的路径
* @param {string} folderPath
*
* @return {Array<string>} filesPath
*/
const getFilesPath = (folderPath: string): Array<string> => {
let filesPath: string[] = []
try {
let files = fs.readdirSync(folderPath)
files.forEach((file) => {
let tempPath = path.join(folderPath, file)
let stats = fs.statSync(tempPath)
if (stats.isDirectory()) {
filesPath.push.apply(filesPath, getFilesPath(tempPath))
} else {
filesPath.push(tempPath)
}
})
} catch (e) {
console.warn('Path Error:' + folderPath)
return filesPath
}
return filesPath
}
* 读取指定文件 filePath,并将文件中 regExpStr 匹配的片段用 targetStr 替换
* @param {string} filePath
* @param {Array<RegExp> | RegExp} regExpStr
* @param {Array<string> | string} targetStr
*/
const fragmentReplace = (filePath, regExpStr, targetStr) => {
let file = fs.readFileSync(filePath, 'utf8')
if (Array.isArray(regExpStr) && Array.isArray(targetStr) && regExpStr.length === targetStr.length) {
targetStr.forEach((_, index) => {
file = file.replace(regExpStr[index], targetStr[index])
})
} else {
file = file.replace(regExpStr, targetStr)
}
fs.writeFileSync(filePath, file, 'utf8')
}
* 读取指定文件 filePath,并将文件中 regExpStr 匹配的片段用 targetStr 替换
* @param {Array<string>} filesPath
* @param {Array<string> | string} regExpStr
* @param {Array<string> | string} targetStr
*/
const filesFragmentReplace = (
folderPath,
regExpStr: Array<string | RegExp> | string | RegExp,
targetStr: Array<string> | string
) => {
let filesPath = getFilesPath(folderPath)
if (filesPath) {
filesPath.forEach((item) => {
fragmentReplace(item, regExpStr, targetStr)
})
}
}
const isCLIProduction = process.env.TINY_TSUP_ENV === 'production'
const pathJoinFromCLI = (...args: string[]) => {
const cliDir = path.resolve(dirname, '../../', isCLIProduction ? '../' : '')
return path.join(cliDir, ...args)
}
const pathJoinFromCLIEntry = (...args: string[]) => {
return path.join(dirname, '../', ...args)
}
const templatePath = isCLIProduction ? pathJoinFromCLIEntry('./template') : pathJoinFromCLI('./public/template')
export const isValidVersion = (version: string): boolean => /^\d+(\.\d+){2}(-[a-zA-Z0-9\.]+)?$/.test(version)
export {
assetsPath,
capitalize,
capitalizeKebabCase,
changePackageName,
execCmd,
fragmentReplace,
filesFragmentReplace,
getopentinyVersion,
getComponentName,
getComponentOriginVersion,
getComponentVersion,
getCurrentCliTool,
getDependentVersion,
getFilesPath,
getInnerDependenciesVersion,
getInputCmd,
getLiblaryTagVersion,
getNpmTagArray,
getPackageVersion,
getPublichVersion,
getVersion,
isBuildForVue2,
kebabCase,
logCyan,
logGreen,
logRed,
logYellow,
pathJoin,
prettierFormat,
resolveCwd,
walkFileTree,
getBuildTag,
getVueVersion,
pathFromWorkspaceRoot,
getMinorVersion,
pathJoinFromCLI,
pathJoinFromCLIEntry,
templatePath
}