@sigil-dev/grimoire 0.8.5 → 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/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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface InteractiveOptions {
|
|
2
|
+
onQuit: () => void | Promise<void>;
|
|
3
|
+
onRestart: () => void | Promise<void>;
|
|
4
|
+
onClear: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a promise that resolves only when explicitly settled.
|
|
9
|
+
* Used to keep the dev/start process alive until the user quits.
|
|
10
|
+
*/
|
|
11
|
+
export function createWaiter() {
|
|
12
|
+
let resolve!: () => void;
|
|
13
|
+
const promise = new Promise<void>((r) => { resolve = r; });
|
|
14
|
+
return { promise, resolve };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Listen for interactive keypresses in dev mode.
|
|
19
|
+
* Mirrors Vite's UX: q=quit, r=restart, c=clear.
|
|
20
|
+
* Falls back gracefully if stdin is not a TTY (CI, piped input).
|
|
21
|
+
*/
|
|
22
|
+
export function listenForKeys(opts: InteractiveOptions) {
|
|
23
|
+
if (!process.stdin.isTTY) return;
|
|
24
|
+
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
process.stdin.resume();
|
|
27
|
+
process.stdin.setEncoding("utf8");
|
|
28
|
+
|
|
29
|
+
process.stdin.on("data", async (key: string) => {
|
|
30
|
+
// ctrl+c
|
|
31
|
+
if (key === "\x03") {
|
|
32
|
+
await opts.onQuit();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
switch (key.toLowerCase()) {
|
|
37
|
+
case "q":
|
|
38
|
+
await opts.onQuit();
|
|
39
|
+
break;
|
|
40
|
+
case "r":
|
|
41
|
+
await opts.onRestart();
|
|
42
|
+
break;
|
|
43
|
+
case "c":
|
|
44
|
+
opts.onClear();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function stopListening() {
|
|
51
|
+
if (!process.stdin.isTTY) return;
|
|
52
|
+
process.stdin.setRawMode(false);
|
|
53
|
+
process.stdin.pause();
|
|
54
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,206 +3,208 @@ import type { RouteFile, RouteTree } from "./routing/scanner";
|
|
|
3
3
|
|
|
4
4
|
/// <reference types="bun-types">
|
|
5
5
|
declare global {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
namespace App {
|
|
7
|
+
// augment in your own project via src/app.d.ts
|
|
8
|
+
interface Locals { }
|
|
9
|
+
interface Error {
|
|
10
|
+
message: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
interface PageData { }
|
|
14
|
+
interface Platform { }
|
|
15
|
+
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface Route {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
path: string; // /blog/:slug
|
|
20
|
+
params: Record<string, string>;
|
|
21
|
+
filePath: string; // absolute path to +page.tsx
|
|
22
|
+
loadPath?: string; // absolute path to +page.ts
|
|
23
|
+
serverPath?: string; // absolute path to +server.ts
|
|
24
|
+
layoutPath?: string; // absolute path to +layout.tsx
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface RouteInfo {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
path: string; // /blog/:slug
|
|
29
|
+
filePath: string; // absolute path
|
|
30
|
+
type: string; // page, layout etc
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface LoadContext {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
request: Request;
|
|
35
|
+
params: Record<string, string>;
|
|
36
|
+
url: URL;
|
|
37
|
+
locals: Record<string, unknown>; // set by plugins/hooks
|
|
38
|
+
fetch: typeof globalThis.fetch; // ← add
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export interface TypedLoadContext<
|
|
41
|
-
|
|
42
|
+
P extends Record<string, string> = Record<string, string>,
|
|
42
43
|
> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
request: Request;
|
|
45
|
+
params: P;
|
|
46
|
+
url: URL;
|
|
47
|
+
locals: App.Locals;
|
|
48
|
+
fetch: typeof fetch;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface RenderContext {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
route: RouteInfo;
|
|
53
|
+
request: Request;
|
|
54
|
+
params: Record<string, string>;
|
|
55
|
+
data: unknown; // return value of load()
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface BuildResult {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
success: boolean;
|
|
60
|
+
outputs: string[];
|
|
61
|
+
errors: string[];
|
|
60
62
|
}
|
|
61
63
|
export type Server = { port: number; hostname: string; stop(): void };
|
|
62
64
|
|
|
63
65
|
export interface GrimoirePlugin {
|
|
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
|
-
|
|
66
|
+
name: string;
|
|
67
|
+
|
|
68
|
+
// server lifecycle
|
|
69
|
+
onStart?(server: Server): void | Promise<void>;
|
|
70
|
+
onStop?(reason: "shutdown" | "restart"): void | Promise<void>;
|
|
71
|
+
|
|
72
|
+
// request pipeline
|
|
73
|
+
onRequest?(
|
|
74
|
+
req: Request,
|
|
75
|
+
next: () => Promise<Response>,
|
|
76
|
+
): Response | Promise<Response>;
|
|
77
|
+
|
|
78
|
+
// route lifecycle
|
|
79
|
+
onRouteLoad?(route: Route, context: LoadContext): void | Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Called after the page HTML is fully rendered.
|
|
82
|
+
* NOTE: When any plugin implements this hook, the streaming response is
|
|
83
|
+
* buffered into a single string. Streaming is preserved for routes where
|
|
84
|
+
* no plugin uses this hook. To avoid buffering, use onRequest to intercept
|
|
85
|
+
* at the Response level.
|
|
86
|
+
*/
|
|
87
|
+
onRouteRender?(
|
|
88
|
+
html: string,
|
|
89
|
+
context: RenderContext,
|
|
90
|
+
): string | Promise<string>;
|
|
91
|
+
|
|
92
|
+
// build
|
|
93
|
+
onBuildStart?(): void | Promise<void>;
|
|
94
|
+
onBuildEnd?(result: BuildResult): void | Promise<void>;
|
|
95
|
+
|
|
96
|
+
// config
|
|
97
|
+
config?: (config: GrimoireConfig) => GrimoireConfig | undefined;
|
|
98
|
+
|
|
99
|
+
// process lifecycle
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Runs AFTER Sigil/Babel compilation. Receives compiled JS, not source TSX.
|
|
103
|
+
* Suitable for string-level transforms (import rewriting, comment injection, etc.).
|
|
104
|
+
* Return null/undefined to pass through unchanged.
|
|
105
|
+
*/
|
|
106
|
+
transform?(
|
|
107
|
+
code: string,
|
|
108
|
+
id: string,
|
|
109
|
+
): string | null | undefined | Promise<string | null | undefined>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fires once after all workers are spawned and ready.
|
|
113
|
+
* Use for coordinator-level setup (e.g. opening a shared store).
|
|
114
|
+
*/
|
|
115
|
+
onCoordinatorStart?(ctx: CoordinatorContext): void | Promise<void>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fires for each worker before Bun.spawn is called.
|
|
119
|
+
* Return WorkerEnv to inject env vars or set the worker name.
|
|
120
|
+
* ALL plugins are called and results are merged.
|
|
121
|
+
*/
|
|
122
|
+
onWorkerSpawn?(
|
|
123
|
+
worker: WorkerDescriptor,
|
|
124
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: doesn't have to return a thing.
|
|
125
|
+
): WorkerEnv | void | Promise<WorkerEnv | void>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fires when a worker process is up and accepting requests.
|
|
129
|
+
*/
|
|
130
|
+
onWorkerReady?(worker: WorkerDescriptor): void | Promise<void>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fires when a worker process exits.
|
|
134
|
+
* reason "crash" = unexpected exit, "intentional" = coordinator stopped it.
|
|
135
|
+
* Use for cleanup or respawn logic (see plugin-scale).
|
|
136
|
+
*/
|
|
137
|
+
onWorkerDeath?(
|
|
138
|
+
worker: WorkerDescriptor,
|
|
139
|
+
reason: "crash" | "intentional",
|
|
140
|
+
): void | Promise<void>;
|
|
141
|
+
|
|
142
|
+
// ── Locals boundary ────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Called by coordinator before forwarding a request to a worker.
|
|
146
|
+
* Return a serialized string representation of locals.
|
|
147
|
+
* First plugin to return non-null wins. Default: signed JSON header.
|
|
148
|
+
*/
|
|
149
|
+
serializeLocals?(locals: App.Locals): string | Promise<string>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Called by worker on receiving a forwarded request.
|
|
153
|
+
* Return the deserialized locals object.
|
|
154
|
+
* First plugin to return non-null wins. Default: verify + JSON.parse.
|
|
155
|
+
*/
|
|
156
|
+
deserializeLocals?(raw: string): App.Locals | Promise<App.Locals>;
|
|
157
|
+
|
|
158
|
+
// ── Routing policy ─────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Called by coordinator to decide which worker handles a request.
|
|
162
|
+
* First plugin to return non-null wins.
|
|
163
|
+
* Default: ws routes → consistent hash, everything else → round robin.
|
|
164
|
+
* routes is the full RouteTree so plugins can make route-aware decisions.
|
|
165
|
+
*/
|
|
166
|
+
routeRequest?(
|
|
167
|
+
req: Request,
|
|
168
|
+
workers: WorkerDescriptor[],
|
|
169
|
+
routes: RouteTree,
|
|
170
|
+
): WorkerDescriptor | Promise<WorkerDescriptor>;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
export interface GrimoireConfig {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
port?: number;
|
|
175
|
+
host?: string;
|
|
176
|
+
plugins?: GrimoirePlugin[];
|
|
177
|
+
routes?: string; // glob, default 'src/routes/**';
|
|
178
|
+
dev?: boolean;
|
|
179
|
+
devEditor?: boolean;
|
|
180
|
+
vitePort?: number;
|
|
181
|
+
alias?: Record<string, string>; // e.g. { "$lib": "src/lib" } — resolved relative to cwd
|
|
182
|
+
cspNonce?: string; // CSP nonce for <script>/<style> tags
|
|
183
|
+
bundleStrategy?: "split" | "single"; // 'split' (default) or 'single' bundle
|
|
184
|
+
_skipBuild?: boolean;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
export function defineConfig(config: GrimoireConfig): GrimoireConfig {
|
|
186
|
-
|
|
188
|
+
return config;
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
/** Generic load type for +page.server.ts. For per-route typed params import PageServerLoad from './$types'. */
|
|
190
192
|
export type PageServerLoad<
|
|
191
|
-
|
|
193
|
+
P extends Record<string, string> = Record<string, string>,
|
|
192
194
|
> = (
|
|
193
|
-
|
|
195
|
+
ctx: TypedLoadContext<P>,
|
|
194
196
|
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
195
197
|
|
|
196
198
|
/** Generic load type for +layout.server.ts. For per-route typed params import LayoutServerLoad from './$types'. */
|
|
197
199
|
export type LayoutServerLoad<
|
|
198
|
-
|
|
200
|
+
P extends Record<string, string> = Record<string, string>,
|
|
199
201
|
> = (
|
|
200
|
-
|
|
202
|
+
ctx: TypedLoadContext<P>,
|
|
201
203
|
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
202
204
|
|
|
203
205
|
/** Generic handler type for +server.ts API routes. For per-route typed params import RequestHandler from './$types'. */
|
|
204
206
|
export type RequestHandler<
|
|
205
|
-
|
|
207
|
+
P extends Record<string, string> = Record<string, string>,
|
|
206
208
|
> = (ctx: TypedLoadContext<P>) => Response | Promise<Response>;
|
|
207
209
|
|
|
208
210
|
/**
|
|
@@ -211,14 +213,14 @@ export type RequestHandler<
|
|
|
211
213
|
* For per-route typing import WebSocketHandler from './$types'.
|
|
212
214
|
*/
|
|
213
215
|
export interface WsRouteHandler<T = Record<string, unknown>> {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
open?(ws: ServerWebSocket<T>): void | Promise<void>;
|
|
217
|
+
message?(ws: ServerWebSocket<T>, data: string | Buffer): void | Promise<void>;
|
|
218
|
+
close?(
|
|
219
|
+
ws: ServerWebSocket<T>,
|
|
220
|
+
code: number,
|
|
221
|
+
reason?: string,
|
|
222
|
+
): void | Promise<void>;
|
|
223
|
+
drain?(ws: ServerWebSocket<T>): void | Promise<void>;
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
// ***BIG ONE***
|
|
@@ -241,13 +243,13 @@ export type WorkerMode = BuiltinWorkerMode | (string & {});
|
|
|
241
243
|
* mode is informational and for plugin use only.
|
|
242
244
|
*/
|
|
243
245
|
export interface WorkerDescriptor {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
mode: WorkerMode;
|
|
247
|
+
index: number; // index within this mode (api-0, api-1...)
|
|
248
|
+
globalIndex: number; // index across all workers
|
|
249
|
+
internalUrl: string; // coordinator → worker base URL
|
|
250
|
+
name?: string; // set by namepool or onWorkerSpawn
|
|
251
|
+
routes: RouteFile[]; // route slice assigned by coordinator
|
|
252
|
+
pid?: number; // set after Bun.spawn
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
/**
|
|
@@ -255,16 +257,16 @@ export interface WorkerDescriptor {
|
|
|
255
257
|
* are merged — env from all plugins is combined, last name wins.
|
|
256
258
|
*/
|
|
257
259
|
export interface WorkerEnv {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
env?: Record<string, string>;
|
|
261
|
+
name?: string;
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
/**
|
|
263
265
|
* Passed to onCoordinatorStart once the coordinator is fully initialized.
|
|
264
266
|
*/
|
|
265
267
|
export interface CoordinatorContext {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
workers: WorkerDescriptor[];
|
|
269
|
+
port: number;
|
|
270
|
+
secret: string; // ephemeral signing secret, generated per-run
|
|
271
|
+
routes: RouteTree;
|
|
270
272
|
}
|