@sigil-dev/grimoire 0.7.4 → 0.7.6

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 (50) hide show
  1. package/.grimoire/_routes.dom.js +8 -0
  2. package/.grimoire/_routes.hydrate.js +8 -0
  3. package/.grimoire/tsconfig.generated.json +11 -0
  4. package/.grimoire/types/ambient.d.ts +59 -0
  5. package/.grimoire/types/api/hello/$types.d.ts +50 -0
  6. package/.grimoire/types/api/items/$types.d.ts +50 -0
  7. package/.grimoire/types/echo/$types.d.ts +50 -0
  8. package/.grimoire/types/env-private.d.ts +5 -0
  9. package/.grimoire/types/env-public.d.ts +5 -0
  10. package/.grimoire/types/mixed/$types.d.ts +50 -0
  11. package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
  12. package/.grimoire/types/reject/$types.d.ts +50 -0
  13. package/index.ts +34 -34
  14. package/package.json +8 -4
  15. package/preload.js +2 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/src/client/head.ts +29 -0
  19. package/src/client/router.ts +224 -76
  20. package/src/env/index.ts +25 -0
  21. package/src/env/plugin.ts +13 -0
  22. package/src/env/private.ts +5 -0
  23. package/src/env/public.ts +7 -0
  24. package/src/env/typegen.ts +51 -0
  25. package/src/integrations/vite.ts +72 -72
  26. package/src/rendering/head.ts +22 -2
  27. package/src/rendering/hydrate.ts +81 -26
  28. package/src/rendering/index.ts +199 -186
  29. package/src/rendering/ssrPlugin.ts +53 -42
  30. package/src/routing/manifest-gen.ts +39 -26
  31. package/src/routing/router.ts +106 -98
  32. package/src/routing/scanner.ts +135 -129
  33. package/src/routing/transform-routes.ts +101 -96
  34. package/src/server/build.ts +147 -90
  35. package/src/server/coordinator.ts +306 -297
  36. package/src/server/hooks.ts +24 -3
  37. package/src/server/index.ts +148 -71
  38. package/src/server/worker.ts +59 -59
  39. package/src/typegen/index.ts +353 -340
  40. package/src/types.ts +269 -260
  41. package/test/context.test.ts +52 -52
  42. package/test/hydration.test.ts +119 -119
  43. package/test/middleware.test.ts +223 -221
  44. package/test/rendering.test.ts +425 -425
  45. package/test/routing.test.ts +83 -45
  46. package/test/scanning.test.ts +181 -169
  47. package/test/server.test.ts +229 -229
  48. package/test/streaming.test.ts +106 -106
  49. package/test/transform-routes.test.ts +84 -84
  50. package/test/typegen.test.ts +19 -1
@@ -1,45 +1,83 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { matchRoute } from "../src/routing/router";
3
- import type { RouteFile, RouteTree } from "../src/routing/scanner";
4
-
5
- const makeRoute = (path: string, type: RouteFile["type"]): RouteFile => ({
6
- path,
7
- filePath: `/app/routes${path}`,
8
- type,
9
- paramNames: path
10
- .split("/")
11
- .filter((p) => p.startsWith(":"))
12
- .map((p) => p.slice(1)),
13
- });
14
-
15
- const tree: RouteTree = {
16
- routes: [
17
- makeRoute("/", "page"),
18
- makeRoute("/about", "simple"),
19
- makeRoute("/blog", "page"),
20
- makeRoute("/blog/:slug", "page"),
21
- makeRoute("/blog/:slug", "pageServer"),
22
- ],
23
- layouts: [makeRoute("/", "layout"), makeRoute("/blog", "layout")],
24
- servers: [makeRoute("/api/todos", "server")],
25
- errors: [],
26
- };
27
-
28
- describe("matchRoute", () => {
29
- test("matches static route", () => {
30
- const match = matchRoute(tree, new URL("http://localhost/about"));
31
- expect(match?.route.path).toBe("/about");
32
- expect(match?.params).toEqual({});
33
- });
34
-
35
- test("matches dynamic route and extracts params", () => {
36
- const match = matchRoute(tree, new URL("http://localhost/blog/hello"));
37
- expect(match?.route.path).toBe("/blog/:slug");
38
- expect(match?.params).toEqual({ slug: "hello" });
39
- });
40
-
41
- test("returns null for unknown route", () => {
42
- const match = matchRoute(tree, new URL("http://localhost/unknown"));
43
- expect(match).toBeNull();
44
- });
45
- });
1
+ import { describe, expect, test } from "bun:test";
2
+ import { matchRoute } from "../src/routing/router";
3
+ import type { RouteFile, RouteTree } from "../src/routing/scanner";
4
+
5
+ const makeRoute = (path: string, type: RouteFile["type"]): RouteFile => ({
6
+ path,
7
+ filePath: `/app/routes${path}`,
8
+ type,
9
+ paramNames: path
10
+ .split("/")
11
+ .filter((p) => p.startsWith(":"))
12
+ .map((p) => p.slice(1)),
13
+ restParamNames: path
14
+ .split("/")
15
+ .filter((p) => p.startsWith("*"))
16
+ .map((p) => p.slice(1)),
17
+ });
18
+
19
+ const tree: RouteTree = {
20
+ routes: [
21
+ makeRoute("/", "page"),
22
+ makeRoute("/about", "simple"),
23
+ makeRoute("/blog", "page"),
24
+ makeRoute("/blog/:slug", "page"),
25
+ makeRoute("/blog/:slug", "pageServer"),
26
+ ],
27
+ layouts: [makeRoute("/", "layout"), makeRoute("/blog", "layout")],
28
+ servers: [makeRoute("/api/todos", "server")],
29
+ errors: [],
30
+ };
31
+
32
+ describe("matchRoute", () => {
33
+ test("matches static route", () => {
34
+ const match = matchRoute(tree, new URL("http://localhost/about"));
35
+ expect(match?.route.path).toBe("/about");
36
+ expect(match?.params).toEqual({});
37
+ });
38
+
39
+ test("matches dynamic route and extracts params", () => {
40
+ const match = matchRoute(tree, new URL("http://localhost/blog/hello"));
41
+ expect(match?.route.path).toBe("/blog/:slug");
42
+ expect(match?.params).toEqual({ slug: "hello" });
43
+ });
44
+
45
+ test("returns null for unknown route", () => {
46
+ const match = matchRoute(tree, new URL("http://localhost/unknown"));
47
+ expect(match).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe("matchRoute — rest params (catch-all)", () => {
52
+ const restTree: RouteTree = {
53
+ routes: [],
54
+ layouts: [],
55
+ servers: [
56
+ makeRoute("/docs/*path", "server"),
57
+ ],
58
+ errors: [],
59
+ };
60
+
61
+ test("matches single remaining segment", () => {
62
+ const match = matchRoute(restTree, new URL("http://localhost/docs/abc"));
63
+ expect(match?.route.path).toBe("/docs/*path");
64
+ expect(match?.params).toEqual({ path: "abc" });
65
+ });
66
+
67
+ test("matches multiple remaining segments joined by /", () => {
68
+ const match = matchRoute(restTree, new URL("http://localhost/docs/a/b/c"));
69
+ expect(match?.route.path).toBe("/docs/*path");
70
+ expect(match?.params).toEqual({ path: "a/b/c" });
71
+ });
72
+
73
+ test("matches zero remaining segments as empty string", () => {
74
+ const match = matchRoute(restTree, new URL("http://localhost/docs/"));
75
+ expect(match?.route.path).toBe("/docs/*path");
76
+ expect(match?.params).toEqual({ path: "" });
77
+ });
78
+
79
+ test("does not match when prefix segments don't line up", () => {
80
+ const match = matchRoute(restTree, new URL("http://localhost/other/a"));
81
+ expect(match).toBeNull();
82
+ });
83
+ });
@@ -1,169 +1,181 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { mkdir, rm, writeFile } from "fs/promises";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import { filePathToRoutePath, scanRoutes } from "../src/routing/scanner";
6
-
7
- describe("File scanning", () => {
8
- test("index.tsx → /", () => {
9
- expect(filePathToRoutePath("/app/routes/index.tsx", "/app/routes")).toEqual(
10
- { pattern: "/", params: [] },
11
- );
12
- });
13
-
14
- test("about.tsx → /about", () => {
15
- expect(filePathToRoutePath("/app/routes/about.tsx", "/app/routes")).toEqual(
16
- { pattern: "/about", params: [] },
17
- );
18
- });
19
-
20
- test("[slug].tsx → /:slug", () => {
21
- expect(
22
- filePathToRoutePath("/app/routes/[slug].tsx", "/app/routes"),
23
- ).toEqual({ pattern: "/:slug", params: ["slug"] });
24
- });
25
-
26
- test("blog/+page.tsx → /blog", () => {
27
- expect(
28
- filePathToRoutePath("/app/routes/blog/+page.tsx", "/app/routes"),
29
- ).toEqual({ pattern: "/blog", params: [] });
30
- });
31
-
32
- test("blog/[slug]/+page.tsx → /blog/:slug", () => {
33
- expect(
34
- filePathToRoutePath("/app/routes/blog/[slug]/+page.tsx", "/app/routes"),
35
- ).toEqual({ pattern: "/blog/:slug", params: ["slug"] });
36
- });
37
-
38
- test("blog/[slug]/+page.server.ts → /blog/:slug", () => {
39
- expect(
40
- filePathToRoutePath(
41
- "/app/routes/blog/[slug]/+page.server.ts",
42
- "/app/routes",
43
- ),
44
- ).toEqual({ pattern: "/blog/:slug", params: ["slug"] });
45
- });
46
-
47
- test("+layout.tsx → /", () => {
48
- expect(
49
- filePathToRoutePath("/app/routes/+layout.tsx", "/app/routes"),
50
- ).toEqual({ pattern: "/", params: [] });
51
- });
52
-
53
- test("nested +layout.tsx → /blog", () => {
54
- expect(
55
- filePathToRoutePath("/app/routes/blog/+layout.tsx", "/app/routes"),
56
- ).toEqual({ pattern: "/blog", params: [] });
57
- });
58
-
59
- test("about/index.tsx, about/+page.tsx, and about.tsx all resolve to /about", () => {
60
- const index = filePathToRoutePath(
61
- "/app/routes/about/index.tsx",
62
- "/app/routes",
63
- );
64
- const page = filePathToRoutePath(
65
- "/app/routes/about/+page.tsx",
66
- "/app/routes",
67
- );
68
- const bare = filePathToRoutePath("/app/routes/about.tsx", "/app/routes");
69
- expect(index.pattern).toBe("/about");
70
- expect(page.pattern).toBe("/about");
71
- expect(bare.pattern).toBe("/about");
72
- });
73
-
74
- test("+server.ts resolves to the same path as +page.tsx", () => {
75
- const server = filePathToRoutePath(
76
- "/app/routes/api/items/+server.ts",
77
- "/app/routes",
78
- );
79
- expect(server.pattern).toBe("/api/items");
80
- });
81
-
82
- test("+error.tsx resolves to its directory path", () => {
83
- const root = filePathToRoutePath("/app/routes/+error.tsx", "/app/routes");
84
- const nested = filePathToRoutePath(
85
- "/app/routes/blog/+error.tsx",
86
- "/app/routes",
87
- );
88
- expect(root.pattern).toBe("/");
89
- expect(nested.pattern).toBe("/blog");
90
- });
91
- });
92
-
93
- describe("Duplicate route detection", () => {
94
- let tmpDir: string;
95
-
96
- beforeAll(async () => {
97
- tmpDir = join(tmpdir(), `grimoire-scan-${Date.now()}`);
98
- await mkdir(tmpDir, { recursive: true });
99
- });
100
-
101
- afterAll(async () => {
102
- await rm(tmpDir, { recursive: true });
103
- });
104
-
105
- test("throws when two page files resolve to the same path", async () => {
106
- const dir = join(tmpDir, "dup-pages");
107
- await mkdir(join(dir, "about"), { recursive: true });
108
- await writeFile(join(dir, "about.tsx"), "export default () => null");
109
- await writeFile(
110
- join(dir, "about", "index.tsx"),
111
- "export default () => null",
112
- );
113
- await expect(scanRoutes(dir)).rejects.toThrow('Duplicate page at "/about"');
114
- });
115
-
116
- test("+page.tsx and +page.server.ts at the same path do NOT conflict", async () => {
117
- const dir = join(tmpDir, "no-conflict");
118
- await mkdir(join(dir, "blog"), { recursive: true });
119
- await writeFile(
120
- join(dir, "blog", "+page.tsx"),
121
- "export default () => null",
122
- );
123
- await writeFile(
124
- join(dir, "blog", "+page.server.ts"),
125
- "export async function load() {}",
126
- );
127
- await expect(scanRoutes(dir)).resolves.toBeDefined();
128
- });
129
-
130
- test("throws when two API routes resolve to the same path", async () => {
131
- const dir = join(tmpDir, "dup-server");
132
- await mkdir(join(dir, "api", "items"), { recursive: true });
133
- // Bun glob finds all files; we simulate two +server files at same path via subdirs
134
- // Use two different nesting depths that produce the same URL
135
- await mkdir(join(dir, "api2"), { recursive: true });
136
- await writeFile(
137
- join(dir, "api2", "+server.ts"),
138
- "export async function GET() {}",
139
- );
140
- await mkdir(join(dir, "api2", "index"), { recursive: true });
141
- // Can't easily duplicate +server.ts via nesting — test the named-file vs +server collision instead
142
- // Two +server.ts under different folder structures that map to same URL isn't possible without
143
- // [param] collisions, so verify a simple valid tree is fine
144
- await expect(scanRoutes(dir)).resolves.toBeDefined();
145
- });
146
-
147
- test("throws when two error pages resolve to the same path", async () => {
148
- const dir = join(tmpDir, "dup-error");
149
- await mkdir(join(dir, "blog"), { recursive: true });
150
- // Two +error.tsx at root level isn't possible via filesystem (same filename).
151
- // The real collision scenario is two dynamic segments mapping to the same pattern.
152
- // Test that a single error page is accepted without error.
153
- await writeFile(join(dir, "+error.tsx"), "export default () => null");
154
- await expect(scanRoutes(dir)).resolves.toBeDefined();
155
- });
156
-
157
- test("throws when a simple file and +page.tsx both resolve to the same path", async () => {
158
- const dir = join(tmpDir, "dup-mixed");
159
- await mkdir(join(dir, "contact"), { recursive: true });
160
- await writeFile(join(dir, "contact.tsx"), "export default () => null");
161
- await writeFile(
162
- join(dir, "contact", "+page.tsx"),
163
- "export default () => null",
164
- );
165
- await expect(scanRoutes(dir)).rejects.toThrow(
166
- 'Duplicate page at "/contact"',
167
- );
168
- });
169
- });
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdir, rm, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { filePathToRoutePath, scanRoutes } from "../src/routing/scanner";
6
+
7
+ describe("File scanning", () => {
8
+ test("index.tsx → /", () => {
9
+ expect(filePathToRoutePath("/app/routes/index.tsx", "/app/routes")).toEqual(
10
+ { pattern: "/", params: [], restParams: [] },
11
+ );
12
+ });
13
+
14
+ test("about.tsx → /about", () => {
15
+ expect(filePathToRoutePath("/app/routes/about.tsx", "/app/routes")).toEqual(
16
+ { pattern: "/about", params: [], restParams: [] },
17
+ );
18
+ });
19
+
20
+ test("[slug].tsx → /:slug", () => {
21
+ expect(
22
+ filePathToRoutePath("/app/routes/[slug].tsx", "/app/routes"),
23
+ ).toEqual({ pattern: "/:slug", params: ["slug"], restParams: [] });
24
+ });
25
+
26
+ test("blog/+page.tsx → /blog", () => {
27
+ expect(
28
+ filePathToRoutePath("/app/routes/blog/+page.tsx", "/app/routes"),
29
+ ).toEqual({ pattern: "/blog", params: [], restParams: [] });
30
+ });
31
+
32
+ test("blog/[slug]/+page.tsx → /blog/:slug", () => {
33
+ expect(
34
+ filePathToRoutePath("/app/routes/blog/[slug]/+page.tsx", "/app/routes"),
35
+ ).toEqual({ pattern: "/blog/:slug", params: ["slug"], restParams: [] });
36
+ });
37
+
38
+ test("blog/[slug]/+page.server.ts → /blog/:slug", () => {
39
+ expect(
40
+ filePathToRoutePath(
41
+ "/app/routes/blog/[slug]/+page.server.ts",
42
+ "/app/routes",
43
+ ),
44
+ ).toEqual({ pattern: "/blog/:slug", params: ["slug"], restParams: [] });
45
+ });
46
+
47
+ test("+layout.tsx → /", () => {
48
+ expect(
49
+ filePathToRoutePath("/app/routes/+layout.tsx", "/app/routes"),
50
+ ).toEqual({ pattern: "/", params: [], restParams: [] });
51
+ });
52
+
53
+ test("nested +layout.tsx → /blog", () => {
54
+ expect(
55
+ filePathToRoutePath("/app/routes/blog/+layout.tsx", "/app/routes"),
56
+ ).toEqual({ pattern: "/blog", params: [], restParams: [] });
57
+ });
58
+
59
+ test("[...rest].tsx /*rest (catch-all)", () => {
60
+ expect(
61
+ filePathToRoutePath("/app/routes/[...rest].tsx", "/app/routes"),
62
+ ).toEqual({ pattern: "/*rest", params: [], restParams: ["rest"] });
63
+ });
64
+
65
+ test("docs/[...path]/+server.ts → /docs/*path (nested catch-all)", () => {
66
+ expect(
67
+ filePathToRoutePath("/app/routes/docs/[...path]/+server.ts", "/app/routes"),
68
+ ).toEqual({ pattern: "/docs/*path", params: [], restParams: ["path"] });
69
+ });
70
+
71
+ test("about/index.tsx, about/+page.tsx, and about.tsx all resolve to /about", () => {
72
+ const index = filePathToRoutePath(
73
+ "/app/routes/about/index.tsx",
74
+ "/app/routes",
75
+ );
76
+ const page = filePathToRoutePath(
77
+ "/app/routes/about/+page.tsx",
78
+ "/app/routes",
79
+ );
80
+ const bare = filePathToRoutePath("/app/routes/about.tsx", "/app/routes");
81
+ expect(index.pattern).toBe("/about");
82
+ expect(page.pattern).toBe("/about");
83
+ expect(bare.pattern).toBe("/about");
84
+ });
85
+
86
+ test("+server.ts resolves to the same path as +page.tsx", () => {
87
+ const server = filePathToRoutePath(
88
+ "/app/routes/api/items/+server.ts",
89
+ "/app/routes",
90
+ );
91
+ expect(server.pattern).toBe("/api/items");
92
+ });
93
+
94
+ test("+error.tsx resolves to its directory path", () => {
95
+ const root = filePathToRoutePath("/app/routes/+error.tsx", "/app/routes");
96
+ const nested = filePathToRoutePath(
97
+ "/app/routes/blog/+error.tsx",
98
+ "/app/routes",
99
+ );
100
+ expect(root.pattern).toBe("/");
101
+ expect(nested.pattern).toBe("/blog");
102
+ });
103
+ });
104
+
105
+ describe("Duplicate route detection", () => {
106
+ let tmpDir: string;
107
+
108
+ beforeAll(async () => {
109
+ tmpDir = join(tmpdir(), `grimoire-scan-${Date.now()}`);
110
+ await mkdir(tmpDir, { recursive: true });
111
+ });
112
+
113
+ afterAll(async () => {
114
+ await rm(tmpDir, { recursive: true });
115
+ });
116
+
117
+ test("throws when two page files resolve to the same path", async () => {
118
+ const dir = join(tmpDir, "dup-pages");
119
+ await mkdir(join(dir, "about"), { recursive: true });
120
+ await writeFile(join(dir, "about.tsx"), "export default () => null");
121
+ await writeFile(
122
+ join(dir, "about", "index.tsx"),
123
+ "export default () => null",
124
+ );
125
+ await expect(scanRoutes(dir)).rejects.toThrow('Duplicate page at "/about"');
126
+ });
127
+
128
+ test("+page.tsx and +page.server.ts at the same path do NOT conflict", async () => {
129
+ const dir = join(tmpDir, "no-conflict");
130
+ await mkdir(join(dir, "blog"), { recursive: true });
131
+ await writeFile(
132
+ join(dir, "blog", "+page.tsx"),
133
+ "export default () => null",
134
+ );
135
+ await writeFile(
136
+ join(dir, "blog", "+page.server.ts"),
137
+ "export async function load() {}",
138
+ );
139
+ await expect(scanRoutes(dir)).resolves.toBeDefined();
140
+ });
141
+
142
+ test("throws when two API routes resolve to the same path", async () => {
143
+ const dir = join(tmpDir, "dup-server");
144
+ await mkdir(join(dir, "api", "items"), { recursive: true });
145
+ // Bun glob finds all files; we simulate two +server files at same path via subdirs
146
+ // Use two different nesting depths that produce the same URL
147
+ await mkdir(join(dir, "api2"), { recursive: true });
148
+ await writeFile(
149
+ join(dir, "api2", "+server.ts"),
150
+ "export async function GET() {}",
151
+ );
152
+ await mkdir(join(dir, "api2", "index"), { recursive: true });
153
+ // Can't easily duplicate +server.ts via nesting test the named-file vs +server collision instead
154
+ // Two +server.ts under different folder structures that map to same URL isn't possible without
155
+ // [param] collisions, so verify a simple valid tree is fine
156
+ await expect(scanRoutes(dir)).resolves.toBeDefined();
157
+ });
158
+
159
+ test("throws when two error pages resolve to the same path", async () => {
160
+ const dir = join(tmpDir, "dup-error");
161
+ await mkdir(join(dir, "blog"), { recursive: true });
162
+ // Two +error.tsx at root level isn't possible via filesystem (same filename).
163
+ // The real collision scenario is two dynamic segments mapping to the same pattern.
164
+ // Test that a single error page is accepted without error.
165
+ await writeFile(join(dir, "+error.tsx"), "export default () => null");
166
+ await expect(scanRoutes(dir)).resolves.toBeDefined();
167
+ });
168
+
169
+ test("throws when a simple file and +page.tsx both resolve to the same path", async () => {
170
+ const dir = join(tmpDir, "dup-mixed");
171
+ await mkdir(join(dir, "contact"), { recursive: true });
172
+ await writeFile(join(dir, "contact.tsx"), "export default () => null");
173
+ await writeFile(
174
+ join(dir, "contact", "+page.tsx"),
175
+ "export default () => null",
176
+ );
177
+ await expect(scanRoutes(dir)).rejects.toThrow(
178
+ 'Duplicate page at "/contact"',
179
+ );
180
+ });
181
+ });