UNPKG

lemon-devkit

Version:

Lemon Serverless Micro-Service Platform for local development

270 lines (265 loc) 11.7 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.main = void 0; /** * `lemon-fields` CLI * - `migrate`(one-shot codemod), `gen`(기본 생성기) subcommand를 제공한다. * - `--check`는 파일을 쓰지 않고 CI drift guard로 동작한다. * * @author Claire <claire@lemoncloud.io> * @date 2026-04-17 added `lemon-fields` CLI. * @copyright (C) lemoncloud.io 2026 - All Rights Reserved. */ const path = __importStar(require("path")); const field_gen_1 = require("../fields/field-gen"); const field_guard_common_1 = require("../fields/field-guard-common"); const field_migrate_1 = require("../fields/field-migrate"); const USAGE = ` lemon-fields — materialise \`fieldKeys.<name><T>()\` call sites into a committed registry. USAGE lemon-fields [gen] Regenerate src/generated/field-registry.ts (default) lemon-fields migrate One-shot codemod: rewrite legacy \`keys<T>()\` sites. lemon-fields guard-common Upsert common field expect2 guards into checkAllKeys. lemon-fields --check CI guard: fail if the generated file is out-of-date. OPTIONS (both subcommands) --tsconfig <path> default: tsconfig.json --out <path> default: src/generated/field-registry.ts --include-spec default: true (pass --no-include-spec to exclude) --paths <glob> [--paths ...] limit scan to these files GEN-ONLY --check do not write; exit 1 if out-of-date --report print generated key/source/field table --allow-legacy accept leftover \`keys<T>()\` sites mid-migration --allow-empty do not fail if no call sites are found MIGRATE-ONLY --dry-run compute changes without writing --diff print a compact source diff --allow-skips report unrewritable sites instead of failing --update-tsconfig remove ts-transformer-keys transformer plugin after full migration --report print summary table GUARD-COMMON-ONLY --target <name> default: checkAllKeys --dry-run compute changes without writing --diff print a compact source diff --report print updated guard table `.trim(); const parse = (argv) => { const [head, ...tail] = argv; const hasSubcommand = head === 'migrate' || head === 'gen' || head === 'guard-common'; const subcommand = hasSubcommand ? head : 'gen'; const tokens = hasSubcommand ? tail : argv; return { subcommand, flags: parseFlags(tokens) }; }; const requireFlagValue = (key, value) => { if (!value || value.startsWith('--')) throw new Error(`missing value for --${key}\n\n${USAGE}`); return value; }; const parseFlags = (tokens, flags = {}) => { const [tok, value, ...rest] = tokens; if (!tok) return flags; if (!tok.startsWith('--')) return parseFlags(tokens.slice(1), flags); const key = tok.slice(2); switch (key) { case 'check': case 'allow-legacy': case 'allow-empty': case 'allow-skips': case 'dry-run': case 'diff': case 'report': case 'update-tsconfig': case 'include-spec': return parseFlags(tokens.slice(1), { ...flags, [key]: true }); case 'no-include-spec': return parseFlags(tokens.slice(1), { ...flags, 'include-spec': false }); case 'tsconfig': return parseFlags(rest, { ...flags, tsconfig: requireFlagValue(key, value) }); case 'out': return parseFlags(rest, { ...flags, out: requireFlagValue(key, value) }); case 'paths': return parseFlags(rest, { ...flags, paths: [...(flags.paths ?? []), requireFlagValue(key, value)] }); case 'target': return parseFlags(rest, { ...flags, target: requireFlagValue(key, value) }); case 'help': case 'h': return parseFlags(tokens.slice(1), { ...flags, help: true }); default: throw new Error(`unknown flag: --${key}\n\n${USAGE}`); } }; const commonOpts = (flags) => ({ tsconfig: flags.tsconfig ?? 'tsconfig.json', out: flags.out ?? 'src/generated/field-registry.ts', includeSpec: flags['include-spec'] === false ? false : true, paths: flags.paths, }); const genReportLine = (entry) => { const legacy = entry.legacy ? ' [legacy]' : ''; return ` ${entry.name} ${entry.relPath}#${entry.typeArgText} fields(${entry.fields.length}): ${entry.fields.join(', ')}${legacy}\n`; }; const writeGenReport = (entries, content) => { const label = `${entries.length} generated entr${entries.length === 1 ? 'y' : 'ies'}`; // fieldRegistryMeta checksum 앞 8자를 report에 포함해 dev가 CI/runtime 값과 비교할 수 있도록 함 const checksumMatch = content.match(/"checksum":\s*"([0-9a-f]{16})"/); const checksumHint = checksumMatch ? ` checksum: ${checksumMatch[1].slice(0, 8)}…` : ''; process.stdout.write(`[lemon-fields] report — ${label}${checksumHint}:\n`); for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) { process.stdout.write(genReportLine(entry)); } }; const runGenCmd = (flags) => { const common = commonOpts(flags); const cwd = process.cwd(); const outAbs = path.resolve(cwd, common.out); const isCheck = Boolean(flags['check']); const res = (0, field_gen_1.runGen)({ ...common, allowLegacy: Boolean(flags['allow-legacy']), allowEmpty: Boolean(flags['allow-empty']), repairDuplicateNames: !isCheck, cwd, }); if (isCheck) { if (res.changed) { process.stderr.write(`[lemon-fields] registry drift detected at ${common.out}. Re-run \`lemon-fields gen\` and commit.\n`); return 1; } process.stdout.write(`[lemon-fields] ok — ${res.entries.length} entries up-to-date.\n`); return 0; } if (res.changed) (0, field_gen_1.writeRegistry)(outAbs, res.content); process.stdout.write(`[lemon-fields] gen — ${res.entries.length} entries, ${res.changed ? 'wrote' : 'no change to'} ${common.out}\n`); if (res.repairs.length > 0) { process.stdout.write(`[lemon-fields] repaired ${res.repairs.length} duplicate registry name(s):\n`); for (const r of res.repairs) { process.stdout.write(` ${r.relPath} -> fieldKeys.${r.name}<${r.typeArgText}>()\n`); } } if (flags['report']) writeGenReport(res.entries, res.content); if (res.skipped.length > 0) { process.stderr.write(`[lemon-fields] skipped ${res.skipped.length} site(s):\n`); for (const s of res.skipped) process.stderr.write(` - ${s.relPath} :: ${s.typeArgText} (${s.reason})\n`); } if (res.legacyLeftovers.length > 0) { process.stderr.write(`[lemon-fields] legacy leftovers (--allow-legacy): ${res.legacyLeftovers.length}\n`); } return 0; }; const runMigrateCmd = (flags) => { const common = commonOpts(flags); const res = (0, field_migrate_1.runMigrate)({ ...common, dryRun: Boolean(flags['dry-run']), diff: Boolean(flags['diff']), allowSkips: Boolean(flags['allow-skips']), updateTsconfig: Boolean(flags['update-tsconfig']), cwd: process.cwd(), }); const mode = flags['dry-run'] ? '[dry-run] ' : ''; process.stdout.write(`[lemon-fields] ${mode}migrate — rewrote ${res.rewrites.length} site(s), ` + `${res.changedFiles.length} file(s)${res.wroteStub ? '; wrote bootstrap stub' : ''}.\n`); if (flags['report']) { for (const r of res.rewrites) process.stdout.write(` ${r.relPath} keys<${r.typeArgText}>() -> fieldKeys.${r.name}<${r.typeArgText}>()\n`); } if (res.diffs?.length) { process.stdout.write(`${res.diffs.join('\n')}\n`); } if (res.updatedTsconfig) { process.stdout.write(`[lemon-fields] ${flags['dry-run'] ? 'would update' : 'updated'} tsconfig: removed ts-transformer-keys transformer plugin.\n`); } if (res.skipped.length > 0) { process.stderr.write(`[lemon-fields] skipped ${res.skipped.length} site(s):\n`); for (const s of res.skipped) process.stderr.write(` - ${s.relPath} :: keys<${s.typeArgText}>() — ${s.reason}\n`); } return 0; }; const runGuardCommonCmd = (flags) => { const common = commonOpts(flags); const res = (0, field_guard_common_1.runGuardCommon)({ tsconfig: common.tsconfig, out: common.out, paths: common.paths, targetName: flags.target ?? 'checkAllKeys', dryRun: Boolean(flags['dry-run']), diff: Boolean(flags['diff']), cwd: process.cwd(), }); const mode = flags['dry-run'] ? '[dry-run] ' : ''; process.stdout.write(`[lemon-fields] ${mode}guard-common — ${res.guards.length} guard(s), ` + `${res.changedFiles.length} file(s)${flags['dry-run'] ? '' : ' updated'}.\n`); if (flags['report']) { for (const g of res.guards) { process.stdout.write(` ${g.relPath} ${g.targetName} ${g.varName} ${g.action}: ${g.expected}\n`); } } if (res.diffs?.length) process.stdout.write(`${res.diffs.join('\n')}\n`); return 0; }; const main = (argv) => { try { const { subcommand, flags } = parse(argv); if (flags['help']) { process.stdout.write(`${USAGE}\n`); return 0; } if (subcommand === 'migrate') return runMigrateCmd(flags); if (subcommand === 'guard-common') return runGuardCommonCmd(flags); return runGenCmd(flags); } catch (err) { const msg = err instanceof Error ? err.message : String(err); process.stderr.write(`[lemon-fields] ERROR: ${msg}\n`); return 1; } }; exports.main = main; //* 직접 실행된 경우에만 CLI main을 수행한다. (test import 시에는 실행하지 않음) if (require.main === module) { process.exit((0, exports.main)(process.argv.slice(2))); } //# sourceMappingURL=lemon-fields.js.map