@tschk/moonshine-compiler 0.3.5 → 0.3.7
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-compiler",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tschk/moonshine-framework": "^0.3.
|
|
19
|
-
"@tschk/moonshine-router": "^0.3.
|
|
18
|
+
"@tschk/moonshine-framework": "^0.3.6",
|
|
19
|
+
"@tschk/moonshine-router": "^0.3.6"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "1.3.14",
|
package/src/build.ts
CHANGED
|
@@ -63,6 +63,30 @@ function generateServerEntry(
|
|
|
63
63
|
: ""
|
|
64
64
|
}`);
|
|
65
65
|
}
|
|
66
|
+
// Layouts, middleware, and error boundaries are route dependencies the server
|
|
67
|
+
// renders through, so they belong in the bundle alongside the pages. Without
|
|
68
|
+
// them the renderer falls back to `await import(file)` on their SOURCE paths,
|
|
69
|
+
// which only resolves where the source tree sits next to the build output —
|
|
70
|
+
// never in a deployed image, and never on a runtime that cannot resolve a
|
|
71
|
+
// dynamic import at all. A page would render locally and 500 in production.
|
|
72
|
+
const supportFiles: string[] = [];
|
|
73
|
+
for (const route of routes) {
|
|
74
|
+
for (const file of [
|
|
75
|
+
...(route.layouts ?? []),
|
|
76
|
+
...(route.middleware ?? []),
|
|
77
|
+
...(route.errorBoundary ? [route.errorBoundary] : []),
|
|
78
|
+
]) {
|
|
79
|
+
if (file && !supportFiles.includes(file)) supportFiles.push(file);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < supportFiles.length; i++) {
|
|
83
|
+
const file = supportFiles[i]!;
|
|
84
|
+
imports.push(`import * as support_${i} from "${importPath(buildDir, file)}";`);
|
|
85
|
+
// Registered unwrapped: these export a component or middleware, not a route
|
|
86
|
+
// module with loader/action semantics.
|
|
87
|
+
moduleBodys.push(` "${toPosix(file)}": support_${i}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
return `${imports.join("\n")}\n\n${moduleHelpers.join("\n")}\n\nconst ${routes.map((_, i) => `module_${i} = makeModule_${i}(route_${i}, ${routes[i]!.dataFile ? `data_${i}` : "undefined"})`).join(";\nconst ")};\n\nexport const routes = {\n${bodies.join(",\n")}\n};\n\nexport const modules = {\n${moduleBodys.join(",\n")}\n};\n`;
|
|
67
91
|
}
|
|
68
92
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { buildProject } from "../src/manifest";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The server bundle must carry every module the server renders through, not
|
|
8
|
+
* just the page.
|
|
9
|
+
*
|
|
10
|
+
* A page's layout is loaded during server rendering. When it is missing from
|
|
11
|
+
* `modules`, the renderer falls back to `await import(file)` on the layout's
|
|
12
|
+
* SOURCE path. That resolves in a checkout, where the source sits beside the
|
|
13
|
+
* build output, and fails in a deployed image that ships only the build — so
|
|
14
|
+
* the route renders locally and returns 500 in production. It also fails
|
|
15
|
+
* outright on runtimes that cannot resolve a dynamic import.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const projectDir = resolve(import.meta.dir, "fixtures", "project");
|
|
19
|
+
const outDir = join(projectDir, ".moonshine");
|
|
20
|
+
|
|
21
|
+
function clean() {
|
|
22
|
+
if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterAll(clean);
|
|
26
|
+
|
|
27
|
+
describe("server bundle support modules", () => {
|
|
28
|
+
test("registers the layouts its routes render through", async () => {
|
|
29
|
+
const manifest = await buildProject({ projectDir });
|
|
30
|
+
|
|
31
|
+
const layouts = [
|
|
32
|
+
...new Set(manifest.routes.flatMap((route) => route.layouts ?? [])),
|
|
33
|
+
];
|
|
34
|
+
expect(layouts.length).toBeGreaterThan(0);
|
|
35
|
+
|
|
36
|
+
const { modules } = await import(join(outDir, "dist", "server.js"));
|
|
37
|
+
// The bundle keys modules by their build-time absolute path, while the
|
|
38
|
+
// manifest stores them relative to the project.
|
|
39
|
+
for (const layout of layouts) {
|
|
40
|
+
expect(Object.keys(modules)).toContain(join(projectDir, layout));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a registered layout is the real module, not a route wrapper", async () => {
|
|
45
|
+
const manifest = await buildProject({ projectDir });
|
|
46
|
+
const layout = join(
|
|
47
|
+
projectDir,
|
|
48
|
+
manifest.routes.flatMap((route) => route.layouts ?? [])[0]!,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { modules } = await import(join(outDir, "dist", "server.js"));
|
|
52
|
+
|
|
53
|
+
// Layouts export a component; wrapping them in loader/action semantics
|
|
54
|
+
// would make the renderer read `default` off the wrong object.
|
|
55
|
+
expect(modules[layout]).toBeDefined();
|
|
56
|
+
expect(typeof modules[layout].default).toBe("function");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("still registers pages by id and file", async () => {
|
|
60
|
+
const manifest = await buildProject({ projectDir });
|
|
61
|
+
const route = manifest.routes[0]!;
|
|
62
|
+
|
|
63
|
+
const { modules } = await import(join(outDir, "dist", "server.js"));
|
|
64
|
+
|
|
65
|
+
expect(modules[route.id]).toBeDefined();
|
|
66
|
+
expect(modules[join(projectDir, route.file)]).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|