@cosmicdrift/kumiko-dev-server 0.248.0 → 0.249.0

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.
@@ -26,7 +26,7 @@ import {
26
26
  formatServerBuildResult,
27
27
  readExtraRuntimeExternals,
28
28
  } from "../src/build";
29
- import { runCodegen } from "../src/codegen";
29
+ import { formatScanWarning, runCodegen } from "../src/codegen";
30
30
 
31
31
  const explicit = process.argv[2];
32
32
  const cwd = explicit ? resolve(process.cwd(), explicit) : process.cwd();
@@ -61,7 +61,7 @@ try {
61
61
  if (cgResult.warnings.length > 0) {
62
62
  for (const w of cgResult.warnings) {
63
63
  // biome-ignore lint/suspicious/noConsole: CLI-Output
64
- console.warn(`${yellow}!${reset} [codegen] ${w.file}:${w.line} — ${w.reason}`);
64
+ console.warn(`${yellow}!${reset} [codegen] ${formatScanWarning(w)}`);
65
65
  }
66
66
  }
67
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.248.0",
3
+ "version": "0.249.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -59,9 +59,9 @@
59
59
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
60
60
  },
61
61
  "dependencies": {
62
- "@cosmicdrift/kumiko-bundled-features": "0.248.0",
63
- "@cosmicdrift/kumiko-framework": "0.248.0",
64
- "@cosmicdrift/kumiko-server-runtime": "0.248.0",
62
+ "@cosmicdrift/kumiko-bundled-features": "0.249.0",
63
+ "@cosmicdrift/kumiko-framework": "0.249.0",
64
+ "@cosmicdrift/kumiko-server-runtime": "0.249.0",
65
65
  "ts-morph": "^28.0.0"
66
66
  },
67
67
  "publishConfig": {
@@ -57,7 +57,8 @@ export const myFeature = defineFeature("myFeat", (r) => {
57
57
  `,
58
58
  );
59
59
 
60
- const result = runCodegen({ appRoot });
60
+ // handlerQns: [] opts out of the #2757 manifest-fallback warning — out of scope here.
61
+ const result = runCodegen({ appRoot, handlerQns: [] });
61
62
 
62
63
  expect(result.eventCount).toBe(1);
63
64
  expect(result.warnings).toEqual([]);
@@ -122,7 +123,7 @@ export const myFeature = defineFeature("inlineFeat", (r) => {
122
123
  `,
123
124
  );
124
125
 
125
- const result = runCodegen({ appRoot });
126
+ const result = runCodegen({ appRoot, handlerQns: [] });
126
127
  expect(result.eventCount).toBe(1);
127
128
  expect(result.warnings).toEqual([]);
128
129
  expect(result.didWriteSchemas).toBe(true);
@@ -169,7 +170,7 @@ export const myFeature = defineFeature("billing", (r) => {
169
170
  `,
170
171
  );
171
172
 
172
- const result = runCodegen({ appRoot });
173
+ const result = runCodegen({ appRoot, handlerQns: [] });
173
174
  expect(result.eventCount).toBe(2);
174
175
  expect(result.warnings).toEqual([]);
175
176
 
@@ -201,7 +202,7 @@ export const myFeature = defineFeature("billing", (r) => {
201
202
  `,
202
203
  );
203
204
 
204
- const result = runCodegen({ appRoot });
205
+ const result = runCodegen({ appRoot, handlerQns: [] });
205
206
  expect(result.eventCount).toBe(1);
206
207
  expect(result.warnings).toEqual([]);
207
208
  expect(result.didWriteSchemas).toBe(true);
@@ -522,4 +523,143 @@ export default defineFeature("pkgjson", (r) => {
522
523
  },
523
524
  });
524
525
  });
526
+
527
+ // #2757 — `kumiko-build` runs `runCodegen({ appRoot })` with no
528
+ // `handlerQns`. Without a `feature-manifest.json` that falls back to
529
+ // an empty list, which used to mean "render types.generated.d.ts and
530
+ // define.ts as if there were no write handlers at all" — silently
531
+ // deleting an already-checked-in WriteHandlerQn union / TypedDispatcher
532
+ // that a previous dev-server run had generated from the real registry.
533
+ describe("preserving the handler-derived block when handlerQns is omitted (#2757)", () => {
534
+ function writeSingleEventFeature(appRoot: string): void {
535
+ write(
536
+ appRoot,
537
+ "src/feature/events.ts",
538
+ `import { z } from "zod";\nexport const sSchema = z.object({ id: z.string() });\n`,
539
+ );
540
+ write(
541
+ appRoot,
542
+ "src/feature/feature.ts",
543
+ `import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
544
+ import { sSchema } from "./events";
545
+ export default defineFeature("app", (r) => {
546
+ r.defineEvent("only", sSchema);
547
+ });
548
+ `,
549
+ );
550
+ }
551
+
552
+ test("a block generated with handlerQns survives a later run without handlerQns", () => {
553
+ const appRoot = makeAppDir();
554
+ writeSingleEventFeature(appRoot);
555
+
556
+ runCodegen({ appRoot, handlerQns: ["app:write:create"] });
557
+ const typesPath = join(appRoot, ".kumiko", "types.generated.d.ts");
558
+ const definePath = join(appRoot, ".kumiko", "define.ts");
559
+ expect(readFileSync(typesPath, "utf-8")).toContain('| "app:write:create"');
560
+ expect(readFileSync(definePath, "utf-8")).toContain("createTypedDispatcher");
561
+
562
+ // No feature-manifest.json in appRoot — this is the kumiko-build path.
563
+ const result = runCodegen({ appRoot });
564
+
565
+ expect(readFileSync(typesPath, "utf-8")).toContain('| "app:write:create"');
566
+ expect(readFileSync(definePath, "utf-8")).toContain("createTypedDispatcher");
567
+ expect(result.warnings.some((w) => w.message.includes("feature-manifest.json"))).toBe(true);
568
+ // Round-trip is byte-identical, not just "still contains the QN" — writeIfChanged
569
+ // must see no diff once the preserved block is reattached.
570
+ expect(result.didWriteTypes).toBe(false);
571
+ expect(result.didWriteDefine).toBe(false);
572
+ });
573
+
574
+ test("a block from a pre-#2757 file (no marker) survives a run without handlerQns", () => {
575
+ const appRoot = makeAppDir();
576
+ writeSingleEventFeature(appRoot);
577
+ // Legacy shape: what the released renderer wrote before markers existed.
578
+ write(
579
+ appRoot,
580
+ ".kumiko/types.generated.d.ts",
581
+ `export {};\n\nexport type WriteHandlerQn =\n | "app:write:legacy"\n;\n`,
582
+ );
583
+ write(
584
+ appRoot,
585
+ ".kumiko/define.ts",
586
+ `export function defineQueryHandler() {}\n\nimport type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";\nexport function createTypedDispatcher() {}\n`,
587
+ );
588
+
589
+ const result = runCodegen({ appRoot });
590
+
591
+ expect(readFileSync(join(appRoot, ".kumiko", "types.generated.d.ts"), "utf-8")).toContain(
592
+ '| "app:write:legacy"',
593
+ );
594
+ expect(readFileSync(join(appRoot, ".kumiko", "define.ts"), "utf-8")).toContain(
595
+ "createTypedDispatcher",
596
+ );
597
+ expect(result.warnings.some((w) => w.message.includes("feature-manifest.json"))).toBe(true);
598
+ });
599
+
600
+ test("no handlerQns and no prior generated file → no block, no crash", () => {
601
+ const appRoot = makeAppDir();
602
+ writeSingleEventFeature(appRoot);
603
+
604
+ const result = runCodegen({ appRoot });
605
+
606
+ const typesPath = join(appRoot, ".kumiko", "types.generated.d.ts");
607
+ const definePath = join(appRoot, ".kumiko", "define.ts");
608
+ expect(readFileSync(typesPath, "utf-8")).not.toContain("WriteHandlerQn");
609
+ expect(readFileSync(definePath, "utf-8")).not.toContain("TypedDispatcher");
610
+ expect(result.skipped).toBe(false);
611
+ });
612
+
613
+ test("a preserved block is not duplicated across repeated runs without handlerQns", () => {
614
+ const appRoot = makeAppDir();
615
+ writeSingleEventFeature(appRoot);
616
+
617
+ runCodegen({ appRoot, handlerQns: ["app:write:create"] });
618
+ runCodegen({ appRoot });
619
+ runCodegen({ appRoot });
620
+
621
+ const typesPath = join(appRoot, ".kumiko", "types.generated.d.ts");
622
+ const definePath = join(appRoot, ".kumiko", "define.ts");
623
+ const typesOccurrences =
624
+ readFileSync(typesPath, "utf-8").split("app:write:create").length - 1;
625
+ const defineOccurrences =
626
+ readFileSync(definePath, "utf-8").split("createTypedDispatcher").length - 1;
627
+ expect(typesOccurrences).toBe(1);
628
+ expect(defineOccurrences).toBe(1);
629
+ });
630
+
631
+ test("a later run with handlerQns overwrites a preserved stale block", () => {
632
+ const appRoot = makeAppDir();
633
+ writeSingleEventFeature(appRoot);
634
+
635
+ runCodegen({ appRoot, handlerQns: ["app:write:create"] });
636
+ runCodegen({ appRoot }); // preserves app:write:create
637
+ runCodegen({ appRoot, handlerQns: ["app:write:update"] });
638
+
639
+ const typesPath = join(appRoot, ".kumiko", "types.generated.d.ts");
640
+ const typesContent = readFileSync(typesPath, "utf-8");
641
+ expect(typesContent).toContain('| "app:write:update"');
642
+ expect(typesContent).not.toContain("app:write:create");
643
+ });
644
+
645
+ test("the missing-manifest warning appears only when handlerQns is omitted", () => {
646
+ const appRoot = makeAppDir();
647
+ writeSingleEventFeature(appRoot);
648
+
649
+ const withoutOption = runCodegen({ appRoot });
650
+ expect(withoutOption.warnings.some((w) => w.message.includes("feature-manifest.json"))).toBe(
651
+ true,
652
+ );
653
+
654
+ const explicitEmpty = runCodegen({ appRoot, handlerQns: [] });
655
+ expect(explicitEmpty.warnings.some((w) => w.message.includes("feature-manifest.json"))).toBe(
656
+ false,
657
+ );
658
+
659
+ const explicitList = runCodegen({ appRoot, handlerQns: ["app:write:create"] });
660
+ expect(explicitList.warnings.some((w) => w.message.includes("feature-manifest.json"))).toBe(
661
+ false,
662
+ );
663
+ });
664
+ });
525
665
  });
@@ -6,6 +6,7 @@ export {
6
6
  } from "./render";
7
7
  export { type CodegenOptions, type CodegenResult, runCodegen } from "./run-codegen";
8
8
  export {
9
+ formatScanWarning,
9
10
  qualifiedNameToConstName,
10
11
  rewriteImportPath,
11
12
  type ScannedEvent,
@@ -26,11 +26,24 @@ import { basename, relative } from "node:path";
26
26
  import type { ScannedEvent } from "./scan-events";
27
27
  import { rewriteImportPath } from "./scan-events";
28
28
 
29
+ /**
30
+ * Start-of-block markers for the two handler-derived sections that
31
+ * `run-codegen.ts` may need to carry forward verbatim from a previous
32
+ * run (when the current run has no `handlerQns` to render a fresh
33
+ * block from). Each marker is always the last thing its renderer
34
+ * emits, so a caller can locate it in a previously-written file and
35
+ * slice from the marker to EOF to recover the whole block.
36
+ */
37
+ export const WRITE_HANDLER_QN_MARKER =
38
+ "// --- WriteHandlerQn (generated from live registry / feature-manifest.json) ---";
39
+ export const TYPED_DISPATCHER_MARKER =
40
+ "// --- TypedDispatcher (generated from live registry / feature-manifest.json) ---";
41
+
29
42
  const HEADER = [
30
43
  "// =====================================================================",
31
44
  "// AUTO-GENERATED — DO NOT EDIT BY HAND",
32
- "// Run `bun kumiko codegen` to regenerate (or rely on the dev-server's",
33
- "// file-watcher, which calls it on every r.defineEvent change).",
45
+ "// Regenerated by `bun run build` (kumiko-build) or by the dev-server's",
46
+ "// file-watcher, which runs codegen on every r.defineEvent change.",
34
47
  "// =====================================================================",
35
48
  ].join("\n");
36
49
 
@@ -129,8 +142,9 @@ export function renderInlineSchemasFile(
129
142
  "// Schema extracts purely for type inference: this file is referenced",
130
143
  "// from types.generated.d.ts via `import type`. ts-strip elides it at",
131
144
  "// build time, so there is NO runtime duplication of the inline schemas",
132
- "// in feature files. When an event schema changes: re-run `bun kumiko",
133
- "// codegen` otherwise the z.infer type drifts from the runtime schema.",
145
+ "// in feature files. When an event schema changes: rebuild (`bun run",
146
+ "// build`) or let the dev-server regenerate it otherwise the z.infer",
147
+ "// type drifts from the runtime schema.",
134
148
  "",
135
149
  `import { z } from "zod";`,
136
150
  "",
@@ -174,6 +188,7 @@ export function renderDefineFile(handlerQns: readonly string[] = []): string {
174
188
  handlerQns.length > 0
175
189
  ? [
176
190
  "",
191
+ TYPED_DISPATCHER_MARKER,
177
192
  `import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`,
178
193
  `import type { WriteHandlerQn } from "./types.generated";`,
179
194
  "",
@@ -282,5 +297,7 @@ export function renderWriteHandlerTypes(handlerQns: readonly string[]): string {
282
297
  if (handlerQns.length === 0) return "";
283
298
 
284
299
  const lines = handlerQns.map((qn) => ` | "${qn}"`);
285
- return ["", `export type WriteHandlerQn =`, ...lines, ";", ""].join("\n");
300
+ return ["", WRITE_HANDLER_QN_MARKER, `export type WriteHandlerQn =`, ...lines, ";", ""].join(
301
+ "\n",
302
+ );
286
303
  }
@@ -1,6 +1,6 @@
1
- // runCodegen — Top-Level Entry-Point. Wird vom Dev-Server (auf Boot),
2
- // vom Build-Step (vor Bundle) und von der CLI (`bun kumiko codegen`)
3
- // aufgerufen.
1
+ // runCodegen — top-level entry point. Called by the dev-server (on boot,
2
+ // then on every watched file-change) and by `kumiko-build` (before
3
+ // bundling).
4
4
  //
5
5
  // Lifecycle:
6
6
  // 1. Scan `<appRoot>/src/**` nach r.defineEvent.
@@ -21,6 +21,8 @@ import {
21
21
  renderInlineSchemasFile,
22
22
  renderTypesAugmentation,
23
23
  renderWriteHandlerTypes,
24
+ TYPED_DISPATCHER_MARKER,
25
+ WRITE_HANDLER_QN_MARKER,
24
26
  } from "./render";
25
27
  import { type ScanWarning, scanEvents } from "./scan-events";
26
28
 
@@ -77,7 +79,15 @@ export function runCodegen(opts: CodegenOptions): CodegenResult {
77
79
  mkdirSync(outputDir, { recursive: true });
78
80
 
79
81
  const typesContent = renderTypesAugmentation(scan.events, outputDir);
82
+ // `opts.handlerQns` undefined means the caller (kumiko-build) has no
83
+ // live feature registry to draw from and relies on the manifest
84
+ // fallback. That fallback can't tell "no handlers exist" apart from
85
+ // "the manifest doesn't exist" — both come back as an empty array —
86
+ // so an empty result here is a known-lossy read, not authoritative
87
+ // data the way run-dev-app's live registry is.
88
+ const usingManifestFallback = opts.handlerQns === undefined;
80
89
  const handlerQns = opts.handlerQns ?? readHandlerQnsFromManifest(opts.appRoot);
90
+ const hasHandlerQns = handlerQns.length > 0;
81
91
  const defineContent = renderDefineFile(handlerQns);
82
92
  const schemasContent = renderInlineSchemasFile(scan.events, opts.appRoot);
83
93
  // package.json — turns `.kumiko/` into a real installable package
@@ -91,21 +101,53 @@ export function runCodegen(opts: CodegenOptions): CodegenResult {
91
101
  // WriteHandlerQn-Union an den types-Content anhängen (optional). Der
92
102
  // Dev-Server übergibt handlerQns aus der lebenden Registry (genauer);
93
103
  // der CLI-Fallback liest das feature-manifest.json falls vorhanden.
94
- const finalTypesContent =
95
- handlerQns.length > 0 ? `${typesContent}${renderWriteHandlerTypes(handlerQns)}` : typesContent;
104
+ //
105
+ // Preserve only applies to the manifest-fallback path: a caller that
106
+ // explicitly passes `handlerQns: []` (the dev-server, when an app
107
+ // truly registers zero write handlers) means it — that's real data,
108
+ // not a missing-manifest gap, so it clears the block like before.
109
+ // Only the manifest fallback coming back empty is ambiguous enough to
110
+ // warrant carrying the previous block forward instead of dropping it.
111
+ const shouldPreserveStaleBlock = !hasHandlerQns && usingManifestFallback;
112
+ const finalTypesContent = shouldPreserveStaleBlock
113
+ ? withPreservedBlock(
114
+ typesContent,
115
+ typesPath,
116
+ WRITE_HANDLER_QN_MARKER,
117
+ LEGACY_WRITE_HANDLER_QN_ANCHOR,
118
+ )
119
+ : `${typesContent}${renderWriteHandlerTypes(handlerQns)}`;
120
+ const finalDefineContent = shouldPreserveStaleBlock
121
+ ? withPreservedBlock(
122
+ defineContent,
123
+ definePath,
124
+ TYPED_DISPATCHER_MARKER,
125
+ LEGACY_TYPED_DISPATCHER_ANCHOR,
126
+ )
127
+ : defineContent;
96
128
 
97
129
  const didWriteTypes = writeIfChanged(typesPath, finalTypesContent);
98
- const didWriteDefine = writeIfChanged(definePath, defineContent);
130
+ const didWriteDefine = writeIfChanged(definePath, finalDefineContent);
99
131
  writeIfChanged(packageJsonPath, packageJsonContent);
100
132
  const didWriteSchemas =
101
133
  schemasContent !== undefined
102
134
  ? writeIfChanged(schemasPath, schemasContent)
103
135
  : removeIfExists(schemasPath);
104
136
 
137
+ const warnings: readonly ScanWarning[] = shouldPreserveStaleBlock
138
+ ? [
139
+ ...scan.warnings,
140
+ {
141
+ message:
142
+ "No feature-manifest.json found — the WriteHandlerQn/TypedDispatcher block could not be determined from here; run the dev-server to get the exact handler list.",
143
+ },
144
+ ]
145
+ : scan.warnings;
146
+
105
147
  return {
106
148
  outputDir,
107
149
  eventCount: scan.events.length,
108
- warnings: scan.warnings,
150
+ warnings,
109
151
  didWriteTypes,
110
152
  didWriteSchemas,
111
153
  didWriteDefine,
@@ -151,17 +193,55 @@ function renderKumikoPackageJson(): string {
151
193
  * happen on every mtime change).
152
194
  */
153
195
  function writeIfChanged(path: string, content: string): boolean {
154
- let existing: string | undefined;
155
- try {
156
- existing = readFileSync(path, "utf-8");
157
- } catch {
158
- existing = undefined;
159
- }
196
+ const existing = readExistingOrUndefined(path);
160
197
  if (existing === content) return false;
161
198
  writeFileSync(path, content, "utf-8");
162
199
  return true;
163
200
  }
164
201
 
202
+ function readExistingOrUndefined(path: string): string | undefined {
203
+ try {
204
+ return readFileSync(path, "utf-8");
205
+ } catch {
206
+ return undefined;
207
+ }
208
+ }
209
+
210
+ // Pre-marker renderer output has no WRITE_HANDLER_QN_MARKER/TYPED_DISPATCHER_MARKER
211
+ // line to find — a file generated before this fix still needs to round-trip.
212
+ // Each anchor is the first line of its block under the old renderer, so slicing
213
+ // from the preceding newline recovers the same block the marker path recovers.
214
+ const LEGACY_WRITE_HANDLER_QN_ANCHOR = `export type WriteHandlerQn =`;
215
+ const LEGACY_TYPED_DISPATCHER_ANCHOR = `import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`;
216
+
217
+ /**
218
+ * Appends whichever handler-derived block a previous run left behind
219
+ * in `existingPath`, found via its start-of-block `marker` (or, for a
220
+ * file generated before markers existed, `legacyAnchor`). Used when
221
+ * the current run has no `handlerQns` of its own — the block can't be
222
+ * re-derived, so the only options are "delete it" (the #2757 bug) or
223
+ * "leave it exactly as it was". The marker is always emitted as the
224
+ * last thing its renderer produces, so slicing from it to EOF recovers
225
+ * the whole block regardless of how many times this preserve-path runs
226
+ * in a row.
227
+ */
228
+ function withPreservedBlock(
229
+ freshContent: string,
230
+ existingPath: string,
231
+ marker: string,
232
+ legacyAnchor: string,
233
+ ): string {
234
+ const existing = readExistingOrUndefined(existingPath);
235
+ const preserved = extractBlock(existing, marker) ?? extractBlock(existing, legacyAnchor);
236
+ return preserved !== undefined ? `${freshContent}${preserved}` : freshContent;
237
+ }
238
+
239
+ function extractBlock(content: string | undefined, anchor: string): string | undefined {
240
+ if (content === undefined) return undefined;
241
+ const idx = content.indexOf(`\n${anchor}`);
242
+ return idx === -1 ? undefined : content.slice(idx);
243
+ }
244
+
165
245
  /**
166
246
  * Remove a previously-generated file when the latest scan no longer
167
247
  * produces it (e.g. the last inline-schema was refactored to a named
@@ -91,8 +91,10 @@ export type SchemaSource =
91
91
  };
92
92
 
93
93
  export type ScanWarning = {
94
- readonly file: string;
95
- readonly line: number;
94
+ /** Absent for warnings that aren't tied to a scanned source line
95
+ * (e.g. a codegen-level warning about a missing manifest file). */
96
+ readonly file?: string;
97
+ readonly line?: number;
96
98
  readonly message: string;
97
99
  };
98
100
 
@@ -101,6 +103,13 @@ export type ScanResult = {
101
103
  readonly warnings: readonly ScanWarning[];
102
104
  };
103
105
 
106
+ /** Renders a `ScanWarning` for terminal output; omits the location prefix when the warning has no source line. */
107
+ export function formatScanWarning(warning: ScanWarning): string {
108
+ return warning.file !== undefined
109
+ ? `${warning.file}:${warning.line} — ${warning.message}`
110
+ : warning.message;
111
+ }
112
+
104
113
  export type ScanOptions = {
105
114
  /** App-Wurzel — alles unter `<root>/src` wird gescannt. Tests +
106
115
  * generated-files (`.kumiko`, `dist*`, `node_modules`) sind raus. */
@@ -17,6 +17,7 @@
17
17
  import { type FSWatcher, watch } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { runCodegen } from "./run-codegen";
20
+ import { formatScanWarning } from "./scan-events";
20
21
 
21
22
  export type WatchOptions = {
22
23
  /** App-Wurzel — gleiche Bedeutung wie für `runCodegen`. */
@@ -74,7 +75,7 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
74
75
  if (result.warnings.length > 0) {
75
76
  for (const w of result.warnings) {
76
77
  // biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
77
- console.warn(`[codegen] ${w.file}:${w.line} — ${w.message}`);
78
+ console.warn(`[codegen] ${formatScanWarning(w)}`);
78
79
  }
79
80
  }
80
81
  }