@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,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,16 +59,15 @@ 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
 
60
- registerSSRPlugin(plugins);
61
71
  let tree: any;
62
72
  if (!config._skipBuild) {
63
73
  const { result, tree: _tree } = await buildProject(finalConfig, plugins);
@@ -65,27 +75,51 @@ export async function createServer(config: GrimoireConfig = {}) {
65
75
  tree = _tree;
66
76
  } else {
67
77
  // worker — build already done by loom, just scan routes
68
- const { isAbsolute, join } = await import("node:path");
69
- const { scanRoutes } = await import("../routing/scanner");
70
78
  const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
79
+ const { scanRoutes } = await import("../routing/scanner");
71
80
  tree = await scanRoutes(routesDir, process.cwd());
72
81
  }
73
82
 
74
- // Load hooks.index.ts
75
- const { handle: hooksHandle, init: hooksInit } = await loadHooks(
76
- process.cwd(),
77
- );
83
+ // SSR plugin should NOT INTNERCEPT FILES BUNDLEDFOR CLIENT
84
+ // WHAT ARE WE DOINGGGGGGGGG
85
+ registerSSRPlugin(plugins);
86
+
87
+ // Load hooks.server.ts
88
+ const {
89
+ handle: hooksHandle,
90
+ init: hooksInit,
91
+ handleError: hooksHandleError,
92
+ handleFetch: hooksHandleFetch,
93
+ } = await loadHooks(process.cwd());
78
94
 
79
95
  // Run init hook if present
80
96
  await hooksInit?.();
81
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
+
82
117
  const server = Bun.serve({
83
118
  port,
84
119
  hostname: host,
85
120
  fetch: async (req) => {
86
- // Shared locals object for this request — same reference across all route handlers.
87
- // hooks.index.ts handle() runs via resolve() for page routes only.
88
- // 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).
89
123
  // If this request came from a coordinator, deserialize its locals
90
124
  const rawLocals = req.headers.get("X-Grimoire-Locals");
91
125
  const locals: App.Locals = rawLocals
@@ -115,49 +149,6 @@ export async function createServer(config: GrimoireConfig = {}) {
115
149
  return new Response("Not Found", { status: 404 });
116
150
  }
117
151
 
118
- // API routes (+server.ts)
119
- if (matched.route.type === "server") {
120
- const mod = await import(matched.route.filePath);
121
-
122
- // WebSocket upgrade path
123
- const isWsUpgrade =
124
- req.headers.get("upgrade")?.toLowerCase() === "websocket";
125
- if (isWsUpgrade && mod.websocket) {
126
- let extraData: Record<string, unknown> = {};
127
- if (mod.upgrade) {
128
- try {
129
- const result = await mod.upgrade({
130
- request: req,
131
- params: matched.params,
132
- url,
133
- locals,
134
- });
135
- if (result && typeof result === "object") extraData = result;
136
- } catch {
137
- return new Response("Upgrade Required", { status: 426 });
138
- }
139
- }
140
- const wsData: _WsInternalData = {
141
- params: matched.params,
142
- __handler: mod.websocket,
143
- ...extraData,
144
- };
145
- //@ts-expect-error i dont know what you are talking about please
146
- if (server.upgrade(req, { data: wsData })) {
147
- // Bun sends the 101 response — return undefined to signal no HTTP response
148
- return undefined as unknown as Response;
149
- }
150
- return new Response("Upgrade Required", { status: 426 });
151
- }
152
-
153
- // HTTP method dispatch
154
- const handler = mod[req.method];
155
- if (!handler) {
156
- return new Response("Method Not Allowed", { status: 405 });
157
- }
158
- return handler({ request: req, params: matched.params, url, locals });
159
- }
160
-
161
152
  const HTTP_METHODS = [
162
153
  "GET",
163
154
  "POST",
@@ -179,12 +170,72 @@ export async function createServer(config: GrimoireConfig = {}) {
179
170
  params: matched.params,
180
171
  locals,
181
172
  cookies,
173
+ route: { id: matched.route.path },
174
+ fetch: globalThis.fetch,
182
175
  setHeaders: (headers) => Object.assign(setHeadersMap, headers),
183
176
  };
184
177
 
185
- // 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)
186
189
  const resolve: ResolveFunction = async (evt) => {
187
- // 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)
188
239
  if (
189
240
  matched.pageServer &&
190
241
  HTTP_METHODS.includes(evt.request.method as any)
@@ -264,6 +315,7 @@ export async function createServer(config: GrimoireConfig = {}) {
264
315
  undefined,
265
316
  evt.locals,
266
317
  plugins,
318
+ cspNonce,
267
319
  );
268
320
 
269
321
  if (evt.request.headers.get("x-grimoire-navigate") === "1") {
@@ -291,17 +343,42 @@ export async function createServer(config: GrimoireConfig = {}) {
291
343
  return response;
292
344
  };
293
345
 
294
- // If no hooks handle, resolve directly
295
- if (!hooksHandle) {
296
- 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 });
297
380
  }
298
381
 
299
- // Run through hooks chain
300
- let response = await hooksHandle({
301
- event,
302
- resolve,
303
- });
304
-
305
382
  // Apply setHeaders
306
383
  const setCookieHeaders = cookies.toHeaders();
307
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
+ }