UNPKG

lemon-devkit

Version:

Lemon Serverless Micro-Service Platform for local development

362 lines 16.4 kB
"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.runGuardCommon = void 0; /** * `field-guard-common.ts` * - transformer spec의 `checkAllKeys`에 common field guard를 삽입/갱신한다. * * @author Claire <claire@lemoncloud.io> * @date 2026-04-27 added common field guard codemod. * @copyright (C) lemoncloud.io 2026 - All Rights Reserved. */ const fs = __importStar(require("fs")); const path = __importStar(require("path")); const ts_morph_1 = require("ts-morph"); const common_fields_1 = require("./common-fields"); const DEFAULT_PATHS = ['src/**/*.spec.ts']; const CANDIDATE_VAR_RE = /^\$(node|mock|temp)$/; const toPosix = (p) => p.split(path.sep).join('/'); const relPathOf = (cwd, abs) => toPosix(path.relative(cwd, abs)); const stripQuotes = (s) => s.replace(/^['"]|['"]$/g, ''); const renderDiff = (relPath, before, after) => { const beforeLines = before.split('\n'); const afterLines = after.split('\n'); const prefix = beforeLines.findIndex((line, i) => line !== afterLines[i]); if (prefix < 0) return ''; const commonTail = [...beforeLines] .reverse() .findIndex((line, i) => line !== afterLines[afterLines.length - 1 - i]); const tail = commonTail < 0 ? 0 : commonTail; const removed = beforeLines.slice(prefix, beforeLines.length - tail).map(line => `-${line}`); const added = afterLines.slice(prefix, afterLines.length - tail).map(line => `+${line}`); return [`--- ${relPath}`, `+++ ${relPath}`, `@@`, ...removed, ...added].join('\n'); }; const findTargetBlocks = (sf, targetName) => { const blocks = []; for (const fn of sf.getFunctions()) { if (fn.getName() !== targetName) continue; const block = fn.getBody()?.asKind(ts_morph_1.SyntaxKind.Block); if (block) blocks.push(block); } for (const decl of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) { if (decl.getNameNode().getText() !== targetName) continue; const init = decl.getInitializer(); if (!init || !ts_morph_1.Node.isArrowFunction(init)) continue; const block = init.getBody().asKind(ts_morph_1.SyntaxKind.Block); if (block) blocks.push(block); } return blocks; }; const findBaseVarFromCommons = (block) => { for (const decl of block.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) { if (decl.getNameNode().getText() !== 'commons') continue; const init = decl.getInitializer(); const match = init?.getText().match(/(\$[A-Za-z0-9_]+)\.includes\s*\(/); if (match) return match[1]; } return undefined; }; const findBaseVarFromNotInModel = (sf) => { for (const decl of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) { if (decl.getNameNode().getText() !== 'notInModel') continue; const match = decl .getInitializer() ?.getText() .match(/!\s*(\$[A-Za-z0-9_]+)\.includes\s*\(/); if (match) return match[1]; } return undefined; }; const findBaseVarDeclaration = (sf, name) => { const decls = sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration); if (name) return decls.find(decl => decl.getNameNode().getText() === name); return decls.find(decl => CANDIDATE_VAR_RE.test(decl.getNameNode().getText())); }; const includesDollarFromArrayLiteral = (node) => node .asKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression) ?.getElements() .some(el => el.getText().replace(/['"]/g, '') === '$') ?? false; const includesDollarFromInitializer = (decl) => { const init = decl?.getInitializer(); if (!init) return false; const text = init.getText(); if (/\bCORE_FIELDS\b/.test(text)) return true; const call = init.asKind(ts_morph_1.SyntaxKind.CallExpression); if (!call) return false; const args = call.getArguments(); if (args.length < 2) return false; return includesDollarFromArrayLiteral(args[1]); }; const resolveRelativeModule = (fromFile, spec) => { if (!spec.startsWith('.')) return undefined; const resolved = path.resolve(path.dirname(fromFile), spec); return path.extname(resolved) ? resolved : `${resolved}.ts`; }; const registryBindingsFor = (sf, outAbs) => { const bindings = new Set(); for (const decl of sf.getImportDeclarations()) { const resolved = resolveRelativeModule(sf.getFilePath(), decl.getModuleSpecifierValue()); if (!resolved || path.normalize(resolved) !== path.normalize(outAbs)) continue; for (const spec of decl.getNamedImports()) { if (spec.getName() === 'fieldKeys') bindings.add(spec.getAliasNode()?.getText() ?? 'fieldKeys'); } } return bindings; }; const readRegistryFields = (project, outAbs) => { const result = new Map(); if (!fs.existsSync(outAbs)) return result; const sf = project.addSourceFileAtPathIfExists(outAbs); const fieldKeys = sf ?.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration) .find(decl => decl.getNameNode().getText() === 'fieldKeys'); const init = fieldKeys?.getInitializer(); const obj = init?.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression) ?? init?.asKind(ts_morph_1.SyntaxKind.AsExpression)?.getExpression().asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression); if (!obj) return result; for (const prop of obj.getProperties()) { if (!ts_morph_1.Node.isPropertyAssignment(prop)) continue; const name = stripQuotes(prop.getName()); const arr = prop.getInitializer()?.getDescendantsOfKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression)[0]; if (!arr) continue; const fields = arr.getElements().map(el => stripQuotes(el.getText())); result.set(name, fields); } return result; }; const fieldKeyNamesFromInitializer = (decl, bindings) => { const init = decl?.getInitializer(); if (!init || bindings.size === 0) return []; return init.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression).flatMap(call => { const expr = call.getExpression(); if (!ts_morph_1.Node.isPropertyAccessExpression(expr)) return []; if (!ts_morph_1.Node.isIdentifier(expr.getExpression())) return []; if (!bindings.has(expr.getExpression().getText())) return []; return [expr.getName()]; }); }; const literalFieldsFromInitializer = (decl) => { const init = decl?.getInitializer(); if (!init) return []; return init.getDescendantsOfKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression).flatMap(arr => arr.getElements().flatMap(el => { const text = el.getText(); return /^['"].*['"]$/.test(text) ? [stripQuotes(text)] : []; })); }; const functionTextInSource = (sf, name) => { const fn = sf.getFunctions().find(x => x.getName() === name); if (fn) return fn.getText(); const decl = sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration).find(x => x.getNameNode().getText() === name); return decl?.getText(); }; const importedFunctionText = (sf, project, name) => { for (const decl of sf.getImportDeclarations()) { const spec = decl.getNamedImports().find(x => (x.getAliasNode()?.getText() ?? x.getName()) === name); if (!spec) continue; const resolved = resolveRelativeModule(sf.getFilePath(), decl.getModuleSpecifierValue()); if (!resolved || !fs.existsSync(resolved)) continue; const target = project.getSourceFile(resolved) ?? project.addSourceFileAtPathIfExists(resolved); const text = target ? functionTextInSource(target, spec.getName()) : undefined; if (text) return text; } return undefined; }; const filterFunctionExcludesId = (text) => Boolean(text && /(?:field|[_a-zA-Z][_a-zA-Z0-9]*)\s*!={1,2}\s*['"]_id['"]|['"]_id['"]\s*!={1,2}\s*(?:field|[_a-zA-Z][_a-zA-Z0-9]*)/.test(text)); const baseInitializerExcludesId = (project, sf, decl) => { const init = decl?.getInitializer(); if (!init) return false; const call = init.asKind(ts_morph_1.SyntaxKind.CallExpression); const calls = call ? [call, ...init.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)] : init.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression); return calls.some(call => { const expr = call.getExpression(); if (!ts_morph_1.Node.isIdentifier(expr)) return false; const name = expr.getText(); const text = functionTextInSource(sf, name) ?? importedFunctionText(sf, project, name); return filterFunctionExcludesId(text); }); }; const unique = (xs) => xs.reduce((L, x) => { if (!L.includes(x)) L.push(x); return L; }, []); const expectedFieldsForBaseVar = (project, sf, baseDecl, registryFields, outAbs) => { const bindings = registryBindingsFor(sf, outAbs); const fromRegistry = fieldKeyNamesFromInitializer(baseDecl, bindings).flatMap(name => registryFields.get(name) ?? []); if (fromRegistry.length === 0) return undefined; const initText = baseDecl?.getInitializer()?.getText() ?? ''; const fromBase = literalFieldsFromInitializer(baseDecl); const fromCore = /\bCORE_FIELDS\b/.test(initText) ? ['$'] : []; const fields = unique([...fromRegistry, ...fromBase, ...fromCore]); return baseInitializerExcludesId(project, sf, baseDecl) ? fields.filter(field => field !== '_id') : fields; }; const renderGuard = (varName, expected) => [ `//* 최소한, 만일 \`keys()\`가 잘 작동했다면, 공통 필드를 가지고 있어야함.`, `const commons = fields.reduce<string[]>((L, a) => {`, ` if (${varName}.includes(a)) L.push(a);`, ` return L;`, `}, []);`, `expect2(() => ${varName}?.sort((a, b) => a.length - b.length || a.localeCompare(b)).join(',')).toEqual(`, ` '${expected}',`, `);`, `expect2(() => commons?.sort((a, b) => a.length - b.length || a.localeCompare(b)).join(',')).toEqual(`, ` '${expected}',`, `);`, ].join('\n'); const guardStatements = (block, varName) => block.getStatements().filter(stmt => { const text = stmt.getText(); return (text.includes('const commons = fields.reduce') || (text.includes('expect2') && text.includes(`${varName}?.sort`) && text.includes('.toEqual(')) || (text.includes('expect2') && text.includes('commons?.sort') && text.includes('.toEqual('))); }); const firstNonGuardInsertIndex = (block) => { const statements = block.getStatements(); const idx = statements.findIndex(stmt => { const text = stmt.getText(); return text.includes('const keys = Object.keys') || text.includes('const alls = notInModel'); }); return idx < 0 ? 0 : idx; }; const upsertGuard = (block, varName, expected) => { const next = renderGuard(varName, expected); const existing = guardStatements(block, varName); if (existing.length > 0) { const before = existing.map(stmt => stmt.getText()).join('\n'); const expectedCount = before.split(`'${expected}'`).length - 1; if (before.includes('const commons = fields.reduce') && before.includes(`${varName}?.sort`) && before.includes('commons?.sort') && expectedCount === 2) { return undefined; } existing[0].replaceWithText(next); for (const dup of existing.slice(1).reverse()) dup.remove(); return before === next && existing.length === 1 ? undefined : 'updated'; } block.insertStatements(firstNonGuardInsertIndex(block), next); return 'inserted'; }; /** `checkAllKeys`의 실제 base 변수 초기화 형태를 보고 `$` 포함 여부에 맞는 literal guard를 삽입/갱신한다. */ const runGuardCommon = (opts) => { const cwd = opts.cwd ? path.resolve(opts.cwd) : path.resolve(path.dirname(opts.tsconfig)); const tsconfigPath = path.resolve(opts.tsconfig); const outAbs = path.resolve(cwd, opts.out ?? 'src/generated/field-registry.ts'); const project = new ts_morph_1.Project({ tsConfigFilePath: tsconfigPath }); const registryFields = readRegistryFields(project, outAbs); const paths = opts.paths && opts.paths.length > 0 ? opts.paths : DEFAULT_PATHS; for (const pat of paths) project.addSourceFilesAtPaths(path.isAbsolute(pat) ? pat : path.join(cwd, pat)); const scanFiles = paths.flatMap(pat => { const files = project.getSourceFiles(pat); if (files.length > 0 || path.isAbsolute(pat)) return files; return project.getSourceFiles(path.join(cwd, pat)); }); const uniqueFiles = Array.from(new Map(scanFiles.map(sf => [sf.getFilePath(), sf])).values()).filter(sf => !sf.getFilePath().includes(`${path.sep}node_modules${path.sep}`)); const originalTextByPath = new Map(uniqueFiles.map(sf => [sf.getFilePath(), sf.getFullText()])); const changedFiles = new Set(); const guards = []; for (const sf of uniqueFiles) { const relPath = relPathOf(cwd, sf.getFilePath()); const fallbackVar = findBaseVarFromNotInModel(sf); for (const block of findTargetBlocks(sf, opts.targetName)) { const varName = findBaseVarFromCommons(block) ?? fallbackVar; if (!varName) continue; const baseDecl = findBaseVarDeclaration(sf, varName); const includeDollar = includesDollarFromInitializer(baseDecl); const expectedFields = expectedFieldsForBaseVar(project, sf, baseDecl, registryFields, outAbs) ?? common_fields_1.DEFAULT_COMMON_MODEL_FIELDS; const expected = (0, common_fields_1.formatCommonFields)(expectedFields, includeDollar ? {} : { exclude: ['$'] }); const action = upsertGuard(block, varName, expected); if (!action) continue; changedFiles.add(sf.getFilePath()); guards.push({ relPath, targetName: opts.targetName, varName, expected, action }); } } const diffs = opts.diff ? Array.from(changedFiles) .map(filePath => renderDiff(relPathOf(cwd, filePath), originalTextByPath.get(filePath) ?? '', project.getSourceFileOrThrow(filePath).getFullText())) .filter(Boolean) : undefined; if (!opts.dryRun) { for (const filePath of changedFiles) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); project.getSourceFileOrThrow(filePath).saveSync(); } } return { guards, changedFiles: Array.from(changedFiles), diffs }; }; exports.runGuardCommon = runGuardCommon; //# sourceMappingURL=field-guard-common.js.map