@tschk/moonshine-compiler 0.4.0 → 0.4.4
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/README.md +43 -0
- package/package.json +10 -6
- package/src/build.ts +535 -75
- package/src/compile-graph.ts +215 -0
- package/src/compile-output.ts +91 -0
- package/src/crepus-source.ts +116 -0
- package/src/discover.ts +29 -2
- package/src/inherit.ts +7 -4
- package/src/manifest.ts +156 -12
- package/src/path.ts +16 -0
- package/src/source.ts +8 -0
- package/test/analyze.test.ts +0 -146
- package/test/classify.test.ts +0 -99
- package/test/client-process-env.test.ts +0 -73
- package/test/discover.test.ts +0 -76
- package/test/fixtures/modes/api.ts +0 -3
- package/test/fixtures/modes/client.client.tsx +0 -3
- package/test/fixtures/modes/interactive.tsx +0 -3
- package/test/fixtures/modes/loader.ts +0 -5
- package/test/fixtures/modes/near-miss.tsx +0 -5
- package/test/fixtures/modes/renamed-source.tsx +0 -5
- package/test/fixtures/modes/request-utils.ts +0 -3
- package/test/fixtures/modes/request.ts +0 -1
- package/test/fixtures/modes/ssr-helpers.ts +0 -5
- package/test/fixtures/modes/static.tsx +0 -3
- package/test/fixtures/modes/unresolved.ts +0 -5
- package/test/fixtures/project/src/routes/+error.tsx +0 -3
- package/test/fixtures/project/src/routes/+layout.tsx +0 -3
- package/test/fixtures/project/src/routes/+middleware.ts +0 -3
- package/test/fixtures/project/src/routes/about.data.ts +0 -7
- package/test/fixtures/project/src/routes/about.tsx +0 -7
- package/test/fixtures/project/src/routes/api/health.ts +0 -3
- package/test/fixtures/project/src/routes/client.client.tsx +0 -8
- package/test/fixtures/project/src/routes/counter.tsx +0 -3
- package/test/fixtures/project/src/routes/docs/[...path].tsx +0 -3
- package/test/fixtures/project/src/routes/index.tsx +0 -3
- package/test/fixtures/routes/+error.tsx +0 -3
- package/test/fixtures/routes/+layout.tsx +0 -3
- package/test/fixtures/routes/+middleware.ts +0 -3
- package/test/fixtures/routes/about.tsx +0 -3
- package/test/fixtures/routes/account/[[tab]].tsx +0 -3
- package/test/fixtures/routes/api/health.server.ts +0 -7
- package/test/fixtures/routes/blog/[slug].data.ts +0 -7
- package/test/fixtures/routes/blog/[slug].tsx +0 -3
- package/test/fixtures/routes/docs/[...path].tsx +0 -3
- package/test/fixtures/routes/index.tsx +0 -3
- package/test/inherit.test.ts +0 -50
- package/test/manifest.test.ts +0 -96
- package/test/path.test.ts +0 -30
- package/test/server-bundle-support-modules.test.ts +0 -68
- package/tsconfig.json +0 -4
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const request = new Request("http://example.com");
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Reads process.env deliberately: browsers have no `process`, so this is the
|
|
2
|
-
// shape that used to throw `ReferenceError: process is not defined` while the
|
|
3
|
-
// client entry evaluated, leaving the page blank with no error boundary.
|
|
4
|
-
const apiBase = process.env.MOONSHINE_TEST_API_BASE ?? "unset";
|
|
5
|
-
|
|
6
|
-
export default function ClientPage() {
|
|
7
|
-
return <h1>client {apiBase}</h1>;
|
|
8
|
-
}
|
package/test/inherit.test.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mergeRoutes } from "../src/inherit";
|
|
3
|
-
import type { RouteDefinition } from "@tschk/moonshine-framework";
|
|
4
|
-
|
|
5
|
-
export interface InheritableLayout {
|
|
6
|
-
id: string;
|
|
7
|
-
layouts: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe("mergeRoutes", () => {
|
|
11
|
-
test("combines nested route definitions with inheritable layouts", () => {
|
|
12
|
-
const routes: RouteDefinition[] = [
|
|
13
|
-
{ id: "index", path: "/", file: "index.tsx" },
|
|
14
|
-
{ id: "about", path: "/about", file: "about.tsx" },
|
|
15
|
-
{ id: "nested/index", path: "/nested", file: "nested/index.tsx" },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const layouts: InheritableLayout[] = [
|
|
19
|
-
{ id: "index", layouts: ["layout.tsx"] },
|
|
20
|
-
{ id: "about", layouts: ["layout.tsx"] },
|
|
21
|
-
{ id: "nested/index", layouts: ["layout.tsx", "nested/layout.tsx"] },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const merged = mergeRoutes(routes, layouts as unknown as RouteDefinition[]);
|
|
25
|
-
|
|
26
|
-
expect(merged.length).toBe(3);
|
|
27
|
-
|
|
28
|
-
const indexRoute = merged.find((r) => r.id === "index");
|
|
29
|
-
expect(indexRoute?.layouts).toEqual(["layout.tsx"]);
|
|
30
|
-
|
|
31
|
-
const aboutRoute = merged.find((r) => r.id === "about");
|
|
32
|
-
expect(aboutRoute?.layouts).toEqual(["layout.tsx"]);
|
|
33
|
-
|
|
34
|
-
const nestedRoute = merged.find((r) => r.id === "nested/index");
|
|
35
|
-
expect(nestedRoute?.layouts).toEqual(["layout.tsx", "nested/layout.tsx"]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("handles empty layouts gracefully", () => {
|
|
39
|
-
const routes: RouteDefinition[] = [
|
|
40
|
-
{ id: "index", path: "/", file: "index.tsx" },
|
|
41
|
-
];
|
|
42
|
-
const layouts: InheritableLayout[] = [];
|
|
43
|
-
|
|
44
|
-
const merged = mergeRoutes(routes, layouts as unknown as RouteDefinition[]);
|
|
45
|
-
|
|
46
|
-
expect(merged.length).toBe(1);
|
|
47
|
-
const indexRoute = merged.find((r) => r.id === "index");
|
|
48
|
-
expect(indexRoute?.layouts).toBeUndefined();
|
|
49
|
-
});
|
|
50
|
-
});
|
package/test/manifest.test.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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, readManifest } from "../src/manifest";
|
|
5
|
-
|
|
6
|
-
const projectDir = resolve(import.meta.dir, "fixtures", "project");
|
|
7
|
-
const outDir = join(projectDir, ".moonshine");
|
|
8
|
-
|
|
9
|
-
function clean() {
|
|
10
|
-
if (existsSync(outDir)) {
|
|
11
|
-
rmSync(outDir, { recursive: true, force: true });
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
afterAll(clean);
|
|
16
|
-
|
|
17
|
-
describe("buildProject", () => {
|
|
18
|
-
test("emits a versioned, deterministic manifest with relative paths", async () => {
|
|
19
|
-
const manifest = await buildProject({
|
|
20
|
-
projectDir,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
expect(manifest.version).toBe(1);
|
|
24
|
-
expect(manifest.frameworkVersion).toMatch(/^\d+\.\d+\.\d+/);
|
|
25
|
-
expect(manifest.routes.map((r) => r.path)).toEqual([
|
|
26
|
-
"/",
|
|
27
|
-
"/about",
|
|
28
|
-
"/api/health",
|
|
29
|
-
"/client",
|
|
30
|
-
"/counter",
|
|
31
|
-
"/docs/*path",
|
|
32
|
-
]);
|
|
33
|
-
expect(manifest.routes.map((r) => r.mode)).toEqual([
|
|
34
|
-
"static",
|
|
35
|
-
"ssr",
|
|
36
|
-
"api",
|
|
37
|
-
"spa",
|
|
38
|
-
"island",
|
|
39
|
-
"static",
|
|
40
|
-
]);
|
|
41
|
-
expect(manifest.routes.map((r) => r.decision)).toEqual([
|
|
42
|
-
"deterministic build-time route",
|
|
43
|
-
"uses request-time server data",
|
|
44
|
-
"exports request handler",
|
|
45
|
-
"route root is a client boundary",
|
|
46
|
-
"contains an interactive client subtree",
|
|
47
|
-
"deterministic build-time route",
|
|
48
|
-
]);
|
|
49
|
-
expect(manifest.routes.every((r) => r.runtime === "bun")).toBe(true);
|
|
50
|
-
expect(manifest.entries.server).toBe("dist/server.js");
|
|
51
|
-
expect(manifest.entries.client).toBe("/client.js");
|
|
52
|
-
expect(manifest.capabilities).toEqual(["islands", "streaming"]);
|
|
53
|
-
// Client assets build into public/ because that is the only directory the
|
|
54
|
-
// server exposes; the server bundle must stay outside it.
|
|
55
|
-
expect(manifest.assets.map((a) => a.file)).toEqual([
|
|
56
|
-
"dist/server.js",
|
|
57
|
-
"public/client.js",
|
|
58
|
-
]);
|
|
59
|
-
for (const asset of manifest.assets) {
|
|
60
|
-
const served = asset.file.startsWith("public/");
|
|
61
|
-
expect(asset.path).toBe(
|
|
62
|
-
served ? `/${asset.file.slice("public/".length)}` : `/${asset.file}`,
|
|
63
|
-
);
|
|
64
|
-
expect(asset.integrity).toMatch(/^sha256-[A-Za-z0-9+/]+=?$/);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const fromDisk = await readManifest(join(outDir, "manifest.json"));
|
|
68
|
-
expect(fromDisk.version).toBe(1);
|
|
69
|
-
expect(fromDisk.routes.map((r) => r.path)).toEqual(
|
|
70
|
-
manifest.routes.map((r) => r.path),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const text = JSON.stringify(manifest);
|
|
74
|
-
expect(text).not.toMatch(/\\\\/);
|
|
75
|
-
expect(text).not.toMatch(/^\//);
|
|
76
|
-
expect(text).not.toMatch(/process\.env/);
|
|
77
|
-
|
|
78
|
-
for (const route of manifest.routes) {
|
|
79
|
-
expect(route.file.startsWith("src/")).toBe(true);
|
|
80
|
-
if (route.dataFile) expect(route.dataFile.startsWith("src/")).toBe(true);
|
|
81
|
-
if (route.layouts) {
|
|
82
|
-
for (const l of route.layouts) {
|
|
83
|
-
expect(l.startsWith("src/")).toBe(true);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (route.middleware) {
|
|
87
|
-
for (const m of route.middleware) {
|
|
88
|
-
expect(m.startsWith("src/")).toBe(true);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (route.errorBoundary) {
|
|
92
|
-
expect(route.errorBoundary.startsWith("src/")).toBe(true);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
});
|
package/test/path.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { toPosix } from "../src/path";
|
|
3
|
-
|
|
4
|
-
describe("toPosix", () => {
|
|
5
|
-
test("converts Windows paths to POSIX paths", () => {
|
|
6
|
-
expect(toPosix("C:\\Users\\John\\Documents\\file.txt")).toBe(
|
|
7
|
-
"C:/Users/John/Documents/file.txt",
|
|
8
|
-
);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("preserves already correct POSIX paths", () => {
|
|
12
|
-
expect(toPosix("/usr/local/bin/node")).toBe("/usr/local/bin/node");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("handles empty strings", () => {
|
|
16
|
-
expect(toPosix("")).toBe("");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("handles mixed slashes", () => {
|
|
20
|
-
expect(toPosix("foo\\bar/baz\\qux")).toBe("foo/bar/baz/qux");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("handles consecutive backslashes", () => {
|
|
24
|
-
expect(toPosix("\\\\server\\share\\folder")).toBe("//server/share/folder");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("handles trailing and leading backslashes", () => {
|
|
28
|
-
expect(toPosix("\\foo\\bar\\")).toBe("/foo/bar/");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
});
|
package/tsconfig.json
DELETED