@sigil-dev/grimoire 0.7.5 → 0.7.7

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 (58) 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 +21 -20
  14. package/package.json +13 -7
  15. package/preload.js +3 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/server.ts +13 -13
  19. package/src/client/head.ts +29 -0
  20. package/src/client/router.ts +254 -40
  21. package/src/dev/compile-module.ts +173 -0
  22. package/src/dev/effect-registry.ts +23 -0
  23. package/src/dev/graph.ts +114 -0
  24. package/src/dev/hmr-client.ts +158 -0
  25. package/src/dev/hmr-server.ts +187 -0
  26. package/src/dev/loader.ts +47 -0
  27. package/src/dev/paths.ts +14 -0
  28. package/src/dev/runtime-bundle.ts +49 -0
  29. package/src/dev/watcher.ts +44 -0
  30. package/src/env/index.ts +25 -0
  31. package/src/env/plugin.ts +13 -0
  32. package/src/env/private.ts +5 -0
  33. package/src/env/public.ts +7 -0
  34. package/src/env/typegen.ts +51 -0
  35. package/src/integrations/vite.ts +1 -0
  36. package/src/rendering/head.ts +22 -2
  37. package/src/rendering/hydrate.ts +111 -18
  38. package/src/rendering/index.ts +263 -153
  39. package/src/rendering/ssrPlugin.ts +59 -39
  40. package/src/routing/manifest-gen.ts +18 -2
  41. package/src/routing/router.ts +94 -83
  42. package/src/routing/scanner.ts +26 -14
  43. package/src/routing/transform-routes.ts +68 -68
  44. package/src/server/build.ts +225 -76
  45. package/src/server/coordinator.ts +9 -0
  46. package/src/server/hooks.ts +24 -3
  47. package/src/server/index.ts +388 -104
  48. package/src/typegen/index.ts +30 -14
  49. package/src/types.ts +12 -2
  50. package/test/middleware.test.ts +6 -4
  51. package/test/rendering.test.ts +510 -356
  52. package/test/routing.test.ts +36 -0
  53. package/test/scanning.test.ts +39 -8
  54. package/test/scope.test.ts +24 -8
  55. package/test/server.test.ts +27 -7
  56. package/test/streaming.test.ts +117 -98
  57. package/test/typegen.test.ts +52 -24
  58. package/tsconfig.json +1 -0
@@ -1,4 +1,11 @@
1
- import type { ServerWebSocket } from "bun";
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import { plugin as bunPlugin, type ServerWebSocket } from "bun";
4
+ import { DevGraph } from "../dev/graph.ts";
5
+ import { HMR_CLIENT_SOURCE } from "../dev/hmr-client.ts";
6
+ import { HmrServer, handleChange } from "../dev/hmr-server.ts";
7
+ import { makeDevLoader } from "../dev/loader.ts";
8
+ import { startWatcher } from "../dev/watcher.ts";
2
9
  import { renderRoute } from "../rendering";
3
10
  import { registerSSRPlugin } from "../rendering/ssrPlugin";
4
11
  import { findClosestError, matchRoute } from "../routing/router";
@@ -6,10 +13,12 @@ import { isErrorResult } from "../sentinels/error.ts";
6
13
  import { isFailResult } from "../sentinels/fail.ts";
7
14
  import { isRedirectResult } from "../sentinels/redirect.ts";
8
15
  import type { GrimoireConfig, WsRouteHandler } from "../types";
9
- import { buildProject } from "./build";
16
+ import { buildProject, rebuildForChange } from "./build";
10
17
  import { createCookies } from "./cookie-utils";
11
18
  import type {
12
19
  Handle,
20
+ HandleError,
21
+ HandleFetch,
13
22
  InitFunction,
14
23
  RequestEvent,
15
24
  ResolveFunction,
@@ -17,15 +26,23 @@ import type {
17
26
  import { runDeserializeLocals, runHook, runRequestHooks } from "./plugins";
18
27
 
19
28
  /**
20
- * Try to load hooks.index.ts from the project root.
29
+ * Try to load hooks.server.ts from the project root.
21
30
  */
22
- async function loadHooks(
23
- projectRoot: string,
24
- ): Promise<{ handle?: Handle; init?: InitFunction }> {
31
+ async function loadHooks(projectRoot: string): Promise<{
32
+ handle?: Handle;
33
+ init?: InitFunction;
34
+ handleError?: HandleError;
35
+ handleFetch?: HandleFetch;
36
+ }> {
25
37
  const hooksPath = `${projectRoot}/hooks.server.ts`;
26
38
  try {
27
39
  const mod = await import(hooksPath);
28
- return { handle: mod.handle, init: mod.init };
40
+ return {
41
+ handle: mod.handle,
42
+ init: mod.init,
43
+ handleError: mod.handleError,
44
+ handleFetch: mod.handleFetch,
45
+ };
29
46
  } catch {
30
47
  // no hooks file — that's fine
31
48
  return {};
@@ -48,12 +65,15 @@ export async function createServer(config: GrimoireConfig = {}) {
48
65
  finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
49
66
  }
50
67
 
51
- //w timings????
52
68
  const {
53
69
  port = 3000,
54
70
  host = "localhost",
55
71
  plugins = [],
72
+ dev = false,
56
73
  routes = "src/routes",
74
+ cspNonce,
75
+ //biome-ignore lint: not implemented yet
76
+ devEditor = "code",
57
77
  _skipBuild = false,
58
78
  } = finalConfig;
59
79
 
@@ -64,9 +84,8 @@ export async function createServer(config: GrimoireConfig = {}) {
64
84
  tree = _tree;
65
85
  } else {
66
86
  // 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
87
  const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
88
+ const { scanRoutes } = await import("../routing/scanner");
70
89
  tree = await scanRoutes(routesDir, process.cwd());
71
90
  }
72
91
 
@@ -74,91 +93,234 @@ export async function createServer(config: GrimoireConfig = {}) {
74
93
  // WHAT ARE WE DOINGGGGGGGGG
75
94
  registerSSRPlugin(plugins);
76
95
 
77
- // Load hooks.index.ts
78
- const { handle: hooksHandle, init: hooksInit } = await loadHooks(
79
- process.cwd(),
80
- );
96
+ const graph = new DevGraph();
97
+ const hmr = dev ? new HmrServer() : null;
98
+
99
+ if (dev) {
100
+ const { ensureRuntimeBundle } = await import("../dev/runtime-bundle.ts");
101
+ await ensureRuntimeBundle(process.cwd());
102
+
103
+ // clean dev-modules cache from previous session
104
+ const { rmSync, mkdirSync } = await import("node:fs");
105
+ const devModulesDir = join(process.cwd(), ".grimoire/dev-modules");
106
+ try {
107
+ rmSync(devModulesDir, { recursive: true, force: true });
108
+ } catch {}
109
+ mkdirSync(devModulesDir, { recursive: true });
110
+
111
+ // register route files and scan their imports for the graph
112
+ const srcDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
113
+ const allFiles = [
114
+ ...tree.routes,
115
+ ...tree.layouts,
116
+ ...tree.servers,
117
+ ...tree.errors,
118
+ ];
119
+
120
+ await Promise.all(
121
+ allFiles.map(async (rf) => {
122
+ graph.getOrCreate(rf.filePath, rf.filePath);
123
+ graph.markRoute(rf.filePath, rf);
124
+ try {
125
+ const source = await Bun.file(rf.filePath).text();
126
+ const imports = new Bun.Transpiler({ loader: "tsx" }).scanImports(
127
+ source,
128
+ );
129
+ for (const imp of imports) {
130
+ if (imp.path.startsWith(".") || imp.path.startsWith("$lib")) {
131
+ const resolved = imp.path.startsWith("$lib")
132
+ ? join(process.cwd(), "src/lib", imp.path.slice(5))
133
+ : join(dirname(rf.filePath), imp.path);
134
+ graph.addEdge(rf.filePath, resolved);
135
+ // also register lib files as graph nodes
136
+ graph.getOrCreate(resolved, resolved);
137
+ }
138
+ }
139
+ } catch {}
140
+ }),
141
+ );
142
+
143
+ startWatcher(srcDir, (f) =>
144
+ handleChange(f, graph, hmr!, srcDir, process.cwd(), tree),
145
+ );
146
+ }
147
+
148
+ const loader = dev
149
+ ? makeDevLoader(graph, process.cwd())
150
+ : (p: string) => import(p);
151
+ // Load hooks.server.ts
152
+ const {
153
+ handle: hooksHandle,
154
+ init: hooksInit,
155
+ handleError: hooksHandleError,
156
+ handleFetch: hooksHandleFetch,
157
+ } = await loadHooks(process.cwd());
81
158
 
82
159
  // Run init hook if present
83
160
  await hooksInit?.();
84
161
 
162
+ // Register alias plugin so dynamically-imported +server.ts routes can resolve custom aliases
163
+ const aliases = finalConfig.alias ?? {};
164
+ if (Object.keys(aliases).length > 0) {
165
+ bunPlugin({
166
+ name: "grimoire-alias",
167
+ setup(build) {
168
+ for (const [prefix, target] of Object.entries(aliases)) {
169
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
170
+ build.onResolve(
171
+ { filter: new RegExp(`^${escaped}(/|$)`) },
172
+ (args) => ({
173
+ path: args.path.replace(prefix, join(process.cwd(), target)),
174
+ }),
175
+ );
176
+ }
177
+ },
178
+ });
179
+ }
85
180
  const server = Bun.serve({
86
181
  port,
87
182
  hostname: host,
88
183
  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().
92
- // If this request came from a coordinator, deserialize its locals
93
184
  const rawLocals = req.headers.get("X-Grimoire-Locals");
94
185
  const locals: App.Locals = rawLocals
95
186
  ? ((await runDeserializeLocals(plugins, rawLocals)) ?? {})
96
187
  : {};
188
+ const url = new URL(req.url);
97
189
 
98
- return runRequestHooks(plugins, req, async () => {
99
- const url = new URL(req.url);
100
-
101
- const publicFile = Bun.file(`${process.cwd()}/public${url.pathname}`);
102
- if (await publicFile.exists()) {
103
- return new Response(publicFile);
190
+ if (dev && url.pathname === "/__grimoire__/hmr") {
191
+ //@ts-expect-error holy shut up
192
+ if (server.upgrade(req, { data: { __hmrClient: true } })) {
193
+ return undefined as unknown as Response;
194
+ }
195
+ return new Response("Upgrade Required", { status: 426 });
196
+ }
197
+ if (dev && url.pathname === "/__grimoire__/hmr-client.js") {
198
+ return new Response(HMR_CLIENT_SOURCE, {
199
+ headers: { "Content-Type": "application/javascript" },
200
+ });
201
+ }
202
+ if (dev && url.pathname === "/__grimoire__/open") {
203
+ const file = url.searchParams.get("file") ?? "";
204
+ const line = url.searchParams.get("line") ?? "1";
205
+ const col = url.searchParams.get("col") ?? "1";
206
+ const editor = finalConfig.devEditor ?? "code";
207
+ //@ts-expect-error Bun spawn has werird types just shut up
208
+ Bun.spawn([editor, "--goto", `${file}:${line}:${col}`]);
209
+ return new Response(null, { status: 204 });
210
+ }
211
+ if (dev && url.pathname.startsWith("/__grimoire__/dep/")) {
212
+ const depName = decodeURIComponent(
213
+ url.pathname.slice("/__grimoire__/dep/".length).replace(/\.js$/, ""),
214
+ );
215
+
216
+ // grimoire shim
217
+ if (depName.includes("@sigil-dev__grimoire")) {
218
+ return Response.redirect("/__grimoire__/grimoire-client.js", 302);
104
219
  }
105
220
 
106
- const matched = matchRoute(tree, url);
107
-
108
- if (!matched) {
109
- const error = findClosestError(tree.errors, url.pathname);
110
- if (error) {
111
- const mod = await import(error.filePath);
112
- const html = mod.default({ status: 404, message: "Not Found" });
113
- return new Response(html, {
114
- status: 404,
115
- headers: { "Content-Type": "text/html" },
221
+ // convert URL-safe name back to package name
222
+ // @codex__shared → @codex/shared
223
+ const pkgName = depName.startsWith("@")
224
+ ? depName.replace("__", "/")
225
+ : depName;
226
+
227
+ const depsDir = join(process.cwd(), ".grimoire/deps");
228
+ mkdirSync(depsDir, { recursive: true });
229
+ const outFile = join(depsDir, depName + ".js");
230
+
231
+ if (!(await Bun.file(outFile).exists())) {
232
+ console.log("[sigil hmr] bundling dep:", pkgName);
233
+ try {
234
+ const entry = Bun.resolveSync(pkgName, process.cwd());
235
+ const result = await Bun.build({
236
+ entrypoints: [entry],
237
+ outdir: depsDir,
238
+ naming: depName + ".js",
239
+ target: "browser",
240
+ format: "esm",
241
+ minify: false,
242
+ external: ["@sigil-dev/runtime"],
116
243
  });
244
+ if (!result.success) {
245
+ console.error(
246
+ "[sigil hmr] dep bundle failed:",
247
+ pkgName,
248
+ result.logs,
249
+ );
250
+ return new Response(
251
+ `throw new Error("failed to bundle dep: ${pkgName}")`,
252
+ { headers: { "Content-Type": "application/javascript" } },
253
+ );
254
+ }
255
+ console.log("[sigil hmr] bundled dep:", pkgName);
256
+ } catch (e: any) {
257
+ return new Response(
258
+ `throw new Error("dep not found: ${pkgName}: ${e.message}")`,
259
+ { headers: { "Content-Type": "application/javascript" } },
260
+ );
117
261
  }
118
- return new Response("Not Found", { status: 404 });
119
262
  }
120
263
 
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 });
264
+ return new Response(Bun.file(outFile), {
265
+ headers: {
266
+ "Content-Type": "application/javascript",
267
+ "Cache-Control": "no-store",
268
+ },
269
+ });
270
+ }
271
+
272
+ if (dev && url.pathname.startsWith("/__grimoire__/m/")) {
273
+ const clientPath = decodeURIComponent(
274
+ url.pathname.slice("/__grimoire__/m/".length),
275
+ );
276
+ const v = url.searchParams.get("v");
277
+
278
+ if (v) {
279
+ // versioned request — serve from dev-modules cache
280
+ // find the cache file for this version
281
+ const { createHash } = await import("node:crypto");
282
+ const absPath = join(process.cwd(), clientPath.replace(/\.js$/, ""));
283
+ const fileHash = createHash("md5")
284
+ .update(absPath)
285
+ .digest("hex")
286
+ .slice(0, 8);
287
+ const cachePath = join(
288
+ process.cwd(),
289
+ `.grimoire/dev-modules/${fileHash}_v${v}.js`,
290
+ );
291
+ const file = Bun.file(cachePath);
292
+ if (await file.exists()) {
293
+ return new Response(file, {
294
+ headers: {
295
+ "Content-Type": "application/javascript",
296
+ "Cache-Control": "no-store",
297
+ },
298
+ });
154
299
  }
300
+ }
155
301
 
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 });
302
+ // unversioned or cache miss — compile on demand
303
+ const absPath = join(process.cwd(), clientPath.replace(/\.js$/, ""));
304
+ try {
305
+ const { compileForBrowser } = await import("../dev/compile-module");
306
+ const result = await compileForBrowser(absPath, process.cwd());
307
+ return new Response(result.js, {
308
+ headers: {
309
+ "Content-Type": "application/javascript",
310
+ "Cache-Control": "no-store",
311
+ },
312
+ });
313
+ } catch (e: any) {
314
+ return new Response(`throw new Error(${JSON.stringify(e.message)})`, {
315
+ headers: { "Content-Type": "application/javascript" },
316
+ });
317
+ }
318
+ }
319
+
320
+ return runRequestHooks(plugins, req, async () => {
321
+ const publicFile = Bun.file(`${process.cwd()}/public${url.pathname}`);
322
+ if (await publicFile.exists()) {
323
+ return new Response(publicFile);
162
324
  }
163
325
 
164
326
  const HTTP_METHODS = [
@@ -171,7 +333,7 @@ export async function createServer(config: GrimoireConfig = {}) {
171
333
  "OPTIONS",
172
334
  ] as const;
173
335
 
174
- // Build RequestEvent for hooks
336
+ // Build event BEFORE matching — handle() runs pre-match like SvelteKit
175
337
  const cookieHeader = req.headers.get("cookie") ?? "";
176
338
  const cookies = createCookies(cookieHeader);
177
339
  const setHeadersMap: Record<string, string> = {};
@@ -179,20 +341,112 @@ export async function createServer(config: GrimoireConfig = {}) {
179
341
  const event: RequestEvent = {
180
342
  request: req,
181
343
  url,
182
- params: matched.params,
344
+ params: {},
183
345
  locals,
184
346
  cookies,
347
+ route: { id: "" },
348
+ fetch: globalThis.fetch,
185
349
  setHeaders: (headers) => Object.assign(setHeadersMap, headers),
186
350
  };
187
351
 
188
- // Resolve function: runs the actual page/form action logic
352
+ if (hooksHandleFetch) {
353
+ event.fetch = (reqInfo: RequestInfo | URL, init?: RequestInit) => {
354
+ const r = new Request(reqInfo, init);
355
+ return Promise.resolve(
356
+ hooksHandleFetch({ request: r, fetch: globalThis.fetch, event }),
357
+ );
358
+ };
359
+ }
360
+
189
361
  const resolve: ResolveFunction = async (evt) => {
190
- // form actions
362
+ // matching happens here — after handle() has run
363
+ const matched = matchRoute(tree, evt.url);
364
+
365
+ if (!matched) {
366
+ const error = findClosestError(tree.errors, evt.url.pathname);
367
+ if (error) {
368
+ const mod = await loader(error.filePath);
369
+ const html = mod.default({ status: 404, message: "Not Found" });
370
+ return new Response(html, {
371
+ status: 404,
372
+ headers: { "Content-Type": "text/html" },
373
+ });
374
+ }
375
+ return new Response("Not Found", { status: 404 });
376
+ }
377
+
378
+ // patch event with match results so handle() epilogue can read them
379
+ evt.params = matched.params;
380
+ evt.route = { id: matched.route.path };
381
+
382
+ // API routes (+server.ts)
383
+ if (matched.route.type === "server") {
384
+ const mod = await loader(matched.route.filePath);
385
+
386
+ const isWsUpgrade =
387
+ evt.request.headers.get("upgrade")?.toLowerCase() === "websocket";
388
+ if (isWsUpgrade && mod.websocket) {
389
+ let extraData: Record<string, unknown> = {};
390
+ if (mod.upgrade) {
391
+ try {
392
+ const result = await mod.upgrade({
393
+ request: evt.request,
394
+ params: matched.params,
395
+ url: evt.url,
396
+ locals: evt.locals,
397
+ });
398
+ if (result && typeof result === "object") extraData = result;
399
+ } catch {
400
+ return new Response("Upgrade Required", { status: 426 });
401
+ }
402
+ }
403
+ const wsData: _WsInternalData = {
404
+ params: matched.params,
405
+ __handler: mod.websocket,
406
+ ...extraData,
407
+ };
408
+ //@ts-expect-error i dont know what you are talking about please
409
+ if (server.upgrade(req, { data: wsData })) {
410
+ return undefined as unknown as Response;
411
+ }
412
+ return new Response("Upgrade Required", { status: 426 });
413
+ }
414
+
415
+ const handler = mod[evt.request.method];
416
+ if (!handler) {
417
+ return new Response("Method Not Allowed", { status: 405 });
418
+ }
419
+ return handler({
420
+ request: evt.request,
421
+ params: matched.params,
422
+ url: evt.url,
423
+ locals: evt.locals,
424
+ });
425
+ }
426
+
427
+ // form actions (+page.server.ts)
191
428
  if (
192
429
  matched.pageServer &&
193
430
  HTTP_METHODS.includes(evt.request.method as any)
194
431
  ) {
195
- const mod = await import(matched.pageServer.filePath);
432
+ const mod = await loader(matched.pageServer.filePath);
433
+
434
+ if (
435
+ mod.csrf !== false &&
436
+ evt.request.method !== "GET" &&
437
+ evt.request.method !== "HEAD"
438
+ ) {
439
+ const cookieToken = cookies.get("_csrf");
440
+ const formData = await evt.request
441
+ .clone()
442
+ .formData()
443
+ .catch(() => null);
444
+ const bodyToken = formData?.get("_csrf");
445
+ if (!cookieToken || cookieToken !== String(bodyToken)) {
446
+ return new Response("Forbidden", { status: 403 });
447
+ }
448
+ }
449
+
196
450
  const handler = mod[evt.request.method];
197
451
  if (handler) {
198
452
  let result: any;
@@ -225,7 +479,7 @@ export async function createServer(config: GrimoireConfig = {}) {
225
479
  evt.url.pathname,
226
480
  );
227
481
  if (errorPage) {
228
- const errMod = await import(errorPage.filePath);
482
+ const errMod = await loader(errorPage.filePath);
229
483
  const html = errMod.default({
230
484
  status: e.status,
231
485
  message: e.message,
@@ -264,16 +518,17 @@ export async function createServer(config: GrimoireConfig = {}) {
264
518
  matched,
265
519
  evt.request,
266
520
  tree.errors,
267
- undefined,
521
+ loader,
268
522
  evt.locals,
269
523
  plugins,
524
+ cspNonce,
525
+ dev,
270
526
  );
271
527
 
272
528
  if (evt.request.headers.get("x-grimoire-navigate") === "1") {
273
529
  return response;
274
530
  }
275
531
 
276
- // Streaming SSR: if onRouteRender plugins exist, consume stream
277
532
  const hasRenderPlugins = plugins.some((p) => p.onRouteRender);
278
533
  if (hasRenderPlugins) {
279
534
  let html = await response.text();
@@ -294,18 +549,41 @@ export async function createServer(config: GrimoireConfig = {}) {
294
549
  return response;
295
550
  };
296
551
 
297
- // If no hooks handle, resolve directly
298
- if (!hooksHandle) {
299
- return resolve(event);
552
+ let response: Response;
553
+ try {
554
+ if (!hooksHandle) {
555
+ response = await resolve(event);
556
+ } else {
557
+ response = await hooksHandle({ event, resolve });
558
+ }
559
+ } catch (err) {
560
+ console.error(
561
+ `[grimoire] ${event.request.method} ${event.url.pathname} failed:`,
562
+ err,
563
+ );
564
+ await hooksHandleError?.({
565
+ error: err,
566
+ event,
567
+ status: 500,
568
+ message: "Internal Server Error",
569
+ });
570
+ const errorHtmlPath = `${process.cwd()}/error.html`;
571
+ try {
572
+ const errorFile = Bun.file(errorHtmlPath);
573
+ if (await errorFile.exists()) {
574
+ return new Response(errorFile, {
575
+ status: 500,
576
+ headers: { "Content-Type": "text/html" },
577
+ });
578
+ }
579
+ } catch {}
580
+ const message =
581
+ process.env.NODE_ENV === "production"
582
+ ? "Internal Server Error"
583
+ : `${err instanceof Error ? `${err.message}\n${err.stack}` : String(err)}`;
584
+ return new Response(message, { status: 500 });
300
585
  }
301
586
 
302
- // Run through hooks chain
303
- let response = await hooksHandle({
304
- event,
305
- resolve,
306
- });
307
-
308
- // Apply setHeaders
309
587
  const setCookieHeaders = cookies.toHeaders();
310
588
  if (
311
589
  setCookieHeaders.length > 0 ||
@@ -329,21 +607,27 @@ export async function createServer(config: GrimoireConfig = {}) {
329
607
  });
330
608
  },
331
609
  websocket: {
332
- open(ws: ServerWebSocket<_WsInternalData>) {
333
- ws.data.__handler?.open?.(ws);
610
+ open(ws: ServerWebSocket<any>) {
611
+ if (ws.data.__hmrClient) {
612
+ hmr?.addClient(ws);
613
+ return;
614
+ }
615
+ (ws.data as _WsInternalData).__handler?.open?.(ws);
334
616
  },
335
- message(ws: ServerWebSocket<_WsInternalData>, data: string | Buffer) {
336
- ws.data.__handler?.message?.(ws, data);
617
+ message(ws: ServerWebSocket<any>, data: string | Buffer) {
618
+ if (ws.data.__hmrClient) return;
619
+ (ws.data as _WsInternalData).__handler?.message?.(ws, data);
337
620
  },
338
- close(
339
- ws: ServerWebSocket<_WsInternalData>,
340
- code: number,
341
- reason?: string,
342
- ) {
343
- ws.data.__handler?.close?.(ws, code, reason);
621
+ close(ws: ServerWebSocket<any>, code: number, reason?: string) {
622
+ if (ws.data.__hmrClient) {
623
+ hmr?.removeClient(ws);
624
+ return;
625
+ }
626
+ (ws.data as _WsInternalData).__handler?.close?.(ws, code, reason);
344
627
  },
345
- drain(ws: ServerWebSocket<_WsInternalData>) {
346
- ws.data.__handler?.drain?.(ws);
628
+ drain(ws: ServerWebSocket<any>) {
629
+ if (ws.data.__hmrClient) return;
630
+ (ws.data as _WsInternalData).__handler?.drain?.(ws);
347
631
  },
348
632
  },
349
633
  });