적재 계층 load.ts
JSON 적재 + Ajv schema 검증 + 평탄화.
load.ts는 구조 검증(JSON Schema)만 합니다. 의미 검증(타입과 값의 일치, 참조 무결성, 순환 감지)은 resolve.ts에서 완료합니다.
interface LoadedTokenFile { fileName: string namespace: string // 네임스페이스 접두사(color / radius / zIndex, 빈 문자열은 최상위 키가 곧 네임스페이스) isDark: boolean defaultType?: TokenType // 파일 수준 $type description?: string flat: Map<string, RawToken> // path -> RawToken(평탄화됨, 네임스페이스 접두사 포함)}함수 목록
섹션 제목: “함수 목록”readJson<T>(filePath: string): T
섹션 제목: “readJson<T>(filePath: string): T”JSON 파일을 읽어 JSON.parse합니다. 내부 도구 함수입니다.
listJsonFiles(dir: string): string[]
섹션 제목: “listJsonFiles(dir: string): string[]”디렉터리 아래의 .json 파일을 나열합니다, schema.json은 제외, 정렬한 뒤 절대 경로로 바꿉니다. 정렬은 적재 순서를 안정적으로 보장합니다(오류 메시지와 산출물의 재현성에 영향).
namespaceFromFileName(fileName: string): string
섹션 제목: “namespaceFromFileName(fileName: string): string”파일 이름으로 네임스페이스를 유도($namespace가 없을 때의 폴백 로직): .json을 떼어내고 -로 분할, 첫 단어는 그대로, 나머지 단어의 첫 글자를 대문자로 바꾼 뒤 이어 붙입니다.
| 파일 이름 | 유도 결과 |
|---|---|
color.json |
color |
z-index.json |
zIndex |
color-dark.json |
colorDark |
flatten(obj, prefix, out, inheritedType?): void
섹션 제목: “flatten(obj, prefix, out, inheritedType?): void”재귀적으로 중첩 JSON을 평면 Map<path, RawToken>로 접습니다:
$로 시작하는 모든 키를 건너뜀(메타 속성)- 경로 이어붙이기:
prefix ? prefix + '.' + key : key - 리프도 객체도 아닌 노드 →
토큰 경로 X는 리프 토큰도 그룹도 아님을 throw
타입 상속의 핵심 로직: 그룹(파일 루트 포함)이 선언한 $type을 트리 아래로 전달하지만, 리터럴 리프에만 구워 넣습니다:
// 리터럴 토큰은 그룹 타입을 상속, ref 토큰은 undefined를 유지하고 resolve가 참조 목표에서 타입을 취함if (node.type === undefined && node.ref === undefined && groupType !== undefined) { out.set(path, { ...node, type: groupType })}ref 리프는 의도적으로 구워 넣지 않고, 타입 결정권을 resolve 단계에 넘깁니다(3계층 타입 우선순위의 첫 단계, 자세한 내용은 타입 시스템 참고).
isRawToken(node: unknown): node is RawToken
섹션 제목: “isRawToken(node: unknown): node is RawToken”타입 가드: 노드가 value 또는 ref 키를 포함하면 리프 토큰으로, 그렇지 않으면 그룹으로 봅니다.
loadTokenFiles(tokensDir = <root>/tokens): LoadedTokenFile[]
섹션 제목: “loadTokenFiles(tokensDir = <root>/tokens): LoadedTokenFile[]”토큰 디렉터리를 적재합니다:
tokens/schema.json을 읽고, Ajv(allErrors: true, strict: false)로 컴파일- 파일별로 JSON을 읽고 검증, 실패 시
토큰 파일 검증 실패: <path>와 포맷팅된 오류 목록을 throw - 네임스페이스:
raw.$namespace(문자열인 경우)가 우선, 그렇지 않으면namespaceFromFileName flatten(raw, namespace, flat)로 접기{ fileName, namespace, isDark: raw.$dark === true, defaultType: raw.$type, description: raw.$description, flat }조립
loadSpecs(specsDir = <root>/specs): ComponentSpec[]
섹션 제목: “loadSpecs(specsDir = <root>/specs): ComponentSpec[]”같은 구조로 specs/*.json을 적재하며, 검증 실패 시 컴포넌트 계약 검증 실패를 throw합니다.
loadCases(casesDir = <root>/cases): ComponentCases[]
섹션 제목: “loadCases(casesDir = <root>/cases): ComponentCases[]”같은 구조로 cases/*.json을 적재하며, 검증 실패 시 골든 케이스 검증 실패를 throw합니다.
formatErrors(errors: ErrorObject[]): string
섹션 제목: “formatErrors(errors: ErrorObject[]): string”Ajv의 오류 객체를 들여쓰기 목록으로 포맷팅합니다(instancePath + message).
readVersion(): string
섹션 제목: “readVersion(): string”package.json의 version을 읽어 각 산출물 머리 주석의 일부로 사용합니다(“소스: @lh-ui/core tokens @ v0.0.1”).
검증 계층화
섹션 제목: “검증 계층화”| 계층 | 위치 | 하는 일 |
|---|---|---|
| 구조 검증 | load.ts |
JSON Schema: 필수 필드, 열거값, 명명 정규식, additionalProperties: false |
| 의미 검증 | resolve.ts |
타입과 값의 일치(assertTypedValue), 참조 존재, 참조 순환 감지 |
| 일치성 검증 | scripts/verify-specs.mts |
spec과 cases의 교차 일치성 |
이렇게 계층화한 이유는 JSON Schema가 “ref 목표는 반드시 존재”, “타입은 값과 일치” 같은 크로스 파일, 크로스 노드 제약을 표현할 수 없기 때문입니다.
Copyright © 2024-2026 FengMo WenXue