@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.
Files changed (50) hide show
  1. package/.grimoire/_routes.dom.js +8 -0
  2. package/.grimoire/_routes.hydrate.js +8 -0
  3. package/.grimoire/tsconfig.generated.json +11 -0
  4. package/.grimoire/types/ambient.d.ts +59 -0
  5. package/.grimoire/types/api/hello/$types.d.ts +50 -0
  6. package/.grimoire/types/api/items/$types.d.ts +50 -0
  7. package/.grimoire/types/echo/$types.d.ts +50 -0
  8. package/.grimoire/types/env-private.d.ts +5 -0
  9. package/.grimoire/types/env-public.d.ts +5 -0
  10. package/.grimoire/types/mixed/$types.d.ts +50 -0
  11. package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
  12. package/.grimoire/types/reject/$types.d.ts +50 -0
  13. package/index.ts +34 -34
  14. package/package.json +8 -4
  15. package/preload.js +2 -0
  16. package/public/__grimoire__/hydrate.js +585 -0
  17. package/public/__grimoire__/index.js +490 -0
  18. package/src/client/head.ts +29 -0
  19. package/src/client/router.ts +224 -76
  20. package/src/env/index.ts +25 -0
  21. package/src/env/plugin.ts +13 -0
  22. package/src/env/private.ts +5 -0
  23. package/src/env/public.ts +7 -0
  24. package/src/env/typegen.ts +51 -0
  25. package/src/integrations/vite.ts +72 -72
  26. package/src/rendering/head.ts +22 -2
  27. package/src/rendering/hydrate.ts +81 -27
  28. package/src/rendering/index.ts +199 -186
  29. package/src/rendering/ssrPlugin.ts +53 -47
  30. package/src/routing/manifest-gen.ts +39 -26
  31. package/src/routing/router.ts +106 -98
  32. package/src/routing/scanner.ts +135 -129
  33. package/src/routing/transform-routes.ts +101 -101
  34. package/src/server/build.ts +147 -90
  35. package/src/server/coordinator.ts +306 -297
  36. package/src/server/hooks.ts +24 -3
  37. package/src/server/index.ts +144 -70
  38. package/src/server/worker.ts +59 -59
  39. package/src/typegen/index.ts +353 -340
  40. package/src/types.ts +269 -260
  41. package/test/context.test.ts +52 -52
  42. package/test/hydration.test.ts +119 -119
  43. package/test/middleware.test.ts +223 -221
  44. package/test/rendering.test.ts +425 -425
  45. package/test/routing.test.ts +83 -45
  46. package/test/scanning.test.ts +181 -169
  47. package/test/server.test.ts +229 -229
  48. package/test/streaming.test.ts +106 -106
  49. package/test/transform-routes.test.ts +84 -84
  50. package/test/typegen.test.ts +19 -1
@@ -1,340 +1,353 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, join, relative } from "node:path";
3
- import type { RouteFile, RouteTree } from "../routing/scanner.ts";
4
-
5
- export interface TypegenConfig {
6
- projectRoot: string;
7
- routesDir: string;
8
- outDir: string; // absolute path to .grimoire/types
9
- }
10
-
11
- export interface RouteGroup {
12
- dir: string;
13
- paramNames: string[];
14
- page?: RouteFile;
15
- pageServer?: RouteFile;
16
- layout?: RouteFile;
17
- layoutServer?: RouteFile;
18
- server?: RouteFile;
19
- error?: RouteFile;
20
- }
21
-
22
- // On Windows, relative() returns the absolute `to` path when src and dest are on
23
- // different drives. Detect that and fall back to routes-dir-relative so join()
24
- // doesn't produce an invalid double-rooted path like "D:\out\C:\Users\...".
25
- function safeRelativeDir(
26
- projectRoot: string,
27
- routesDir: string,
28
- groupDir: string,
29
- ): string {
30
- const rel = relative(projectRoot, groupDir);
31
- return isAbsolute(rel) ? relative(routesDir, groupDir) : rel;
32
- }
33
-
34
- export function groupByDirectory(tree: RouteTree): Map<string, RouteGroup> {
35
- const groups = new Map<string, RouteGroup>();
36
-
37
- // Only process + convention files — simple files don't get $types
38
- const files: RouteFile[] = [
39
- ...tree.routes.filter((f) => f.type !== "simple"),
40
- ...tree.layouts,
41
- ...tree.servers,
42
- ...tree.errors,
43
- ];
44
-
45
- for (const file of files) {
46
- const dir = dirname(file.filePath);
47
- if (!groups.has(dir)) {
48
- groups.set(dir, { dir, paramNames: file.paramNames });
49
- }
50
- const g = groups.get(dir)!;
51
- if (file.paramNames.length > g.paramNames.length) {
52
- g.paramNames = file.paramNames;
53
- }
54
- switch (file.type) {
55
- case "page":
56
- g.page = file;
57
- break;
58
- case "pageServer":
59
- g.pageServer = file;
60
- break;
61
- case "layout":
62
- g.layout = file;
63
- break;
64
- case "layoutServer":
65
- g.layoutServer = file;
66
- break;
67
- case "server":
68
- g.server = file;
69
- break;
70
- case "error":
71
- g.error = file;
72
- break;
73
- }
74
- }
75
-
76
- return groups;
77
- }
78
-
79
- export function toImportPath(from: string, to: string): string {
80
- return relative(from, to)
81
- .replace(/\\/g, "/")
82
- .replace(/\.tsx?$/, ".js");
83
- }
84
-
85
- export function buildParams(paramNames: string[]): string {
86
- if (paramNames.length === 0) return "{}";
87
- const props = paramNames.map((p) => ` ${p}: string;`).join("\n");
88
- return `{\n${props}\n}`;
89
- }
90
-
91
- function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
92
- const lines: string[] = [
93
- "// Auto-generated by @sigil-dev/grimoire — do not edit",
94
- "",
95
- ];
96
-
97
- const paramsType = buildParams(group.paramNames);
98
-
99
- // --- Params ---
100
- lines.push(`export type Params = ${paramsType};`, "");
101
-
102
- // --- PageData from +page.server.ts ---
103
- if (group.pageServer) {
104
- const imp = toImportPath(outFileDir, group.pageServer.filePath);
105
- // Use `typeof import(...)` (not `import type * as NS`) so the module type
106
- // can be used in `keyof` and indexed access without TS2709.
107
- lines.push(
108
- `type _PS = typeof import("${imp}");`,
109
- `export type PageData = "load" extends keyof _PS`,
110
- ` ? Awaited<ReturnType<_PS["load"]>>`,
111
- ` : Record<string, never>;`,
112
- "",
113
- );
114
- lines.push(
115
- `export type PageProps = { data: PageData; params: Params };`,
116
- "",
117
- );
118
- // PageServerLoad annotate parameter, NOT return type, to preserve inference
119
- lines.push(
120
- `/** Annotate the load() parameter only — not the return type — or PageData loses its concrete keys. */`,
121
- `export type PageServerLoad = (ctx: {`,
122
- ` params: Params;`,
123
- ` request: Request;`,
124
- ` url: URL;`,
125
- ` locals: App.Locals;`,
126
- `}) => Record<string, unknown> | Promise<Record<string, unknown>>;`,
127
- "",
128
- );
129
- // Actions — all exports except load and default
130
- lines.push(
131
- `type _ActionKeys = Exclude<keyof _PS, "load" | "default">;`,
132
- `export type Actions = {`,
133
- ` [K in _ActionKeys]: (ctx: {`,
134
- ` params: Params;`,
135
- ` request: Request;`,
136
- ` url: URL;`,
137
- ` locals: App.Locals;`,
138
- ` }) => unknown | Promise<unknown>;`,
139
- `};`,
140
- "",
141
- );
142
- } else {
143
- lines.push(
144
- `export type PageData = Record<string, never>;`,
145
- `export type PageProps = { data: PageData; params: Params };`,
146
- `export type PageServerLoad = never;`,
147
- `export type Actions = Record<string, never>;`,
148
- "",
149
- );
150
- }
151
-
152
- // --- LayoutData from +layout.server.ts ---
153
- if (group.layoutServer) {
154
- const imp = toImportPath(outFileDir, group.layoutServer.filePath);
155
- lines.push(
156
- `type _LS = typeof import("${imp}");`,
157
- `export type LayoutData = "load" extends keyof _LS`,
158
- ` ? Awaited<ReturnType<_LS["load"]>>`,
159
- ` : Record<string, never>;`,
160
- "",
161
- );
162
- } else {
163
- lines.push(`export type LayoutData = Record<string, never>;`);
164
- }
165
-
166
- lines.push(
167
- `export type LayoutProps = { data: LayoutData; params: Params; children?: unknown };`,
168
- `export type LayoutServerLoad = (ctx: {`,
169
- ` params: Params;`,
170
- ` request: Request;`,
171
- ` url: URL;`,
172
- ` locals: App.Locals;`,
173
- `}) => Record<string, unknown> | Promise<Record<string, unknown>>;`,
174
- "",
175
- );
176
-
177
- // --- +server.ts API route handlers ---
178
- if (group.server) {
179
- const imp = toImportPath(outFileDir, group.server.filePath);
180
- lines.push(
181
- `type _SRV = typeof import("${imp}");`,
182
- `type _HttpMethods = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";`,
183
- `type _ServerKeys = Extract<keyof _SRV, _HttpMethods>;`,
184
- `export type ServerHandlers = {`,
185
- ` [K in _ServerKeys]: (ctx: {`,
186
- ` params: Params;`,
187
- ` request: Request;`,
188
- ` url: URL;`,
189
- ` locals: App.Locals;`,
190
- ` }) => Response | Promise<Response>;`,
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
- `};`,
213
- "",
214
- );
215
- }
216
-
217
- // --- +error.tsx ---
218
- if (group.error) {
219
- lines.push(
220
- `export type ErrorProps = { status: number; message: string };`,
221
- "",
222
- );
223
- }
224
-
225
- return lines.join("\n");
226
- }
227
-
228
- function generateAmbient(): string {
229
- return [
230
- "// Auto-generated by @sigil-dev/grimoire do not edit",
231
- "",
232
- "declare namespace App {",
233
- " // Extend this interface in your app's src/app.d.ts to add typed locals",
234
- " interface Locals extends Record<string, unknown> {}",
235
- "}",
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
- "",
290
- ].join("\n");
291
- }
292
-
293
- function generateTsConfig(): string {
294
- return JSON.stringify(
295
- {
296
- compilerOptions: {
297
- // Merges ".." (project root) and "./types" (.grimoire/types) into one
298
- // virtual root so `import from './$types'` resolves without path aliases.
299
- rootDirs: ["..", "./types"],
300
- },
301
- include: ["types/**/*.d.ts"],
302
- },
303
- null,
304
- 2,
305
- );
306
- }
307
-
308
- export async function generateTypes(
309
- tree: RouteTree,
310
- config: TypegenConfig,
311
- ): Promise<void> {
312
- const { projectRoot, outDir } = config;
313
-
314
- await rm(outDir, { recursive: true, force: true });
315
- await mkdir(outDir, { recursive: true });
316
-
317
- const groups = groupByDirectory(tree);
318
-
319
- for (const group of groups.values()) {
320
- const routeRelDir = safeRelativeDir(
321
- projectRoot,
322
- config.routesDir,
323
- group.dir,
324
- );
325
- const outFileDir = join(outDir, routeRelDir);
326
- await mkdir(outFileDir, { recursive: true });
327
-
328
- const content = generateTypesForGroup(group, outFileDir);
329
- await writeFile(join(outFileDir, "$types.d.ts"), content, "utf-8");
330
- }
331
-
332
- await writeFile(join(outDir, "ambient.d.ts"), generateAmbient(), "utf-8");
333
-
334
- const grimoireDir = dirname(outDir);
335
- await writeFile(
336
- join(grimoireDir, "tsconfig.generated.json"),
337
- generateTsConfig(),
338
- "utf-8",
339
- );
340
- }
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative } from "node:path";
4
+ import type { RouteFile, RouteTree } from "../routing/scanner.ts";
5
+
6
+ export interface TypegenConfig {
7
+ projectRoot: string;
8
+ routesDir: string;
9
+ outDir: string; // absolute path to .grimoire/types
10
+ }
11
+
12
+ export interface RouteGroup {
13
+ dir: string;
14
+ paramNames: string[];
15
+ restParamNames: string[];
16
+ page?: RouteFile;
17
+ pageServer?: RouteFile;
18
+ layout?: RouteFile;
19
+ layoutServer?: RouteFile;
20
+ server?: RouteFile;
21
+ error?: RouteFile;
22
+ }
23
+
24
+ // On Windows, relative() returns the absolute `to` path when src and dest are on
25
+ // different drives. Detect that and fall back to routes-dir-relative so join()
26
+ // doesn't produce an invalid double-rooted path like "D:\out\C:\Users\...".
27
+ function safeRelativeDir(
28
+ projectRoot: string,
29
+ routesDir: string,
30
+ groupDir: string,
31
+ ): string {
32
+ const rel = relative(projectRoot, groupDir);
33
+ return isAbsolute(rel) ? relative(routesDir, groupDir) : rel;
34
+ }
35
+
36
+ export function groupByDirectory(tree: RouteTree): Map<string, RouteGroup> {
37
+ const groups = new Map<string, RouteGroup>();
38
+
39
+ // Only process + convention files — simple files don't get $types
40
+ const files: RouteFile[] = [
41
+ ...tree.routes.filter((f) => f.type !== "simple"),
42
+ ...tree.layouts,
43
+ ...tree.servers,
44
+ ...tree.errors,
45
+ ];
46
+
47
+ for (const file of files) {
48
+ const dir = dirname(file.filePath);
49
+ if (!groups.has(dir)) {
50
+ groups.set(dir, {
51
+ dir,
52
+ paramNames: file.paramNames,
53
+ restParamNames: file.restParamNames,
54
+ });
55
+ }
56
+ const g = groups.get(dir)!;
57
+ if (file.paramNames.length > g.paramNames.length) {
58
+ g.paramNames = file.paramNames;
59
+ }
60
+ if (file.restParamNames.length > g.restParamNames.length) {
61
+ g.restParamNames = file.restParamNames;
62
+ }
63
+ switch (file.type) {
64
+ case "page":
65
+ g.page = file;
66
+ break;
67
+ case "pageServer":
68
+ g.pageServer = file;
69
+ break;
70
+ case "layout":
71
+ g.layout = file;
72
+ break;
73
+ case "layoutServer":
74
+ g.layoutServer = file;
75
+ break;
76
+ case "server":
77
+ g.server = file;
78
+ break;
79
+ case "error":
80
+ g.error = file;
81
+ break;
82
+ }
83
+ }
84
+
85
+ return groups;
86
+ }
87
+
88
+ export function toImportPath(from: string, to: string): string {
89
+ return relative(from, to)
90
+ .replace(/\\/g, "/")
91
+ .replace(/\.tsx?$/, ".js");
92
+ }
93
+
94
+ export function buildParams(paramNames: string[], restParamNames: string[] = []): string {
95
+ const all = [...paramNames, ...restParamNames];
96
+ if (all.length === 0) return "{}";
97
+ const props = all.map((p) => ` ${p}: string;`).join("\n");
98
+ return `{\n${props}\n}`;
99
+ }
100
+
101
+ function generateTypesForGroup(group: RouteGroup, outFileDir: string): string {
102
+ const lines: string[] = [
103
+ "// Auto-generated by @sigil-dev/grimoire — do not edit",
104
+ "",
105
+ ];
106
+
107
+ const paramsType = buildParams(group.paramNames, group.restParamNames);
108
+
109
+ // --- Params ---
110
+ lines.push(`export type Params = ${paramsType};`, "");
111
+
112
+ // --- PageData from +page.server.ts ---
113
+ if (group.pageServer) {
114
+ const imp = toImportPath(outFileDir, group.pageServer.filePath);
115
+ // Use `typeof import(...)` (not `import type * as NS`) so the module type
116
+ // can be used in `keyof` and indexed access without TS2709.
117
+ lines.push(
118
+ `type _PS = typeof import("${imp}");`,
119
+ `export type PageData = "load" extends keyof _PS`,
120
+ ` ? Awaited<ReturnType<_PS["load"]>>`,
121
+ ` : Record<string, never>;`,
122
+ "",
123
+ );
124
+ lines.push(
125
+ `export type PageProps = { data: PageData; params: Params };`,
126
+ "",
127
+ );
128
+ // PageServerLoad — annotate parameter, NOT return type, to preserve inference
129
+ lines.push(
130
+ `/** Annotate the load() parameter only — not the return type — or PageData loses its concrete keys. */`,
131
+ `export type PageServerLoad = (ctx: {`,
132
+ ` params: Params;`,
133
+ ` request: Request;`,
134
+ ` url: URL;`,
135
+ ` locals: App.Locals;`,
136
+ `}) => Record<string, unknown> | Promise<Record<string, unknown>>;`,
137
+ "",
138
+ );
139
+ // Actions — all exports except load and default
140
+ lines.push(
141
+ `type _ActionKeys = Exclude<keyof _PS, "load" | "default">;`,
142
+ `export type Actions = {`,
143
+ ` [K in _ActionKeys]: (ctx: {`,
144
+ ` params: Params;`,
145
+ ` request: Request;`,
146
+ ` url: URL;`,
147
+ ` locals: App.Locals;`,
148
+ ` }) => unknown | Promise<unknown>;`,
149
+ `};`,
150
+ "",
151
+ );
152
+ } else {
153
+ lines.push(
154
+ `export type PageData = Record<string, never>;`,
155
+ `export type PageProps = { data: PageData; params: Params };`,
156
+ `export type PageServerLoad = never;`,
157
+ `export type Actions = Record<string, never>;`,
158
+ "",
159
+ );
160
+ }
161
+
162
+ // --- LayoutData from +layout.server.ts ---
163
+ if (group.layoutServer) {
164
+ const imp = toImportPath(outFileDir, group.layoutServer.filePath);
165
+ lines.push(
166
+ `type _LS = typeof import("${imp}");`,
167
+ `export type LayoutData = "load" extends keyof _LS`,
168
+ ` ? Awaited<ReturnType<_LS["load"]>>`,
169
+ ` : Record<string, never>;`,
170
+ "",
171
+ );
172
+ } else {
173
+ lines.push(`export type LayoutData = Record<string, never>;`);
174
+ }
175
+
176
+ lines.push(
177
+ `export type LayoutProps = { data: LayoutData; params: Params; children?: unknown };`,
178
+ `export type LayoutServerLoad = (ctx: {`,
179
+ ` params: Params;`,
180
+ ` request: Request;`,
181
+ ` url: URL;`,
182
+ ` locals: App.Locals;`,
183
+ `}) => Record<string, unknown> | Promise<Record<string, unknown>>;`,
184
+ "",
185
+ );
186
+
187
+ // --- +server.ts API route handlers ---
188
+ if (group.server) {
189
+ const imp = toImportPath(outFileDir, group.server.filePath);
190
+ lines.push(
191
+ `type _SRV = typeof import("${imp}");`,
192
+ `type _HttpMethods = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";`,
193
+ `type _ServerKeys = Extract<keyof _SRV, _HttpMethods>;`,
194
+ `export type ServerHandlers = {`,
195
+ ` [K in _ServerKeys]: (ctx: {`,
196
+ ` params: Params;`,
197
+ ` request: Request;`,
198
+ ` url: URL;`,
199
+ ` locals: App.Locals;`,
200
+ ` }) => Response | Promise<Response>;`,
201
+ `};`,
202
+ ``,
203
+ `export type UpgradeContext = {`,
204
+ ` request: Request;`,
205
+ ` params: Params;`,
206
+ ` url: URL;`,
207
+ ` locals: App.Locals;`,
208
+ `};`,
209
+ ``,
210
+ `type _UpgradeReturn = "upgrade" extends keyof _SRV`,
211
+ ` ? _SRV["upgrade"] extends (...args: any[]) => any`,
212
+ ` ? Awaited<ReturnType<_SRV["upgrade"]>>`,
213
+ ` : {}`,
214
+ ` : {};`,
215
+ `export type WsData = { params: Params } & Omit<_UpgradeReturn, "__handler">;`,
216
+ ``,
217
+ `export type WebSocketHandler = {`,
218
+ ` open?(ws: ServerWebSocket<WsData>): void | Promise<void>;`,
219
+ ` message?(ws: ServerWebSocket<WsData>, data: string | Buffer): void | Promise<void>;`,
220
+ ` close?(ws: ServerWebSocket<WsData>, code: number, reason?: string): void | Promise<void>;`,
221
+ ` drain?(ws: ServerWebSocket<WsData>): void | Promise<void>;`,
222
+ `};`,
223
+ "",
224
+ );
225
+ }
226
+
227
+ // --- +error.tsx ---
228
+ if (group.error) {
229
+ lines.push(
230
+ `export type ErrorProps = { status: number; message: string };`,
231
+ "",
232
+ );
233
+ }
234
+
235
+ return lines.join("\n");
236
+ }
237
+
238
+ function generateAmbient(): string {
239
+ return [
240
+ "// Auto-generated by @sigil-dev/grimoire do not edit",
241
+ "",
242
+ "declare namespace App {",
243
+ " // Extend this interface in your app's src/app.d.ts to add typed locals",
244
+ " interface Locals extends Record<string, unknown> {}",
245
+ "}",
246
+ "",
247
+ "// Available in all route files without import.",
248
+ "// Params default to Record<string,string>; for per-route typed params import from './$types'.",
249
+ "// Note: using these globals widens return types — PageData loses concrete keys.",
250
+ "// For precise data inference annotate load() with PageServerLoad from './$types'.",
251
+ "",
252
+ "type PageServerLoad<",
253
+ " P extends Record<string, string> = Record<string, string>",
254
+ "> = (ctx: {",
255
+ " params: P;",
256
+ " request: Request;",
257
+ " url: URL;",
258
+ " locals: App.Locals;",
259
+ "}) => Record<string, unknown> | Promise<Record<string, unknown>>;",
260
+ "",
261
+ "type LayoutServerLoad<",
262
+ " P extends Record<string, string> = Record<string, string>",
263
+ "> = (ctx: {",
264
+ " params: P;",
265
+ " request: Request;",
266
+ " url: URL;",
267
+ " locals: App.Locals;",
268
+ "}) => Record<string, unknown> | Promise<Record<string, unknown>>;",
269
+ "",
270
+ "type RequestHandler<",
271
+ " P extends Record<string, string> = Record<string, string>",
272
+ "> = (ctx: {",
273
+ " params: P;",
274
+ " request: Request;",
275
+ " url: URL;",
276
+ " locals: App.Locals;",
277
+ "}) => Response | Promise<Response>;",
278
+ "",
279
+ "type PageProps<",
280
+ " D extends Record<string, unknown> = Record<string, unknown>,",
281
+ " P extends Record<string, string> = Record<string, string>",
282
+ "> = { data: D; params: P };",
283
+ "",
284
+ "type LayoutProps<",
285
+ " D extends Record<string, unknown> = Record<string, unknown>,",
286
+ " P extends Record<string, string> = Record<string, string>",
287
+ "> = { data: D; params: P; children?: unknown };",
288
+ "",
289
+ "type ErrorProps = { status: number; message: string };",
290
+ "",
291
+ "type UpgradeContext<",
292
+ " P extends Record<string, string> = Record<string, string>",
293
+ "> = {",
294
+ " params: P;",
295
+ " request: Request;",
296
+ " url: URL;",
297
+ " locals: App.Locals;",
298
+ "};",
299
+ "",
300
+ ].join("\n");
301
+ }
302
+
303
+ function generateTsConfig(): string {
304
+ return JSON.stringify(
305
+ {
306
+ compilerOptions: {
307
+ // Merges ".." (project root) and "./types" (.grimoire/types) into one
308
+ // virtual root so `import from './$types'` resolves without path aliases.
309
+ rootDirs: ["..", "./types"],
310
+ },
311
+ include: ["types/**/*.d.ts"],
312
+ },
313
+ null,
314
+ 2,
315
+ );
316
+ }
317
+
318
+ export async function generateTypes(
319
+ tree: RouteTree,
320
+ config: TypegenConfig,
321
+ ): Promise<void> {
322
+ const { projectRoot, outDir } = config;
323
+
324
+ // Skip rm — on Windows, rm+mkdir in quick succession can race and leave dirs
325
+ // in an unusable state. Stale type files are harmless; we overwrite what we need.
326
+ await mkdir(outDir, { recursive: true }).catch(() => {});
327
+
328
+ const groups = groupByDirectory(tree);
329
+
330
+ for (const group of groups.values()) {
331
+ const routeRelDir = safeRelativeDir(
332
+ projectRoot,
333
+ config.routesDir,
334
+ group.dir,
335
+ );
336
+ const outFileDir = join(outDir, routeRelDir);
337
+ // Use mkdirSync — async mkdir on Windows/Bun can report success before
338
+ // the directory is actually visible to subsequent fs calls
339
+ mkdirSync(outFileDir, { recursive: true });
340
+
341
+ const content = generateTypesForGroup(group, outFileDir);
342
+ writeFileSync(join(outFileDir, "$types.d.ts"), content, "utf-8");
343
+ }
344
+
345
+ writeFileSync(join(outDir, "ambient.d.ts"), generateAmbient(), "utf-8");
346
+
347
+ const grimoireDir = dirname(outDir);
348
+ writeFileSync(
349
+ join(grimoireDir, "tsconfig.generated.json"),
350
+ generateTsConfig(),
351
+ "utf-8",
352
+ );
353
+ }