@sigil-dev/grimoire 0.4.0 → 0.6.0
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/README.md +174 -174
- package/index.ts +47 -23
- package/package.json +6 -6
- package/src/{enhance.ts → client/enhance.ts} +2 -1
- package/src/client/index.ts +5 -0
- package/src/{client-router.ts → client/router.ts} +1 -1
- package/src/{vite-plugin.ts → integrations/vite.ts} +4 -4
- package/src/{hydrate.ts → rendering/hydrate.ts} +2 -2
- package/src/{renderer.ts → rendering/index.ts} +39 -15
- package/src/{ssrPlugin.ts → rendering/ssrPlugin.ts} +8 -2
- package/src/{scanner.ts → routing/scanner.ts} +16 -4
- package/src/{transform-routes.ts → routing/transform-routes.ts} +7 -1
- package/src/{fail.ts → sentinels/fail.ts} +1 -1
- package/src/server/build.ts +90 -0
- package/src/{cookie-utils.ts → server/cookie-utils.ts} +66 -66
- package/src/server/coordinator.ts +297 -0
- package/src/{hooks.ts → server/hooks.ts} +1 -1
- package/src/{server.ts → server/index.ts} +153 -105
- package/src/server/plugins.ts +119 -0
- package/src/server/worker.ts +59 -0
- package/src/{typegen.ts → typegen/index.ts} +81 -4
- package/src/types.ts +176 -1
- package/test/context.test.ts +1 -1
- package/test/fail.test.ts +46 -46
- package/test/headers.test.ts +100 -96
- package/test/hydration.test.ts +1 -1
- package/test/middleware.test.ts +221 -217
- package/test/preload.ts +1 -1
- package/test/redirect-error.test.ts +112 -112
- package/test/rendering.test.ts +319 -310
- package/test/routing.test.ts +2 -2
- package/test/scanning.test.ts +40 -11
- package/test/scope.test.ts +25 -10
- package/test/server.test.ts +150 -1
- package/test/streaming.test.ts +145 -132
- package/test/transform-routes.test.ts +2 -2
- package/test/typegen.test.ts +10 -8
- package/tsconfig.json +3 -1
- package/.grimoire/_routes.dom.js +0 -4
- package/.grimoire/_routes.hydrate.js +0 -4
- package/.grimoire/_routes.ts +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_protected__page.dom.js +0 -9
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_protected__page.hydrate.js +0 -11
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_protected__page.dom.js +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_protected__page.hydrate.js +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_login.dom.js +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_login.hydrate.js +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_login.dom.js +0 -4
- package/.grimoire/compiled/0-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_login.hydrate.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_login__page.dom.js +0 -8
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503001657_login__page.hydrate.js +0 -9
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_login__page.dom.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503161592_login__page.hydrate.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_protected.dom.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503395872_protected.hydrate.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_protected.dom.js +0 -4
- package/.grimoire/compiled/1-C__Users_Cane1712_AppData_Local_Temp_grimoire-actions-1780503614350_protected.hydrate.js +0 -4
- package/.grimoire/tsconfig.generated.json +0 -11
- package/.grimoire/types/ambient.d.ts +0 -6
- package/.grimoire/types/api/hello/$types.d.ts +0 -29
- package/.grimoire/types/api/items/$types.d.ts +0 -29
- package/public/__grimoire__/client.js +0 -86
- package/public/__grimoire__/hydrate.js +0 -101
- package/src/client.ts +0 -4
- package/src/plugins.ts +0 -25
- package/src/sync.ts +0 -18
- /package/src/{scope.ts → client/scope.ts} +0 -0
- /package/src/{head.ts → rendering/head.ts} +0 -0
- /package/src/{manifest-gen.ts → routing/manifest-gen.ts} +0 -0
- /package/src/{router.ts → routing/router.ts} +0 -0
- /package/src/{error.ts → sentinels/error.ts} +0 -0
- /package/src/{redirect.ts → sentinels/redirect.ts} +0 -0
- /package/src/{context.ts → server/context.ts} +0 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import { renderRoute } from "../rendering";
|
|
3
|
+
import { registerSSRPlugin } from "../rendering/ssrPlugin";
|
|
4
|
+
import { findClosestError, matchRoute } from "../routing/router";
|
|
5
|
+
import { isErrorResult } from "../sentinels/error.ts";
|
|
6
|
+
import { isFailResult } from "../sentinels/fail.ts";
|
|
7
|
+
import { isRedirectResult } from "../sentinels/redirect.ts";
|
|
8
|
+
import type { GrimoireConfig, WsRouteHandler } from "../types";
|
|
9
|
+
import { buildProject } from "./build";
|
|
5
10
|
import { createCookies } from "./cookie-utils";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import { scanRoutes } from "./scanner";
|
|
14
|
-
import { registerSSRPlugin } from "./ssrPlugin";
|
|
15
|
-
import { transformRoutes } from "./transform-routes";
|
|
16
|
-
import { generateTypes } from "./typegen";
|
|
17
|
-
import type { GrimoireConfig } from "./types";
|
|
11
|
+
import type {
|
|
12
|
+
Handle,
|
|
13
|
+
InitFunction,
|
|
14
|
+
RequestEvent,
|
|
15
|
+
ResolveFunction,
|
|
16
|
+
} from "./hooks";
|
|
17
|
+
import { runDeserializeLocals, runHook, runRequestHooks } from "./plugins";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Try to load hooks.
|
|
20
|
+
* Try to load hooks.index.ts from the project root.
|
|
21
21
|
*/
|
|
22
22
|
async function loadHooks(
|
|
23
23
|
projectRoot: string,
|
|
24
24
|
): Promise<{ handle?: Handle; init?: InitFunction }> {
|
|
25
|
-
const hooksPath =
|
|
25
|
+
const hooksPath = `${projectRoot}/hooks.server.ts`;
|
|
26
26
|
try {
|
|
27
27
|
const mod = await import(hooksPath);
|
|
28
28
|
return { handle: mod.handle, init: mod.init };
|
|
@@ -32,91 +32,49 @@ async function loadHooks(
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
interface _WsInternalData {
|
|
36
|
+
params: Record<string, string>;
|
|
37
|
+
// always present at runtime — optional only to satisfy TypeScript;
|
|
38
|
+
// upgrade block gates on mod.websocket before calling server.upgrade()
|
|
39
|
+
__handler?: WsRouteHandler<any>;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
export async function createServer(config: GrimoireConfig = {}) {
|
|
44
|
+
// run config hooks
|
|
45
|
+
let finalConfig = config;
|
|
46
|
+
const earlyPlugins = config.plugins ?? [];
|
|
47
|
+
for (const plugin of earlyPlugins) {
|
|
48
|
+
finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
|
|
49
|
+
}
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
//w timings????
|
|
38
52
|
const {
|
|
39
53
|
port = 3000,
|
|
40
54
|
host = "localhost",
|
|
41
55
|
plugins = [],
|
|
42
56
|
routes = "src/routes",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
_skipBuild = false,
|
|
58
|
+
} = finalConfig;
|
|
59
|
+
|
|
60
|
+
registerSSRPlugin(plugins);
|
|
61
|
+
let tree: any;
|
|
62
|
+
if (!config._skipBuild) {
|
|
63
|
+
const { result, tree: _tree } = await buildProject(finalConfig, plugins);
|
|
64
|
+
if (!result.success) throw new Error("Grimoire: build failed");
|
|
65
|
+
tree = _tree;
|
|
66
|
+
} else {
|
|
67
|
+
// 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
|
+
const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
|
|
71
|
+
tree = await scanRoutes(routesDir, process.cwd());
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
: join(process.cwd(), routes).replace(/\0/g, "");
|
|
55
|
-
const tree = await scanRoutes(routesDir, process.cwd());
|
|
56
|
-
|
|
57
|
-
await mkdir(join(process.cwd(), ".grimoire"), { recursive: true });
|
|
58
|
-
|
|
59
|
-
await generateTypes(tree, {
|
|
60
|
-
projectRoot: process.cwd(),
|
|
61
|
-
routesDir,
|
|
62
|
-
outDir: join(process.cwd(), ".grimoire/types"),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const compiledDir = join(process.cwd(), ".grimoire/compiled");
|
|
66
|
-
await mkdir(compiledDir, { recursive: true });
|
|
67
|
-
|
|
68
|
-
// Bun 1.3.13 bug: Bun.build's native parser runs before onLoad hooks fire,
|
|
69
|
-
// so .tsx files with TS syntax crash the bundler even with a sigil plugin.
|
|
70
|
-
// Fix: pre-transform route files to plain JS before Bun.build sees them,
|
|
71
|
-
// then generate per-mode manifests pointing at the compiled .js files.
|
|
72
|
-
const pageRoutes = tree.routes.filter(
|
|
73
|
-
(r) => r.type === "page" || r.type === "simple",
|
|
74
|
+
// Load hooks.index.ts
|
|
75
|
+
const { handle: hooksHandle, init: hooksInit } = await loadHooks(
|
|
76
|
+
process.cwd(),
|
|
74
77
|
);
|
|
75
|
-
const [hydrateFiles, domFiles] = await Promise.all([
|
|
76
|
-
transformRoutes(pageRoutes, compiledDir, "hydrate"),
|
|
77
|
-
transformRoutes(pageRoutes, compiledDir, "dom"),
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
const hydrateManifest = join(process.cwd(), ".grimoire/_routes.hydrate.js");
|
|
81
|
-
const domManifest = join(process.cwd(), ".grimoire/_routes.dom.js");
|
|
82
|
-
await Promise.all([
|
|
83
|
-
Bun.write(hydrateManifest, generateManifest(pageRoutes, hydrateFiles)),
|
|
84
|
-
Bun.write(domManifest, generateManifest(pageRoutes, domFiles)),
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const makeRoutesPlugin = (manifestPath: string) => ({
|
|
88
|
-
name: "grimoire-routes",
|
|
89
|
-
setup(build: any) {
|
|
90
|
-
build.onResolve({ filter: /^#grimoire-routes$/ }, () => ({
|
|
91
|
-
path: manifestPath,
|
|
92
|
-
}));
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const [hydrateResult, domResult] = await Promise.all([
|
|
97
|
-
Bun.build({
|
|
98
|
-
entrypoints: [join(import.meta.dir, "./hydrate.ts")],
|
|
99
|
-
outdir: join(process.cwd(), "public/__grimoire__"),
|
|
100
|
-
plugins: [sigil({ mode: "hydrate" }), makeRoutesPlugin(hydrateManifest)],
|
|
101
|
-
}),
|
|
102
|
-
Bun.build({
|
|
103
|
-
entrypoints: [join(import.meta.dir, "./client.ts")],
|
|
104
|
-
outdir: join(process.cwd(), "public/__grimoire__"),
|
|
105
|
-
plugins: [sigil({ mode: "dom" }), makeRoutesPlugin(domManifest)],
|
|
106
|
-
}),
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
if (!hydrateResult.success) {
|
|
110
|
-
for (const log of hydrateResult.logs) console.error(log);
|
|
111
|
-
throw new Error("Grimoire: hydrate bundle build failed");
|
|
112
|
-
}
|
|
113
|
-
if (!domResult.success) {
|
|
114
|
-
for (const log of domResult.logs) console.error(log);
|
|
115
|
-
throw new Error("Grimoire: dom bundle build failed");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Load hooks.server.ts
|
|
119
|
-
const { handle: hooksHandle, init: hooksInit } = await loadHooks(process.cwd());
|
|
120
78
|
|
|
121
79
|
// Run init hook if present
|
|
122
80
|
await hooksInit?.();
|
|
@@ -125,12 +83,19 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
125
83
|
port,
|
|
126
84
|
hostname: host,
|
|
127
85
|
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().
|
|
89
|
+
// If this request came from a coordinator, deserialize its locals
|
|
90
|
+
const rawLocals = req.headers.get("X-Grimoire-Locals");
|
|
91
|
+
const locals: App.Locals = rawLocals
|
|
92
|
+
? ((await runDeserializeLocals(plugins, rawLocals)) ?? {})
|
|
93
|
+
: {};
|
|
94
|
+
|
|
128
95
|
return runRequestHooks(plugins, req, async () => {
|
|
129
96
|
const url = new URL(req.url);
|
|
130
97
|
|
|
131
|
-
const publicFile = Bun.file(
|
|
132
|
-
join(process.cwd(), "public", url.pathname),
|
|
133
|
-
);
|
|
98
|
+
const publicFile = Bun.file(`${process.cwd()}/public${url.pathname}`);
|
|
134
99
|
if (await publicFile.exists()) {
|
|
135
100
|
return new Response(publicFile);
|
|
136
101
|
}
|
|
@@ -150,14 +115,47 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
150
115
|
return new Response("Not Found", { status: 404 });
|
|
151
116
|
}
|
|
152
117
|
|
|
153
|
-
// API routes
|
|
118
|
+
// API routes (+server.ts)
|
|
154
119
|
if (matched.route.type === "server") {
|
|
155
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
|
|
156
154
|
const handler = mod[req.method];
|
|
157
155
|
if (!handler) {
|
|
158
156
|
return new Response("Method Not Allowed", { status: 405 });
|
|
159
157
|
}
|
|
160
|
-
return handler({ request: req, params: matched.params, url });
|
|
158
|
+
return handler({ request: req, params: matched.params, url, locals });
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
const HTTP_METHODS = [
|
|
@@ -179,7 +177,7 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
179
177
|
request: req,
|
|
180
178
|
url,
|
|
181
179
|
params: matched.params,
|
|
182
|
-
locals
|
|
180
|
+
locals,
|
|
183
181
|
cookies,
|
|
184
182
|
setHeaders: (headers) => Object.assign(setHeadersMap, headers),
|
|
185
183
|
};
|
|
@@ -187,7 +185,10 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
187
185
|
// Resolve function: runs the actual page/form action logic
|
|
188
186
|
const resolve: ResolveFunction = async (evt) => {
|
|
189
187
|
// form actions
|
|
190
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
matched.pageServer &&
|
|
190
|
+
HTTP_METHODS.includes(evt.request.method as any)
|
|
191
|
+
) {
|
|
191
192
|
const mod = await import(matched.pageServer.filePath);
|
|
192
193
|
const handler = mod[evt.request.method];
|
|
193
194
|
if (handler) {
|
|
@@ -207,17 +208,25 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
210
|
if (isErrorResult(e)) {
|
|
210
|
-
const isApi = evt.request.headers
|
|
211
|
+
const isApi = evt.request.headers
|
|
212
|
+
.get("accept")
|
|
213
|
+
?.includes("application/json");
|
|
211
214
|
if (isApi) {
|
|
212
215
|
return Response.json(
|
|
213
216
|
{ error: true, status: e.status, message: e.message },
|
|
214
217
|
{ status: e.status },
|
|
215
218
|
);
|
|
216
219
|
}
|
|
217
|
-
const errorPage = findClosestError(
|
|
220
|
+
const errorPage = findClosestError(
|
|
221
|
+
tree.errors,
|
|
222
|
+
evt.url.pathname,
|
|
223
|
+
);
|
|
218
224
|
if (errorPage) {
|
|
219
225
|
const errMod = await import(errorPage.filePath);
|
|
220
|
-
const html = errMod.default({
|
|
226
|
+
const html = errMod.default({
|
|
227
|
+
status: e.status,
|
|
228
|
+
message: e.message,
|
|
229
|
+
});
|
|
221
230
|
return new Response(html, {
|
|
222
231
|
status: e.status,
|
|
223
232
|
headers: { "Content-Type": "text/html" },
|
|
@@ -248,7 +257,14 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
// page routes
|
|
251
|
-
const response = await renderRoute(
|
|
260
|
+
const response = await renderRoute(
|
|
261
|
+
matched,
|
|
262
|
+
evt.request,
|
|
263
|
+
tree.errors,
|
|
264
|
+
undefined,
|
|
265
|
+
evt.locals,
|
|
266
|
+
plugins,
|
|
267
|
+
);
|
|
252
268
|
|
|
253
269
|
if (evt.request.headers.get("x-grimoire-navigate") === "1") {
|
|
254
270
|
return response;
|
|
@@ -288,7 +304,10 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
288
304
|
|
|
289
305
|
// Apply setHeaders
|
|
290
306
|
const setCookieHeaders = cookies.toHeaders();
|
|
291
|
-
if (
|
|
307
|
+
if (
|
|
308
|
+
setCookieHeaders.length > 0 ||
|
|
309
|
+
Object.keys(setHeadersMap).length > 0
|
|
310
|
+
) {
|
|
292
311
|
const headers = new Headers(response.headers);
|
|
293
312
|
for (const [k, v] of Object.entries(setHeadersMap)) {
|
|
294
313
|
headers.set(k, v);
|
|
@@ -306,8 +325,37 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
306
325
|
return response;
|
|
307
326
|
});
|
|
308
327
|
},
|
|
328
|
+
websocket: {
|
|
329
|
+
open(ws: ServerWebSocket<_WsInternalData>) {
|
|
330
|
+
ws.data.__handler?.open?.(ws);
|
|
331
|
+
},
|
|
332
|
+
message(ws: ServerWebSocket<_WsInternalData>, data: string | Buffer) {
|
|
333
|
+
ws.data.__handler?.message?.(ws, data);
|
|
334
|
+
},
|
|
335
|
+
close(
|
|
336
|
+
ws: ServerWebSocket<_WsInternalData>,
|
|
337
|
+
code: number,
|
|
338
|
+
reason?: string,
|
|
339
|
+
) {
|
|
340
|
+
ws.data.__handler?.close?.(ws, code, reason);
|
|
341
|
+
},
|
|
342
|
+
drain(ws: ServerWebSocket<_WsInternalData>) {
|
|
343
|
+
ws.data.__handler?.drain?.(ws);
|
|
344
|
+
},
|
|
345
|
+
},
|
|
309
346
|
});
|
|
310
347
|
|
|
348
|
+
let stopping = false;
|
|
349
|
+
const handleShutdown = async () => {
|
|
350
|
+
if (stopping) return;
|
|
351
|
+
stopping = true;
|
|
352
|
+
await runHook(plugins, "onStop", "shutdown");
|
|
353
|
+
server.stop();
|
|
354
|
+
process.exit(0);
|
|
355
|
+
};
|
|
356
|
+
process.on("SIGINT", handleShutdown);
|
|
357
|
+
process.on("SIGTERM", handleShutdown);
|
|
358
|
+
|
|
311
359
|
await runHook(plugins, "onStart", {
|
|
312
360
|
port: server.port,
|
|
313
361
|
hostname: server.hostname,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { RouteTree } from "../routing/scanner";
|
|
2
|
+
import type {
|
|
3
|
+
BuildResult,
|
|
4
|
+
CoordinatorContext,
|
|
5
|
+
GrimoirePlugin,
|
|
6
|
+
LoadContext,
|
|
7
|
+
Route,
|
|
8
|
+
Server,
|
|
9
|
+
WorkerDescriptor,
|
|
10
|
+
WorkerEnv,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
// Fire-and-forget hooks routed through runHook.
|
|
14
|
+
// onRequest and onRouteRender are intentionally excluded: they have distinct
|
|
15
|
+
// calling conventions (middleware chain / inline transform loop) handled in index.ts.
|
|
16
|
+
// Keep HookArgs in sync with GrimoirePlugin in types.ts.
|
|
17
|
+
type HookArgs = {
|
|
18
|
+
onStart: [server: Server];
|
|
19
|
+
onStop: [reason: "shutdown" | "restart"];
|
|
20
|
+
onBuildStart: [];
|
|
21
|
+
onBuildEnd: [result: BuildResult];
|
|
22
|
+
onRouteLoad: [route: Route, context: LoadContext];
|
|
23
|
+
onCoordinatorStart: [ctx: CoordinatorContext];
|
|
24
|
+
onWorkerReady: [worker: WorkerDescriptor];
|
|
25
|
+
onWorkerDeath: [worker: WorkerDescriptor, reason: "crash" | "intentional"];
|
|
26
|
+
};
|
|
27
|
+
type FireAndForgetHook = keyof HookArgs;
|
|
28
|
+
|
|
29
|
+
export async function runHook<K extends FireAndForgetHook>(
|
|
30
|
+
plugins: GrimoirePlugin[],
|
|
31
|
+
hook: K,
|
|
32
|
+
...args: HookArgs[K]
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
for (const plugin of plugins) {
|
|
35
|
+
const fn = plugin[hook] as ((...a: any[]) => any) | undefined;
|
|
36
|
+
await fn?.(...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runRequestHooks(
|
|
41
|
+
plugins: GrimoirePlugin[],
|
|
42
|
+
req: Request,
|
|
43
|
+
final: () => Promise<Response>,
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
const chain = plugins
|
|
46
|
+
.filter((p) => p.onRequest)
|
|
47
|
+
.reduceRight(
|
|
48
|
+
(next, plugin) => async () => plugin.onRequest!(req, next),
|
|
49
|
+
final,
|
|
50
|
+
);
|
|
51
|
+
return chain();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calls onWorkerSpawn on ALL plugins and merges results.
|
|
56
|
+
* env is combined (later plugins override earlier on conflict).
|
|
57
|
+
* name is last-write-wins.
|
|
58
|
+
*/
|
|
59
|
+
export async function runWorkerSpawn(
|
|
60
|
+
plugins: GrimoirePlugin[],
|
|
61
|
+
worker: WorkerDescriptor,
|
|
62
|
+
): Promise<WorkerEnv> {
|
|
63
|
+
const merged: WorkerEnv = { env: {} };
|
|
64
|
+
for (const plugin of plugins) {
|
|
65
|
+
const result = await plugin.onWorkerSpawn?.(worker);
|
|
66
|
+
//@ts-expect-error WTF TS? its result? meaning its not gonna do anything if null.
|
|
67
|
+
if (result?.env) Object.assign(merged.env!, result.env);
|
|
68
|
+
//@ts-expect-error WTF TS? its result? meaning its not gonna do anything if null.
|
|
69
|
+
if (result?.name) merged.name = result.name;
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* First plugin to return a non-null string wins.
|
|
76
|
+
* Returns null if no plugin handles it — coordinator uses default.
|
|
77
|
+
*/
|
|
78
|
+
export async function runSerializeLocals(
|
|
79
|
+
plugins: GrimoirePlugin[],
|
|
80
|
+
locals: App.Locals,
|
|
81
|
+
): Promise<string | null> {
|
|
82
|
+
for (const plugin of plugins) {
|
|
83
|
+
const result = await plugin.serializeLocals?.(locals);
|
|
84
|
+
if (result != null) return result;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* First plugin to return non-null wins.
|
|
91
|
+
* Returns null if no plugin handles it — worker uses default.
|
|
92
|
+
*/
|
|
93
|
+
export async function runDeserializeLocals(
|
|
94
|
+
plugins: GrimoirePlugin[],
|
|
95
|
+
raw: string,
|
|
96
|
+
): Promise<App.Locals | null> {
|
|
97
|
+
for (const plugin of plugins) {
|
|
98
|
+
const result = await plugin.deserializeLocals?.(raw);
|
|
99
|
+
if (result != null) return result;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* First plugin to return a WorkerDescriptor wins.
|
|
106
|
+
* Returns null if no plugin handles it — coordinator uses default routing.
|
|
107
|
+
*/
|
|
108
|
+
export async function runRouteRequest(
|
|
109
|
+
plugins: GrimoirePlugin[],
|
|
110
|
+
req: Request,
|
|
111
|
+
workers: WorkerDescriptor[],
|
|
112
|
+
routes: RouteTree,
|
|
113
|
+
): Promise<WorkerDescriptor | null> {
|
|
114
|
+
for (const plugin of plugins) {
|
|
115
|
+
const result = await plugin.routeRequest?.(req, workers, routes);
|
|
116
|
+
if (result != null) return result;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
@@ -0,0 +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,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
3
|
-
import type { RouteFile, RouteTree } from "
|
|
3
|
+
import type { RouteFile, RouteTree } from "../routing/scanner.ts";
|
|
4
4
|
|
|
5
5
|
export interface TypegenConfig {
|
|
6
6
|
projectRoot: string;
|
|
@@ -111,7 +111,10 @@ function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
|
|
|
111
111
|
` : Record<string, never>;`,
|
|
112
112
|
"",
|
|
113
113
|
);
|
|
114
|
-
lines.push(
|
|
114
|
+
lines.push(
|
|
115
|
+
`export type PageProps = { data: PageData; params: Params };`,
|
|
116
|
+
"",
|
|
117
|
+
);
|
|
115
118
|
// PageServerLoad — annotate parameter, NOT return type, to preserve inference
|
|
116
119
|
lines.push(
|
|
117
120
|
`/** Annotate the load() parameter only — not the return type — or PageData loses its concrete keys. */`,
|
|
@@ -139,7 +142,7 @@ function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
|
|
|
139
142
|
} else {
|
|
140
143
|
lines.push(
|
|
141
144
|
`export type PageData = Record<string, never>;`,
|
|
142
|
-
`export type PageProps = { params: Params };`,
|
|
145
|
+
`export type PageProps = { data: PageData; params: Params };`,
|
|
143
146
|
`export type PageServerLoad = never;`,
|
|
144
147
|
`export type Actions = Record<string, never>;`,
|
|
145
148
|
"",
|
|
@@ -161,7 +164,7 @@ function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
lines.push(
|
|
164
|
-
`export type LayoutProps =
|
|
167
|
+
`export type LayoutProps = { data: LayoutData; params: Params; children?: unknown };`,
|
|
165
168
|
`export type LayoutServerLoad = (ctx: {`,
|
|
166
169
|
` params: Params;`,
|
|
167
170
|
` request: Request;`,
|
|
@@ -186,6 +189,27 @@ function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
|
|
|
186
189
|
` locals: App.Locals;`,
|
|
187
190
|
` }) => Response | Promise<Response>;`,
|
|
188
191
|
`};`,
|
|
192
|
+
``,
|
|
193
|
+
`export type UpgradeContext = {`,
|
|
194
|
+
` request: Request;`,
|
|
195
|
+
` params: Params;`,
|
|
196
|
+
` url: URL;`,
|
|
197
|
+
` locals: App.Locals;`,
|
|
198
|
+
`};`,
|
|
199
|
+
``,
|
|
200
|
+
`type _UpgradeReturn = "upgrade" extends keyof _SRV`,
|
|
201
|
+
` ? _SRV["upgrade"] extends (...args: any[]) => any`,
|
|
202
|
+
` ? Awaited<ReturnType<_SRV["upgrade"]>>`,
|
|
203
|
+
` : {}`,
|
|
204
|
+
` : {};`,
|
|
205
|
+
`export type WsData = { params: Params } & Omit<_UpgradeReturn, "__handler">;`,
|
|
206
|
+
``,
|
|
207
|
+
`export type WebSocketHandler = {`,
|
|
208
|
+
` open?(ws: ServerWebSocket<WsData>): void | Promise<void>;`,
|
|
209
|
+
` message?(ws: ServerWebSocket<WsData>, data: string | Buffer): void | Promise<void>;`,
|
|
210
|
+
` close?(ws: ServerWebSocket<WsData>, code: number, reason?: string): void | Promise<void>;`,
|
|
211
|
+
` drain?(ws: ServerWebSocket<WsData>): void | Promise<void>;`,
|
|
212
|
+
`};`,
|
|
189
213
|
"",
|
|
190
214
|
);
|
|
191
215
|
}
|
|
@@ -210,6 +234,59 @@ function generateAmbient(): string {
|
|
|
210
234
|
" interface Locals extends Record<string, unknown> {}",
|
|
211
235
|
"}",
|
|
212
236
|
"",
|
|
237
|
+
"// Available in all route files without import.",
|
|
238
|
+
"// Params default to Record<string,string>; for per-route typed params import from './$types'.",
|
|
239
|
+
"// Note: using these globals widens return types — PageData loses concrete keys.",
|
|
240
|
+
"// For precise data inference annotate load() with PageServerLoad from './$types'.",
|
|
241
|
+
"",
|
|
242
|
+
"type PageServerLoad<",
|
|
243
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
244
|
+
"> = (ctx: {",
|
|
245
|
+
" params: P;",
|
|
246
|
+
" request: Request;",
|
|
247
|
+
" url: URL;",
|
|
248
|
+
" locals: App.Locals;",
|
|
249
|
+
"}) => Record<string, unknown> | Promise<Record<string, unknown>>;",
|
|
250
|
+
"",
|
|
251
|
+
"type LayoutServerLoad<",
|
|
252
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
253
|
+
"> = (ctx: {",
|
|
254
|
+
" params: P;",
|
|
255
|
+
" request: Request;",
|
|
256
|
+
" url: URL;",
|
|
257
|
+
" locals: App.Locals;",
|
|
258
|
+
"}) => Record<string, unknown> | Promise<Record<string, unknown>>;",
|
|
259
|
+
"",
|
|
260
|
+
"type RequestHandler<",
|
|
261
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
262
|
+
"> = (ctx: {",
|
|
263
|
+
" params: P;",
|
|
264
|
+
" request: Request;",
|
|
265
|
+
" url: URL;",
|
|
266
|
+
" locals: App.Locals;",
|
|
267
|
+
"}) => Response | Promise<Response>;",
|
|
268
|
+
"",
|
|
269
|
+
"type PageProps<",
|
|
270
|
+
" D extends Record<string, unknown> = Record<string, unknown>,",
|
|
271
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
272
|
+
"> = { data: D; params: P };",
|
|
273
|
+
"",
|
|
274
|
+
"type LayoutProps<",
|
|
275
|
+
" D extends Record<string, unknown> = Record<string, unknown>,",
|
|
276
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
277
|
+
"> = { data: D; params: P; children?: unknown };",
|
|
278
|
+
"",
|
|
279
|
+
"type ErrorProps = { status: number; message: string };",
|
|
280
|
+
"",
|
|
281
|
+
"type UpgradeContext<",
|
|
282
|
+
" P extends Record<string, string> = Record<string, string>",
|
|
283
|
+
"> = {",
|
|
284
|
+
" params: P;",
|
|
285
|
+
" request: Request;",
|
|
286
|
+
" url: URL;",
|
|
287
|
+
" locals: App.Locals;",
|
|
288
|
+
"};",
|
|
289
|
+
"",
|
|
213
290
|
].join("\n");
|
|
214
291
|
}
|
|
215
292
|
|