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