@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.
- package/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +34 -34
- package/package.json +8 -4
- package/preload.js +2 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +224 -76
- package/src/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +72 -72
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +81 -27
- package/src/rendering/index.ts +199 -186
- package/src/rendering/ssrPlugin.ts +53 -47
- package/src/routing/manifest-gen.ts +39 -26
- package/src/routing/router.ts +106 -98
- package/src/routing/scanner.ts +135 -129
- package/src/routing/transform-routes.ts +101 -101
- package/src/server/build.ts +147 -90
- package/src/server/coordinator.ts +306 -297
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +144 -70
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +353 -340
- package/src/types.ts +269 -260
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -221
- package/test/rendering.test.ts +425 -425
- package/test/routing.test.ts +83 -45
- package/test/scanning.test.ts +181 -169
- package/test/server.test.ts +229 -229
- package/test/streaming.test.ts +106 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +19 -1
package/src/server/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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.
|
|
23
|
+
* Try to load hooks.server.ts from the project root.
|
|
21
24
|
*/
|
|
22
|
-
async function loadHooks(
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
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.
|
|
78
|
-
const {
|
|
79
|
-
|
|
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
|
|
90
|
-
// hooks.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
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 (
|
package/src/server/worker.ts
CHANGED
|
@@ -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
|
+
}
|