작성자: Claire claire@lemoncloud.io
lemon-fields는 ts-transformer-keys의 keys<T>()를 generated registry 방식으로 옮기는 도구이다.
기존 방식:
import { keys } from 'ts-transformer-keys';
export const USER_FIELDS = keys<UserModel>();변경 후:
import { fieldKeys } from './generated/field-registry';
export const USER_FIELDS = fieldKeys.userModel<UserModel>();migrate: 기존keys<T>()호출을fieldKeys.<name><T>()로 바꾼다.gen: TypeScript checker로<T>의 field 목록을 읽고src/generated/field-registry.ts를 만든다.--check: generated registry가 최신인지 CI에서 확인한다.guard-common: transformer spec의checkAllKeys안에 공통 모델 필드 검증을 삽입/갱신한다.- runtime에서는 transformer 없이 generated TypeScript 파일만 사용한다.
npm i --save-dev lemon-devkitnpx lemon-fields migrate --dry-run --report --update-tsconfig이 명령은 source를 쓰지 않는다. 어떤 keys<T>()가 어떤 fieldKeys.<name><T>()로 바뀔지만 확인한다.
npx lemon-fields migrate --report --update-tsconfig이 단계에서 기존 코드가 아래처럼 바뀐다.
// before
export const USER_FIELDS = keys<UserModel>();
// after
export const USER_FIELDS = fieldKeys.userModel<UserModel>();--update-tsconfig를 함께 쓰면 migration 완료 후 ts-transformer-keys/transformer plugin도 제거한다.
npx lemon-fields gen --report생성되는 파일은 기본적으로 아래 위치이다.
src/generated/field-registry.ts생성 결과는 이런 형태이다.
export const fieldKeys = {
userModel: <T extends object>() =>
["id", "name"] as Array<Extract<keyof T, string>>,
} as const;npx lemon-fields --checkgenerated registry가 source와 다르면 exit code 1로 실패한다. CI에서는 이 명령을 쓰면 된다.
npx lemon-fields guard-common --report이 명령은 src/**/*.spec.ts에서 checkAllKeys를 찾고, 공통필드 검증 블록이 없으면 자동으로 넣는다.
fieldKeys base는 registry 기준으로 맞춘다.
개발자는 보통 test:watch 앞에 붙여두면 된다.
{
"scripts": {
"test:watch": "lemon-fields guard-common && LS=1 jest --config=jest.config.json --watchAll"
}
}처음 실행하면 필요한 spec 파일이 업데이트된다.
[lemon-fields] guard-common — 3 guard(s), 3 file(s) updated.이미 guard가 있으면 아무 파일도 고치지 않고 바로 test watch로 넘어간다.
[lemon-fields] guard-common — 0 guard(s), 0 file(s) updated.$ 포함 여부는 spec의 base field 변수에서 자동 판별한다. CORE_FIELDS를 쓰면 $를 포함하고, ['meta']처럼 $가 없는 base를 쓰면 $를 제외한다. _id 필터도 감지한다.
{
"scripts": {
"fields:migrate": "lemon-fields migrate --report --update-tsconfig",
"fields:gen": "lemon-fields gen --report",
"fields:check": "lemon-fields --check",
"fields:guard-common": "lemon-fields guard-common --report",
"test:watch": "lemon-fields guard-common && LS=1 jest --config=jest.config.json --watchAll",
"build": "npm run fields:gen && tsc"
}
}src/generated/field-registry.ts는 source와 함께 commit 하는 것을 권장한다. 이 파일은 build 산출물처럼 보이지만, 실제 source가 import해서 사용하는 코드이다.
새로운 field list가 필요하면 개발자가 직접 fieldKeys.<name><T>() 호출을 추가한다.
import { fieldKeys } from '../generated/field-registry';
export const USER_FIELDS = fieldKeys.userModel<UserModel>();여기서 userModel이 registry key이다. gen은 이 이름을 다시 계산하지 않고 그대로 보존한다.
이름을 바꾸고 싶으면 generated 파일을 고치지 않는다. 호출 위치의 key를 바꾸고 다시 gen을 실행한다.
export const USER_FIELDS = fieldKeys.publicUser<UserModel>();npx lemon-fields gen아래 코드에는 이름이 두 개 있다.
export const USER_FIELDS = fieldKeys.userModel<UserModel>();
// ^^^^^^^^^^^ ^^^^^^^^^
// TypeScript export 이름 lemon-fields registry keyUSER_FIELDS는 TypeScript module export 이름이다. 파일이 다르면 같은 이름을 써도 바로 충돌하지 않는다.
// src/a.ts
export const USER_FIELDS = fieldKeys.userModel<UserModel>();
// src/b.ts
export const USER_FIELDS = fieldKeys.userModel<UserModel>();다만 한 파일에서 둘 다 import 하거나 barrel export로 묶으면 TypeScript 일반 규칙에 따라 alias가 필요할 수 있다.
import { USER_FIELDS as A_USER_FIELDS } from './a';
import { USER_FIELDS as B_USER_FIELDS } from './b';fieldKeys.userModel의 userModel은 generated registry의 key이다. 서로 다른 파일의 UserModel이 이름만 같아도 field 목록이 다르면 gen이 실패한다.
// src/a.ts
interface UserModel {
id: string;
name: string;
}
export const USER_FIELDS = fieldKeys.userModel<UserModel>();
// src/b.ts
interface UserModel {
id: string;
email: string;
}
export const USER_FIELDS = fieldKeys.userModel<UserModel>();이 경우 userModel key가 서로 다른 field 목록을 가리키므로 모호하다.
duplicate registry name `userModel` with divergent field sets:
- src/a.ts :: UserModel -> [id, name]
- src/b.ts :: UserModel -> [id, email]
Hand-edit one of the call sites to a distinct name.호출 위치의 key를 직접 분리한다.
// src/a.ts
export const USER_FIELDS = fieldKeys.adminUserModel<UserModel>();
// src/b.ts
export const USER_FIELDS = fieldKeys.customerUserModel<UserModel>();같은 key가 같은 field 목록을 가리키는 것은 허용한다. 같은 field list를 여러 파일에서 같은 이름으로 재사용하는 경우는 모호하지 않기 때문이다.
아래는 lemon-templates-api에 local tarball을 설치하고 실제로 실행한 결과를 줄인 예시이다.
$ npm ls lemon-devkit typescript --depth=0
lemon-templates-api@0.26.215
├── lemon-devkit@0.0.9
└── typescript@4.7.4lemon-devkit만 교체되고 consumer project의 TypeScript 4.7.4는 유지되었다.
$ ./node_modules/.bin/lemon-fields migrate --dry-run --report --update-tsconfig
[lemon-fields] [dry-run] migrate — rewrote 15 site(s), 9 file(s).
src/cores/abstract-services.spec.ts keys<TestModel>() -> fieldKeys.testModel<TestModel>()
src/cores/abstract-services.ts keys<CoreModel>() -> fieldKeys.coreModel<CoreModel>()
src/view/transformer.spec.ts keys<Model>() -> fieldKeys.viewTransformerModel<Model>()
src/modules/callback/model.ts keys<CallbackHead>() -> fieldKeys.callbackHead<CallbackHead>()
src/modules/callback/model.ts keys<CallbackModel>() -> fieldKeys.callbackModel<CallbackModel>()
src/modules/mock/model.ts keys<MockModel>() -> fieldKeys.mockModel<MockModel>()
src/modules/mock/model.ts keys<TestModel>() -> fieldKeys.mockTestModel<TestModel>()
src/modules/search/model.ts keys<SearchModel>() -> fieldKeys.searchModel<SearchModel>()
src/modules/search/model.ts keys<SearchQueryModel>() -> fieldKeys.searchQueryModel<SearchQueryModel>()
src/modules/search/model.ts keys<SearchBatchModel>() -> fieldKeys.searchBatchModel<SearchBatchModel>()
[lemon-fields] would update tsconfig: removed ts-transformer-keys transformer plugin.전체 결과는 15개 호출, 9개 파일이었다. 위 예시는 핵심 위치만 발췌했다.
src/modules/mock/model.ts의 MockModel은 mockModel로 생성된다. Model처럼 일반적인 이름도 path context가 이미 model로 끝나면 mockModelModel처럼 중복 suffix를 만들지 않는다.
$ ./node_modules/.bin/lemon-fields migrate --report --update-tsconfig
[lemon-fields] migrate — rewrote 15 site(s), 9 file(s); wrote bootstrap stub.
[lemon-fields] updated tsconfig: removed ts-transformer-keys transformer plugin.$ ./node_modules/.bin/lemon-fields gen --report
[lemon-fields] gen — 15 entries, wrote src/generated/field-registry.ts
[lemon-fields] report — 15 generated entries:
callbackHead src/modules/callback/model.ts#CallbackHead fields(3): id, stereo, name
callbackModel src/modules/callback/model.ts#CallbackModel fields(33): id, stereo, name, domain, clientIp, userAgent, state, parentId, parent$, no, target, qid, handler, result, requestedAt, responsedAt, $request, meta, hidden, optional, $, ns, type, sid, uid, gid, lock, next, createdAt, updatedAt, deletedAt, error, _id
coreModel src/cores/abstract-services.ts#CoreModel fields(16): $, ns, type, stereo, sid, uid, gid, lock, next, meta, createdAt, updatedAt, deletedAt, error, id, _id
mockModel src/modules/mock/model.ts#MockModel fields(18): name, aliasId, meta, id, $, ns, type, stereo, sid, uid, gid, lock, next, createdAt, updatedAt, deletedAt, error, _id
mockTestModel src/modules/mock/model.ts#TestModel fields(23): name, count, extra, Model, $identity, $, ns, type, stereo, sid, uid, gid, lock, next, meta, createdAt, updatedAt, deletedAt, error, id, _id, _idx, _date
searchModel src/modules/search/model.ts#SearchModel fields(17): id, name, $, ns, type, stereo, sid, uid, gid, lock, next, meta, createdAt, updatedAt, deletedAt, error, _id
testModel src/cores/abstract-services.spec.ts#TestModel fields(21): name, test, extra, Model, $identity, $, ns, type, stereo, sid, uid, gid, lock, next, meta, createdAt, updatedAt, deletedAt, error, id, _idgen --report는 generated registry를 열어보지 않아도 key, source 위치, type, field 목록을 바로 확인하기 위한 옵션이다.
출력이 길면 필요한 key만 확인하면 된다.
$ ./node_modules/.bin/lemon-fields gen --report | grep mockModel
mockModel src/modules/mock/model.ts#MockModel fields(18): name, aliasId, meta, id, $, ns, type, stereo, sid, uid, gid, lock, next, createdAt, updatedAt, deletedAt, error, _id$ ./node_modules/.bin/lemon-fields --check
[lemon-fields] ok — 15 entries up-to-date.
$ npm run build-ts
> lemon-templates-api@0.26.215 build-ts
> ttsc위 project에서는 migration 후 build-ts가 통과했다. 일부 service spec 실패가 있었지만, field migration과 직접 관련된 transformer/mock/callback/view spec은 통과했다.
$ ./node_modules/.bin/lemon-fields guard-common --dry-run --diff --report
[lemon-fields] [dry-run] guard-common — 3 guard(s), 3 file(s).
src/view/transformer.spec.ts checkAllKeys $mock inserted: id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt
src/modules/callback/transformer.spec.ts checkAllKeys $temp inserted: id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt
src/modules/mock/transformer.spec.ts checkAllKeys $mock inserted: id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt실제 적용:
$ ./node_modules/.bin/lemon-fields guard-common --report
[lemon-fields] guard-common — 3 guard(s), 3 file(s) updated.
$ ./node_modules/.bin/lemon-fields guard-common --report
[lemon-fields] guard-common — 0 guard(s), 0 file(s) updated.삽입되는 guard는 기존 notInModel, keys, alls, return 로직을 바꾸지 않고 checkAllKeys 맨 앞에 들어간다.
const commons = fields.reduce<string[]>((L, a) => {
if ($mock.includes(a)) L.push(a);
return L;
}, []);
expect2(() => $mock?.sort((a, b) => a.length - b.length || a.localeCompare(b)).join(',')).toEqual(
'id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt',
);
expect2(() => commons?.sort((a, b) => a.length - b.length || a.localeCompare(b)).join(',')).toEqual(
'id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt',
);공통 옵션:
--tsconfig <path>: 기본값tsconfig.json--out <path>: 기본값src/generated/field-registry.ts--include-spec: spec 파일 포함. 기본값 true--no-include-spec: spec 파일 제외--paths <glob>: scan 대상 파일만 제한. 타입 해석 context는 전체 project를 유지
gen 옵션:
--check: 파일을 쓰지 않고 drift 여부만 검사--report: 생성된 registry key, source 위치, field 목록 출력--allow-legacy: migration 중 남은keys<T>()를 임시로 허용--allow-empty: 호출 위치가 없어도 실패하지 않고 bootstrap stub 생성
migrate 옵션:
--dry-run: source/stub을 쓰지 않고 결과만 계산--diff: 변경될 source diff 출력. 보통--dry-run과 함께 사용--allow-skips: 자동 처리 불가 위치를 실패 대신 skipped로 보고--update-tsconfig: 전체 migration 완료 후ts-transformer-keys/transformerplugin 제거--report: rewrite 결과 요약 출력
guard-common 옵션:
--dry-run: source를 쓰지 않고 삽입/갱신 결과만 계산--diff: 변경될 source diff 출력. 보통--dry-run과 함께 사용--paths <glob>: 특정 spec 파일만 대상으로 제한--target <name>: 기본값checkAllKeys--report: 삽입/갱신된 guard 위치와 expected 문자열 출력--out <path>: registry 기준 field 확인
guard-common은 개발자가 test:watch를 실행할 때 transformer spec의 공통필드 검증을 자동으로 맞춰주는 명령이다.
기본 사용법:
npx lemon-fields guard-common --report보통은 package.json의 test:watch 앞에 붙인다.
{
"scripts": {
"test:watch": "lemon-fields guard-common && LS=1 jest --config=jest.config.json --watchAll"
}
}이렇게 해두면 동작은 단순하다.
- guard가 없으면 최초 1회 spec 파일에 자동 삽입
- guard가 이미 맞으면 no-op
- expected가 다르면 자동 갱신
- 그 다음 기존
jest --watchAll실행
개발자가 $ 포함 여부를 직접 맞출 필요는 없다. guard-common이 현재 spec의 base field 변수를 보고 expected 문자열을 고른다.
fieldKeys base는 registry field를 사용한다.
예를 들어 CORE_FIELDS를 base로 쓰는 경우는 $를 포함한다.
const $node = filterFields(keys<Model>(), CORE_FIELDS);삽입되는 expected:
$,id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt반대로 ['meta']처럼 $가 없는 base를 쓰면 $를 제외한다.
const $mock = filterFields(keys<Model>(), ['meta']);
const $temp = filterFields(keys<Model>(), ['meta']);삽입되는 expected:
id,ns,gid,sid,uid,lock,meta,next,type,error,stereo,createdAt,deletedAt,updatedAt즉 repo마다 $ 포함 여부를 사람이 맞추지 않아도 된다.
filterFields(... _id 제외)도 반영한다.
watch 시작 전에 한 번 실행하면 된다.
{
"scripts": {
"test:watch": "lemon-fields guard-common && LS=1 jest --config=jest.config.json --watchAll"
}
}처음 실행하면 필요한 spec 파일을 고친다.
[lemon-fields] guard-common — 8 guard(s), 8 file(s) updated.이후 실행은 no-op이다.
[lemon-fields] guard-common — 0 guard(s), 0 file(s) updated.src/generated/field-registry.ts는 source와 함께 commit 한다.
이 파일이 없으면 아래 import가 깨진다.
import { fieldKeys } from './generated/field-registry';CI에서는 아래 명령으로 최신 상태인지 확인한다.
npm run fields:check아래 코드의 T는 실제 모델이 아니라 함수의 generic parameter이다.
import { keys } from 'ts-transformer-keys';
export function fieldsOf<T>() {
return keys<T>();
}lemon-fields는 build 전에 concrete field 목록을 파일로 써야 한다. 그런데 위 코드의 T는 호출될 때마다 달라질 수 있어서 field 목록을 확정할 수 없다.
이런 코드는 모델별 concrete 호출로 바꾼다.
export const USER_FIELDS = fieldKeys.userModel<UserModel>();
export const POST_FIELDS = fieldKeys.postModel<PostModel>();자동 처리할 수 없는 위치를 보고만 받고 싶다면 아래 옵션을 쓴다.
npx lemon-fields migrate --allow-skips --report기본적으로 gen은 legacy keys<T>()가 남아 있으면 실패한다.
import { keys } from 'ts-transformer-keys';
export const USER_FIELDS = keys<UserModel>();마이그레이션을 한 번에 끝내기 어려울 때만 임시로 허용할 수 있다.
npx lemon-fields gen --allow-legacy다만 이 모드는 남아있는 keys<T>()의 이름을 그때그때 derive 한다. 최종 상태에서는 source에 이름이 고정되는 아래 형태가 더 안전하다.
import { fieldKeys } from './generated/field-registry';
export const USER_FIELDS = fieldKeys.userModel<UserModel>();ts-transformer-keys와 맞추기 위해 property가 0개인 타입은 skipped로 버리지 않고 빈 배열로 materialise 한다.
export const EMPTY = fieldKeys.empty<{}>();
export const UNION = fieldKeys.union<A | B>();
export const RECORD = fieldKeys.record<Record<string, unknown>>();위와 같은 호출은 모두 generated registry에 []로 기록된다.
실패로 보는 경우는 property가 0개인 상황이 아니라, TypeScript checker가 property 이름을 안정적으로 materialise 하지 못한 경우이다.
| 케이스 이름 | 케이스 간단 설명 | error / throw 여부 | 예시 코드 |
|---|---|---|---|
| legacy leftover | 아직 keys<T>()가 남아 있으면 gen이 실패한다. 먼저 migrate를 돌린다. |
기본값: throw--allow-legacy: 허용 |
const FIELDS = keys<User>(); |
| divergent registry key | 같은 fieldKeys.<name>이 서로 다른 field set을 가리키면 실패한다. 이름을 분리해야 한다. |
throw |
fieldKeys.userModel<User>();fieldKeys.userModel<UserSummary>(); |
| materialise failure | checker가 타입 property를 확정하지 못하면 실패한다. 타입을 더 구체적으로 만든다. | throw |
fieldKeys.userModel<T>(); |
| empty scan | scan 범위에 callsite가 하나도 없으면 실패한다. --paths와 tsconfig를 확인한다. |
기본값: throw--allow-empty: 허용 |
npx lemon-fields gen --paths 'src/user/**/*.ts' |
partial generation by --paths |
실패는 아니지만 일부 key만 생성된다. 최종 생성/CI에는 쓰지 않는다. | throw 없음 |
npx lemon-fields gen --paths 'src/user/**/*.ts' |
| scan blind spot | alias/wrapper 호출은 누락될 수 있다. 직접 호출만 안전하다. | 보통 throw 없음전체 누락 시 empty scan throw 가능 |
const fk = fieldKeys;fk.userModel<User>(); |
직접 호출만 안정적으로 scan 된다.
import { fieldKeys } from './generated/field-registry';
export const USER_FIELDS = fieldKeys.userModel<UserModel>();다음 케이스들은 실패가 아니라 정상 동작이며 []로 생성된다.
| 케이스 이름 | 케이스 간단 설명 | error / throw 여부 | 예시 코드 |
|---|---|---|---|
| empty object | property가 0개인 타입은 실패가 아니라 []로 생성된다. |
throw 없음 |
fieldKeys.empty<{}>() |
| union | 결과 property가 0개면 실패가 아니라 []로 생성된다. |
throw 없음 |
`fieldKeys.union<A |
| record | key가 동적인 record도 실패가 아니라 []로 생성된다. |
throw 없음 |
fieldKeys.record<Record<string, unknown>>() |
--paths는 “어느 파일에서 fieldKeys.<name>() 호출을 찾을지”만 제한한다.
npx lemon-fields gen --paths 'src/modules/user/**/*.ts'타입 해석에는 imported model/source가 계속 필요하다.
import { UserModel } from '../../models/user-model';
export const USER_FIELDS = fieldKeys.userModel<UserModel>();위 코드를 스캔하려면 src/modules/user/**/*.ts만 지정해도 된다. 하지만 UserModel이 정의된 파일은 tsconfig.json의 include 안에 있어야 한다.
{
"include": ["src/**/*"]
}즉 --paths는 작업 범위를 줄이는 옵션이고, TypeScript project 자체를 줄이는 옵션은 아니다.
ts-transformer-keys 호출이 모두 fieldKeys.<name>()로 바뀌면 transformer plugin은 더 이상 필요하지 않다.
{
"compilerOptions": {
"plugins": [
{
"transform": "ts-transformer-keys/transformer"
}
]
}
}전체 migration을 끝낸 뒤 아래처럼 정리한다.
npx lemon-fields migrate --update-tsconfig --report--update-tsconfig는 --paths와 함께 쓰지 않는다. 일부 파일만 migration 한 상태에서 transformer plugin을 제거하면 아직 남아 있는 keys<T>() 호출이 깨질 수 있기 때문이다.
lemon-fields는 consumer project의 TypeScript checker를 사용한다.
lemon-devkit은 TypeScript >=4.7 <6 범위를 허용하므로, 기존 project가 쓰던 TypeScript version을 가능하면 그대로 유지한다.
쉽게 말해, 검사는 4번 한다.
| 단계 | 도구 | 무엇을 잡나 |
|---|---|---|
| Dev | gen --report |
생성 결과를 사람이 눈으로 확인 |
| CI | gen --check |
source는 바뀌었는데 gen을 안 돌린 상태 |
| Runtime | assertFieldRegistry |
이미 생성된 registry 파일 손상/누락 |
| Spec | guard-common |
transformer spec의 common field guard 누락 |
짧게 정리하면:
gen --report: 사람이 눈으로 확인gen --check: CI가gen누락을 자동으로 차단assertFieldRegistry: 실행 중에 generated file 손상 여부 확인guard-common: spec의checkAllKeys에 공통필드 검증이 없으면 자동 삽입
쉽게 말해, generated registry 파일이 안 깨졌는지 실행 초기에 한 번 확인하는 기능이다.
여기서 "앱 시작"은 보통 아래를 뜻한다.
- 일반 Node 서버:
app.listen()전 - Lambda: cold start 시점, 즉
handler바깥
처음 켤 때:
lemon-fields gen을 한 번 다시 실행해야 한다. 예전 형식 registry에는fieldRegistryMeta가 없어META_MISSING이 날 수 있다.
assertFieldRegistry는 위 초기화 구간에서 한 번만 호출한다.
import { fieldKeys, fieldRegistryMeta } from './generated/field-registry';
import { assertFieldRegistry } from 'lemon-devkit';
// 서버 시작 전 / Lambda cold start 시점에 1번
assertFieldRegistry({ fieldKeys, fieldRegistryMeta });fieldRegistryMeta 안의 핵심 값은 아래 3개다.
kind: 이 파일이 임시 stub인지, 실제 생성 완료본인지entryCount:fieldKeys안에 entry가 몇 개인지checksum: entry 이름과 field 목록 내용이 안 바뀌었는지
이 검사가 잡는 것:
- bootstrap stub을 그대로 commit한 경우
fieldRegistryMeta가 없거나 깨진 경우- generated file을 손으로 수정한 경우
- entry 함수가 깨진 경우
이 검사가 못 잡는 것은 하나다:
- source를 바꿨는데
lemon-fields gen을 다시 안 돌린 경우
이 경우는 CI의 gen --check가 잡는다.
예시 에러:
field registry validation failed:
[CHECKSUM_MISMATCH] registry checksum does not match meta.checksum자주 보게 될 코드는 아래 4개다.
| Code | 의미 |
|---|---|
META_MISSING |
fieldRegistryMeta가 없거나 shape가 잘못됨 |
BOOTSTRAP_STUB |
아직 임시 stub 파일 상태라 gen이 더 필요함 |
ENTRY_COUNT_MISMATCH |
entry 개수가 맞지 않음 |
CHECKSUM_MISMATCH |
entry 내용이 손상되었거나 수동 수정됨 |
그 외 코드:
UNSUPPORTED_SCHEMA: registry 형식 버전이 맞지 않음NON_FUNCTION_ENTRY: entry가 함수가 아님ENTRY_EVAL_FAILED: entry 함수 실행 중 에러INVALID_FIELD_LIST: entry가string[]를 반환하지 않음EMPTY_CONCRETE_REGISTRY: concrete registry인데 entry가 0개
참고:
- field 하나만 손으로 지우면 보통
CHECKSUM_MISMATCH - entry 하나를 통째로 지우면
ENTRY_COUNT_MISMATCH와CHECKSUM_MISMATCH가 같이 날 수 있다
문제 목록만 받고 싶으면 validateFieldRegistry()를 쓰고, 문제 있으면 바로 실패시키고 싶으면 assertFieldRegistry()를 쓴다.
import { validateFieldRegistry } from 'lemon-devkit';
const result = validateFieldRegistry({ fieldKeys, fieldRegistryMeta });
if (!result.ok) {
console.log(result.issues);
}예전 방식은 build할 때 특별한 변환기가 필요했다.
const FIELDS = keys<User>();이 코드는 build 중에 transformer가 keys<User>()를 실제 field 목록으로 바꿔줘야 한다. 그래서 plain tsc만으로는 부족했고 ttsc 같은 도구가 필요했다.
지금 방식은 gen이 결과를 미리 generated file로 만들어 둔다.
const FIELDS = fieldKeys.user<User>();쉽게 말해:
- 예전: build할 때 field 목록을 계산
- 지금:
gen할 때 한 번 계산해서 파일로 저장
그래서 migration이 끝나면 runtime과 build에서는 plain tsc로 충분하다.
| 단계 | 코드 모습 | 추가 도구 | 한 줄 설명 |
|---|---|---|---|
| Before migration | keys<User>() |
ttsc 필요 |
아직 예전 방식 |
| During migration | keys<User>() + fieldKeys.user<User>() 혼용 |
ttsc 유지 |
옮기는 중 |
| After migration | fieldKeys.user<User>() + generated registry |
불필요 | plain tsc만 쓰면 됨 |
다음 4개가 모두 맞으면 ttsc·ttypescript·ts-transformer-keys/transformer를 제거해도 된다.
- 모든
keys<T>()호출이fieldKeys.<name><T>()로 바뀌었다 lemon-fields gen으로 concrete registry를 commit했다lemon-fields migrate --update-tsconfig로 transformer plugin을 tsconfig에서 제거했다- 프로젝트에 다른 custom transformer가 남아 있지 않다
예를 들어, repo 전체에서 keys< 검색 결과가 더 이상 없고 gen 결과도 커밋했다면 제거할 준비가 된 것이다.
compilerOptions.plugins만 넣으면 왜 안 되나?
tsc는 여기 있는 plugins를 IDE용으로만 보고, 실제 build 때 transformer를 실행하지 않는다. 그래서 keys<T>()가 변환되지 않아 보통 빈 배열이 남는다.
예:
const FIELDS = keys<User>(); // 기대: ['id', 'name'], 실제: []--update-tsconfig 후에도 ttypescript가 필요한 경우는?
다른 custom transformer를 아직 쓰고 있으면 필요할 수 있다.
예: 같은 tsconfig에서 ts-nameof를 계속 쓰는 프로젝트
--allow-legacy는 왜 임시 옵션인가?
--allow-legacy는 migration 중에만 잠깐 쓰는 옵션이다. 이 상태에서는 이름이 임시로 만들어져 registry가 흔들릴 수 있다.
예: 같은 field 목록인데 호출 위치에 따라 다른 key 이름이 생길 수 있다.
아래 핵심 동작은 자동 테스트로 확인했다.
- CLI 기본 동작:
--help, 잘못된 옵션 처리 - migration 동작:
migrate --report,--dry-run,--diff,--update-tsconfig - generation 동작:
gen --report,gen --check, output directory 생성 - scan 규칙: generated registry 재스캔 방지,
--paths는 scan 대상만 줄임 - 이름 규칙: 이름 충돌 감지,
ModelModel같은 중복 suffix 방지 - 빈 타입 처리: property가 0개인 타입은 실패 대신
[]생성 - meta/validation:
fieldRegistryMeta, checksum, bootstrap meta 생성 - runtime validator:
assertFieldRegistry와validateFieldRegistry의 오류 검출 - common field guard: registry base,
$,_id필터를 반영하고checkAllKeysguard 삽입/갱신
어느 파일을 봐야 할지 빠르게 찾고 싶다면 아래만 보면 된다.
types.ts: 공통 타입 모음. 옵션/반환값 shape가 궁금할 때validate.ts: runtime validator.META_MISSING같은 오류 흐름을 보고 싶을 때field-derive-name.ts: registry key 이름 규칙. 자동 이름이 왜 그렇게 붙었는지 볼 때field-migrate.ts:keys<T>()를fieldKeys.<name><T>()로 바꾸는 로직field-gen.ts: scan 후 concrete registry를 만드는 로직field-guard-common.ts:checkAllKeys의 common field guard를 삽입/갱신하는 로직../bin/lemon-fields.ts: CLI 시작점.gen,migrate,--check,--report처리를 볼 때