@sigil-dev/grimoire 0.5.0 → 0.6.0

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 (51) hide show
  1. package/README.md +174 -174
  2. package/index.ts +34 -16
  3. package/package.json +6 -6
  4. package/src/{enhance.ts → client/enhance.ts} +2 -1
  5. package/src/client/index.ts +5 -0
  6. package/src/{client-router.ts → client/router.ts} +1 -1
  7. package/src/{vite-plugin.ts → integrations/vite.ts} +4 -4
  8. package/src/{hydrate.ts → rendering/hydrate.ts} +2 -2
  9. package/src/{renderer.ts → rendering/index.ts} +29 -16
  10. package/src/{ssrPlugin.ts → rendering/ssrPlugin.ts} +3 -2
  11. package/src/{scanner.ts → routing/scanner.ts} +16 -4
  12. package/src/{transform-routes.ts → routing/transform-routes.ts} +3 -2
  13. package/src/{fail.ts → sentinels/fail.ts} +1 -1
  14. package/src/{build.ts → server/build.ts} +12 -10
  15. package/src/server/coordinator.ts +297 -0
  16. package/src/{hooks.ts → server/hooks.ts} +1 -1
  17. package/src/{server.ts → server/index.ts} +85 -37
  18. package/src/server/plugins.ts +119 -0
  19. package/src/server/worker.ts +59 -0
  20. package/src/{typegen.ts → typegen/index.ts} +5 -2
  21. package/src/types.ts +134 -3
  22. package/test/context.test.ts +1 -1
  23. package/test/fail.test.ts +1 -1
  24. package/test/headers.test.ts +6 -2
  25. package/test/hydration.test.ts +1 -1
  26. package/test/middleware.test.ts +8 -4
  27. package/test/preload.ts +1 -1
  28. package/test/redirect-error.test.ts +2 -2
  29. package/test/rendering.test.ts +15 -6
  30. package/test/routing.test.ts +2 -2
  31. package/test/scanning.test.ts +40 -11
  32. package/test/scope.test.ts +25 -10
  33. package/test/server.test.ts +39 -10
  34. package/test/streaming.test.ts +21 -8
  35. package/test/transform-routes.test.ts +2 -2
  36. package/test/typegen.test.ts +5 -3
  37. package/tsconfig.json +3 -1
  38. package/.grimoire/_routes.ts +0 -4
  39. package/public/__grimoire__/client.js +0 -55
  40. package/public/__grimoire__/hydrate.js +0 -63
  41. package/src/client.ts +0 -4
  42. package/src/plugins.ts +0 -39
  43. package/src/sync.ts +0 -18
  44. /package/src/{scope.ts → client/scope.ts} +0 -0
  45. /package/src/{head.ts → rendering/head.ts} +0 -0
  46. /package/src/{manifest-gen.ts → routing/manifest-gen.ts} +0 -0
  47. /package/src/{router.ts → routing/router.ts} +0 -0
  48. /package/src/{error.ts → sentinels/error.ts} +0 -0
  49. /package/src/{redirect.ts → sentinels/redirect.ts} +0 -0
  50. /package/src/{context.ts → server/context.ts} +0 -0
  51. /package/src/{cookie-utils.ts → server/cookie-utils.ts} +0 -0
@@ -73,7 +73,9 @@ describe("Server — API routes (+server.ts)", () => {
73
73
 
74
74
  test("unsupported method returns 405", async () => {
75
75
  const server = await createServer({ port: 3004, routes: tmpDir });
76
- const res = await fetch("http://localhost:3004/api/items", { method: "DELETE" });
76
+ const res = await fetch("http://localhost:3004/api/items", {
77
+ method: "DELETE",
78
+ });
77
79
  expect(res.status).toBe(405);
78
80
  server.stop();
79
81
  });
@@ -143,15 +145,25 @@ export const websocket = {
143
145
  return new Promise((resolve, reject) => {
144
146
  const ws = new WebSocket(`ws://localhost:3005${path}`);
145
147
  ws.onopen = () => resolve(ws);
146
- ws.onerror = () => reject(new Error(`WebSocket connection failed: ${path}`));
148
+ ws.onerror = () =>
149
+ reject(new Error(`WebSocket connection failed: ${path}`));
147
150
  });
148
151
  }
149
152
 
150
153
  function nextMessage(ws: WebSocket): Promise<string> {
151
154
  return new Promise((resolve, reject) => {
152
- const timer = setTimeout(() => reject(new Error("timeout waiting for message")), 2000);
153
- ws.onmessage = (ev) => { clearTimeout(timer); resolve(String(ev.data)); };
154
- ws.onerror = () => { clearTimeout(timer); reject(new Error("ws error")); };
155
+ const timer = setTimeout(
156
+ () => reject(new Error("timeout waiting for message")),
157
+ 2000,
158
+ );
159
+ ws.onmessage = (ev) => {
160
+ clearTimeout(timer);
161
+ resolve(String(ev.data));
162
+ };
163
+ ws.onerror = () => {
164
+ clearTimeout(timer);
165
+ reject(new Error("ws error"));
166
+ };
155
167
  });
156
168
  }
157
169
 
@@ -169,8 +181,14 @@ export const websocket = {
169
181
  // just verify connection opens cleanly (sessionId is in ws.data server-side)
170
182
  await new Promise<void>((resolve, reject) => {
171
183
  const t = setTimeout(() => reject(new Error("timeout")), 2000);
172
- ws.onopen = () => { clearTimeout(t); resolve(); };
173
- ws.onerror = () => { clearTimeout(t); reject(new Error("connect failed")); };
184
+ ws.onopen = () => {
185
+ clearTimeout(t);
186
+ resolve();
187
+ };
188
+ ws.onerror = () => {
189
+ clearTimeout(t);
190
+ reject(new Error("connect failed"));
191
+ };
174
192
  });
175
193
  ws.close();
176
194
  });
@@ -185,9 +203,20 @@ export const websocket = {
185
203
  await new Promise<void>((resolve, reject) => {
186
204
  const ws = new WebSocket("ws://localhost:3005/reject");
187
205
  const t = setTimeout(() => reject(new Error("timeout")), 2000);
188
- ws.onopen = () => { clearTimeout(t); ws.close(); reject(new Error("should not have opened")); };
189
- ws.onerror = () => { clearTimeout(t); resolve(); };
190
- ws.onclose = (ev) => { clearTimeout(t); if (ev.code !== 1006) resolve(); else resolve(); };
206
+ ws.onopen = () => {
207
+ clearTimeout(t);
208
+ ws.close();
209
+ reject(new Error("should not have opened"));
210
+ };
211
+ ws.onerror = () => {
212
+ clearTimeout(t);
213
+ resolve();
214
+ };
215
+ ws.onclose = (ev) => {
216
+ clearTimeout(t);
217
+ if (ev.code !== 1006) resolve();
218
+ else resolve();
219
+ };
191
220
  });
192
221
  });
193
222
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { renderRoute } from "../src/renderer";
2
+ import { renderRoute } from "../src/rendering";
3
3
 
4
4
  describe("Streaming SSR", () => {
5
5
  test("returns a ReadableStream response", async () => {
@@ -7,7 +7,8 @@ describe("Streaming SSR", () => {
7
7
  route: {
8
8
  path: "/",
9
9
  params: {},
10
- filePath: "data:text/javascript,export default (p) => '<div>hello</div>'",
10
+ filePath:
11
+ "data:text/javascript,export default (p) => '<div>hello</div>'",
11
12
  },
12
13
  params: {},
13
14
  } as any;
@@ -27,7 +28,10 @@ describe("Streaming SSR", () => {
27
28
  params: {},
28
29
  } as any;
29
30
 
30
- const res = await renderRoute(matched, new Request("http://localhost/test"));
31
+ const res = await renderRoute(
32
+ matched,
33
+ new Request("http://localhost/test"),
34
+ );
31
35
  const html = await res.text();
32
36
 
33
37
  expect(html).toContain("<!DOCTYPE html>");
@@ -69,7 +73,10 @@ describe("Streaming SSR", () => {
69
73
  params: {},
70
74
  } as any;
71
75
 
72
- const res = await renderRoute(matched, new Request("http://localhost/head-test"));
76
+ const res = await renderRoute(
77
+ matched,
78
+ new Request("http://localhost/head-test"),
79
+ );
73
80
  const html = await res.text();
74
81
 
75
82
  const hydrateScript = html.indexOf("hydrate.js");
@@ -90,7 +97,10 @@ describe("Streaming SSR", () => {
90
97
  params: {},
91
98
  } as any;
92
99
 
93
- const res = await renderRoute(matched, new Request("http://localhost/chunked"));
100
+ const res = await renderRoute(
101
+ matched,
102
+ new Request("http://localhost/chunked"),
103
+ );
94
104
  const reader = res.body!.getReader();
95
105
  const chunks: string[] = [];
96
106
 
@@ -98,7 +108,11 @@ describe("Streaming SSR", () => {
98
108
  const { done, value } = await reader.read();
99
109
  if (done) break;
100
110
  // Bun ReadableStream returns Uint8Array
101
- chunks.push(typeof value === "string" ? value : new TextDecoder().decode(value.buffer));
111
+ chunks.push(
112
+ typeof value === "string"
113
+ ? value
114
+ : new TextDecoder().decode(value.buffer),
115
+ );
102
116
  }
103
117
 
104
118
  // Should have multiple chunks (at least DOCTYPE + body)
@@ -114,8 +128,7 @@ describe("Streaming SSR", () => {
114
128
  route: {
115
129
  path: "/nav",
116
130
  params: {},
117
- filePath:
118
- "data:text/javascript,export default (p) => '<div>nav</div>'",
131
+ filePath: "data:text/javascript,export default (p) => '<div>nav</div>'",
119
132
  },
120
133
  params: {},
121
134
  } as any;
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
- import type { RouteFile } from "../src/scanner";
6
- import { transformRoutes } from "../src/transform-routes";
5
+ import type { RouteFile } from "../src/routing/scanner";
6
+ import { transformRoutes } from "../src/routing/transform-routes";
7
7
 
8
8
  describe("transformRoutes", () => {
9
9
  test("pre-transforms TypeScript TSX routes to unique JavaScript files", async () => {
@@ -3,8 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, sep } from "node:path";
6
- import type { RouteFile } from "../src/scanner";
7
- import { scanRoutes } from "../src/scanner";
6
+ import type { RouteFile } from "../src/routing/scanner";
7
+ import { scanRoutes } from "../src/routing/scanner";
8
8
  import {
9
9
  buildParams,
10
10
  generateTypes,
@@ -418,7 +418,9 @@ describe("generateTypes — route without +page.server.ts", () => {
418
418
 
419
419
  test("PageProps contains data and params", async () => {
420
420
  const content = await readGenerated("src/routes/blog");
421
- expect(content).toContain("export type PageProps = { data: PageData; params: Params };");
421
+ expect(content).toContain(
422
+ "export type PageProps = { data: PageData; params: Params };",
423
+ );
422
424
  });
423
425
  });
424
426
 
package/tsconfig.json CHANGED
@@ -2,6 +2,8 @@
2
2
  "compilerOptions": {
3
3
  "module": "Preserve",
4
4
  "moduleResolution": "bundler",
5
- "lib": ["ESNext", "DOM"]
5
+ "lib": ["ESNext", "DOM"],
6
+ "allowImportingTsExtensions": true,
7
+ "noEmit": true
6
8
  }
7
9
  }
@@ -1,4 +0,0 @@
1
-
2
- export const routes: Record<string, (props: any) => any> = {
3
-
4
- };
@@ -1,55 +0,0 @@
1
- // .grimoire/_routes.ts
2
- var routes = {};
3
-
4
- // src/client-router.ts
5
- var routeMap = {};
6
- async function navigate(path) {
7
- const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
8
- const json = await res.json();
9
- console.log("navigate response:", json);
10
- const { data, params, pattern, head } = json;
11
- const Page = routeMap[pattern];
12
- if (!Page) {
13
- window.location.href = path;
14
- return;
15
- }
16
- const node = Page({ ...data, params });
17
- const slot = document.getElementById("grimoire-page");
18
- if (!slot)
19
- return;
20
- slot.replaceChildren(node);
21
- updateHead(head);
22
- history.pushState({}, "", path);
23
- window.scrollTo(0, 0);
24
- }
25
- function initRouter(routes2) {
26
- routeMap = routes2;
27
- document.addEventListener("click", handleClick);
28
- window.addEventListener("popstate", () => navigate(location.pathname));
29
- }
30
- function updateHead(headHtml) {
31
- document.querySelectorAll("[data-grimoire-head]").forEach((el) => el.remove());
32
- if (!headHtml)
33
- return;
34
- const tmp = document.createElement("head");
35
- tmp.innerHTML = headHtml;
36
- for (const el of Array.from(tmp.children)) {
37
- el.dataset.grimoireHead = "1";
38
- document.head.appendChild(el);
39
- }
40
- }
41
- function handleClick(e) {
42
- const a = e.target.closest("a");
43
- if (!a)
44
- return;
45
- const href = a.getAttribute("href");
46
- if (!href)
47
- return;
48
- if (/^(https?:\/\/|\/\/|#|mailto:|tel:)/.test(href))
49
- return;
50
- e.preventDefault();
51
- navigate(href);
52
- }
53
-
54
- // src/client.ts
55
- initRouter(routes);
@@ -1,63 +0,0 @@
1
- // .grimoire/_routes.ts
2
- var routes = {};
3
-
4
- // src/client-router.ts
5
- var routeMap = {};
6
- async function navigate(path) {
7
- const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
8
- const json = await res.json();
9
- console.log("navigate response:", json);
10
- const { data, params, pattern, head } = json;
11
- const Page = routeMap[pattern];
12
- if (!Page) {
13
- window.location.href = path;
14
- return;
15
- }
16
- const node = Page({ ...data, params });
17
- const slot = document.getElementById("grimoire-page");
18
- if (!slot)
19
- return;
20
- slot.replaceChildren(node);
21
- updateHead(head);
22
- history.pushState({}, "", path);
23
- window.scrollTo(0, 0);
24
- }
25
- function initRouter(routes2) {
26
- routeMap = routes2;
27
- document.addEventListener("click", handleClick);
28
- window.addEventListener("popstate", () => navigate(location.pathname));
29
- }
30
- function updateHead(headHtml) {
31
- document.querySelectorAll("[data-grimoire-head]").forEach((el) => el.remove());
32
- if (!headHtml)
33
- return;
34
- const tmp = document.createElement("head");
35
- tmp.innerHTML = headHtml;
36
- for (const el of Array.from(tmp.children)) {
37
- el.dataset.grimoireHead = "1";
38
- document.head.appendChild(el);
39
- }
40
- }
41
- function handleClick(e) {
42
- const a = e.target.closest("a");
43
- if (!a)
44
- return;
45
- const href = a.getAttribute("href");
46
- if (!href)
47
- return;
48
- if (/^(https?:\/\/|\/\/|#|mailto:|tel:)/.test(href))
49
- return;
50
- e.preventDefault();
51
- navigate(href);
52
- }
53
-
54
- // src/hydrate.ts
55
- var stateEl = document.getElementById("__grimoire_state__");
56
- if (stateEl) {
57
- const state = JSON.parse(stateEl.textContent);
58
- const Page = routes[state.pattern];
59
- if (Page) {
60
- Page({ ...state.data, params: state.params });
61
- }
62
- }
63
- initRouter(routes);
package/src/client.ts DELETED
@@ -1,4 +0,0 @@
1
- import { routes } from "#grimoire-routes";
2
- import { initRouter } from "./client-router.ts";
3
-
4
- initRouter(routes);
package/src/plugins.ts DELETED
@@ -1,39 +0,0 @@
1
- import type { BuildResult, GrimoirePlugin, LoadContext, Route, Server } from "./types";
2
-
3
- // Fire-and-forget hooks routed through runHook.
4
- // onRequest and onRouteRender are intentionally excluded: they have distinct
5
- // calling conventions (middleware chain / inline transform loop) handled in server.ts.
6
- // Keep HookArgs in sync with GrimoirePlugin in types.ts.
7
- type HookArgs = {
8
- onStart: [server: Server];
9
- onStop: [reason: "shutdown" | "restart"];
10
- onBuildStart: [];
11
- onBuildEnd: [result: BuildResult];
12
- onRouteLoad: [route: Route, context: LoadContext];
13
- };
14
- type FireAndForgetHook = keyof HookArgs;
15
-
16
- export async function runHook<K extends FireAndForgetHook>(
17
- plugins: GrimoirePlugin[],
18
- hook: K,
19
- ...args: HookArgs[K]
20
- ): Promise<void> {
21
- for (const plugin of plugins) {
22
- const fn = plugin[hook] as ((...a: any[]) => any) | undefined;
23
- await fn?.(...args);
24
- }
25
- }
26
-
27
- export async function runRequestHooks(
28
- plugins: GrimoirePlugin[],
29
- req: Request,
30
- final: () => Promise<Response>,
31
- ): Promise<Response> {
32
- const chain = plugins
33
- .filter((p) => p.onRequest)
34
- .reduceRight(
35
- (next, plugin) => async () => plugin.onRequest!(req, next),
36
- final,
37
- );
38
- return chain();
39
- }
package/src/sync.ts DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { mkdir } from "node:fs/promises";
3
- import { isAbsolute, join } from "node:path";
4
- import { scanRoutes } from "./scanner";
5
- import { generateTypes } from "./typegen";
6
-
7
- const routes = process.env.GRIMOIRE_ROUTES ?? "src/routes";
8
- const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
9
-
10
- await mkdir(join(process.cwd(), ".grimoire"), { recursive: true });
11
- const tree = await scanRoutes(routesDir, process.cwd());
12
- await generateTypes(tree, {
13
- projectRoot: process.cwd(),
14
- routesDir,
15
- outDir: join(process.cwd(), ".grimoire/types"),
16
- });
17
-
18
- console.log("Grimoire: types generated");
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes