@akanjs/devkit 3.0.0-beta.23 → 3.0.0-beta.25

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.
Files changed (60) hide show
  1. package/DEV_RUNTIME_KNOBS.md +37 -22
  2. package/akanApp/akanApp.host.ts +34 -1
  3. package/akanApp/devBootLatch.test.ts +39 -0
  4. package/akanApp/devBootLatch.ts +39 -0
  5. package/akanApp/devHostPolicy.test.ts +25 -0
  6. package/akanApp/devHostPolicy.ts +27 -2
  7. package/cloud/globalConfig.ts +3 -2
  8. package/csrE2e/csrE2eHarness.fixture.ts +455 -0
  9. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  10. package/csrE2e/csrPageStack.e2e.test.ts +187 -0
  11. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  12. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  13. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  14. package/fileSys.ts +16 -1
  15. package/frontendBuild/buildRouteClient.test.ts +61 -0
  16. package/frontendBuild/bundleDefine.test.ts +12 -0
  17. package/frontendBuild/bundleDefine.ts +5 -1
  18. package/frontendBuild/clientEntryDiscovery.test.ts +37 -0
  19. package/frontendBuild/clientEntryDiscovery.ts +10 -5
  20. package/frontendBuild/csrArtifactBuilder.ts +3 -6
  21. package/frontendBuild/csrDevArtifactWriter.test.ts +143 -0
  22. package/frontendBuild/csrDevArtifactWriter.ts +303 -0
  23. package/frontendBuild/csrDevBundler.test.ts +66 -0
  24. package/frontendBuild/csrDevBundler.ts +95 -0
  25. package/frontendBuild/csrDevModuleCompiler.ts +304 -0
  26. package/frontendBuild/csrDevPatcher.test.ts +340 -0
  27. package/frontendBuild/csrDevPatcher.ts +374 -0
  28. package/frontendBuild/csrDevPaths.ts +124 -0
  29. package/frontendBuild/csrDevResolver.test.ts +165 -0
  30. package/frontendBuild/csrDevResolver.ts +234 -0
  31. package/frontendBuild/csrDevTypes.ts +59 -0
  32. package/frontendBuild/csrEntryFiles.ts +24 -0
  33. package/frontendBuild/cssCompiler.ts +2 -4
  34. package/frontendBuild/devRegistryBundler.ts +177 -0
  35. package/frontendBuild/hmrWatcher.test.ts +51 -0
  36. package/frontendBuild/hmrWatcher.ts +17 -3
  37. package/frontendBuild/index.ts +4 -0
  38. package/frontendBuild/pagesBundleBuilder.ts +24 -1
  39. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  40. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  41. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  42. package/frontendBuild/routeClientBuilder.ts +79 -18
  43. package/frontendBuild/serverGraphFile.test.ts +77 -0
  44. package/frontendBuild/serverGraphFile.ts +93 -0
  45. package/frontendBuild/ssrDevBundler.ts +91 -0
  46. package/incrementalBuilder/buildBatch.proc.ts +119 -6
  47. package/incrementalBuilder/buildBatchProtocol.ts +6 -3
  48. package/incrementalBuilder/buildBatchRunner.test.ts +31 -0
  49. package/incrementalBuilder/buildBatchRunner.ts +17 -8
  50. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  51. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  52. package/incrementalBuilder/incrementalBuilder.host.test.ts +1 -1
  53. package/incrementalBuilder/incrementalBuilder.host.ts +33 -3
  54. package/incrementalBuilder/incrementalBuilder.proc.test.ts +129 -2
  55. package/incrementalBuilder/incrementalBuilder.proc.ts +518 -52
  56. package/integration/devStability.integration.test.ts +18 -7
  57. package/package.json +3 -3
  58. package/transforms/rscUseClientTransform.ts +7 -0
  59. package/transforms/transforms.test.ts +13 -0
  60. package/transforms/useClientBundlePlugin.ts +4 -1
@@ -0,0 +1,340 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { CSR_DEV_MANIFEST_FILE, type CsrDevManifest } from "akanjs/server/hmr/csrDevManifest";
7
+ import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
8
+ import type { App } from "../commandDecorators";
9
+ import { CsrDevArtifactWriter } from "./csrDevArtifactWriter";
10
+ import { CsrDevPatcher } from "./csrDevPatcher";
11
+ import { CsrDevPaths } from "./csrDevPaths";
12
+ import type { CsrDevContext, CsrDevGraph } from "./csrDevTypes";
13
+ import { DevRegistryBundler } from "./devRegistryBundler";
14
+
15
+ const dirs: string[] = [];
16
+ //? A rename that changes only the case is one only where the disk folds case (APFS, NTFS); ext4 keeps both names.
17
+ const foldsCase = (() => {
18
+ const probe = fs.mkdtempSync(path.join(os.tmpdir(), "akan-case-"));
19
+ try {
20
+ return fs.existsSync(path.join(path.dirname(probe), path.basename(probe).toUpperCase()));
21
+ } finally {
22
+ fs.rmSync(probe, { recursive: true, force: true });
23
+ }
24
+ })();
25
+ const context: CsrDevContext = {
26
+ pageEntries: [],
27
+ basePaths: [],
28
+ htmlBasePaths: [],
29
+ define: {},
30
+ optimizeImports: [],
31
+ configKey: "key",
32
+ refreshFile: null,
33
+ };
34
+
35
+ //? Only what an update touches before it compiles anything: a registry with no modules has nothing to patch.
36
+ const makeRegistry = async (metadata: string | undefined) => {
37
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-patcher-"));
38
+ dirs.push(dir);
39
+ const writer = new CsrDevArtifactWriter(dir, { routePrefix: "/_akan/ssr-dev/" });
40
+ const graph: CsrDevGraph = {
41
+ version: 1,
42
+ configKey: "key",
43
+ refresh: "refresh.js",
44
+ entries: {},
45
+ modules: {},
46
+ resolution: {},
47
+ pending: [],
48
+ ...(metadata ? { metadata } : {}),
49
+ };
50
+ const manifest: CsrDevManifest = { version: 1, generation: 4, appGeneration: 4, vendorFile: "v.js", entries: {} };
51
+ await writer.writeState(graph, manifest);
52
+ const stamp = { current: metadata ?? "" };
53
+ const bundler = {
54
+ context: async () => context,
55
+ writer,
56
+ paths: new CsrDevPaths(dir),
57
+ reloadsOnEntryChange: false,
58
+ isMetadataFile: isAkanRuntimeMetadataFile,
59
+ metadataFingerprint: async () => stamp.current,
60
+ writeEntries: async () => ({ files: {}, changed: [] }),
61
+ entryIds: () => ({}),
62
+ } as unknown as DevRegistryBundler;
63
+ return { bundler, stamp, dir };
64
+ };
65
+
66
+ afterAll(async () => {
67
+ await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
68
+ });
69
+
70
+ describe("CsrDevPatcher", () => {
71
+ test("hands a registry read from disk back to a whole build once a signal save moved its metadata", async () => {
72
+ const { bundler, stamp } = await makeRegistry("before");
73
+ stamp.current = "after";
74
+ const result = await new CsrDevPatcher(bundler).update([]);
75
+ expect(result).toMatchObject({ kind: "delegate", generation: 5 });
76
+ expect(result.kind === "delegate" ? result.reason : "").toContain("metadata");
77
+ });
78
+
79
+ test("patches a registry whose metadata still matches, and one built before the stamp existed is rebuilt", async () => {
80
+ const matching = await makeRegistry("same");
81
+ expect(await new CsrDevPatcher(matching.bundler).update([])).toEqual({ kind: "unchanged" });
82
+ const unstamped = await makeRegistry(undefined);
83
+ unstamped.stamp.current = "now";
84
+ expect((await new CsrDevPatcher(unstamped.bundler).update([])).kind).toBe("delegate");
85
+ });
86
+
87
+ test("a resident patcher checks the stamp once, when it reads the registry", async () => {
88
+ const { bundler, stamp } = await makeRegistry("same");
89
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
90
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
91
+ stamp.current = "moved";
92
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
93
+ patcher.forget();
94
+ expect((await patcher.update([])).kind).toBe("delegate");
95
+ });
96
+
97
+ test("a saved metadata file goes to a whole build even when the stamp matches", async () => {
98
+ const { bundler } = await makeRegistry("same");
99
+ const result = await new CsrDevPatcher(bundler).update(["/repo/apps/demo/lib/user/user.signal.ts"]);
100
+ expect(result).toMatchObject({ kind: "delegate", reason: "user.signal.ts changed" });
101
+ });
102
+
103
+ test("a whole build cut short between its writes goes to another whole build, not to a patch", async () => {
104
+ const { bundler, dir } = await makeRegistry("same");
105
+ await Bun.write(path.join(dir, ".building"), "123");
106
+ expect(await new CsrDevPatcher(bundler, { resident: true }).update([])).toMatchObject({
107
+ kind: "delegate",
108
+ reason: "the last whole build was cut short",
109
+ generation: 5,
110
+ });
111
+ });
112
+
113
+ //? Compiled for real: a move resolves through what is on disk now, which a resident process's resolver cache is not.
114
+ class MovedRegistry extends DevRegistryBundler {
115
+ readonly reloadsOnEntryChange = false;
116
+ readonly #entry: string;
117
+ constructor(root: string, entry: string) {
118
+ const app = {
119
+ workspace: { workspaceRoot: root },
120
+ cwdPath: path.join(root, "apps/demo"),
121
+ verbose: () => undefined,
122
+ };
123
+ super(app as unknown as App, { dirName: "registry", routePrefix: "/_akan/registry/", library: true });
124
+ this.#entry = entry;
125
+ }
126
+ async context() {
127
+ return context;
128
+ }
129
+ async writeEntries() {
130
+ return { files: { "": this.#entry }, changed: [] };
131
+ }
132
+ protected async rootFiles(entryFiles: string[]) {
133
+ return entryFiles;
134
+ }
135
+ override async metadataFingerprint() {
136
+ return "same";
137
+ }
138
+ wanted: Set<string> | null = null;
139
+ override async wantedRoots(files: string[], graph: CsrDevGraph) {
140
+ if (!this.wanted) return await super.wantedRoots(files, graph);
141
+ const wanted = this.wanted;
142
+ return new Set(files.filter((file) => wanted.has(file)));
143
+ }
144
+ }
145
+
146
+ const movedRegistry = async (files: Record<string, string>) => {
147
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
148
+ dirs.push(root);
149
+ const file = (relative: string) => path.join(CsrDevPaths.realpath(root), "apps/demo", relative);
150
+ for (const [relative, text] of Object.entries(files)) await Bun.write(file(relative), text);
151
+ const bundler = new MovedRegistry(root, file("entry.ts"));
152
+ expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
153
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
154
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
155
+ const app = async () => await Bun.file(path.join(bundler.outDir, "app.js")).text();
156
+ const changedIds = (result: Awaited<ReturnType<CsrDevPatcher["update"]>>) =>
157
+ result.kind === "update" ? result.update.changedIds.sort((a, b) => a.localeCompare(b)) : [];
158
+ return { bundler, patcher, file, app, changedIds };
159
+ };
160
+
161
+ test("a dependency moved into a folder or renamed recompiles its importer, so app.js defines the new file", async () => {
162
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
163
+ dirs.push(root);
164
+ const file = (relative: string) => path.join(root, "apps/demo", relative);
165
+ await Bun.write(file("ui/Foo.tsx"), 'export const foo = () => "foo";\n');
166
+ await Bun.write(file("ui/util.ts"), 'export const util = () => "util";\n');
167
+ await Bun.write(
168
+ file("ui/Page.tsx"),
169
+ 'import { foo } from "./Foo";\nimport { util } from "./util";\nexport const page = () => foo() + util();\n',
170
+ );
171
+ await Bun.write(file("entry.ts"), 'import { page } from "./ui/Page";\nexport const run = page;\n');
172
+ const bundler = new MovedRegistry(root, file("entry.ts"));
173
+ expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
174
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
175
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
176
+
177
+ await rm(file("ui/Foo.tsx"));
178
+ await Bun.write(file("ui/Foo/index.tsx"), 'export const foo = () => "moved";\n');
179
+ await rm(file("ui/util.ts"));
180
+ await Bun.write(file("ui/util.tsx"), 'export const util = () => "renamed";\n');
181
+ const moved = await patcher.update([
182
+ file("ui/Foo.tsx"),
183
+ file("ui/Foo/index.tsx"),
184
+ file("ui/util.ts"),
185
+ file("ui/util.tsx"),
186
+ ]);
187
+
188
+ expect(moved.kind).toBe("update");
189
+ expect(moved.kind === "update" ? moved.update.changedIds.sort((a, b) => a.localeCompare(b)) : []).toEqual([
190
+ "apps/demo/ui/Foo/index.tsx",
191
+ "apps/demo/ui/Page.tsx",
192
+ "apps/demo/ui/util.tsx",
193
+ ]);
194
+ const app = await Bun.file(path.join(bundler.outDir, "app.js")).text();
195
+ expect(app).toContain('__akan.define("apps/demo/ui/Foo/index.tsx"');
196
+ expect(app).toContain('__akan.define("apps/demo/ui/util.tsx"');
197
+ expect(app).not.toContain('__akan.define("apps/demo/ui/Foo.tsx"');
198
+ expect(app).not.toContain('__akan.define("apps/demo/ui/util.ts"');
199
+ });
200
+
201
+ test.skipIf(!foldsCase)(
202
+ "a rename that changes only the case moves the module to its new id, and later saves leave it alone",
203
+ async () => {
204
+ const { patcher, file, app, changedIds } = await movedRegistry({
205
+ "ui/card.tsx": 'export const card = () => "v1";\n',
206
+ "ui/Page.tsx": 'import { card } from "./Card";\nexport const page = () => card();\n',
207
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
208
+ });
209
+ fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
210
+ await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card() + "!";\n');
211
+ const renamed = await patcher.update([file("ui/card.tsx"), file("ui/Card.tsx"), file("ui/Page.tsx")]);
212
+ expect(changedIds(renamed)).toEqual(["apps/demo/ui/Card.tsx", "apps/demo/ui/Page.tsx"]);
213
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Card.tsx"');
214
+ expect(await app()).not.toContain('__akan.define("apps/demo/ui/card.tsx"');
215
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
216
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
217
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
218
+ expect(await app()).toContain('"v2"');
219
+ },
220
+ );
221
+
222
+ test("a new root that fails to compile holds no later save, and leaves pending once no route reaches it", async () => {
223
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
224
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
225
+ "ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
226
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
227
+ });
228
+ //? Named by a route, as a route build's check names the entries it asks for.
229
+ bundler.wanted = new Set([file("ui/Broken.tsx")]);
230
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
231
+
232
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
233
+ const announced: string[][] = [];
234
+ await expect(
235
+ patcher.update([file("ui/Card.tsx")], { announce: (update) => announced.push(update.changedIds) }),
236
+ ).rejects.toThrow();
237
+ expect(announced).toEqual([["apps/demo/ui/Card.tsx"]]);
238
+
239
+ bundler.wanted = new Set();
240
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v3";\n');
241
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
242
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
243
+ expect(graph.pending).toEqual([]);
244
+ });
245
+
246
+ test("a pending root whose fix needs a whole-app build goes to a worker with it, still pending", async () => {
247
+ //? Written before the first build: Bun's resolver keeps a directory's listing, so a node_modules made later is missed.
248
+ const { bundler, patcher, file } = await movedRegistry({
249
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
250
+ "ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
251
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
252
+ "../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
253
+ "../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
254
+ });
255
+ bundler.wanted = new Set([file("ui/Broken.tsx")]);
256
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
257
+ await Bun.write(file("ui/Broken.tsx"), 'import { gone } from "fake-pkg";\nexport const broken = () => gone;\n');
258
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
259
+ const result = await patcher.update([file("ui/Card.tsx"), file("ui/Broken.tsx")]);
260
+ expect(result.kind).toBe("delegate");
261
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
262
+ expect(graph.pending).toContain("apps/demo/ui/Broken.tsx");
263
+ });
264
+
265
+ test("a broken file no module imports any more stops failing saves, even brought back after its importer moved on", async () => {
266
+ const { patcher, file, changedIds } = await movedRegistry({
267
+ "ui/X.tsx": "export const x = 1;\n",
268
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
269
+ "ui/Page.tsx":
270
+ 'import { x } from "./X";\nimport { card } from "./Card";\nexport const page = () => x + card();\n',
271
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
272
+ });
273
+ await rm(file("ui/X.tsx"));
274
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
275
+ await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
276
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
277
+
278
+ await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card();\n');
279
+ const announced: string[][] = [];
280
+ await patcher
281
+ .update([file("ui/Page.tsx")], { announce: (update) => announced.push(update.changedIds) })
282
+ .catch(() => undefined);
283
+ expect(announced[0]).toContain("apps/demo/ui/Page.tsx");
284
+
285
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
286
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
287
+ });
288
+
289
+ test("by default a pending root counts as reached only while an entry or a module imports it", async () => {
290
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
291
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
292
+ "ui/Broken.tsx": "export const broken = ;\n",
293
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
294
+ });
295
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
296
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
297
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
298
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
299
+ expect(graph.pending).toEqual([]);
300
+ });
301
+
302
+ test("a module whose factory file is missing is compiled again instead of failing every read", async () => {
303
+ const { bundler, patcher, app, changedIds } = await movedRegistry({
304
+ "ui/Page.tsx": 'export const page = () => "page";\n',
305
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
306
+ });
307
+ const manifest = (await bundler.writer.readJson<CsrDevManifest>(CSR_DEV_MANIFEST_FILE)) as CsrDevManifest;
308
+ await bundler.writer.writeJson(CSR_DEV_MANIFEST_FILE, { ...manifest, generation: 2, appGeneration: 1 });
309
+ await rm(path.join(bundler.outDir, "modules"), { recursive: true, force: true });
310
+ patcher.forget();
311
+ const healed = await patcher.update([]);
312
+ expect(changedIds(healed)).toEqual(["apps/demo/entry.ts", "apps/demo/ui/Page.tsx"]);
313
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Page.tsx"');
314
+ });
315
+
316
+ test("a file created beside a folder a relative import resolved to takes that import over", async () => {
317
+ const { patcher, file, app, changedIds } = await movedRegistry({
318
+ "ui/Foo/index.tsx": 'export const foo = () => "folder";\n',
319
+ "ui/Page.tsx": 'import { foo } from "./Foo";\nexport const page = () => foo();\n',
320
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
321
+ });
322
+ await Bun.write(file("ui/Foo.tsx"), 'export const foo = () => "file";\n');
323
+ expect(changedIds(await patcher.update([file("ui/Foo.tsx")]))).toEqual([
324
+ "apps/demo/ui/Foo.tsx",
325
+ "apps/demo/ui/Page.tsx",
326
+ ]);
327
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Foo.tsx"');
328
+ });
329
+
330
+ test("a registry with no manifest is a first build, which no tab holds a module of", async () => {
331
+ const { bundler, dir } = await makeRegistry("same");
332
+ await rm(path.join(dir, CSR_DEV_MANIFEST_FILE), { force: true });
333
+ expect(await new CsrDevPatcher(bundler).update([])).toMatchObject({
334
+ kind: "delegate",
335
+ reason: "first build",
336
+ generation: 1,
337
+ first: true,
338
+ });
339
+ });
340
+ });