diff --git a/package.json b/package.json index 7982c4e..847b021 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "AlbertLin0923", "scripts": { "release": "dev-scripts releasePackage", - "dev": "pnpm -r --filter './packages/**' run dev", + "dev": "tsx scripts/dev.mts", "build": "pnpm -r --filter './packages/**' run build", "prepare": "husky install", "preinstall": "npx only-allow pnpm", @@ -34,6 +34,7 @@ "@commitlint/config-conventional": "^18.4.3", "@mango-scripts/dev-scripts": "workspace:*", "@mango-scripts/esp-config": "^2.0.8", + "@mango-scripts/utils": "^2.0.4", "@types/node": "^20.10.5", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", diff --git a/packages/dev-scripts/package.json b/packages/dev-scripts/package.json index 901062f..f243183 100644 --- a/packages/dev-scripts/package.json +++ b/packages/dev-scripts/package.json @@ -34,7 +34,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { - "@mango-scripts/utils": "^2.0.4", + "@mango-scripts/utils": "workspace:*", "npm-keyword": "^7.0.0", "package-json": "^8.1.1" }, diff --git a/packages/dev-scripts/src/cli.mts b/packages/dev-scripts/src/cli.mts index 503a6d5..ca94ab8 100644 --- a/packages/dev-scripts/src/cli.mts +++ b/packages/dev-scripts/src/cli.mts @@ -15,7 +15,7 @@ import { import changeExtname from './scripts/changeExtname.mjs' import addPackage from './scripts/addPackage.mjs' import gitGkd from './scripts/gitGkd.mjs' -import releasePackage from './scripts/releasePackage.mjs' +import releasePackage from './scripts/releasePackage/index.mjs' const packageJson = fs.readJSONSync( path.resolve( diff --git a/packages/dev-scripts/src/scripts/addPackage.mts b/packages/dev-scripts/src/scripts/addPackage.mts index 550f4e5..9222fc3 100644 --- a/packages/dev-scripts/src/scripts/addPackage.mts +++ b/packages/dev-scripts/src/scripts/addPackage.mts @@ -1,8 +1,13 @@ import npmKeyword from 'npm-keyword' import packageJson from 'package-json' -import { pico, consola, inquirer } from '@mango-scripts/utils' +import { + pico, + consola, + inquirer, + getMonorepoPkgListInfo, +} from '@mango-scripts/utils' -import { run, getPkgInfoList } from '../utils/releaseUtils.mjs' +import { run } from '../utils/index.mjs' const typeMap = [ { name: 'dependencies', value: '--save-prod' }, @@ -10,7 +15,7 @@ const typeMap = [ ] const addPackage = async (): Promise => { - const appList = await getPkgInfoList() + const appList = await getMonorepoPkgListInfo() const { appName, installPkgName, type } = await inquirer.prompt([ { diff --git a/packages/dev-scripts/src/scripts/gitGkd.mts b/packages/dev-scripts/src/scripts/gitGkd.mts index 46f534d..ccf1042 100644 --- a/packages/dev-scripts/src/scripts/gitGkd.mts +++ b/packages/dev-scripts/src/scripts/gitGkd.mts @@ -1,6 +1,6 @@ import { pico, consola, getGitRepoInfo } from '@mango-scripts/utils' -import { run } from '../utils/releaseUtils.mjs' +import { run } from '../utils/index.mjs' type GitGkdOptionsType = { targetBranch: string[] diff --git a/packages/dev-scripts/src/scripts/releasePackage.mts b/packages/dev-scripts/src/scripts/releasePackage.mts deleted file mode 100644 index 651b5fa..0000000 --- a/packages/dev-scripts/src/scripts/releasePackage.mts +++ /dev/null @@ -1,186 +0,0 @@ -// Forked from https://github.com/vitejs/vite/blob/main/scripts - -import path from 'node:path' - -import { - pico, - minimist, - consola, - semver, - prompts, - getGitRepoInfo, -} from '@mango-scripts/utils' - -import { - getPkgInfoList, - getVersionChoices, - logCommitsForPackage, - run, - step, - updateVersion, - publishPkg, - isWorktreeEmpty, - confirmRegistry, -} from '../utils/releaseUtils.mjs' - -export const release = async (): Promise => { - const { branch } = getGitRepoInfo() - consola.info(pico.cyan(`当前分支: ${branch}`)) - - // if (!(await isWorktreeEmpty())) { - // consola.error('当前工作区有尚未提交的代码,请先提交代码') - // return - // } - - // if (!(await confirmRegistry())) { - // return - // } - - const pkgList = await getPkgInfoList() - - console.log('pkgList', pkgList) - - const pkgListWithCommitList = ( - await Promise.all( - pkgList.map(async (pkg) => { - const { pkgName } = pkg - const commitList = await logCommitsForPackage(pkgName) - - console.log('commitList', pkgName, commitList) - return { ...pkg, commitList } - }), - ) - ).filter((pkg) => { - return pkg.commitList.length > 0 - }) - - const { selectedPkg } = await prompts({ - type: 'select', - name: 'selectedPkg', - message: '选择待发布的包', - choices: pkgListWithCommitList.map((pkg) => ({ - value: pkg, - title: `${pkg.pkgName}[${pkg.commitList.length}]`, - })), - }) - - // if (!pkg) return - - // const { pkgName, pkgJsonFilePath, pkgDirPath, pkgCurrentVersion } = pkg - - // await logRecentCommits(pkgName) - - // if (!targetVersion) { - // const { releaseType }: { releaseType: string } = await prompts({ - // type: 'select', - // name: 'releaseType', - // message: 'Select release type', - // choices: getVersionChoices(pkgCurrentVersion), - // }) - - // if (releaseType === 'custom') { - // const res: { version: string } = await prompts({ - // type: 'text', - // name: 'version', - // message: 'Input custom version', - // initial: pkgCurrentVersion, - // }) - // targetVersion = res.version - // } else { - // targetVersion = releaseType - // } - // } - - // if (!semver.valid(targetVersion)) { - // throw new Error(`invalid target version: ${targetVersion}`) - // } - - // const tag = `${pkgName}@${targetVersion}` - - // const { yes }: { yes: boolean } = await prompts({ - // type: 'confirm', - // name: 'yes', - // message: `Releasing ${pico.yellow(tag)} Confirm?`, - // }) - - // if (!yes) { - // return - // } - - // step('\nUpdating package version...') - - // await updateVersion(pkgJsonFilePath, targetVersion) - - // step('\nGenerating changelog...') - - // const changelogArgs = [ - // 'conventional-changelog', - // '-p', - // 'conventionalcommits', - // '-i', - // 'CHANGELOG.md', - // '-s', - // '--commit-path', - // '.', - // ] - // changelogArgs.push('--lerna-package', pkgName) - - // await run('npx', changelogArgs, { cwd: pkgDirPath }) - - // const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) - // if (stdout) { - // step('\nCommitting changes...') - // await run('git', ['add', '-A']) - // await run('git', ['commit', '-m', `feat: release ${tag}`]) - // await run('git', ['tag', tag]) - // } else { - // consola.info('No changes to commit.') - // return - // } - - // step('\nPushing to GitHub...') - // await run('git', ['push', 'origin', `refs/tags/${tag}`]) - // await run('git', ['push']) - - // consola.success( - // pico.green( - // ` - // Pushed, publishing should starts shortly on CI. - // `, - // ), - // ) -} - -export const publish = async (tag: string) => { - if (!tag) throw new Error('No tag specified') - - const versionSeg = tag.lastIndexOf('@') - - if (versionSeg === -1) { - throw new Error('Tag format error') - } - - const pkgName = tag.slice(0, versionSeg) - - const version = tag.slice(versionSeg + 1) - - const rawPkgName = pkgName.split('/')[1] - - step(`Publishing ${pkgName} package...`) - - await publishPkg( - path.join(process.cwd(), `./packages/${rawPkgName}`), - version.includes('beta') - ? 'beta' - : version.includes('alpha') - ? 'alpha' - : undefined, - ) -} - -const releasePackage = async () => { - const { tag } = minimist(process.argv.slice(2)) - tag ? publish(tag) : release() -} - -export default releasePackage diff --git a/packages/dev-scripts/src/scripts/releasePackage/confirmEnv.mts b/packages/dev-scripts/src/scripts/releasePackage/confirmEnv.mts new file mode 100644 index 0000000..8268615 --- /dev/null +++ b/packages/dev-scripts/src/scripts/releasePackage/confirmEnv.mts @@ -0,0 +1,75 @@ +import { + pico, + consola, + inquirer, + getGitRepoInfo, + ora, +} from '@mango-scripts/utils' + +import { run } from '../../utils/index.mjs' + +export const confirmRegistry = async () => { + const registry = ( + await run('npm', ['config', 'get', 'registry'], { stdio: 'pipe' }) + ).stdout + + const { yes }: { yes: boolean } = await inquirer.prompt([ + { + type: 'confirm', + name: 'yes', + message: `当前 npm registry 为: ${pico.yellow(registry)},确定? `, + }, + ]) + + return yes +} + +export const confirmGitBranch = async () => { + const { branch } = getGitRepoInfo() + const { yes }: { yes: boolean } = await inquirer.prompt([ + { + type: 'confirm', + name: 'yes', + message: `当前发布的分支为: ${pico.yellow(branch)},确定?`, + }, + ]) + + return yes +} + +export const confirmWorktreeEmpty = async () => { + const isWorktreeEmpty = !( + await run('git', ['status', '--porcelain'], { stdio: 'pipe' }) + )?.stdout + + !isWorktreeEmpty && + consola.error('检测到当前工作区有尚未提交的代码,请先提交代码') + + return isWorktreeEmpty +} + +export const confirmNpmLoggedIn = async () => { + try { + const spinner = ora().start(`获取 npm 登录状态...`) + const user = (await run('npm', ['whoami'], { stdio: 'pipe' })).stdout.trim() + spinner.stop() + + if (!user) { + consola.error('检测到你尚未登录 npm,请使用 `npm login` 登录后再继续。') + return false + } + + const { yes }: { yes: boolean } = await inquirer.prompt([ + { + type: 'confirm', + name: 'yes', + message: `当前登录的 npm 用户为: ${pico.cyan(user)},确定?`, + }, + ]) + + return yes + } catch (error) { + consola.error('检测到您尚未登录 npm,请使用 `npm login` 登录后再继续。') + return false + } +} diff --git a/packages/dev-scripts/src/scripts/releasePackage/diffPkgList.mts b/packages/dev-scripts/src/scripts/releasePackage/diffPkgList.mts new file mode 100644 index 0000000..30d44f2 --- /dev/null +++ b/packages/dev-scripts/src/scripts/releasePackage/diffPkgList.mts @@ -0,0 +1,185 @@ +import { fs, semver, lodash } from '@mango-scripts/utils' + +import { run } from '../../utils/index.mjs' + +import type { Pkg } from '../../utils/index.mjs' + +const { uniq } = lodash + +// 获取与指定包名相关的最新 Git 标签 +const getLatestTagForPkg = async (pkg: Pkg) => { + const tags = (await run('git', ['tag'], { stdio: 'pipe' })).stdout + .split('\n') + .filter((tag: string) => tag.startsWith(`${pkg.pkgName}@`)) + .map((tag: string) => tag.replace(`${pkg.pkgName}@`, '')) + .filter(semver.valid) + .sort(semver.rcompare) + + return tags[0] ? `${pkg.pkgName}@${tags[0]}` : undefined +} + +// 记录指定包的最近提交列表 +export const logRecentCommitListForPkg = async ( + pkg: Pkg, +): Promise => { + const latestTag = await getLatestTagForPkg(pkg) + const logRange = latestTag ? `${latestTag}..HEAD` : 'HEAD' + + const commitFormat = [ + '--no-pager', + 'log', + logRange, + '--color', + '--graph', + '--pretty=format:%C(Magenta)[%cd]%Cgreen(%cr)%C(bold blue)<%an>%Creset %Cred%h%Creset -%C(yellow)%d%Creset %s', + '--date=format:%Y-%m-%d %H:%M:%S', + '--abbrev-commit', + '--', + `packages/${pkg.pkgDir}`, + ] + + return ( + (await run('git', commitFormat, { stdio: 'pipe' }))?.stdout + ?.split('\n') + ?.filter(Boolean) || [] + ) +} + +// 获取包的依赖列表 +const getDepNameList = async ({ pkgJsonFilePath }: Pkg) => { + if (!(await fs.pathExists(pkgJsonFilePath))) return [] + const { dependencies = {}, devDependencies = {} } = + await fs.readJson(pkgJsonFilePath) + return uniq([...Object.keys(dependencies), ...Object.keys(devDependencies)]) +} +/** +对于packages目录下的内部包:A B C D E F G, +A依赖B,A依赖C,B依赖C,B依赖D,F,G无内部包依赖 + +输出: +const map = [ + { + pkg: 'A', + deps: [ + { pkg: 'B', level: [1] }, + { pkg: 'C', level: [1, 2] }, + { pkg: 'D', level: [2] }, + ], + }, + { + pkg: 'B', + deps: [ + { pkg: 'C', level: [1] }, + { pkg: 'D', level: [1] }, + ], + }, + { + pkg: 'C', + deps: [], + }, + { + pkg: 'D', + deps: [], + }, + { + pkg: 'E', + deps: [], + }, + { + pkg: 'F', + deps: [], + }, + { + pkg: 'G', + deps: [], + }, +] +*/ +const generateDependencyMap = async (allPkgList: Pkg[]) => { + const pkgLevelCache: Map = new Map() + + const getPkgLevels = async (pkg: Pkg, level = 1): Promise => { + // 如果缓存中已有层级数据,直接返回 + if (pkgLevelCache.has(pkg.pkgName)) { + return pkgLevelCache.get(pkg.pkgName) || [] + } + + const depNameList = await getDepNameList(pkg) + const depsWithLevel: number[] = [level] + + // 遍历当前包的依赖,递归计算依赖层级 + for (const depName of depNameList) { + const depPkg = allPkgList.find((p) => p.pkgName === depName) + if (depPkg) { + const depLevels = await getPkgLevels(depPkg, level + 1) + depsWithLevel.push(...depLevels) + } + } + + // 缓存当前包的层级数据 + pkgLevelCache.set(pkg.pkgName, uniq(depsWithLevel)) + return pkgLevelCache.get(pkg.pkgName) || [] + } + + const dependencyMap: { pkg: Pkg; deps: { pkg: Pkg; level: number[] }[] }[] = + [] + + // 遍历所有包,生成每个包的依赖树 + for (const pkg of allPkgList) { + const deps = [] + const depNameList = await getDepNameList(pkg) + + // 对每个依赖包,计算它的层级并生成依赖关系 + for (const depName of depNameList) { + const depPkg = allPkgList.find((p) => p.pkgName === depName) + if (depPkg) { + const depLevels = await getPkgLevels(depPkg) + deps.push({ pkg: depPkg, level: depLevels }) + } + } + + dependencyMap.push({ pkg, deps }) + } + + return dependencyMap +} + +const addChangedDepInfoForPkgList = async ( + pkgListWithCommitList: Pkg[], + pkgList: Pkg[], +) => { + const dependency = await generateDependencyMap(pkgList) + return pkgList.map((pkg) => { + const pkgDependency = dependency.find( + ({ pkg: { pkgName } }) => pkgName === pkg.pkgName, + ) + + return { + ...pkg, + changedDep: + pkgDependency && + pkgDependency?.deps.filter((dep: { pkg: Pkg; level: number[] }) => + pkgListWithCommitList.findIndex( + (d: Pkg) => dep.pkg.pkgName === d.pkgName, + ), + ), + commitList: + pkgListWithCommitList.find((dep: Pkg) => dep.pkgName === pkg.pkgName) + ?.commitList ?? [], + } + }) +} + +export const getDiffPkgList = async (pkgList: Pkg[]) => { + const pkgListWithCommitList = ( + await Promise.all( + pkgList.map(async (pkg) => { + return { ...pkg, commitList: await logRecentCommitListForPkg(pkg) } + }), + ) + ).filter((pkg) => { + return pkg.commitList.length > 0 + }) + + return await addChangedDepInfoForPkgList(pkgListWithCommitList, pkgList) +} diff --git a/packages/dev-scripts/src/scripts/releasePackage/index.mts b/packages/dev-scripts/src/scripts/releasePackage/index.mts new file mode 100644 index 0000000..dfd39a3 --- /dev/null +++ b/packages/dev-scripts/src/scripts/releasePackage/index.mts @@ -0,0 +1,173 @@ +// Inspired by https://github.com/vitejs/vite/blob/main/scripts + +import path from 'node:path' + +import { + pico, + minimist, + consola, + inquirer, + getMonorepoPkgListInfo, +} from '@mango-scripts/utils' + +import { + confirmRegistry, + confirmGitBranch, + confirmWorktreeEmpty, + confirmNpmLoggedIn, +} from './confirmEnv.mjs' +import { updateVersion, updateDepVersion, updateChangelog } from './update.mjs' +import { getDiffPkgList } from './diffPkgList.mjs' +import { setPkgTargetVersion } from './version.mjs' + +import { run, step } from '../../utils/index.mjs' + +import type { Pkg } from '../../utils/index.mjs' + +export const publish = async (tag: string) => { + if (!tag || !tag.includes('@')) throw new Error('无效的发布TAG') + + const [pkgName, version] = tag.split(/@(.+)/) + const rawPkgName = pkgName.split('/')[1] + const releaseType = version.match(/beta|alpha/)?.[0] + + step(`发布 ${pkgName} 中...`) + + const publicArgs = ['publish', '--access', 'public'] + + if (releaseType) { + publicArgs.push(`--tag`, releaseType) + } + publicArgs.push(`--no-git-checks`) + + await run('pnpm', publicArgs, { + cwd: path.join(process.cwd(), `./packages/${rawPkgName}`), + }) +} + +export const release = async (): Promise => { + // if (!(await confirmGitBranch()) || !(await confirmWorktreeEmpty())) return + + const { publishType } = await inquirer.prompt([ + { + type: 'list', + name: 'publishType', + message: '选择发布方式', + choices: [ + { name: '本地打包发布', value: 'local' }, + { name: '远程CI发布', value: 'remote' }, + ], + }, + ]) + + step(`已选择 ${publishType === 'local' ? '[本地打包发布]' : '[远程CI发布]'}`) + + if (publishType === 'local') { + if (!(await confirmRegistry()) || !(await confirmNpmLoggedIn())) return + } else { + consola.info( + pico.red(`使用[远程CI发布]前,你需要确认远程CI上已配置 publish 命令\n`), + ) + } + + const pkgList = await getMonorepoPkgListInfo() + + const diffPkgList = await getDiffPkgList(pkgList) + + const { selectedPkg } = await inquirer.prompt([ + { + type: 'list', + name: 'selectedPkg', + message: '选择待发布的包', + choices: diffPkgList.map((pkg) => { + const commitInfo = pkg?.commitList?.length + ? pico.green(` [新增 ${pkg?.commitList?.length} commit]`) + : '' + const depChangedInfo = + pkg.changedDep && pkg.changedDep?.length > 0 + ? pico.magenta( + ` [依赖已更新][${pkg.changedDep.map((it: { pkg: Pkg; level: number[] }) => `${it.pkg.pkgName}<${it.level.join(',')}级依赖>`).join(', ')}]`, + ) + : '' + return { + name: `${pkg.pkgName}${commitInfo}${depChangedInfo}`, + value: pkg, + } + }), + loop: false, + pageSize: 100, + }, + ]) + + if (!selectedPkg) return + + const { pkgName, pkgJsonFilePath, pkgDirPath, commitList } = selectedPkg + + commitList?.length && + consola.info(`最近 commit: + ${commitList.join('\n')} + `) + + const pkgTargetVersion = await setPkgTargetVersion(selectedPkg) + + if (!pkgTargetVersion) return + + const tag = `${pkgName}@${pkgTargetVersion}` + + const { yes }: { yes: boolean } = await inquirer.prompt([ + { + type: 'confirm', + name: 'yes', + message: `当前的发布TAG: ${pico.yellow(tag)},确定?`, + }, + ]) + + if (!yes) return + + step('升级 package.json 版本号...') + await updateVersion(pkgJsonFilePath, pkgTargetVersion) + + step('升级 package.json 依赖版本号...') + await updateDepVersion(pkgJsonFilePath, pkgList) + + step('生成 changelog...') + await updateChangelog(pkgDirPath, pkgName) + + const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) + if (stdout) { + step('提交version、changelog等代码变更...') + await run('git', ['add', '-A']) + await run('git', ['commit', '-m', `feat: release ${tag}`]) + await run('git', ['tag', tag]) + } else { + consola.info('无变更') + return + } + + step('推送到远程分支...') + await run('git', ['push', 'origin', `refs/tags/${tag}`]) + await run('git', ['push']) + + if (publishType === 'local') { + step('本地打包中...') + await run('pnpm', ['run', 'build'], { cwd: process.cwd() }) + + step('上传到 npm...') + publish(tag) + } else { + consola.success( + pico.green( + ` + 推送到远程仓库成功,远程安装和打包部署即将启动,请在远程仓库查看状态 + `, + ), + ) + } +} + +const releasePackage = async () => { + const { tag } = minimist(process.argv.slice(2)) + tag ? publish(tag) : release() +} + +export default releasePackage diff --git a/packages/dev-scripts/src/scripts/releasePackage/update.mts b/packages/dev-scripts/src/scripts/releasePackage/update.mts new file mode 100644 index 0000000..26edf39 --- /dev/null +++ b/packages/dev-scripts/src/scripts/releasePackage/update.mts @@ -0,0 +1,79 @@ +import { fs, pico } from '@mango-scripts/utils' + +import { run, step } from '../../utils/index.mjs' + +import type { Pkg } from '../../utils/index.mjs' + +export const updateVersion = async ( + pkgPath: string, + version: string, +): Promise => { + const pkg = await fs.readJSON(pkgPath) + pkg.version = version + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') +} + +export const updateDepVersion = async ( + pkgPath: string, + pkgList: Pkg[], +): Promise => { + // 读取 package.json 文件内容 + const pkgJson = await fs.readJSON(pkgPath) + + // 更新指定依赖项版本号 + const _updateVersion = (dependencies: Record | undefined) => { + if (!dependencies) return + for (const [key, value] of Object.entries(dependencies)) { + // 检查依赖项是否使用 workspace:* 前缀 + if (value.startsWith('workspace:')) { + const actualPkg = pkgList.find((pkg) => pkg.pkgName === key) + if (actualPkg) { + // 根据 workspace 前缀类型,更新为对应的版本号格式 + if (value === 'workspace:*') { + dependencies[key] = actualPkg.pkgCurrentVersion + } else if (value.startsWith('workspace:~')) { + dependencies[key] = `~${actualPkg.pkgCurrentVersion}` + } else if (value.startsWith('workspace:^')) { + dependencies[key] = `^${actualPkg.pkgCurrentVersion}` + } else if (value.startsWith('workspace:')) { + dependencies[key] = value.replace('workspace:', '') // 处理带版本号的 workspace 前缀 + } + + step( + `更新依赖包${pico.bgWhite(`[${key}:${value}]`)}到${pico.bgWhite(`[${key}:${dependencies[key]}]`)}版本`, + ) + } + } + } + } + + // 更新 dependencies、devDependencies 和 peerDependencies + _updateVersion(pkgJson.dependencies) + _updateVersion(pkgJson.devDependencies) + _updateVersion(pkgJson.peerDependencies) + + // 将更新后的内容写回 package.json 文件 + fs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n') +} + +export const updateChangelog = async ( + pkgDirPath: string, + pkgName: string, +): Promise => { + await run( + 'npx', + [ + 'conventional-changelog', + '-p', + 'conventionalcommits', + '-i', + 'CHANGELOG.md', + '-s', + '--commit-path', + '.', + '--lerna-package', + pkgName, + ], + { cwd: pkgDirPath }, + ) +} diff --git a/packages/dev-scripts/src/scripts/releasePackage/version.mts b/packages/dev-scripts/src/scripts/releasePackage/version.mts new file mode 100644 index 0000000..2adc609 --- /dev/null +++ b/packages/dev-scripts/src/scripts/releasePackage/version.mts @@ -0,0 +1,115 @@ +import { semver, inquirer, consola } from '@mango-scripts/utils' +import packageJson from 'package-json' + +import type { Pkg } from '../../utils/index.mjs' + +const getVersionChoices = (pkgCurrentVersion: string) => { + const currentBeta = pkgCurrentVersion.includes('beta') + const currentAlpha = pkgCurrentVersion.includes('alpha') + const isStable = !currentBeta && !currentAlpha + + function inc(i: semver.ReleaseType, tag = currentAlpha ? 'alpha' : 'beta') { + return semver.inc(pkgCurrentVersion, i, tag) as string + } + + let versionChoices = [ + { + name: 'next', + value: inc(isStable ? 'patch' : 'prerelease'), + }, + ] + + if (isStable) { + versionChoices.push( + { + name: 'beta-minor', + value: inc('preminor'), + }, + { + name: 'beta-major', + value: inc('premajor'), + }, + { + name: 'alpha-minor', + value: inc('preminor', 'alpha'), + }, + { + name: 'alpha-major', + value: inc('premajor', 'alpha'), + }, + { + name: 'minor', + value: inc('minor'), + }, + { + name: 'major', + value: inc('major'), + }, + ) + } else if (currentAlpha) { + versionChoices.push({ + name: 'beta', + value: inc('patch') + '-beta.0', + }) + } else { + versionChoices.push({ + name: 'stable', + value: inc('patch'), + }) + } + versionChoices.push({ value: 'custom', name: 'custom' }) + + versionChoices = versionChoices.map((i) => { + i.name = `${i.name} (${i.value})` + return i + }) + + return versionChoices +} + +export const setPkgTargetVersion = async (pkg: Pkg) => { + const { pkgName, pkgCurrentVersion } = pkg + let pkgTargetVersion: string + + const remotePkgJson = await packageJson(pkgName) + + const { releaseType } = await inquirer.prompt([ + { + type: 'list', + name: 'releaseType', + message: `选择发包版本 ${remotePkgJson ? `[远程npm registry版本号: ${remotePkgJson.version}]` : null}`, + choices: getVersionChoices(pkgCurrentVersion), + loop: false, + }, + ]) + + if (releaseType === 'custom') { + const res: { version: string } = await inquirer.prompt([ + { + type: 'input', + name: 'version', + message: '输入自定义版本号', + default: pkgCurrentVersion, + }, + ]) + pkgTargetVersion = res.version + } else { + pkgTargetVersion = releaseType + } + + if (!semver.valid(pkgTargetVersion)) { + consola.error(`不合法的版本号: ${pkgTargetVersion},请检查`) + return false + } + + if ( + remotePkgJson && + semver.lt(pkgTargetVersion, remotePkgJson.version as string) + ) { + consola.error( + `本地版本号 ${pkgCurrentVersion} 低于远程 npm registry 版本号 ${remotePkgJson.version},请检查`, + ) + return false + } + return pkgTargetVersion +} diff --git a/packages/dev-scripts/src/utils/index.mts b/packages/dev-scripts/src/utils/index.mts new file mode 100644 index 0000000..abbb273 --- /dev/null +++ b/packages/dev-scripts/src/utils/index.mts @@ -0,0 +1,22 @@ +import { execa, pico, consola } from '@mango-scripts/utils' + +export type Pkg = { + pkgDir: string + pkgDirPath: string + pkgJsonFilePath: string + pkgName: string + pkgCurrentVersion: string + commitList?: string[] + changedDep?: Pkg[] +} + +export const run = async ( + bin: string, + args: string[], + opts: any = {}, +): Promise => { + // consola.info(`Running command: ${bin} ${args.join(' ')}`) + return execa(bin, args, { stdio: 'inherit', ...opts }) +} + +export const step = (msg: string) => consola.info(pico.cyan(msg)) diff --git a/packages/dev-scripts/src/utils/releaseUtils.mts b/packages/dev-scripts/src/utils/releaseUtils.mts deleted file mode 100644 index bb31348..0000000 --- a/packages/dev-scripts/src/utils/releaseUtils.mts +++ /dev/null @@ -1,206 +0,0 @@ -import path from 'node:path' - -import { fs, execa, pico, semver, consola, prompts } from '@mango-scripts/utils' - -export const getPkgInfoList = async (): Promise< - { - pkgDir: string - pkgDirPath: string - pkgJsonFilePath: string - pkgName: string - pkgCurrentVersion: string - }[] -> => { - const pkgInfoList = [] - const targetDirPath = path.resolve(process.cwd(), './packages') - const pkgDirList = await fs.readdir(targetDirPath) - - for (const pkgDir of pkgDirList) { - const pkgDirPath = path.join(targetDirPath, pkgDir) - const pkgJsonFilePath = path.join(pkgDirPath, 'package.json') - if (await fs.pathExists(pkgJsonFilePath)) { - const { name: pkgName, version: pkgCurrentVersion } = - await fs.readJSON(pkgJsonFilePath) - pkgInfoList.push({ - pkgDir, - pkgDirPath, - pkgJsonFilePath, - pkgName, - pkgCurrentVersion, - }) - } - } - - return pkgInfoList -} - -export async function run( - bin: string, - args: string[], - opts: any = {}, -): Promise { - return execa(bin, args, { stdio: 'inherit', ...opts }) -} - -export const step = (msg: string) => { - return consola.info(pico.cyan(msg)) -} - -export const getVersionChoices = (pkgCurrentVersion: string) => { - const currentBeta = pkgCurrentVersion.includes('beta') - const currentAlpha = pkgCurrentVersion.includes('alpha') - const isStable = !currentBeta && !currentAlpha - - function inc(i: semver.ReleaseType, tag = currentAlpha ? 'alpha' : 'beta') { - return semver.inc(pkgCurrentVersion, i, tag) as string - } - - let versionChoices = [ - { - title: 'next', - value: inc(isStable ? 'patch' : 'prerelease'), - }, - ] - - if (isStable) { - versionChoices.push( - { - title: 'beta-minor', - value: inc('preminor'), - }, - { - title: 'beta-major', - value: inc('premajor'), - }, - { - title: 'alpha-minor', - value: inc('preminor', 'alpha'), - }, - { - title: 'alpha-major', - value: inc('premajor', 'alpha'), - }, - { - title: 'minor', - value: inc('minor'), - }, - { - title: 'major', - value: inc('major'), - }, - ) - } else if (currentAlpha) { - versionChoices.push({ - title: 'beta', - value: inc('patch') + '-beta.0', - }) - } else { - versionChoices.push({ - title: 'stable', - value: inc('patch'), - }) - } - versionChoices.push({ value: 'custom', title: 'custom' }) - - versionChoices = versionChoices.map((i) => { - i.title = `${i.title} (${i.value})` - return i - }) - - return versionChoices -} - -export const updateVersion = async ( - pkgPath: string, - version: string, -): Promise => { - const pkg = await fs.readJSON(pkgPath) - pkg.version = version - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') -} - -// 获取与指定包名相关的最新 Git 标签 -export const getLatestTagForPackage = async (pkgName: string) => { - const tags = (await run('git', ['tag'], { stdio: 'pipe' })).stdout - .split('\n') - .filter(Boolean) - return tags - .filter((tag: string) => tag.startsWith(`${pkgName}@`)) - .sort() - .reverse()[0] -} - -// 返回特定包的提交列表 -export const logCommitsForPackage = async ( - pkgName: string, -): Promise => { - const latestTag = await getLatestTagForPackage(pkgName) - - console.log('latestTag', latestTag) - let commits: string[] = [] - - if (latestTag) { - const sha = await run('git', ['rev-list', '-n', '1', latestTag], { - stdio: 'pipe', - }).then((res) => res.stdout.trim()) - - commits = ( - await run( - 'git', - [ - '--no-pager', - 'log', - `${sha}..HEAD`, - '--oneline', - '--', - `packages/${pkgName}`, - ], - { stdio: 'pipe' }, - ) - ).stdout - .split('\n') - .filter(Boolean) - } else { - commits = ( - await run( - 'git', - ['--no-pager', 'log', '--oneline', '--', `packages/${pkgName}`], - { stdio: 'pipe' }, - ) - ).stdout - .split('\n') - .filter(Boolean) - } - - return commits -} - -export const publishPkg = async ( - pkdDir: string, - tag?: string, -): Promise => { - const publicArgs = ['publish', '--access', 'public'] - if (tag) { - publicArgs.push(`--tag`, tag) - } - publicArgs.push(`--no-git-checks`) - await run('pnpm', publicArgs, { - cwd: pkdDir, - }) -} - -export const isWorktreeEmpty = async () => { - return !(await execa('git', ['status', '--porcelain']))?.stdout -} - -export const confirmRegistry = async () => { - const registry = (await execa('npm', ['config', 'get', 'registry'])).stdout - - const { yes }: { yes: boolean } = await prompts({ - type: 'confirm', - name: 'yes', - message: `当前 npm registry 为: ${pico.yellow(registry)},确定? `, - }) - - return yes -} diff --git a/packages/i18n-scripts/package.json b/packages/i18n-scripts/package.json index 55a5a67..48571e2 100644 --- a/packages/i18n-scripts/package.json +++ b/packages/i18n-scripts/package.json @@ -31,7 +31,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { - "@mango-scripts/utils": "^2.0.4", + "@mango-scripts/utils": "workspace:*", "gogocode": "^1.0.55" }, "devDependencies": {}, diff --git a/packages/i18n-utils/package.json b/packages/i18n-utils/package.json index 15312d0..370767f 100644 --- a/packages/i18n-utils/package.json +++ b/packages/i18n-utils/package.json @@ -41,7 +41,7 @@ "@babel/parser": "^7.23.0", "@babel/runtime": "^7.23.2", "@babel/types": "^7.23.0", - "@mango-scripts/utils": "^2.0.2", + "@mango-scripts/utils": "workspace:*", "hyntax": "^1.1.9", "pug": "^3.0.2", "svelte": "^3.59.0", diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index a4231bf..fa8527c 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -37,8 +37,8 @@ }, "dependencies": { "@babel/core": "^7.24.4", - "@mango-scripts/babel-preset-mango": "^0.0.2", - "@mango-scripts/utils": "^2.0.4", + "@mango-scripts/babel-preset-mango": "workspace:*", + "@mango-scripts/utils": "workspace:*", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@rsbuild/core": "^0.6.1", "@rsbuild/plugin-babel": "^0.6.1", diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index f1afba2..e365a32 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,5 @@ +import path from 'node:path' + import fs from 'fs-extra' import pico from 'picocolors' import updateNotifier from 'update-notifier' @@ -121,3 +123,41 @@ export const prepareCli = >( return packageJson } + +export const getMonorepoPkgListInfo = async (): Promise< + { + pkgDir: string + pkgDirPath: string + pkgJsonFilePath: string + pkgName: string + pkgCurrentVersion: string + }[] +> => { + const pkgInfoList: { + pkgDir: string + pkgDirPath: string + pkgJsonFilePath: string + pkgName: string + pkgCurrentVersion: string + }[] = [] + const targetDirPath = path.resolve(process.cwd(), './packages') + const pkgDirList = await fs.readdir(targetDirPath) + + for (const pkgDir of pkgDirList) { + const pkgDirPath = path.join(targetDirPath, pkgDir) + const pkgJsonFilePath = path.join(pkgDirPath, 'package.json') + if (await fs.pathExists(pkgJsonFilePath)) { + const { name: pkgName, version: pkgCurrentVersion } = + await fs.readJSON(pkgJsonFilePath) + pkgInfoList.push({ + pkgDir, + pkgDirPath, + pkgJsonFilePath, + pkgName, + pkgCurrentVersion, + }) + } + } + + return pkgInfoList +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20e3829..0e0c7b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@mango-scripts/esp-config': specifier: ^2.0.8 version: 2.0.8(@types/eslint@9.6.1)(eslint@8.57.1)(postcss@8.4.47)(prettier@3.3.3)(stylelint@16.10.0(typescript@5.6.3))(typescript@5.6.3) + '@mango-scripts/utils': + specifier: ^2.0.4 + version: 2.0.4(typescript@5.6.3) '@types/node': specifier: ^20.10.5 version: 20.14.11 @@ -408,8 +411,8 @@ importers: packages/dev-scripts: dependencies: '@mango-scripts/utils': - specifier: ^2.0.4 - version: 2.0.4(typescript@5.6.3) + specifier: workspace:* + version: link:../utils npm-keyword: specifier: ^7.0.0 version: 7.0.0 @@ -501,8 +504,8 @@ importers: packages/i18n-scripts: dependencies: '@mango-scripts/utils': - specifier: ^2.0.4 - version: 2.0.4(typescript@5.6.3) + specifier: workspace:* + version: link:../utils gogocode: specifier: ^1.0.55 version: 1.0.55(vue@3.5.12(typescript@5.6.3)) @@ -522,8 +525,8 @@ importers: specifier: ^7.23.0 version: 7.26.0 '@mango-scripts/utils': - specifier: ^2.0.2 - version: 2.0.4(typescript@5.6.3) + specifier: workspace:* + version: link:../utils hyntax: specifier: ^1.1.9 version: 1.1.9 @@ -553,11 +556,11 @@ importers: specifier: ^7.24.4 version: 7.26.0 '@mango-scripts/babel-preset-mango': - specifier: ^0.0.2 - version: 0.0.2 + specifier: workspace:* + version: link:../babel-preset-mango '@mango-scripts/utils': - specifier: ^2.0.4 - version: 2.0.4(typescript@5.6.3) + specifier: workspace:* + version: link:../utils '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.5.11 version: 0.5.15(@types/webpack@5.28.5(@swc/core@1.7.39(@swc/helpers@0.5.13))(esbuild@0.20.2)(uglify-js@3.19.3))(react-refresh@0.14.2)(type-fest@4.26.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(esbuild@0.20.2)(uglify-js@3.19.3)))(webpack@5.95.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(esbuild@0.20.2)(uglify-js@3.19.3)) diff --git a/scripts/dev.mts b/scripts/dev.mts new file mode 100644 index 0000000..49b3166 --- /dev/null +++ b/scripts/dev.mts @@ -0,0 +1,52 @@ +import path from 'node:path' + +import { execa, fs } from '@mango-scripts/utils' + +export const getMonorepoPkgListInfo = async (): Promise => { + const pkgDirPathList: string[] = [] + const dirList = await fs.readdir(path.join(process.cwd(), './packages')) + for (const dir of dirList) { + const pkgDirPath = path.resolve(process.cwd(), './packages', dir) + if ((await fs.stat(pkgDirPath)).isDirectory()) { + pkgDirPathList.push(pkgDirPath) + } + } + + return pkgDirPathList +} + +const openInTerminalTab = async (command: string): Promise => { + const script = ` + iterm_path=$(mdfind "kMDItemCFBundleIdentifier == 'com.googlecode.iterm2'") + terminal_path=$(mdfind "kMDItemCFBundleIdentifier == 'com.apple.Terminal'") + + if [[ -n "$iterm_path" ]]; then + osascript -e 'tell application "iTerm" + tell current window + create tab with default profile + tell current session of current tab + write text "${command}" + end tell + end tell + end tell'; + elif [[ -n "$terminal_path" ]]; then + osascript -e 'tell application "Terminal" + do script "${command}" + activate + end tell'; + else + echo "无法找到终端Shell,请检查"; + fi + ` + + await execa('sh', ['-c', script]) +} + +const boot = async () => { + const pkgDirPathList = await getMonorepoPkgListInfo() + for (const dir of pkgDirPathList) { + await openInTerminalTab(`cd ${dir} && pnpm run dev`) + } +} + +boot()