sonamu 0.13.4 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -168,4 +168,19 @@ describe("HTTP validator registry sync 정리", () => {
168
168
  await expect(findChangedFilesUsingChecksums()).resolves.toEqual([]);
169
169
  expect(warn.mock.calls.flat().join(" ")).toContain("queries.generated.ts");
170
170
  });
171
+
172
+ it("forceSync는 lock을 지운 뒤에도 생성물을 drift로 경고하지 않는다", async () => {
173
+ const { apiRootPath, syncer } = await createTestProject(false);
174
+ await writeFile(
175
+ path.join(apiRootPath, "src/application/queries.generated.ts"),
176
+ "// generated output\n",
177
+ );
178
+ await syncer.renewChecksums();
179
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
180
+
181
+ await syncer.forceSync();
182
+
183
+ expect(warn).not.toHaveBeenCalled();
184
+ await expect(findChangedFilesUsingChecksums()).resolves.toEqual([]);
185
+ });
171
186
  });
@@ -56,6 +56,11 @@ export class Syncer {
56
56
  await SyncerActions.actionCopySharedToTargetsIfNotExists();
57
57
  await SyncerActions.actionGenerateSsrEntryServerIfNotExists();
58
58
 
59
+ // lock이 없으면 비교 기준이 없어 모든 파일이 변경으로 잡히므로, 생성물 drift를 판정할 수 없습니다.
60
+ const hasChecksumBaseline = await syncerFileExists(
61
+ path.join(Sonamu.apiRootPath, "sonamu.lock"),
62
+ );
63
+
59
64
  // 바뀐 것이 없으면 그냥 넘어가요.
60
65
  const changedFiles = await findChangedFilesUsingChecksums();
61
66
  if (changedFiles.length === 0) {
@@ -72,7 +77,7 @@ export class Syncer {
72
77
  await runWithGracefulShutdown(
73
78
  async () => {
74
79
  // 얘가 싱크 작업 수행하는 본체입니다.
75
- await this.doSyncActions(changedFiles);
80
+ await this.doSyncActions(changedFiles, { skipDriftCheck: !hasChecksumBaseline });
76
81
  await this.reconcileHttpValidatorRegistry();
77
82
 
78
83
  // 싱크 액션이 끝나면 항상 체크섬을 다시 갱신합니다.
@@ -322,9 +327,13 @@ export class Syncer {
322
327
  * 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
323
328
  *
324
329
  * @param diffFilePaths - 변경된 파일들의 절대 경로 목록
330
+ * @param options.skipDriftCheck - true이면 처리되지 않은 생성물 변경을 drift로 경고하지 않습니다.
325
331
  * @returns diffTypes - 변경된 파일의 타입 목록 (entity, types, model 등)
326
332
  */
327
- async doSyncActions(diffFilePaths: AbsolutePath[]): Promise<{ diffTypes: FileType[] }> {
333
+ async doSyncActions(
334
+ diffFilePaths: AbsolutePath[],
335
+ { skipDriftCheck = false }: { skipDriftCheck?: boolean } = {},
336
+ ): Promise<{ diffTypes: FileType[] }> {
328
337
  const diffGroups = this.calculateDiffGroups(diffFilePaths);
329
338
  const diffTypes =
330
339
  /* SAFETY: TypeScript AST 파싱과 동기화 입력 계약이 이 값의 타입을 보장한다. */ Object.keys(
@@ -371,7 +380,7 @@ export class Syncer {
371
380
  // 위에서 처리 안 된 변경들이 있다?
372
381
  // 체크섬 관리 대상이긴 한데 위에서 핸들링이 안 된 것이라면 결국 생성물입니다.
373
382
  // 이 친구들이 변경된 것은 drift로 간주해도 마땅합니다.
374
- if (unhandledChangesLeft()) {
383
+ if (!skipDriftCheck && unhandledChangesLeft()) {
375
384
  await this.handleDrifts(unhandledPaths());
376
385
  }
377
386
 
@@ -0,0 +1,43 @@
1
+ import { describe, expectTypeOf, it } from "vitest";
2
+ import { type infer as ZodInfer } from "zod";
3
+
4
+ import { type EntityJson, type EntityJsonSchema } from "./types";
5
+
6
+ declare module "./types" {
7
+ interface EntityCustom {
8
+ history?: boolean;
9
+ }
10
+ }
11
+
12
+ describe("EntityJsonSchema custom", () => {
13
+ it("declaration merging으로 정의한 확장 키만 타입에서 허용한다", () => {
14
+ type Parsed = ZodInfer<typeof EntityJsonSchema>;
15
+
16
+ expectTypeOf<Parsed>().toEqualTypeOf<EntityJson>();
17
+
18
+ const withExtension: EntityJson = {
19
+ id: "History",
20
+ title: "이력",
21
+ table: "histories",
22
+ custom: { history: true },
23
+ props: [],
24
+ indexes: [],
25
+ subsets: {},
26
+ enums: {},
27
+ };
28
+ expectTypeOf(withExtension.custom?.history).toEqualTypeOf<boolean | undefined>();
29
+
30
+ const withTypo: EntityJson = {
31
+ id: "HistoryTypo",
32
+ title: "이력 오타",
33
+ table: "history_typos",
34
+ // @ts-expect-error history 오타는 EntityCustom에 없으면 거부되어야 한다.
35
+ custom: { histroy: true },
36
+ props: [],
37
+ indexes: [],
38
+ subsets: {},
39
+ enums: {},
40
+ };
41
+ expectTypeOf(withTypo).toEqualTypeOf<EntityJson>();
42
+ });
43
+ });
@@ -515,11 +515,14 @@ export function getEnumDefValues(def: EnumDef): Record<string, string> {
515
515
  return isEnumDefWithCone(def) ? def.values : def;
516
516
  }
517
517
 
518
+ export interface EntityCustom {}
519
+
518
520
  export type EntityJson = {
519
521
  id: string;
520
522
  parentId?: string;
521
523
  table: string;
522
524
  title?: string;
525
+ custom?: EntityCustom;
523
526
  cone?: Cone; // cone 메타데이터
524
527
  props: EntityProp[];
525
528
  indexes: EntityIndex[];
@@ -1629,12 +1632,15 @@ function validateSearchTextSources(
1629
1632
  });
1630
1633
  }
1631
1634
 
1635
+ const EntityCustomSchema: z.ZodType<EntityCustom> = z.looseObject({});
1636
+
1632
1637
  const EntityJsonBaseSchema = z
1633
1638
  .object({
1634
1639
  id: z.string().describe("PascalCase로 된 Entity ID"),
1635
1640
  title: z.string().describe("Entity 이름"),
1636
1641
  table: z.string().describe("snake_case로 된 테이블명"),
1637
1642
  parentId: z.string().optional().describe("부모 Entity ID"),
1643
+ custom: EntityCustomSchema.optional(),
1638
1644
  cone: ConeSchema.optional(),
1639
1645
  props: z.array(EntityPropSchema),
1640
1646
  indexes: z.array(EntityIndexSchema),