@akanjs/devkit 3.0.0-beta.25 → 3.0.0-beta.27
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.
- package/DEV_RUNTIME_KNOBS.md +9 -6
- package/akanApp/akanApp.host.ts +79 -22
- package/akanApp/devHostPolicy.test.ts +56 -11
- package/akanApp/devHostPolicy.ts +10 -10
- package/csrE2e/csrE2eHarness.fixture.ts +55 -5
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/frontendBuild/clientEntryDiscovery.test.ts +75 -0
- package/frontendBuild/clientEntryDiscovery.ts +112 -70
- package/frontendBuild/csrDevArtifactWriter.test.ts +8 -0
- package/frontendBuild/csrDevArtifactWriter.ts +4 -0
- package/frontendBuild/csrDevBundler.ts +1 -1
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +261 -0
- package/frontendBuild/csrDevModuleCompiler.ts +95 -20
- package/frontendBuild/csrDevPatcher.test.ts +234 -2
- package/frontendBuild/csrDevPatcher.ts +184 -10
- package/frontendBuild/csrDevPaths.ts +32 -11
- package/frontendBuild/csrDevResolver.test.ts +76 -2
- package/frontendBuild/csrDevResolver.ts +141 -12
- package/frontendBuild/csrDevTypes.ts +2 -0
- package/frontendBuild/devRegistryBundler.ts +18 -3
- package/frontendBuild/ssrDevBundler.ts +2 -8
- package/incrementalBuilder/incrementalBuilder.host.test.ts +34 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -3
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +62 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +58 -24
- package/integration/devStability.integration.test.ts +9 -2
- package/mobile/nativeApp.test.ts +15 -0
- package/mobile/nativeApp.ts +54 -14
- package/mobile/nativeAppLine.test.ts +62 -0
- package/mobile/nativeAppLine.ts +63 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { BunPlugin } from "bun";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
|
+
import { CsrDevFactoryCheck } from "./csrDevFactoryCheck";
|
|
4
5
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
5
6
|
import { CsrDevResolver } from "./csrDevResolver";
|
|
6
7
|
import type { CsrDevCompiledModule, CsrDevContext, CsrDevSharedHelpers } from "./csrDevTypes";
|
|
@@ -82,6 +83,7 @@ export class CsrDevModuleCompiler {
|
|
|
82
83
|
while (frontier.length > 0) {
|
|
83
84
|
const refusedVendors = refuseNewVendor ? frontier.filter((file) => CsrDevPaths.isVendorFile(file)) : [];
|
|
84
85
|
if (refusedVendors.length > 0) return { modules: [...compiled.values()], refusedVendors, unresolved: [] };
|
|
86
|
+
await this.#resolver.cover(frontier);
|
|
85
87
|
const round = await this.#compileRound(frontier);
|
|
86
88
|
if (round.misses.length > 0 && !this.#resolver.prepassDone && refusePrepass)
|
|
87
89
|
return { modules: [...compiled.values()], refusedVendors: [], unresolved: round.misses };
|
|
@@ -138,21 +140,29 @@ export class CsrDevModuleCompiler {
|
|
|
138
140
|
),
|
|
139
141
|
);
|
|
140
142
|
const misses: string[] = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
143
|
+
let result: Awaited<ReturnType<typeof Bun.build>>;
|
|
144
|
+
try {
|
|
145
|
+
result = await Bun.build({
|
|
146
|
+
entrypoints: files,
|
|
147
|
+
root: this.#paths.root,
|
|
148
|
+
target: "browser",
|
|
149
|
+
format: "cjs",
|
|
150
|
+
reactFastRefresh: !vendor,
|
|
151
|
+
sourcemap: vendor ? "none" : "external",
|
|
152
|
+
naming: { entry: "[dir]/[name].[ext]", asset: "assets/[name]-[hash].[ext]" },
|
|
153
|
+
publicPath: this.#routePrefix,
|
|
154
|
+
metafile: true,
|
|
155
|
+
env: "AKAN_PUBLIC_*",
|
|
156
|
+
//? A factory runs as a classic script, where `import.meta` is a SyntaxError that takes the whole vendor file
|
|
157
|
+
//? down, and Bun's cjs output keeps `import.meta.env` and spells `import.meta.url` as this disk's `file://` path.
|
|
158
|
+
define: { ...this.#context.define, "import.meta": "__akanMeta" },
|
|
159
|
+
optimizeImports: this.#context.optimizeImports,
|
|
160
|
+
plugins: [this.#registryPlugin(vendor, misses)],
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (vendor && error instanceof AggregateError) return await this.#withoutFailed(files, error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
156
166
|
const { outputs } = result.metafile as unknown as OutputMetafile;
|
|
157
167
|
const sourceMaps = new Map(
|
|
158
168
|
result.outputs.filter((artifact) => artifact.kind === "sourcemap").map((artifact) => [artifact.path, artifact]),
|
|
@@ -177,12 +187,20 @@ export class CsrDevModuleCompiler {
|
|
|
177
187
|
}
|
|
178
188
|
const code = await artifact.text();
|
|
179
189
|
const id = this.#paths.idOf(file);
|
|
190
|
+
const built = CsrDevModuleCompiler.#factory(id, code);
|
|
191
|
+
const problem = CsrDevFactoryCheck.problemOf(built.factory, built.helpers?.definition);
|
|
192
|
+
if (problem && !vendor)
|
|
193
|
+
throw new Error(`[csr-dev] ${id}: its factory does not parse as a classic script: ${problem}`);
|
|
194
|
+
if (problem) {
|
|
195
|
+
modules.push(this.#leftOut(file, problem));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
180
198
|
const sourceMap = sourceMaps.get(`${artifact.path}.map`);
|
|
181
199
|
modules.push({
|
|
182
200
|
id,
|
|
183
201
|
file,
|
|
184
202
|
vendor,
|
|
185
|
-
...
|
|
203
|
+
...built,
|
|
186
204
|
sourceMap: sourceMap ? this.#rebaseSourceMap(await sourceMap.text()) : undefined,
|
|
187
205
|
deps: this.#emittedDeps(code),
|
|
188
206
|
mtimeMs: mtimes.get(file) ?? CsrDevPaths.mtimeOf(file),
|
|
@@ -192,6 +210,50 @@ export class CsrDevModuleCompiler {
|
|
|
192
210
|
return { modules, misses, tangled };
|
|
193
211
|
}
|
|
194
212
|
|
|
213
|
+
//? A package file that cannot be a CommonJS factory would fail every module built beside it, and no save fixes a
|
|
214
|
+
//? package: it is left out and named in the log, and the module requiring it gets the reason. App code still fails.
|
|
215
|
+
async #withoutFailed(files: string[], error: AggregateError): Promise<BuildRound> {
|
|
216
|
+
const reasons = new Map<string, string[]>();
|
|
217
|
+
for (const message of error.errors as {
|
|
218
|
+
message?: unknown;
|
|
219
|
+
position?: { file?: unknown; line?: number } | null;
|
|
220
|
+
}[]) {
|
|
221
|
+
const file = message.position?.file;
|
|
222
|
+
if (typeof file !== "string" || !path.isAbsolute(file)) continue;
|
|
223
|
+
const real = CsrDevPaths.realpath(file);
|
|
224
|
+
const at = message.position?.line ? `:${message.position.line}` : "";
|
|
225
|
+
reasons.set(real, [...(reasons.get(real) ?? []), `${String(message.message)} (${this.#paths.idOf(real)}${at})`]);
|
|
226
|
+
}
|
|
227
|
+
const failed = new Set(files.filter((file) => reasons.has(CsrDevPaths.realpath(file))));
|
|
228
|
+
if (failed.size === 0) throw error;
|
|
229
|
+
const rest = files.filter((file) => !failed.has(file));
|
|
230
|
+
const round = rest.length > 0 ? await this.#bunBuild(rest, true) : { modules: [], misses: [], tangled: [] };
|
|
231
|
+
for (const file of failed) {
|
|
232
|
+
const text = (reasons.get(CsrDevPaths.realpath(file)) ?? []).join("; ");
|
|
233
|
+
const awaits = text.includes('"await" can only be used');
|
|
234
|
+
round.modules.push(
|
|
235
|
+
this.#leftOut(file, awaits ? `a top-level await, which a CommonJS factory cannot wait on: ${text}` : text),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return round;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#leftOut(file: string, problem: string): CsrDevCompiledModule {
|
|
242
|
+
const real = CsrDevPaths.realpath(file);
|
|
243
|
+
const id = this.#paths.idOf(real);
|
|
244
|
+
this.#app.logger.error(`[csr-dev] ${id} is left out of the dev registry: ${problem}`);
|
|
245
|
+
return {
|
|
246
|
+
id,
|
|
247
|
+
file: real,
|
|
248
|
+
vendor: true,
|
|
249
|
+
factory: CsrDevFactoryCheck.thrower(id, problem),
|
|
250
|
+
helpers: null,
|
|
251
|
+
deps: [],
|
|
252
|
+
mtimeMs: CsrDevPaths.mtimeOf(real),
|
|
253
|
+
hash: "",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
195
257
|
#registryPlugin(vendor: boolean, misses: string[]): BunPlugin {
|
|
196
258
|
return {
|
|
197
259
|
name: "akan-csr-registry",
|
|
@@ -214,7 +276,7 @@ export class CsrDevModuleCompiler {
|
|
|
214
276
|
const target = this.#resolver.resolve(
|
|
215
277
|
importer,
|
|
216
278
|
args.path,
|
|
217
|
-
args.kind === "require-call" ? "require" : "import",
|
|
279
|
+
args.kind === "require-call" || args.kind === "require-resolve" ? "require" : "import",
|
|
218
280
|
);
|
|
219
281
|
if (target === CsrDevResolver.inline) return undefined;
|
|
220
282
|
if (target === null) {
|
|
@@ -261,12 +323,20 @@ export class CsrDevModuleCompiler {
|
|
|
261
323
|
// Every rewrite keeps the line count, so a module's source map still lines up inside the factory.
|
|
262
324
|
static #factory(id: string, code: string): { factory: string; helpers: CsrDevSharedHelpers | null } {
|
|
263
325
|
const prefix = CsrDevPaths.modulePrefix;
|
|
264
|
-
|
|
326
|
+
//? Bun keeps a package's `#!` line atop its output, where a script inside a function cannot have one.
|
|
327
|
+
let patched = code
|
|
328
|
+
.replace(/^#!/, "//")
|
|
329
|
+
.replaceAll(`import("${prefix}`, `__akanImport("${prefix}`)
|
|
330
|
+
.replace(/^\/\/# debugId=.*$/m, "");
|
|
331
|
+
//? A file esbuild already bundled (mermaid's parser chunks) defines its own helper under the same name, below Bun's
|
|
332
|
+
//? preamble, and Bun then emits none. Only a copy there is exempt: one in the preamble is Bun's own, changed.
|
|
333
|
+
const marker = patched.search(/^\/\/ /m);
|
|
334
|
+
const body = marker < 0 ? "" : patched.slice(marker);
|
|
265
335
|
for (const [name, params, method] of CsrDevModuleCompiler.#helperPatches) {
|
|
266
336
|
const definition = `var ${name} = ${params} => {`;
|
|
267
337
|
if (patched.includes(definition))
|
|
268
338
|
patched = patched.replace(definition, `var ${name} = __akan.${method}, __bun${name} = ${params} => {`);
|
|
269
|
-
else if (patched.includes(`${name}(`))
|
|
339
|
+
else if (patched.includes(`${name}(`) && !new RegExp(`\\b(?:var|let|const|function)\\s+${name}\\b`).test(body))
|
|
270
340
|
throw new Error(
|
|
271
341
|
`[csr-dev] ${id}: Bun's ${name} helper no longer reads "${definition}"; CsrDevModuleCompiler must follow`,
|
|
272
342
|
);
|
|
@@ -275,8 +345,13 @@ export class CsrDevModuleCompiler {
|
|
|
275
345
|
// re-running the store root is the whole update, and bubbling past it would only reach the page modules.
|
|
276
346
|
if (CsrDevModuleCompiler.#storeRoot.test(id)) patched += "\nmodule.hot.accept();";
|
|
277
347
|
const shared = CsrDevModuleCompiler.#shareHelpers(patched);
|
|
348
|
+
//? What `import.meta` is in a browser module: a `url` and no `env`, so `import.meta.env` checks (jotai, zustand)
|
|
349
|
+
//? branch as they do in the ESM bundle. On the header line, outside the preamble that modules share.
|
|
350
|
+
const meta = /(?<![\w$.])__akanMeta(?![\w$])/.test(patched)
|
|
351
|
+
? ` var __akanMeta = { url: new URL(${JSON.stringify(id.startsWith("/") ? id : `/${id}`)}, self.location.href).href };`
|
|
352
|
+
: "";
|
|
278
353
|
return {
|
|
279
|
-
factory: `function (require, module, exports, $RefreshReg$, $RefreshSig$, __akanImport) {\n${shared?.code ?? patched}\n}`,
|
|
354
|
+
factory: `function (require, module, exports, $RefreshReg$, $RefreshSig$, __akanImport) {${meta}\n${shared?.code ?? patched}\n}`,
|
|
280
355
|
helpers: shared?.helpers ?? null,
|
|
281
356
|
};
|
|
282
357
|
}
|
|
@@ -129,8 +129,10 @@ describe("CsrDevPatcher", () => {
|
|
|
129
129
|
async writeEntries() {
|
|
130
130
|
return { files: { "": this.#entry }, changed: [] };
|
|
131
131
|
}
|
|
132
|
+
//? Roots beside the entries, as the SSR registry takes every client entry a route reaches.
|
|
133
|
+
extraRoots: string[] = [];
|
|
132
134
|
protected async rootFiles(entryFiles: string[]) {
|
|
133
|
-
return entryFiles;
|
|
135
|
+
return [...entryFiles, ...this.extraRoots];
|
|
134
136
|
}
|
|
135
137
|
override async metadataFingerprint() {
|
|
136
138
|
return "same";
|
|
@@ -143,12 +145,13 @@ describe("CsrDevPatcher", () => {
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
const movedRegistry = async (files: Record<string, string
|
|
148
|
+
const movedRegistry = async (files: Record<string, string>, { roots = [] }: { roots?: string[] } = {}) => {
|
|
147
149
|
const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
|
|
148
150
|
dirs.push(root);
|
|
149
151
|
const file = (relative: string) => path.join(CsrDevPaths.realpath(root), "apps/demo", relative);
|
|
150
152
|
for (const [relative, text] of Object.entries(files)) await Bun.write(file(relative), text);
|
|
151
153
|
const bundler = new MovedRegistry(root, file("entry.ts"));
|
|
154
|
+
bundler.extraRoots = roots.map((relative) => file(relative));
|
|
152
155
|
expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
|
|
153
156
|
const patcher = new CsrDevPatcher(bundler, { resident: true });
|
|
154
157
|
expect(await patcher.update([])).toEqual({ kind: "unchanged" });
|
|
@@ -286,6 +289,235 @@ describe("CsrDevPatcher", () => {
|
|
|
286
289
|
expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
|
|
287
290
|
});
|
|
288
291
|
|
|
292
|
+
test("a broken module in the graph whose last importer drops it leaves the graph instead of failing every save", async () => {
|
|
293
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
294
|
+
"ui/X.tsx": "export const x = 1;\n",
|
|
295
|
+
"ui/C.tsx": 'export const C = () => "c1";\n',
|
|
296
|
+
"ui/I.tsx": 'import { x } from "./X";\nexport const I = () => x;\n',
|
|
297
|
+
"entry.ts": 'import { C } from "./ui/C";\nimport { I } from "./ui/I";\nexport const run = [C, I];\n',
|
|
298
|
+
});
|
|
299
|
+
await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
|
|
300
|
+
await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
|
|
301
|
+
await Bun.write(file("ui/I.tsx"), 'import { x } from "./X";\nexport const I = () => x + 1;\n');
|
|
302
|
+
await expect(patcher.update([file("ui/I.tsx")])).rejects.toThrow();
|
|
303
|
+
|
|
304
|
+
await Bun.write(file("ui/I.tsx"), "export const I = () => 0;\n");
|
|
305
|
+
expect(changedIds(await patcher.update([file("ui/I.tsx")]))).toEqual(["apps/demo/ui/I.tsx"]);
|
|
306
|
+
expect(((await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph).pending).toEqual([]);
|
|
307
|
+
await Bun.write(file("ui/C.tsx"), 'export const C = () => "c2";\n');
|
|
308
|
+
expect(changedIds(await patcher.update([file("ui/C.tsx")]))).toEqual(["apps/demo/ui/C.tsx"]);
|
|
309
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
310
|
+
expect(graph.modules["apps/demo/ui/X.tsx"]).toBeUndefined();
|
|
311
|
+
expect(graph.pending).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("a broken module left behind by a detached importer, or in a cycle, leaves with everything unreachable", async () => {
|
|
315
|
+
for (const shape of ["detached", "cycle"] as const) {
|
|
316
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
317
|
+
"ui/X.tsx":
|
|
318
|
+
shape === "cycle" ? 'import { y } from "./Y";\nexport const x = () => y;\n' : "export const x = 1;\n",
|
|
319
|
+
"ui/Y.tsx": 'import { x } from "./X";\nexport const y = () => x;\n',
|
|
320
|
+
"ui/C.tsx": 'export const C = () => "c1";\n',
|
|
321
|
+
"ui/I.tsx": 'import { y } from "./Y";\nexport const I = () => y;\n',
|
|
322
|
+
"entry.ts": 'import { C } from "./ui/C";\nimport { I } from "./ui/I";\nexport const run = [C, I];\n',
|
|
323
|
+
});
|
|
324
|
+
await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
|
|
325
|
+
await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
|
|
326
|
+
await Bun.write(file("ui/I.tsx"), "export const I = () => 0;\n");
|
|
327
|
+
expect(changedIds(await patcher.update([file("ui/I.tsx")]))).toEqual(["apps/demo/ui/I.tsx"]);
|
|
328
|
+
await Bun.write(file("ui/C.tsx"), 'export const C = () => "c2";\n');
|
|
329
|
+
expect(changedIds(await patcher.update([file("ui/C.tsx")]))).toEqual(["apps/demo/ui/C.tsx"]);
|
|
330
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
331
|
+
expect(Object.keys(graph.modules).sort()).toEqual([
|
|
332
|
+
"apps/demo/entry.ts",
|
|
333
|
+
"apps/demo/ui/C.tsx",
|
|
334
|
+
"apps/demo/ui/I.tsx",
|
|
335
|
+
]);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a broken client entry no page names any more leaves the registry, though its last importer was a server file", async () => {
|
|
340
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry(
|
|
341
|
+
{
|
|
342
|
+
"ui/X.tsx": 'export const X = () => "x";\n',
|
|
343
|
+
"ui/Y.tsx": 'export const Y = () => "y1";\n',
|
|
344
|
+
"entry.ts": "export {};\n",
|
|
345
|
+
},
|
|
346
|
+
{ roots: ["ui/X.tsx", "ui/Y.tsx"] },
|
|
347
|
+
);
|
|
348
|
+
await Bun.write(file("ui/X.tsx"), "export const X = () => ;\n");
|
|
349
|
+
await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
|
|
350
|
+
bundler.extraRoots = [file("ui/Y.tsx")];
|
|
351
|
+
await Bun.write(file("page/P.tsx"), "export default () => null;\n");
|
|
352
|
+
expect(await patcher.update([file("page/P.tsx")])).toEqual({ kind: "unchanged" });
|
|
353
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
354
|
+
expect(graph.modules["apps/demo/ui/X.tsx"]).toBeUndefined();
|
|
355
|
+
expect(graph.pending).toEqual([]);
|
|
356
|
+
await Bun.write(file("ui/Y.tsx"), 'export const Y = () => "y2";\n');
|
|
357
|
+
expect(changedIds(await patcher.update([file("ui/Y.tsx")]))).toEqual(["apps/demo/ui/Y.tsx"]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a require.resolve call weighs extensions as a require does", async () => {
|
|
361
|
+
const { bundler } = await movedRegistry({
|
|
362
|
+
"ui/pair/x.mjs": "export const x = 1;\n",
|
|
363
|
+
"ui/pair/x.js": "exports.x = 2;\n",
|
|
364
|
+
"ui/R.ts": 'export const where = require.resolve("./pair/x");\n',
|
|
365
|
+
"entry.ts": 'import { where } from "./ui/R";\nexport const run = where;\n',
|
|
366
|
+
});
|
|
367
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
368
|
+
expect(graph.resolution["apps/demo/ui/R.ts"]?.["./pair/x"]).toBe("apps/demo/ui/pair/x.js");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("two routes' checks that both fail keep both roots pending", async () => {
|
|
372
|
+
const { bundler, patcher, file } = await movedRegistry({
|
|
373
|
+
"ui/Card.tsx": 'export const card = () => "v1";\n',
|
|
374
|
+
"ui/E1.tsx": 'import { gone } from "./missing1";\nexport const e1 = () => gone;\n',
|
|
375
|
+
"ui/E4.tsx": 'import { gone } from "./missing4";\nexport const e4 = () => gone;\n',
|
|
376
|
+
"entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
|
|
377
|
+
});
|
|
378
|
+
bundler.wanted = new Set([file("ui/E1.tsx"), file("ui/E4.tsx")]);
|
|
379
|
+
await expect(patcher.update([], { roots: [file("ui/E1.tsx")], onlyRoots: true })).rejects.toThrow();
|
|
380
|
+
await expect(patcher.update([], { roots: [file("ui/E4.tsx")], onlyRoots: true })).rejects.toThrow();
|
|
381
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
382
|
+
expect(graph.pending.sort()).toEqual(["apps/demo/ui/E1.tsx", "apps/demo/ui/E4.tsx"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("a route's check that adds its own root leaves another route's failed root pending", async () => {
|
|
386
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
387
|
+
"ui/Card.tsx": 'export const card = () => "v1";\n',
|
|
388
|
+
"ui/E1.tsx": 'import { gone } from "./missingHelper";\nexport const e1 = () => gone;\n',
|
|
389
|
+
"ui/E3.tsx": "export const e3 = 3;\n",
|
|
390
|
+
"entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
|
|
391
|
+
});
|
|
392
|
+
bundler.wanted = new Set([file("ui/E1.tsx"), file("ui/E3.tsx")]);
|
|
393
|
+
await expect(patcher.update([], { roots: [file("ui/E1.tsx")], onlyRoots: true })).rejects.toThrow();
|
|
394
|
+
await patcher.update([], { roots: [file("ui/E3.tsx")], onlyRoots: true });
|
|
395
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
396
|
+
expect(graph.pending).toEqual(["apps/demo/ui/E1.tsx"]);
|
|
397
|
+
|
|
398
|
+
await Bun.write(file("ui/E1.tsx"), "export const e1 = () => 1;\n");
|
|
399
|
+
expect(changedIds(await patcher.update([file("ui/E1.tsx")]))).toEqual(["apps/demo/ui/E1.tsx"]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("a pending root a worker failed on waits for an edit of its own or of a file its failure named", async () => {
|
|
403
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
404
|
+
"ui/Card.tsx": 'export const card = () => "v1";\n',
|
|
405
|
+
"ui/Other.tsx": 'export const other = () => "o1";\n',
|
|
406
|
+
"ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
|
|
407
|
+
"entry.ts":
|
|
408
|
+
'import { card } from "./ui/Card";\nimport { other } from "./ui/Other";\nexport const run = [card, other];\n',
|
|
409
|
+
"../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
|
|
410
|
+
"../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
|
|
411
|
+
});
|
|
412
|
+
bundler.wanted = new Set([file("ui/Broken.tsx")]);
|
|
413
|
+
await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
|
|
414
|
+
const broken =
|
|
415
|
+
'import { gone } from "fake-pkg";\nimport { nope } from "./Card";\nexport const broken = () => gone + nope;\n';
|
|
416
|
+
await Bun.write(file("ui/Broken.tsx"), broken);
|
|
417
|
+
expect((await patcher.update([file("ui/Broken.tsx")])).kind).toBe("delegate");
|
|
418
|
+
//? The worker the builder runs next fails in Broken, naming Card: the resident patcher reads the graph it wrote.
|
|
419
|
+
await expect(bundler.update([file("ui/Broken.tsx")])).rejects.toThrow();
|
|
420
|
+
patcher.forget();
|
|
421
|
+
|
|
422
|
+
await Bun.write(file("ui/Other.tsx"), 'export const other = () => "o2";\n');
|
|
423
|
+
expect(changedIds(await patcher.update([file("ui/Other.tsx")]))).toEqual(["apps/demo/ui/Other.tsx"]);
|
|
424
|
+
expect(await patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).toEqual({
|
|
425
|
+
kind: "unchanged",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\nexport const nope = 1;\n');
|
|
429
|
+
expect((await patcher.update([file("ui/Card.tsx")])).kind).toBe("delegate");
|
|
430
|
+
//? Its worker has not run yet: a route's check waits for it rather than handing the root over again.
|
|
431
|
+
expect(await patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).toEqual({
|
|
432
|
+
kind: "unchanged",
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a root handed to a worker that died before writing its failure is handed over again", async () => {
|
|
437
|
+
const { bundler, patcher, file } = await movedRegistry({
|
|
438
|
+
"ui/Card.tsx": 'export const card = () => "v1";\n',
|
|
439
|
+
"entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
|
|
440
|
+
"../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
|
|
441
|
+
"../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
|
|
442
|
+
});
|
|
443
|
+
await Bun.write(file("ui/B.tsx"), 'import { gone } from "fake-pkg";\nexport const b = () => gone;\n');
|
|
444
|
+
bundler.wanted = new Set([file("ui/B.tsx")]);
|
|
445
|
+
const check = async () => await patcher.update([], { roots: [file("ui/B.tsx")], onlyRoots: true });
|
|
446
|
+
expect((await check()).kind).toBe("delegate");
|
|
447
|
+
expect(await check()).toEqual({ kind: "unchanged" });
|
|
448
|
+
//? What the builder does when that worker is killed (an OOM) before it writes anything.
|
|
449
|
+
patcher.releaseHandedOver();
|
|
450
|
+
patcher.forget();
|
|
451
|
+
expect((await check()).kind).toBe("delegate");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("a pending root a worker failed on because of another file is tried again once that file is fixed", async () => {
|
|
455
|
+
const { bundler, patcher, file } = await movedRegistry({
|
|
456
|
+
"ui/Card.tsx": 'export const card = () => "v1";\n',
|
|
457
|
+
"ui/C.tsx": 'export const c = () => "c1";\n',
|
|
458
|
+
"entry.ts": 'import { card } from "./ui/Card";\nimport { c } from "./ui/C";\nexport const run = [card, c];\n',
|
|
459
|
+
"../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
|
|
460
|
+
"../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
|
|
461
|
+
});
|
|
462
|
+
await Bun.write(file("ui/B.tsx"), 'import { gone } from "fake-pkg";\nexport const b = () => gone;\n');
|
|
463
|
+
bundler.wanted = new Set([file("ui/B.tsx")]);
|
|
464
|
+
await Bun.write(file("ui/C.tsx"), "export const c = () => ;\n");
|
|
465
|
+
await expect(patcher.update([file("ui/C.tsx")])).rejects.toThrow();
|
|
466
|
+
expect((await patcher.update([], { roots: [file("ui/B.tsx")], onlyRoots: true })).kind).toBe("delegate");
|
|
467
|
+
await expect(bundler.update([], { roots: [file("ui/B.tsx")] })).rejects.toThrow();
|
|
468
|
+
patcher.forget();
|
|
469
|
+
|
|
470
|
+
await Bun.write(file("ui/C.tsx"), 'export const c = () => "c2";\n');
|
|
471
|
+
expect((await patcher.update([file("ui/C.tsx")])).kind).toBe("delegate");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test.skipIf(!foldsCase)(
|
|
475
|
+
"an importer still naming the old spelling compiles with the new one, so app.js defines what it requires",
|
|
476
|
+
async () => {
|
|
477
|
+
const { patcher, file, app, changedIds } = await movedRegistry({
|
|
478
|
+
"ui/card.tsx": 'export const card = () => "v1";\n',
|
|
479
|
+
"ui/Page.tsx": 'import { card } from "./card";\nexport const page = () => card();\n',
|
|
480
|
+
"entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
|
|
481
|
+
});
|
|
482
|
+
fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
|
|
483
|
+
await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
|
|
484
|
+
expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual([
|
|
485
|
+
"apps/demo/ui/Card.tsx",
|
|
486
|
+
"apps/demo/ui/Page.tsx",
|
|
487
|
+
]);
|
|
488
|
+
expect(await app()).toContain('__akan.define("apps/demo/ui/Card.tsx"');
|
|
489
|
+
expect(await app()).not.toContain('__akan.define("apps/demo/ui/card.tsx"');
|
|
490
|
+
await Bun.write(file("ui/Other.tsx"), "export const other = 1;\n");
|
|
491
|
+
expect(await patcher.update([file("ui/Other.tsx")])).toEqual({ kind: "unchanged" });
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
test.skipIf(!foldsCase)(
|
|
496
|
+
"a case-only rename the patcher never saw moves the module once, and later saves leave it alone",
|
|
497
|
+
async () => {
|
|
498
|
+
for (const resident of [false, true]) {
|
|
499
|
+
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
500
|
+
"ui/card.tsx": 'export const card = () => "v1";\n',
|
|
501
|
+
"ui/Page.tsx": 'import { card } from "./card";\nexport const page = () => card();\n',
|
|
502
|
+
"entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
|
|
503
|
+
});
|
|
504
|
+
fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
|
|
505
|
+
await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card();\n');
|
|
506
|
+
const later = resident ? patcher : new CsrDevPatcher(bundler, { resident: true });
|
|
507
|
+
expect(changedIds(await later.update([file("ui/Page.tsx")]))).toEqual([
|
|
508
|
+
"apps/demo/ui/Card.tsx",
|
|
509
|
+
"apps/demo/ui/Page.tsx",
|
|
510
|
+
]);
|
|
511
|
+
await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
|
|
512
|
+
expect(changedIds(await later.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
|
|
513
|
+
await Bun.write(file("ui/Other.tsx"), "export const other = 1;\n");
|
|
514
|
+
expect(await later.update([file("ui/Other.tsx")])).toEqual({ kind: "unchanged" });
|
|
515
|
+
const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
|
|
516
|
+
expect(graph.modules["apps/demo/ui/card.tsx"]).toBeUndefined();
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
|
|
289
521
|
test("by default a pending root counts as reached only while an entry or a module imports it", async () => {
|
|
290
522
|
const { bundler, patcher, file, changedIds } = await movedRegistry({
|
|
291
523
|
"ui/Card.tsx": 'export const card = () => "v1";\n',
|