@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.
@@ -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
- 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
- }
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
- 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
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
- path: string; // /blog/:slug
29
- filePath: string; // absolute path
30
- type: string; // page, layout etc
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
- request: Request;
35
- params: Record<string, string>;
36
- url: URL;
37
- locals: Record<string, unknown>; // set by plugins/hooks
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
- P extends Record<string, string> = Record<string, string>,
42
+ P extends Record<string, string> = Record<string, string>,
42
43
  > {
43
- request: Request;
44
- params: P;
45
- url: URL;
46
- locals: App.Locals;
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
- route: RouteInfo;
51
- request: Request;
52
- params: Record<string, string>;
53
- data: unknown; // return value of load()
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
- success: boolean;
58
- outputs: string[];
59
- errors: string[];
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
- name: string;
65
-
66
- // server lifecycle
67
- onStart?(server: Server): void | Promise<void>;
68
- onStop?(reason: "shutdown" | "restart"): void | Promise<void>;
69
-
70
- // request pipeline
71
- onRequest?(
72
- req: Request,
73
- next: () => Promise<Response>,
74
- ): Response | Promise<Response>;
75
-
76
- // route lifecycle
77
- onRouteLoad?(route: Route, context: LoadContext): void | Promise<void>;
78
- /**
79
- * Called after the page HTML is fully rendered.
80
- * NOTE: When any plugin implements this hook, the streaming response is
81
- * buffered into a single string. Streaming is preserved for routes where
82
- * no plugin uses this hook. To avoid buffering, use onRequest to intercept
83
- * at the Response level.
84
- */
85
- onRouteRender?(
86
- html: string,
87
- context: RenderContext,
88
- ): string | Promise<string>;
89
-
90
- // build
91
- onBuildStart?(): void | Promise<void>;
92
- onBuildEnd?(result: BuildResult): void | Promise<void>;
93
-
94
- // config
95
- config?: (config: GrimoireConfig) => GrimoireConfig | undefined;
96
-
97
- // process lifecycle
98
-
99
- /**
100
- * Runs AFTER Sigil/Babel compilation. Receives compiled JS, not source TSX.
101
- * Suitable for string-level transforms (import rewriting, comment injection, etc.).
102
- * Return null/undefined to pass through unchanged.
103
- */
104
- transform?(
105
- code: string,
106
- id: string,
107
- ): string | null | undefined | Promise<string | null | undefined>;
108
-
109
- /**
110
- * Fires once after all workers are spawned and ready.
111
- * Use for coordinator-level setup (e.g. opening a shared store).
112
- */
113
- onCoordinatorStart?(ctx: CoordinatorContext): void | Promise<void>;
114
-
115
- /**
116
- * Fires for each worker before Bun.spawn is called.
117
- * Return WorkerEnv to inject env vars or set the worker name.
118
- * ALL plugins are called and results are merged.
119
- */
120
- onWorkerSpawn?(
121
- worker: WorkerDescriptor,
122
- // biome-ignore lint/suspicious/noConfusingVoidType: doesn't have to return a thing.
123
- ): WorkerEnv | void | Promise<WorkerEnv | void>;
124
-
125
- /**
126
- * Fires when a worker process is up and accepting requests.
127
- */
128
- onWorkerReady?(worker: WorkerDescriptor): void | Promise<void>;
129
-
130
- /**
131
- * Fires when a worker process exits.
132
- * reason "crash" = unexpected exit, "intentional" = coordinator stopped it.
133
- * Use for cleanup or respawn logic (see plugin-scale).
134
- */
135
- onWorkerDeath?(
136
- worker: WorkerDescriptor,
137
- reason: "crash" | "intentional",
138
- ): void | Promise<void>;
139
-
140
- // ── Locals boundary ────────────────────────────────────────
141
-
142
- /**
143
- * Called by coordinator before forwarding a request to a worker.
144
- * Return a serialized string representation of locals.
145
- * First plugin to return non-null wins. Default: signed JSON header.
146
- */
147
- serializeLocals?(locals: App.Locals): string | Promise<string>;
148
-
149
- /**
150
- * Called by worker on receiving a forwarded request.
151
- * Return the deserialized locals object.
152
- * First plugin to return non-null wins. Default: verify + JSON.parse.
153
- */
154
- deserializeLocals?(raw: string): App.Locals | Promise<App.Locals>;
155
-
156
- // ── Routing policy ─────────────────────────────────────────
157
-
158
- /**
159
- * Called by coordinator to decide which worker handles a request.
160
- * First plugin to return non-null wins.
161
- * Default: ws routes → consistent hash, everything else → round robin.
162
- * routes is the full RouteTree so plugins can make route-aware decisions.
163
- */
164
- routeRequest?(
165
- req: Request,
166
- workers: WorkerDescriptor[],
167
- routes: RouteTree,
168
- ): WorkerDescriptor | Promise<WorkerDescriptor>;
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
- port?: number;
173
- host?: string;
174
- plugins?: GrimoirePlugin[];
175
- routes?: string; // glob, default 'src/routes/**';
176
- dev?: boolean;
177
- devEditor?: boolean;
178
- vitePort?: number;
179
- alias?: Record<string, string>; // e.g. { "$lib": "src/lib" } — resolved relative to cwd
180
- cspNonce?: string; // CSP nonce for <script>/<style> tags
181
- bundleStrategy?: "split" | "single"; // 'split' (default) or 'single' bundle
182
- _skipBuild?: boolean;
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
- return config;
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
- P extends Record<string, string> = Record<string, string>,
193
+ P extends Record<string, string> = Record<string, string>,
192
194
  > = (
193
- ctx: TypedLoadContext<P>,
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
- P extends Record<string, string> = Record<string, string>,
200
+ P extends Record<string, string> = Record<string, string>,
199
201
  > = (
200
- ctx: TypedLoadContext<P>,
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
- P extends Record<string, string> = Record<string, string>,
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
- open?(ws: ServerWebSocket<T>): void | Promise<void>;
215
- message?(ws: ServerWebSocket<T>, data: string | Buffer): void | Promise<void>;
216
- close?(
217
- ws: ServerWebSocket<T>,
218
- code: number,
219
- reason?: string,
220
- ): void | Promise<void>;
221
- drain?(ws: ServerWebSocket<T>): void | Promise<void>;
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
- mode: WorkerMode;
245
- index: number; // index within this mode (api-0, api-1...)
246
- globalIndex: number; // index across all workers
247
- internalUrl: string; // coordinator → worker base URL
248
- name?: string; // set by namepool or onWorkerSpawn
249
- routes: RouteFile[]; // route slice assigned by coordinator
250
- pid?: number; // set after Bun.spawn
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
- env?: Record<string, string>;
259
- name?: string;
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
- workers: WorkerDescriptor[];
267
- port: number;
268
- secret: string; // ephemeral signing secret, generated per-run
269
- routes: RouteTree;
268
+ workers: WorkerDescriptor[];
269
+ port: number;
270
+ secret: string; // ephemeral signing secret, generated per-run
271
+ routes: RouteTree;
270
272
  }