@luckystack/devkit 0.6.7 → 0.7.1
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/CHANGELOG.md +90 -0
- package/CLAUDE.md +129 -125
- package/LICENSE +21 -21
- package/README.md +44 -44
- package/dist/{chunk-EU2RFSLO.js → chunk-BZTCJ7JY.js} +1 -1
- package/dist/chunk-BZTCJ7JY.js.map +1 -0
- package/dist/cli/validateDeploy.js +1 -1
- package/dist/cli/validateDeploy.js.map +1 -1
- package/dist/index.js +257 -35
- package/dist/index.js.map +1 -1
- package/dist/supervisor.js +96 -45
- package/dist/supervisor.js.map +1 -1
- package/dist/templates/api.template.ts +54 -54
- package/dist/templates/page_dashboard.template.tsx +35 -35
- package/dist/templates/page_plain.template.tsx +32 -32
- package/dist/templates/sync_client.template.ts +40 -40
- package/dist/templates/sync_client_paired.template.ts +38 -38
- package/dist/templates/sync_client_standalone.template.ts +36 -36
- package/dist/templates/sync_server.template.ts +64 -64
- package/docs/cli.md +245 -245
- package/docs/hot-reload.md +365 -365
- package/docs/loader-pipeline.md +324 -324
- package/docs/runtime-type-resolver.md +258 -258
- package/docs/supervisor.md +292 -292
- package/docs/ts-program-cache.md +200 -199
- package/docs/type-map-generation.md +410 -392
- package/package.json +10 -4
- package/dist/chunk-EU2RFSLO.js.map +0 -1
|
@@ -1,392 +1,410 @@
|
|
|
1
|
-
# Type Map Generation (`generateTypeMapFile` + emitters)
|
|
2
|
-
|
|
3
|
-
> Dev-only. `generateTypeMapFile()` runs at build time and during hot reload. The generated artifacts (`apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, `apiDocs.generated.json`) ship with the project source; production servers read them as compiled TypeScript / JSON, never re-running the emitter.
|
|
4
|
-
|
|
5
|
-
The type-map generator is the canonical source of truth for typed `apiRequest` / `syncRequest` calls. It walks every `_api/` and `_sync/` file under `srcDir`, runs the TypeChecker-backed extractors to produce fully-expanded inline types, emits a single typed map plus a Zod schema file plus a docs JSON, and validates that no unresolved type identifiers leaked through.
|
|
6
|
-
|
|
7
|
-
`generateTypeMapFile(options?)` is the only public entry point; the helpers under `typeMap/` are internal building blocks.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Pipeline order
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
export const generateTypeMapFile = (options: GenerateTypeMapOptions = {}): void => {
|
|
15
|
-
const { quiet = false } = options;
|
|
16
|
-
|
|
17
|
-
assertValidRouteNaming({ srcDir: getSrcDir(), context: 'generating API/sync type maps' });
|
|
18
|
-
assertNoDuplicateNormalizedRouteKeys({ srcDir: getSrcDir(), context: 'generating API/sync type maps' });
|
|
19
|
-
|
|
20
|
-
invalidateProgramCache();
|
|
21
|
-
namedImports.clear();
|
|
22
|
-
defaultImports.clear();
|
|
23
|
-
|
|
24
|
-
// 1. Walk apiFiles, extract per-route input/output/stream/meta
|
|
25
|
-
// 2. Walk sync server/client files, join by `pagePath/syncName/version`
|
|
26
|
-
// 3. Build the Functions interface from the server functions tree
|
|
27
|
-
// 4. Abort on unresolved type symbols
|
|
28
|
-
// 5. Build + write the three artifacts
|
|
29
|
-
|
|
30
|
-
// ... (see breakdown below) ...
|
|
31
|
-
};
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
`assertValidRouteNaming` and `assertNoDuplicateNormalizedRouteKeys` come first so a typo aborts before the expensive TypeScript Program build. `invalidateProgramCache()` is mandatory at the top — the generator is called both from build scripts (fresh process, no cache anyway) and from hot reload (cache may be warm but a file just changed).
|
|
35
|
-
|
|
36
|
-
`namedImports` and `defaultImports` are module-level `Map`s that accumulate import statements for the generated file. They are cleared on each run so retries don't leak stale imports.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## API extraction loop
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
const apiFiles = findAllApiFiles(getSrcDir());
|
|
44
|
-
const typesByPage = new Map<string, Map<string, { input, output, stream, method, rateLimit, auth, version }>>();
|
|
45
|
-
const unresolvedTypeAliases = new Set<string>();
|
|
46
|
-
|
|
47
|
-
for (const filePath of apiFiles) {
|
|
48
|
-
const pagePath = extractPagePath(filePath);
|
|
49
|
-
const apiName = extractApiName(filePath);
|
|
50
|
-
const apiVersion = extractApiVersion(filePath);
|
|
51
|
-
if (!pagePath || !apiName) continue;
|
|
52
|
-
|
|
53
|
-
const inputTypeResult = getInputTypeDetailsFromFile(filePath);
|
|
54
|
-
const outputTypeResult = getOutputTypeDetailsFromFile(filePath);
|
|
55
|
-
const streamTypeResult = getApiStreamPayloadTypeDetailsFromFile(filePath);
|
|
56
|
-
const httpMethod = extractHttpMethod(filePath, apiName);
|
|
57
|
-
const rateLimit = extractRateLimit(filePath);
|
|
58
|
-
const auth = extractAuth(filePath);
|
|
59
|
-
|
|
60
|
-
for (const symbol of [
|
|
61
|
-
...inputTypeResult.unresolvedSymbols,
|
|
62
|
-
...outputTypeResult.unresolvedSymbols,
|
|
63
|
-
...streamTypeResult.unresolvedSymbols,
|
|
64
|
-
]) {
|
|
65
|
-
if (!symbol.importPath) {
|
|
66
|
-
unresolvedTypeAliases.add(symbol.name);
|
|
67
|
-
console.error(`[TypeMapGenerator] Unresolved API type (${pagePath}/${apiName}/${apiVersion}): ${symbol.name}`);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
getOrInit(namedImports, symbol.importPath, () => new Set<string>()).add(symbol.name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
getOrInit(typesByPage, pagePath, () => new Map()).set(`${apiName}@${apiVersion}`, {
|
|
74
|
-
input: inputTypeResult.text,
|
|
75
|
-
output: outputTypeResult.text,
|
|
76
|
-
stream: streamTypeResult.text,
|
|
77
|
-
method: httpMethod,
|
|
78
|
-
rateLimit,
|
|
79
|
-
auth,
|
|
80
|
-
version: apiVersion,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Per-file extractor map (`typeMap/extractors.ts`):
|
|
86
|
-
|
|
87
|
-
| Function | Returns | Used for |
|
|
88
|
-
|---|---|---|
|
|
89
|
-
| `getInputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API `data` parameter type |
|
|
90
|
-
| `getOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API return type (the `result` member of `{ status: 'success', result: ... }`) |
|
|
91
|
-
| `getApiStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API stream emitter payload (defaults to `never`) |
|
|
92
|
-
| `getSyncClientDataTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `clientInput` (the `data` param of `_server_v<n>.ts`, or the client-side type if no server file) |
|
|
93
|
-
| `getSyncClientOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `clientOutput` returned by `_client_v<n>.ts` |
|
|
94
|
-
| `getSyncServerOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `serverOutput` returned by `_server_v<n>.ts` |
|
|
95
|
-
| `getSyncServerStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync server stream emitter payload |
|
|
96
|
-
| `getSyncClientStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync client stream emitter payload |
|
|
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
|
-
export const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
export type
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
export type
|
|
264
|
-
export type
|
|
265
|
-
export type
|
|
266
|
-
export type
|
|
267
|
-
export type
|
|
268
|
-
export type
|
|
269
|
-
|
|
270
|
-
export
|
|
271
|
-
export
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
export
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
export type
|
|
278
|
-
export type
|
|
279
|
-
|
|
280
|
-
type
|
|
281
|
-
export
|
|
282
|
-
|
|
283
|
-
export type
|
|
284
|
-
export type
|
|
285
|
-
|
|
286
|
-
export
|
|
287
|
-
export
|
|
288
|
-
|
|
289
|
-
export
|
|
290
|
-
export
|
|
291
|
-
export
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
-
|
|
363
|
-
-
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
##
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1
|
+
# Type Map Generation (`generateTypeMapFile` + emitters)
|
|
2
|
+
|
|
3
|
+
> Dev-only. `generateTypeMapFile()` runs at build time and during hot reload. The generated artifacts (`apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, `apiDocs.generated.json`) ship with the project source; production servers read them as compiled TypeScript / JSON, never re-running the emitter.
|
|
4
|
+
|
|
5
|
+
The type-map generator is the canonical source of truth for typed `apiRequest` / `syncRequest` calls. It walks every `_api/` and `_sync/` file under `srcDir`, runs the TypeChecker-backed extractors to produce fully-expanded inline types, emits a single typed map plus a Zod schema file plus a docs JSON, and validates that no unresolved type identifiers leaked through.
|
|
6
|
+
|
|
7
|
+
`generateTypeMapFile(options?)` is the only public entry point; the helpers under `typeMap/` are internal building blocks.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Pipeline order
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
export const generateTypeMapFile = (options: GenerateTypeMapOptions = {}): void => {
|
|
15
|
+
const { quiet = false } = options;
|
|
16
|
+
|
|
17
|
+
assertValidRouteNaming({ srcDir: getSrcDir(), context: 'generating API/sync type maps' });
|
|
18
|
+
assertNoDuplicateNormalizedRouteKeys({ srcDir: getSrcDir(), context: 'generating API/sync type maps' });
|
|
19
|
+
|
|
20
|
+
invalidateProgramCache();
|
|
21
|
+
namedImports.clear();
|
|
22
|
+
defaultImports.clear();
|
|
23
|
+
|
|
24
|
+
// 1. Walk apiFiles, extract per-route input/output/stream/meta
|
|
25
|
+
// 2. Walk sync server/client files, join by `pagePath/syncName/version`
|
|
26
|
+
// 3. Build the Functions interface from the server functions tree
|
|
27
|
+
// 4. Abort on unresolved type symbols
|
|
28
|
+
// 5. Build + write the three artifacts
|
|
29
|
+
|
|
30
|
+
// ... (see breakdown below) ...
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`assertValidRouteNaming` and `assertNoDuplicateNormalizedRouteKeys` come first so a typo aborts before the expensive TypeScript Program build. `invalidateProgramCache()` is mandatory at the top — the generator is called both from build scripts (fresh process, no cache anyway) and from hot reload (cache may be warm but a file just changed).
|
|
35
|
+
|
|
36
|
+
`namedImports` and `defaultImports` are module-level `Map`s that accumulate import statements for the generated file. They are cleared on each run so retries don't leak stale imports.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## API extraction loop
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const apiFiles = findAllApiFiles(getSrcDir());
|
|
44
|
+
const typesByPage = new Map<string, Map<string, { input, output, stream, method, rateLimit, auth, version }>>();
|
|
45
|
+
const unresolvedTypeAliases = new Set<string>();
|
|
46
|
+
|
|
47
|
+
for (const filePath of apiFiles) {
|
|
48
|
+
const pagePath = extractPagePath(filePath);
|
|
49
|
+
const apiName = extractApiName(filePath);
|
|
50
|
+
const apiVersion = extractApiVersion(filePath);
|
|
51
|
+
if (!pagePath || !apiName) continue;
|
|
52
|
+
|
|
53
|
+
const inputTypeResult = getInputTypeDetailsFromFile(filePath);
|
|
54
|
+
const outputTypeResult = getOutputTypeDetailsFromFile(filePath);
|
|
55
|
+
const streamTypeResult = getApiStreamPayloadTypeDetailsFromFile(filePath);
|
|
56
|
+
const httpMethod = extractHttpMethod(filePath, apiName);
|
|
57
|
+
const rateLimit = extractRateLimit(filePath);
|
|
58
|
+
const auth = extractAuth(filePath);
|
|
59
|
+
|
|
60
|
+
for (const symbol of [
|
|
61
|
+
...inputTypeResult.unresolvedSymbols,
|
|
62
|
+
...outputTypeResult.unresolvedSymbols,
|
|
63
|
+
...streamTypeResult.unresolvedSymbols,
|
|
64
|
+
]) {
|
|
65
|
+
if (!symbol.importPath) {
|
|
66
|
+
unresolvedTypeAliases.add(symbol.name);
|
|
67
|
+
console.error(`[TypeMapGenerator] Unresolved API type (${pagePath}/${apiName}/${apiVersion}): ${symbol.name}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
getOrInit(namedImports, symbol.importPath, () => new Set<string>()).add(symbol.name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getOrInit(typesByPage, pagePath, () => new Map()).set(`${apiName}@${apiVersion}`, {
|
|
74
|
+
input: inputTypeResult.text,
|
|
75
|
+
output: outputTypeResult.text,
|
|
76
|
+
stream: streamTypeResult.text,
|
|
77
|
+
method: httpMethod,
|
|
78
|
+
rateLimit,
|
|
79
|
+
auth,
|
|
80
|
+
version: apiVersion,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Per-file extractor map (`typeMap/extractors.ts`):
|
|
86
|
+
|
|
87
|
+
| Function | Returns | Used for |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `getInputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API `data` parameter type |
|
|
90
|
+
| `getOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API return type (the `result` member of `{ status: 'success', result: ... }`) |
|
|
91
|
+
| `getApiStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | API stream emitter payload (defaults to `never`) |
|
|
92
|
+
| `getSyncClientDataTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `clientInput` (the `data` param of `_server_v<n>.ts`, or the client-side type if no server file) |
|
|
93
|
+
| `getSyncClientOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `clientOutput` returned by `_client_v<n>.ts` |
|
|
94
|
+
| `getSyncServerOutputTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync `serverOutput` returned by `_server_v<n>.ts` |
|
|
95
|
+
| `getSyncServerStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync server stream emitter payload |
|
|
96
|
+
| `getSyncClientStreamPayloadTypeDetailsFromFile(filePath)` | `{ text, unresolvedSymbols }` | sync client stream emitter payload |
|
|
97
|
+
|
|
98
|
+
### Wire contract
|
|
99
|
+
|
|
100
|
+
- Outputs and stream payloads describe serialized values: `Date -> string`,
|
|
101
|
+
`toJSON() -> return type`, omitted object values disappear/become optional, and
|
|
102
|
+
omitted array/tuple slots become `null`.
|
|
103
|
+
- Binary values (`Buffer`, `ArrayBuffer`, typed arrays, Blob/File) abort generation.
|
|
104
|
+
Socket.io delivers them as binary attachments while HTTP emits JSON; one shared
|
|
105
|
+
route map cannot truthfully describe both. Return an explicit DTO/base64 string
|
|
106
|
+
or use a transport-specific custom route.
|
|
107
|
+
- Inputs remain unprojected for fail-closed validation, but `Date` annotations are
|
|
108
|
+
rejected. Declare an ISO `string`, validate it, and convert explicitly in the
|
|
109
|
+
handler. Otherwise the source handler promises `Date` while JSON delivers a
|
|
110
|
+
string.
|
|
111
|
+
- Extraction throws for API `stream` and sync `serverStream` / `clientStream` are
|
|
112
|
+
persisted as `extraction-error` diagnostics just like input/output failures.
|
|
113
|
+
|
|
114
|
+
Two of these are also exported from the package root for use by the dev loader:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
export { getInputTypeFromFile, getSyncClientDataType } from './typeMap/extractors';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The loader uses them on every `upsertApiFromFile` / `upsertSyncFromFile` to attach an `inputType` string to the live route entry; this string later feeds `runtimeTypeValidation` in `@luckystack/core` via `resolveRuntimeTypeText` (see `runtime-type-resolver.md`).
|
|
121
|
+
|
|
122
|
+
### Route metadata (`typeMap/routeMeta.ts`)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
export const extractApiName(filePath: string): string | null;
|
|
126
|
+
export const extractApiVersion(filePath: string): string;
|
|
127
|
+
export const extractPagePath(filePath: string): string | null;
|
|
128
|
+
export const extractSyncName(filePath: string): string | null;
|
|
129
|
+
export const extractSyncPagePath(filePath: string): string | null;
|
|
130
|
+
export const extractSyncVersion(filePath: string): string;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Filename + path segments -> the components that compose route keys. `pagePath === 'root'` (or `''` for syncs) means the route lives at the project root, not under a page directory.
|
|
134
|
+
|
|
135
|
+
### API meta (`typeMap/apiMeta.ts`)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
139
|
+
export const extractHttpMethod = (filePath: string, apiName: string): HttpMethod;
|
|
140
|
+
export const extractRateLimit = (filePath: string): number | false | undefined;
|
|
141
|
+
export const extractAuth = (filePath: string): { login: boolean; additional?: Record<string, unknown>[] };
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each one walks the source file's top-level statements looking for the matching `export const`. If the API doesn't export a value, sensible defaults apply:
|
|
145
|
+
|
|
146
|
+
- `httpMethod` falls back to `inferHttpMethod(apiName)` (re-exported from `@luckystack/core`) — naming-based inference (e.g. `get*` -> `GET`, `delete*` -> `DELETE`).
|
|
147
|
+
- `rateLimit` returns `undefined`, which the emitter omits from the generated entry.
|
|
148
|
+
- `auth` returns `{ login: true }` (the safer default).
|
|
149
|
+
|
|
150
|
+
There is also `extractValidation(filePath)` which returns `'strict' | 'relaxed' | { input: 'skip' | 'strict' } | undefined` — currently surfaced via `apiMetaMap` in the generated file.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Sync pairing + extraction
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const syncServerFiles = findAllSyncServerFiles(getSrcDir());
|
|
158
|
+
const syncClientFiles = findAllSyncClientFiles(getSrcDir());
|
|
159
|
+
|
|
160
|
+
const allSyncs = new Map<string, {
|
|
161
|
+
pagePath: string;
|
|
162
|
+
syncName: string;
|
|
163
|
+
serverFile?: string;
|
|
164
|
+
clientFile?: string;
|
|
165
|
+
}>();
|
|
166
|
+
|
|
167
|
+
for (const serverFile of syncServerFiles) {
|
|
168
|
+
const key = `${extractSyncPagePath(serverFile)}/${extractSyncName(serverFile)}/${extractSyncVersion(serverFile)}`;
|
|
169
|
+
const existing = allSyncs.get(key) || { pagePath, syncName };
|
|
170
|
+
existing.serverFile = serverFile;
|
|
171
|
+
allSyncs.set(key, existing);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const clientFile of syncClientFiles) {
|
|
175
|
+
// mirror with `clientFile`
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
After both walks, `allSyncs` has one entry per logical sync route, populated with whichever of `serverFile` / `clientFile` exists. Three cases:
|
|
180
|
+
|
|
181
|
+
1. **Server + client** — `clientInput` from the server's `data` param; `serverOutput` from server's return; `clientOutput` from client's return.
|
|
182
|
+
2. **Server-only** — `clientInput` from server; `serverOutput` from server; `clientOutput` defaults to `{ }`.
|
|
183
|
+
3. **Client-only** — `clientInput` from the client file (the framework still needs to know the type the caller sends); `serverOutput` defaults to `{ }`; `clientOutput` from client.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const clientInputTypeResult = serverFile
|
|
187
|
+
? getSyncClientDataTypeDetailsFromFile(serverFile)
|
|
188
|
+
: (clientFile
|
|
189
|
+
? getSyncClientDataTypeDetailsFromFile(clientFile)
|
|
190
|
+
: { text: '{ }', unresolvedSymbols: [] });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Each `*TypeDetailsFromFile` returns `{ text, unresolvedSymbols }`; the symbols are merged into `unresolvedTypeAliases` / `namedImports` exactly the same way as in the API loop.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Functions interface (`generateServerFunctions`)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const functionsInterface = generateServerFunctions({ namedImports, defaultImports });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`generateServerFunctions` walks every configured `serverFunctionDirs` root recursively (legacy singular `serverFunctionsDir` still honored when set) and emits a nested-interface block representing every exported function or value. Per file:
|
|
204
|
+
|
|
205
|
+
1. Parse the source via the TypeScript Program (reuses the same cached `ts.Program` — see `ts-program-cache.md`).
|
|
206
|
+
2. For every `export const <name> = (...)` arrow or function expression, extract a signature string with `extractSignatureFromNode`. Default values are stripped from parameters; generic clauses are preserved; `Promise<unknown>` is the return-type fallback for async functions without annotations.
|
|
207
|
+
3. For every `export const <name>: <Type> = ...` without a function initializer, run `inferValueTypeForExport` — uses `declaration.type` when annotated, falls back to `checker.typeToString` on the inferred type, then runs `simplifyInferredType` to map common framework types (`PrismaClient`, `Redis`) onto bare identifiers.
|
|
208
|
+
4. For re-exports (`export { a } from 'module'`), emit `typeof import('<rel-spec>')['a']`. Relative specifiers are rewritten via `relativizeModuleSpecifier` so they resolve from the generated file's directory (`src/_sockets/apiTypes.generated.ts`).
|
|
209
|
+
5. Defaults are merged into the file-named bucket so `import myFn from './myFn'` shows up at `Functions.<folder>.myFn.myFn` (matching `devFunctions`).
|
|
210
|
+
|
|
211
|
+
The output is one big block of nested `'<folder>': { <file>: { <export>: <signature>; }; };` entries. It is embedded verbatim into `apiTypes.generated.ts` inside `export interface Functions { ... }`.
|
|
212
|
+
|
|
213
|
+
`namedImports` / `defaultImports` from this walk feed the same import-statement builder as the API/sync walks — anything the `Functions` interface references that isn't an inline type is imported at the top of the generated file.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Unresolved symbol handling
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
if (unresolvedTypeAliases.size > 0) {
|
|
221
|
+
const unresolvedList = [...unresolvedTypeAliases].sort().join(', ');
|
|
222
|
+
throw new Error(`[TypeMapGenerator] Aborting generation because unresolved type symbols were found: ${unresolvedList}`);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
A "symbol" with no `importPath` means the TypeChecker found a referenced identifier (e.g. `User`, `Settings`) but couldn't trace its declaration back to a source file. The most common cause is a name collision (two files declare a `User` type) or a missing `import`. Generation aborts so the symptom doesn't surface as broken IntelliSense.
|
|
227
|
+
|
|
228
|
+
Symbols with an `importPath` are added to `namedImports` and become real `import { Symbol } from "<path>";` statements in the generated file.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Artifact build + write
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const { content, docsData, schemasContent } = buildTypeMapArtifacts({
|
|
236
|
+
typesByPage,
|
|
237
|
+
syncTypesByPage,
|
|
238
|
+
namedImports,
|
|
239
|
+
defaultImports,
|
|
240
|
+
functionsInterface,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
writeTypeMapArtifacts({ content, docsData, schemasContent });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`buildTypeMapArtifacts` in `typeMap/emitterArtifacts.ts` renders three strings:
|
|
247
|
+
|
|
248
|
+
### `content` -> `apiTypes.generated.ts`
|
|
249
|
+
|
|
250
|
+
Single file emitted at `getGeneratedSocketTypesPath()` (default `<repo>/src/_sockets/apiTypes.generated.ts`, overridable via `@luckystack/core` path helpers). Layout:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
/* eslint-disable ... */
|
|
254
|
+
|
|
255
|
+
<importStatements>
|
|
256
|
+
|
|
257
|
+
export interface Functions { ... };
|
|
258
|
+
|
|
259
|
+
export type JsonPrimitive = ...;
|
|
260
|
+
export type JsonValue = ...;
|
|
261
|
+
// ... shared scalar helpers ...
|
|
262
|
+
|
|
263
|
+
export type StreamPayload = { [key: string]: unknown };
|
|
264
|
+
export type ApiStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
|
|
265
|
+
export type SyncServerStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
|
|
266
|
+
export type SyncClientStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
|
|
267
|
+
export type SyncBroadcastStreamEmitter<...> = ...;
|
|
268
|
+
export type SyncStreamToEmitter<...> = ...;
|
|
269
|
+
|
|
270
|
+
export type ApiResponse<T = unknown> = ...;
|
|
271
|
+
export type ApiNetworkResponse<T = unknown> = ...;
|
|
272
|
+
|
|
273
|
+
type _ProjectApiTypeMap = { ... per-route input/output/stream/method/rateLimit ... };
|
|
274
|
+
export interface ApiTypeMap extends _ProjectApiTypeMap {}
|
|
275
|
+
|
|
276
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
277
|
+
export type PagePath = ...;
|
|
278
|
+
export type ApiName<P> = ...;
|
|
279
|
+
export type ApiVersion<P, N> = ...;
|
|
280
|
+
export type ApiInput<P, N, V = ApiVersion<P, N>> = ...;
|
|
281
|
+
export type ApiOutput<P, N, V = ApiVersion<P, N>> = ...;
|
|
282
|
+
export type ApiStream<P, N, V = ApiVersion<P, N>> = ...;
|
|
283
|
+
export type ApiMethod<P, N, V = ApiVersion<P, N>> = ...;
|
|
284
|
+
export type FullApiPath<P, N, V> = `api/${P}/${N & string}/${V & string}`;
|
|
285
|
+
|
|
286
|
+
export const apiMethodMap: Record<string, Record<string, Record<string, HttpMethod>>> = { ... };
|
|
287
|
+
export const getApiMethod = (...): HttpMethod | undefined => ...;
|
|
288
|
+
|
|
289
|
+
export interface ApiMetaEntry { method, auth: { login, additional? }, rateLimit? };
|
|
290
|
+
export const apiMetaMap: Record<...> = { ... };
|
|
291
|
+
export const getApiMeta = (...): ApiMetaEntry | undefined => ...;
|
|
292
|
+
|
|
293
|
+
export type SyncServerResponse<T = unknown> = ...;
|
|
294
|
+
export type SyncClientResponse<T = unknown> = ...;
|
|
295
|
+
|
|
296
|
+
type _ProjectSyncTypeMap = { ... per-sync clientInput/serverOutput/clientOutput/serverStream/clientStream ... };
|
|
297
|
+
export interface SyncTypeMap extends _ProjectSyncTypeMap {}
|
|
298
|
+
|
|
299
|
+
export type SyncPagePath = ...;
|
|
300
|
+
export type SyncName<P> = ...;
|
|
301
|
+
export type SyncVersion<P, N> = ...;
|
|
302
|
+
export type SyncClientInput<P, N, V = SyncVersion<P, N>> = ...;
|
|
303
|
+
export type SyncServerOutput<P, N, V = SyncVersion<P, N>> = ...;
|
|
304
|
+
export type SyncClientOutput<P, N, V = SyncVersion<P, N>> = ...;
|
|
305
|
+
export type SyncServerStream<P, N, V = SyncVersion<P, N>> = ...;
|
|
306
|
+
export type SyncClientStream<P, N, V = SyncVersion<P, N>> = ...;
|
|
307
|
+
export type FullSyncPath<P, N, V> = `sync/${P}/${N & string}/${V & string}`;
|
|
308
|
+
|
|
309
|
+
// Module augmentation merges the project's concrete maps into @luckystack/core
|
|
310
|
+
// stub interfaces so framework code (apiRequest / syncRequest) gets the same
|
|
311
|
+
// shapes without deep-relative imports.
|
|
312
|
+
declare module '@luckystack/core' {
|
|
313
|
+
interface ApiTypeMap extends _ProjectApiTypeMap {}
|
|
314
|
+
interface SyncTypeMap extends _ProjectSyncTypeMap {}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Before writing, `validateGeneratedTypeIdentifiers(content)` parses the content with `ts.createSourceFile` and asserts every referenced type identifier is either declared in the file or imported. Anything else (built-ins like `Record`, `Promise`, etc., plus a curated allow-list) throws and aborts the run.
|
|
319
|
+
|
|
320
|
+
### `schemasContent` -> `apiInputSchemas.generated.ts`
|
|
321
|
+
|
|
322
|
+
Built by `buildSchemasContent({ typesByPage })`. Loops over the API map again, runs each input type through `typeTextToZodSource` (from `typeMap/zodEmitter.ts`), and emits:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { z } from 'zod';
|
|
326
|
+
|
|
327
|
+
export const apiInputSchemas: Record<string, Record<string, Record<string, z.ZodTypeAny>>> = {
|
|
328
|
+
'<pagePath>': {
|
|
329
|
+
'<apiName>': {
|
|
330
|
+
'<version>': <z.object(...)>,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export const getApiInputSchema = (pagePath, apiName, version): z.ZodTypeAny | undefined =>
|
|
336
|
+
apiInputSchemas[pagePath]?.[apiName]?.[version];
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`typeTextToZodSource` walks the TS-AST of the inline type literal and emits Zod source. Unsupported shapes fall back to `z.any() /* unparseable input type */` with a TODO. Sync types are NOT in this file — only API inputs (runtime input validation is API-only).
|
|
340
|
+
|
|
341
|
+
### `docsData` -> `apiDocs.generated.json`
|
|
342
|
+
|
|
343
|
+
Pure JSON dump of every API and sync entry. Used by `@luckystack/docs-ui` to render an OpenAPI-like browser:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"apis": {
|
|
348
|
+
"<pagePath>": [
|
|
349
|
+
{ "page", "name", "version", "method", "input", "output", "stream", "rateLimit", "auth", "path" }
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
"syncs": {
|
|
353
|
+
"<pagePath>": [
|
|
354
|
+
{ "page", "name", "version", "clientInput", "serverOutput", "clientOutput", "serverStream", "clientStream", "path" }
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Where each file is written:
|
|
361
|
+
|
|
362
|
+
- `apiTypes.generated.ts` -> `getGeneratedSocketTypesPath()` (configurable via `@luckystack/core`).
|
|
363
|
+
- `apiInputSchemas.generated.ts` -> `getGeneratedApiSchemasPath()`.
|
|
364
|
+
- `apiDocs.generated.json` -> `getGeneratedApiDocsPath()`.
|
|
365
|
+
|
|
366
|
+
`writeTypeMapArtifacts` only rewrites a file when its content changed (`writeFileIfChanged`). This avoids spurious file-modified events on the runner during hot reload (which would otherwise feed back into the watcher and trigger another regeneration).
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## `quiet` option
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
generateTypeMapFile({ quiet: true });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Used by `setupWatchers()` for both the initial-boot regeneration and every subsequent hot-reload regeneration (see `hot-reload.md`). Suppresses:
|
|
377
|
+
|
|
378
|
+
- The two banner lines (`═══════…`).
|
|
379
|
+
- The `[TypeMapGenerator] Found N API files` / `Sync server/client files` headers.
|
|
380
|
+
- Per-API and per-sync logs.
|
|
381
|
+
|
|
382
|
+
The final per-file write logs (`Generated apiTypes.generated.ts`, etc.) still fire, but only when content changed.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Public extractor surface
|
|
387
|
+
|
|
388
|
+
Re-exported from `@luckystack/devkit`:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
export { getInputTypeFromFile, getSyncClientDataType } from './typeMap/extractors';
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Both are thin wrappers over the `*Details` variants — they discard `unresolvedSymbols` and return only `text`. Used by the dev loader to attach inline type text to live route entries (consumed downstream by `runtimeTypeValidation` in `@luckystack/core`).
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Failure modes
|
|
399
|
+
|
|
400
|
+
| Symptom | Cause |
|
|
401
|
+
|---|---|
|
|
402
|
+
| `[TypeProgram] tsconfig.server.json not found` | Project doesn't have a `tsconfig.server.json` next to `package.json`. See `ts-program-cache.md`. |
|
|
403
|
+
| `assertValidRouteNaming` throw | A `_api/`/`_sync/` file fails the version regex. Fix the filename. |
|
|
404
|
+
| `assertNoDuplicateNormalizedRouteKeys` throw | Two files (often case-differing) normalize to the same route key. Rename one. |
|
|
405
|
+
| `[TypeMapGenerator] Aborting generation because unresolved type symbols were found: X, Y, Z` | TypeChecker couldn't trace identifier(s) back to a source file. Usually a missing import or a naming collision. |
|
|
406
|
+
| `[TypeMapGenerator] Generated type map has unresolved type identifiers: ...` | Post-emission validation: a generated symbol wasn't declared or imported. Bug in an extractor or in `generateServerFunctions`. |
|
|
407
|
+
| `[TypeMapGenerator] Error writing type map or docs: ...` | Filesystem error during write. Caught and logged, generator returns normally so hot reload doesn't crash the server. |
|
|
408
|
+
| `declares Date in a transport input` | The handler annotation promises an instance JSON cannot deliver. Change the input field to `string`, validate ISO format, then convert. |
|
|
409
|
+
| `has transport-dependent or non-JSON output semantics` | A route returns binary/BigInt output that cannot share one HTTP + Socket.io type. Return a JSON DTO/base64 string or use a custom transport route. |
|
|
410
|
+
| Stale generated types after a hot reload | `invalidateProgramCache()` not called (shouldn't happen — the generator always invalidates at the top). |
|