@sigil-dev/grimoire 0.7.5 → 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 -27
  28. package/src/rendering/index.ts +199 -186
  29. package/src/rendering/ssrPlugin.ts +53 -47
  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 -101
  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 +144 -70
  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,4 +1,5 @@
1
- import type { ServerWebSocket } from "bun";
1
+ import { isAbsolute, join } from "node:path";
2
+ import { plugin as bunPlugin, type ServerWebSocket } from "bun";
2
3
  import { renderRoute } from "../rendering";
3
4
  import { registerSSRPlugin } from "../rendering/ssrPlugin";
4
5
  import { findClosestError, matchRoute } from "../routing/router";
@@ -10,6 +11,8 @@ import { buildProject } from "./build";
10
11
  import { createCookies } from "./cookie-utils";
11
12
  import type {
12
13
  Handle,
14
+ HandleError,
15
+ HandleFetch,
13
16
  InitFunction,
14
17
  RequestEvent,
15
18
  ResolveFunction,
@@ -17,15 +20,23 @@ import type {
17
20
  import { runDeserializeLocals, runHook, runRequestHooks } from "./plugins";
18
21
 
19
22
  /**
20
- * Try to load hooks.index.ts from the project root.
23
+ * Try to load hooks.server.ts from the project root.
21
24
  */
22
- async function loadHooks(
23
- projectRoot: string,
24
- ): Promise<{ handle?: Handle; init?: InitFunction }> {
25
+ async function loadHooks(projectRoot: string): Promise<{
26
+ handle?: Handle;
27
+ init?: InitFunction;
28
+ handleError?: HandleError;
29
+ handleFetch?: HandleFetch;
30
+ }> {
25
31
  const hooksPath = `${projectRoot}/hooks.server.ts`;
26
32
  try {
27
33
  const mod = await import(hooksPath);
28
- return { handle: mod.handle, init: mod.init };
34
+ return {
35
+ handle: mod.handle,
36
+ init: mod.init,
37
+ handleError: mod.handleError,
38
+ handleFetch: mod.handleFetch,
39
+ };
29
40
  } catch {
30
41
  // no hooks file — that's fine
31
42
  return {};
@@ -48,12 +59,12 @@ export async function createServer(config: GrimoireConfig = {}) {
48
59
  finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
49
60
  }
50
61
 
51
- //w timings????
52
62
  const {
53
63
  port = 3000,
54
64
  host = "localhost",
55
65
  plugins = [],
56
66
  routes = "src/routes",
67
+ cspNonce,
57
68
  _skipBuild = false,
58
69
  } = finalConfig;
59
70
 
@@ -64,9 +75,8 @@ export async function createServer(config: GrimoireConfig = {}) {
64
75
  tree = _tree;
65
76
  } else {
66
77
  // worker — build already done by loom, just scan routes
67
- const { isAbsolute, join } = await import("node:path");
68
- const { scanRoutes } = await import("../routing/scanner");
69
78
  const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
79
+ const { scanRoutes } = await import("../routing/scanner");
70
80
  tree = await scanRoutes(routesDir, process.cwd());
71
81
  }
72
82
 
@@ -74,21 +84,42 @@ export async function createServer(config: GrimoireConfig = {}) {
74
84
  // WHAT ARE WE DOINGGGGGGGGG
75
85
  registerSSRPlugin(plugins);
76
86
 
77
- // Load hooks.index.ts
78
- const { handle: hooksHandle, init: hooksInit } = await loadHooks(
79
- process.cwd(),
80
- );
87
+ // Load hooks.server.ts
88
+ const {
89
+ handle: hooksHandle,
90
+ init: hooksInit,
91
+ handleError: hooksHandleError,
92
+ handleFetch: hooksHandleFetch,
93
+ } = await loadHooks(process.cwd());
81
94
 
82
95
  // Run init hook if present
83
96
  await hooksInit?.();
84
97
 
98
+ // Register alias plugin so dynamically-imported +server.ts routes can resolve custom aliases
99
+ const aliases = finalConfig.alias ?? {};
100
+ if (Object.keys(aliases).length > 0) {
101
+ bunPlugin({
102
+ name: "grimoire-alias",
103
+ setup(build) {
104
+ for (const [prefix, target] of Object.entries(aliases)) {
105
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
106
+ build.onResolve(
107
+ { filter: new RegExp(`^${escaped}(/|$)`) },
108
+ (args) => ({
109
+ path: args.path.replace(prefix, join(process.cwd(), target)),
110
+ }),
111
+ );
112
+ }
113
+ },
114
+ });
115
+ }
116
+
85
117
  const server = Bun.serve({
86
118
  port,
87
119
  hostname: host,
88
120
  fetch: async (req) => {
89
- // Shared locals object for this request — same reference across all route handlers.
90
- // hooks.index.ts handle() runs via resolve() for page routes only.
91
- // For server/WebSocket routes, auth must use cookies/headers in upgrade().
121
+ // Shared locals object for this request.
122
+ // hooks.server.ts handle() runs for ALL routes (page, server, WebSocket).
92
123
  // If this request came from a coordinator, deserialize its locals
93
124
  const rawLocals = req.headers.get("X-Grimoire-Locals");
94
125
  const locals: App.Locals = rawLocals
@@ -118,49 +149,6 @@ export async function createServer(config: GrimoireConfig = {}) {
118
149
  return new Response("Not Found", { status: 404 });
119
150
  }
120
151
 
121
- // API routes (+server.ts)
122
- if (matched.route.type === "server") {
123
- const mod = await import(matched.route.filePath);
124
-
125
- // WebSocket upgrade path
126
- const isWsUpgrade =
127
- req.headers.get("upgrade")?.toLowerCase() === "websocket";
128
- if (isWsUpgrade && mod.websocket) {
129
- let extraData: Record<string, unknown> = {};
130
- if (mod.upgrade) {
131
- try {
132
- const result = await mod.upgrade({
133
- request: req,
134
- params: matched.params,
135
- url,
136
- locals,
137
- });
138
- if (result && typeof result === "object") extraData = result;
139
- } catch {
140
- return new Response("Upgrade Required", { status: 426 });
141
- }
142
- }
143
- const wsData: _WsInternalData = {
144
- params: matched.params,
145
- __handler: mod.websocket,
146
- ...extraData,
147
- };
148
- //@ts-expect-error i dont know what you are talking about please
149
- if (server.upgrade(req, { data: wsData })) {
150
- // Bun sends the 101 response — return undefined to signal no HTTP response
151
- return undefined as unknown as Response;
152
- }
153
- return new Response("Upgrade Required", { status: 426 });
154
- }
155
-
156
- // HTTP method dispatch
157
- const handler = mod[req.method];
158
- if (!handler) {
159
- return new Response("Method Not Allowed", { status: 405 });
160
- }
161
- return handler({ request: req, params: matched.params, url, locals });
162
- }
163
-
164
152
  const HTTP_METHODS = [
165
153
  "GET",
166
154
  "POST",
@@ -182,12 +170,72 @@ export async function createServer(config: GrimoireConfig = {}) {
182
170
  params: matched.params,
183
171
  locals,
184
172
  cookies,
173
+ route: { id: matched.route.path },
174
+ fetch: globalThis.fetch,
185
175
  setHeaders: (headers) => Object.assign(setHeadersMap, headers),
186
176
  };
187
177
 
188
- // Resolve function: runs the actual page/form action logic
178
+ // Patch event.fetch if handleFetch hook is defined
179
+ if (hooksHandleFetch) {
180
+ event.fetch = (reqInfo: RequestInfo | URL, init?: RequestInit) => {
181
+ const r = new Request(reqInfo, init);
182
+ return Promise.resolve(
183
+ hooksHandleFetch({ request: r, fetch: globalThis.fetch, event }),
184
+ );
185
+ };
186
+ }
187
+
188
+ // Resolve function: runs the actual route logic (server, form action, or page)
189
189
  const resolve: ResolveFunction = async (evt) => {
190
- // form actions
190
+ // API routes (+server.ts) — pass through hooks chain like all other routes
191
+ if (matched.route.type === "server") {
192
+ const mod = await import(matched.route.filePath);
193
+
194
+ // WebSocket upgrade path
195
+ const isWsUpgrade =
196
+ evt.request.headers.get("upgrade")?.toLowerCase() === "websocket";
197
+ if (isWsUpgrade && mod.websocket) {
198
+ let extraData: Record<string, unknown> = {};
199
+ if (mod.upgrade) {
200
+ try {
201
+ const result = await mod.upgrade({
202
+ request: evt.request,
203
+ params: matched.params,
204
+ url: evt.url,
205
+ locals: evt.locals,
206
+ });
207
+ if (result && typeof result === "object") extraData = result;
208
+ } catch {
209
+ return new Response("Upgrade Required", { status: 426 });
210
+ }
211
+ }
212
+ const wsData: _WsInternalData = {
213
+ params: matched.params,
214
+ __handler: mod.websocket,
215
+ ...extraData,
216
+ };
217
+ //@ts-expect-error i dont know what you are talking about please
218
+ if (server.upgrade(req, { data: wsData })) {
219
+ // Bun sends the 101 response — return undefined to signal no HTTP response
220
+ return undefined as unknown as Response;
221
+ }
222
+ return new Response("Upgrade Required", { status: 426 });
223
+ }
224
+
225
+ // HTTP method dispatch
226
+ const handler = mod[evt.request.method];
227
+ if (!handler) {
228
+ return new Response("Method Not Allowed", { status: 405 });
229
+ }
230
+ return handler({
231
+ request: evt.request,
232
+ params: matched.params,
233
+ url: evt.url,
234
+ locals: evt.locals,
235
+ });
236
+ }
237
+
238
+ // form actions (+page.server.ts)
191
239
  if (
192
240
  matched.pageServer &&
193
241
  HTTP_METHODS.includes(evt.request.method as any)
@@ -267,6 +315,7 @@ export async function createServer(config: GrimoireConfig = {}) {
267
315
  undefined,
268
316
  evt.locals,
269
317
  plugins,
318
+ cspNonce,
270
319
  );
271
320
 
272
321
  if (evt.request.headers.get("x-grimoire-navigate") === "1") {
@@ -294,17 +343,42 @@ export async function createServer(config: GrimoireConfig = {}) {
294
343
  return response;
295
344
  };
296
345
 
297
- // If no hooks handle, resolve directly
298
- if (!hooksHandle) {
299
- return resolve(event);
346
+ // Run through hooks chain, or resolve directly if no hooks defined
347
+ let response: Response;
348
+ try {
349
+ if (!hooksHandle) {
350
+ response = await resolve(event);
351
+ } else {
352
+ response = await hooksHandle({ event, resolve });
353
+ }
354
+ } catch (err) {
355
+ console.error(
356
+ `[grimoire] ${event.request.method} ${event.url.pathname} failed:`,
357
+ err,
358
+ );
359
+ await hooksHandleError?.({
360
+ error: err,
361
+ event,
362
+ status: 500,
363
+ message: "Internal Server Error",
364
+ });
365
+ const errorHtmlPath = `${process.cwd()}/error.html`;
366
+ try {
367
+ const errorFile = Bun.file(errorHtmlPath);
368
+ if (await errorFile.exists()) {
369
+ return new Response(errorFile, {
370
+ status: 500,
371
+ headers: { "Content-Type": "text/html" },
372
+ });
373
+ }
374
+ } catch {}
375
+ const message =
376
+ process.env.NODE_ENV === "production"
377
+ ? "Internal Server Error"
378
+ : `${err instanceof Error ? `${err.message}\n${err.stack}` : String(err)}`;
379
+ return new Response(message, { status: 500 });
300
380
  }
301
381
 
302
- // Run through hooks chain
303
- let response = await hooksHandle({
304
- event,
305
- resolve,
306
- });
307
-
308
382
  // Apply setHeaders
309
383
  const setCookieHeaders = cookies.toHeaders();
310
384
  if (
@@ -1,59 +1,59 @@
1
- import { join } from "node:path";
2
- import type { GrimoireConfig, GrimoirePlugin, WorkerMode } from "../types";
3
- import { createServer } from "./index";
4
- import { runDeserializeLocals } from "./plugins";
5
-
6
- export interface WorkerOptions {
7
- config: GrimoireConfig;
8
- plugins: GrimoirePlugin[];
9
- secret: string;
10
- mode: WorkerMode;
11
- }
12
-
13
- export async function startWorker(options: WorkerOptions) {
14
- const { secret, plugins } = options;
15
-
16
- const workerAuthPlugin: GrimoirePlugin = {
17
- name: "__grimoire-worker-auth",
18
- async onRequest(req, next) {
19
- if (req.headers.get("X-Grimoire-Internal") !== secret) {
20
- return new Response("Forbidden", { status: 403 });
21
- }
22
- return next();
23
- },
24
- };
25
-
26
- return createServer({
27
- ...options.config,
28
- plugins: [workerAuthPlugin, ...plugins],
29
- _skipBuild: true,
30
- });
31
- }
32
-
33
- // bootstrap when spawned by coordinator
34
- if (process.env.GRIMOIRE_INTERNAL_SECRET) {
35
- const secret = process.env.GRIMOIRE_INTERNAL_SECRET;
36
- const port = Number(process.env.GRIMOIRE_WORKER_PORT ?? 3001);
37
- const cwd = process.cwd();
38
-
39
- let config: GrimoireConfig = {};
40
- try {
41
- const mod = await import(join(cwd, "sigil.config.ts"));
42
- config = mod.default ?? {};
43
- } catch {}
44
-
45
- let finalConfig: GrimoireConfig = { ...config, port, host: "127.0.0.1" };
46
- const plugins = finalConfig.plugins ?? [];
47
- for (const plugin of plugins) {
48
- finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
49
- }
50
-
51
- await startWorker({
52
- config: finalConfig,
53
- plugins,
54
- secret,
55
- mode: (process.env.GRIMOIRE_MODE ?? "full") as WorkerMode,
56
- });
57
-
58
- process.send?.({ ready: true });
59
- }
1
+ import { join } from "node:path";
2
+ import type { GrimoireConfig, GrimoirePlugin, WorkerMode } from "../types";
3
+ import { createServer } from "./index";
4
+ import { runDeserializeLocals } from "./plugins";
5
+
6
+ export interface WorkerOptions {
7
+ config: GrimoireConfig;
8
+ plugins: GrimoirePlugin[];
9
+ secret: string;
10
+ mode: WorkerMode;
11
+ }
12
+
13
+ export async function startWorker(options: WorkerOptions) {
14
+ const { secret, plugins } = options;
15
+
16
+ const workerAuthPlugin: GrimoirePlugin = {
17
+ name: "__grimoire-worker-auth",
18
+ async onRequest(req, next) {
19
+ if (req.headers.get("X-Grimoire-Internal") !== secret) {
20
+ return new Response("Forbidden", { status: 403 });
21
+ }
22
+ return next();
23
+ },
24
+ };
25
+
26
+ return createServer({
27
+ ...options.config,
28
+ plugins: [workerAuthPlugin, ...plugins],
29
+ _skipBuild: true,
30
+ });
31
+ }
32
+
33
+ // bootstrap when spawned by coordinator
34
+ if (process.env.GRIMOIRE_INTERNAL_SECRET) {
35
+ const secret = process.env.GRIMOIRE_INTERNAL_SECRET;
36
+ const port = Number(process.env.GRIMOIRE_WORKER_PORT ?? 3001);
37
+ const cwd = process.cwd();
38
+
39
+ let config: GrimoireConfig = {};
40
+ try {
41
+ const mod = await import(join(cwd, "sigil.config.ts"));
42
+ config = mod.default ?? {};
43
+ } catch {}
44
+
45
+ let finalConfig: GrimoireConfig = { ...config, port, host: "127.0.0.1" };
46
+ const plugins = finalConfig.plugins ?? [];
47
+ for (const plugin of plugins) {
48
+ finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
49
+ }
50
+
51
+ await startWorker({
52
+ config: finalConfig,
53
+ plugins,
54
+ secret,
55
+ mode: (process.env.GRIMOIRE_MODE ?? "full") as WorkerMode,
56
+ });
57
+
58
+ process.send?.({ ready: true });
59
+ }