lemon-devkit
Version:
Lemon Serverless Micro-Service Platform for local development
270 lines (265 loc) • 11.7 kB
JavaScript
;
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