/** * 统一改产品版本号:各端 package.json + 小程序兜底 + API health + HQ 最低版本占位。 * * 用法: * node scripts/set-version.mjs 4.0.19 * node scripts/set-version.mjs v4.0.19 * node scripts/set-version.mjs 4.0.19 --dry-run * pnpm set-version -- 4.0.19 * * 不改内部库(packages/domain、shared-types、weixin-sdk、client-logging 仍为 0.1.0)。 * 加 --include-libs 才会一并改这些库。 */ import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const LIB_PACKAGES = new Set([ 'packages/domain/package.json', 'packages/shared-types/package.json', 'packages/weixin-sdk/package.json', 'packages/client-logging/package.json', ]); const SOURCE_TARGETS = [ { file: 'apps/mini-user/src/lib/client-version.ts', label: '小程序版本兜底', pattern: /(\|\| ')(\d+\.\d+\.\d+)(')/, }, { file: 'server/dukang-api/src/modules/health/health.controller.ts', label: 'API /health version', pattern: /(version:\s*')(\d+\.\d+\.\d+)(')/, }, { file: 'server/dukang-api/src/common/system-config/system-config.registry.ts', label: 'HQ 小程序最低版本占位', pattern: /(placeholder:\s*')(\d+\.\d+\.\d+)(')/, }, ]; function fail(message) { console.error(message); process.exit(1); } function parseArgs(argv) { const flags = new Set(); const rest = []; for (const a of argv) { if (a === '--dry-run' || a === '-n') flags.add('dryRun'); else if (a === '--include-libs') flags.add('includeLibs'); else if (a === '--help' || a === '-h') flags.add('help'); else if (a.startsWith('-')) { fail(`未知参数:${a}\n用法:node scripts/set-version.mjs [--dry-run] [--include-libs]`); } else rest.push(a); } return { flags, versionRaw: rest[0] }; } function normalizeVersion(raw) { const v = String(raw || '').trim().replace(/^v/i, ''); if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(v)) { fail(`版本号须为 semver,例如 4.0.19 或 v4.0.19,收到:${raw || '(空)'}`); } return v; } function walkPackageJson(dir, out = []) { for (const name of readdirSync(dir)) { if (name === 'node_modules' || name === 'dist' || name === '.git') continue; const full = join(dir, name); let st; try { st = statSync(full); } catch { continue; } if (st.isDirectory()) walkPackageJson(full, out); else if (name === 'package.json') out.push(full); } return out; } function rel(file) { return relative(root, file).replaceAll('\\', '/'); } function printHelp() { console.log(`统一改产品版本号 用法: node scripts/set-version.mjs [--dry-run] [--include-libs] pnpm set-version -- 示例: node scripts/set-version.mjs 4.0.19 node scripts/set-version.mjs v4.0.19 --dry-run 会改: - 各端 / API / shared-ui 的 package.json version - 小程序 client-version.ts 兜底 - API GET /health 的 version - HQ「小程序最低版本」placeholder 默认不改内部库 0.1.0(domain / shared-types / weixin-sdk / client-logging)。`); } function main() { const { flags, versionRaw } = parseArgs(process.argv.slice(2)); if (flags.has('help') || !versionRaw) { printHelp(); process.exit(versionRaw ? 0 : 1); } const version = normalizeVersion(versionRaw); const dryRun = flags.has('dryRun'); const includeLibs = flags.has('includeLibs'); const changes = []; for (const file of walkPackageJson(root)) { const key = rel(file); if (key === 'package.json') continue; if (!includeLibs && LIB_PACKAGES.has(key)) continue; const raw = readFileSync(file, 'utf8'); let pkg; try { pkg = JSON.parse(raw); } catch { fail(`无法解析 ${key}`); } if (typeof pkg.version !== 'string') continue; if (pkg.version === version) { changes.push({ file: key, from: pkg.version, to: version, changed: false }); continue; } const next = { ...pkg, version }; const text = `${JSON.stringify(next, null, 2)}\n`; if (!dryRun) writeFileSync(file, text); changes.push({ file: key, from: pkg.version, to: version, changed: true }); } for (const target of SOURCE_TARGETS) { const file = join(root, target.file); const raw = readFileSync(file, 'utf8'); const match = raw.match(target.pattern); if (!match) { changes.push({ file: target.file, from: '(未匹配到 semver)', to: version, changed: false, note: target.label, }); continue; } const from = match[2]; if (from === version) { changes.push({ file: target.file, from, to: version, changed: false, note: target.label }); continue; } const next = raw.replace(target.pattern, `$1${version}$3`); if (!dryRun) writeFileSync(file, next); changes.push({ file: target.file, from, to: version, changed: true, note: target.label }); } const updated = changes.filter((c) => c.changed); const skipped = changes.filter((c) => !c.changed); console.log(`${dryRun ? '[dry-run] ' : ''}目标版本 ${version}`); for (const c of updated) { console.log(` 改 ${c.file}${c.note ? ` (${c.note})` : ''} ${c.from} → ${c.to}`); } for (const c of skipped) { console.log(` 跳过 ${c.file}${c.note ? ` (${c.note})` : ''} 已是 ${c.from}`); } console.log(updated.length ? `共 ${updated.length} 处将写入。` : '没有需要修改的文件。'); } main();