@luckystack/devkit 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +9 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +2 -37
  4. package/dist/cli/validateDeploy.d.ts +2 -0
  5. package/dist/functionRegistry.d.ts +72 -0
  6. package/dist/hotReload.d.ts +1 -0
  7. package/dist/importDependencyGraph.d.ts +2 -0
  8. package/dist/index.d.ts +15 -280
  9. package/dist/index.js +551 -424
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/mapUtils.d.ts +2 -0
  12. package/dist/loader.d.ts +11 -0
  13. package/dist/prismaClientCheck.d.ts +19 -0
  14. package/dist/routeConventions.d.ts +20 -0
  15. package/dist/routeNamingValidation.d.ts +46 -0
  16. package/dist/routingRules.d.ts +62 -0
  17. package/dist/runtimeTypeResolver.d.ts +15 -0
  18. package/dist/supervisor.d.ts +25 -0
  19. package/dist/templateInjector.d.ts +60 -0
  20. package/dist/templateRegistry.d.ts +78 -0
  21. package/dist/typeMap/apiMeta.d.ts +42 -0
  22. package/dist/typeMap/discovery.d.ts +3 -0
  23. package/dist/typeMap/emitterArtifacts.d.ts +94 -0
  24. package/dist/typeMap/extractionDiagnostics.d.ts +16 -0
  25. package/dist/typeMap/extractors.d.ts +18 -0
  26. package/dist/typeMap/functionsMeta.d.ts +5 -0
  27. package/dist/typeMap/routeMeta.d.ts +6 -0
  28. package/dist/typeMap/tsApi.d.ts +12 -0
  29. package/dist/typeMap/tsProgram.d.ts +41 -0
  30. package/dist/typeMap/typeContext.d.ts +21 -0
  31. package/dist/typeMap/zodEmitter.d.ts +8 -0
  32. package/dist/typeMapGenerator.d.ts +5 -0
  33. package/dist/validateDeploy.d.ts +25 -0
  34. package/docs/cli.md +3 -243
  35. package/docs/hot-reload.md +4 -362
  36. package/docs/loader-pipeline.md +3 -322
  37. package/docs/runtime-type-resolver.md +3 -256
  38. package/docs/supervisor.md +3 -290
  39. package/docs/template-customization.md +2 -133
  40. package/docs/ts-program-cache.md +4 -197
  41. package/docs/type-map-generation.md +5 -418
  42. package/package.json +5 -5
  43. package/CLAUDE.md +0 -135
@@ -1,422 +1,9 @@
1
- # Type Map Generation (`generateTypeMapFile` + emitters)
1
+ # Type-map generation
2
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.
3
+ Generated route types, runtime schemas and API documentation must describe the same source route contract. TypeScript source is the authority; generated files are artifacts, not independent hand-maintained interfaces. Transport contracts must reject unsafe shapes rather than implying values can cross JSON or socket boundaries unchanged. A source missing from the compiler Program or an unresolved symbol deserves an explicit diagnostic, not a silent fallback that widens validation.
4
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.
5
+ The emitter detects ambiguous route names before writing contradictory maps. Deep type expansion is bounded so pathological types cannot stall every generation. Development may report a loose schema when strict conversion is impossible, but its diagnostic must remain visible to the developer. Consult the emitter implementation for artifact names, current extraction rules, and schema policy. See `ts-program-cache.md` for the reason caches are reused and invalidated.
6
6
 
7
- `generateTypeMapFile(options?)` is the only public entry point; the helpers under `typeMap/` are internal building blocks.
7
+ **An absent optional sync side is not a fallback.** The client handler of a sync route is optional by design (it runs once per recipient, so a pass-through one should not exist), and an absent side produces the same default text as a present side that declares no shape. Counting it as degraded extraction made the diagnostics gate push authors toward handlers the architecture forbids, so the generator records which sides exist and the diagnostics skip only the missing side's own field — after the thrown-extraction check, which always reports. Keep the exception that narrow: widening it per route, to API defaults, or ahead of the throw check would again make a lost shape indistinguishable from an undeclared one (ADR 0057). Source: `../src/typeMap/emitterArtifacts.ts`.
8
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
- - A sync side that does not exist on disk is not a diagnostic. Its default output
114
- text is indistinguishable from a present-but-shapeless side, so the entry also
115
- carries whether each side was found; only the missing side's own field is
116
- suppressed, and only after the throw check has passed.
117
-
118
- Two of these are also exported from the package root for use by the dev loader:
119
-
120
- ```typescript
121
- export { getInputTypeFromFile, getSyncClientDataType } from './typeMap/extractors';
122
- ```
123
-
124
- 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`).
125
-
126
- ### Route metadata (`typeMap/routeMeta.ts`)
127
-
128
- ```typescript
129
- export const extractApiName(filePath: string): string | null;
130
- export const extractApiVersion(filePath: string): string;
131
- export const extractPagePath(filePath: string): string | null;
132
- export const extractSyncName(filePath: string): string | null;
133
- export const extractSyncPagePath(filePath: string): string | null;
134
- export const extractSyncVersion(filePath: string): string;
135
- ```
136
-
137
- 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.
138
-
139
- ### API meta (`typeMap/apiMeta.ts`)
140
-
141
- ```typescript
142
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
143
- export const extractHttpMethod = (filePath: string, apiName: string): HttpMethod;
144
- export const extractRateLimit = (filePath: string): number | false | undefined;
145
- export const extractAuth = (filePath: string): { login: boolean; additional?: Record<string, unknown>[] };
146
- ```
147
-
148
- 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:
149
-
150
- - `httpMethod` falls back to `inferHttpMethod(apiName)` (re-exported from `@luckystack/core`) — naming-based inference (e.g. `get*` -> `GET`, `delete*` -> `DELETE`).
151
- - `rateLimit` returns `undefined`, which the emitter omits from the generated entry.
152
- - `auth` returns `{ login: true }` (the safer default).
153
-
154
- There is also `extractValidation(filePath)` which returns `'strict' | 'relaxed' | { input: 'skip' | 'strict' } | undefined` — currently surfaced via `apiMetaMap` in the generated file.
155
-
156
- ---
157
-
158
- ## Sync pairing + extraction
159
-
160
- ```typescript
161
- const syncServerFiles = findAllSyncServerFiles(getSrcDir());
162
- const syncClientFiles = findAllSyncClientFiles(getSrcDir());
163
-
164
- const allSyncs = new Map<string, {
165
- pagePath: string;
166
- syncName: string;
167
- serverFile?: string;
168
- clientFile?: string;
169
- }>();
170
-
171
- for (const serverFile of syncServerFiles) {
172
- const key = `${extractSyncPagePath(serverFile)}/${extractSyncName(serverFile)}/${extractSyncVersion(serverFile)}`;
173
- const existing = allSyncs.get(key) || { pagePath, syncName };
174
- existing.serverFile = serverFile;
175
- allSyncs.set(key, existing);
176
- }
177
-
178
- for (const clientFile of syncClientFiles) {
179
- // mirror with `clientFile`
180
- }
181
- ```
182
-
183
- After both walks, `allSyncs` has one entry per logical sync route, populated with whichever of `serverFile` / `clientFile` exists. Three cases:
184
-
185
- 1. **Server + client** — `clientInput` from the server's `data` param; `serverOutput` from server's return; `clientOutput` from client's return.
186
- 2. **Server-only** — `clientInput` from server; `serverOutput` from server; `clientOutput` defaults to `{ }`.
187
- 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.
188
-
189
- ```typescript
190
- const clientInputTypeResult = serverFile
191
- ? getSyncClientDataTypeDetailsFromFile(serverFile)
192
- : (clientFile
193
- ? getSyncClientDataTypeDetailsFromFile(clientFile)
194
- : { text: '{ }', unresolvedSymbols: [] });
195
- ```
196
-
197
- Each `*TypeDetailsFromFile` returns `{ text, unresolvedSymbols }`; the symbols are merged into `unresolvedTypeAliases` / `namedImports` exactly the same way as in the API loop.
198
-
199
- When a server file exists, its `auth` and `rateLimit` are read with the SAME extractors as the API side (`extractAuth` / `extractRateLimit`) and feed `syncMetaMap`. Only server-backed routes (cases 1 and 2) get a `syncMetaMap` / `syncInputSchemas` entry: a client-only sync is not a callable route, so there is nothing for the test-runner to sweep.
200
-
201
- ---
202
-
203
- ## Functions interface (`generateServerFunctions`)
204
-
205
- ```typescript
206
- const functionsInterface = generateServerFunctions({ namedImports, defaultImports });
207
- ```
208
-
209
- `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:
210
-
211
- 1. Parse the source via the TypeScript Program (reuses the same cached `ts.Program` — see `ts-program-cache.md`).
212
- 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.
213
- 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.
214
- 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`).
215
- 5. Defaults are merged into the file-named bucket so `import myFn from './myFn'` shows up at `Functions.<folder>.myFn.myFn` (matching `devFunctions`).
216
-
217
- 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 { ... }`.
218
-
219
- `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.
220
-
221
- ---
222
-
223
- ## Unresolved symbol handling
224
-
225
- ```typescript
226
- if (unresolvedTypeAliases.size > 0) {
227
- const unresolvedList = [...unresolvedTypeAliases].sort().join(', ');
228
- throw new Error(`[TypeMapGenerator] Aborting generation because unresolved type symbols were found: ${unresolvedList}`);
229
- }
230
- ```
231
-
232
- 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.
233
-
234
- Symbols with an `importPath` are added to `namedImports` and become real `import { Symbol } from "<path>";` statements in the generated file.
235
-
236
- ---
237
-
238
- ## Artifact build + write
239
-
240
- ```typescript
241
- const { content, docsData, schemasContent } = buildTypeMapArtifacts({
242
- typesByPage,
243
- syncTypesByPage,
244
- namedImports,
245
- defaultImports,
246
- functionsInterface,
247
- });
248
-
249
- writeTypeMapArtifacts({ content, docsData, schemasContent });
250
- ```
251
-
252
- `buildTypeMapArtifacts` in `typeMap/emitterArtifacts.ts` renders three strings:
253
-
254
- ### `content` -> `apiTypes.generated.ts`
255
-
256
- Single file emitted at `getGeneratedSocketTypesPath()` (default `<repo>/src/_sockets/apiTypes.generated.ts`, overridable via `@luckystack/core` path helpers). Layout:
257
-
258
- ```
259
- /* eslint-disable ... */
260
-
261
- <importStatements>
262
-
263
- export interface Functions { ... };
264
-
265
- export type JsonPrimitive = ...;
266
- export type JsonValue = ...;
267
- // ... shared scalar helpers ...
268
-
269
- export type StreamPayload = { [key: string]: unknown };
270
- export type ApiStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
271
- export type SyncServerStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
272
- export type SyncClientStreamEmitter<T extends StreamPayload = StreamPayload> = ...;
273
- export type SyncBroadcastStreamEmitter<...> = ...;
274
- export type SyncStreamToEmitter<...> = ...;
275
-
276
- export type ApiResponse<T = unknown> = ...;
277
- export type ApiNetworkResponse<T = unknown> = ...;
278
-
279
- type _ProjectApiTypeMap = { ... per-route input/output/stream/method/rateLimit ... };
280
- export interface ApiTypeMap extends _ProjectApiTypeMap {}
281
-
282
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
283
- export type PagePath = ...;
284
- export type ApiName<P> = ...;
285
- export type ApiVersion<P, N> = ...;
286
- export type ApiInput<P, N, V = ApiVersion<P, N>> = ...;
287
- export type ApiOutput<P, N, V = ApiVersion<P, N>> = ...;
288
- export type ApiStream<P, N, V = ApiVersion<P, N>> = ...;
289
- export type ApiMethod<P, N, V = ApiVersion<P, N>> = ...;
290
- export type FullApiPath<P, N, V> = `api/${P}/${N & string}/${V & string}`;
291
-
292
- export const apiMethodMap: Record<string, Record<string, Record<string, HttpMethod>>> = { ... };
293
- export const getApiMethod = (...): HttpMethod | undefined => ...;
294
-
295
- export interface ApiMetaEntry { method, auth: { login, additional? }, rateLimit? };
296
- export const apiMetaMap: Record<...> = { ... };
297
- export const getApiMeta = (...): ApiMetaEntry | undefined => ...;
298
-
299
- export interface SyncMetaEntry { auth: { login, additional? }, rateLimit? };
300
- export const syncMetaMap: Record<...> = { ... }; // server-backed syncs only
301
- export const getSyncMeta = (...): SyncMetaEntry | undefined => ...;
302
-
303
- export type SyncServerResponse<T = unknown> = ...;
304
- export type SyncClientResponse<T = unknown> = ...;
305
-
306
- type _ProjectSyncTypeMap = { ... per-sync clientInput/serverOutput/clientOutput/serverStream/clientStream ... };
307
- export interface SyncTypeMap extends _ProjectSyncTypeMap {}
308
-
309
- export type SyncPagePath = ...;
310
- export type SyncName<P> = ...;
311
- export type SyncVersion<P, N> = ...;
312
- export type SyncClientInput<P, N, V = SyncVersion<P, N>> = ...;
313
- export type SyncServerOutput<P, N, V = SyncVersion<P, N>> = ...;
314
- export type SyncClientOutput<P, N, V = SyncVersion<P, N>> = ...;
315
- export type SyncServerStream<P, N, V = SyncVersion<P, N>> = ...;
316
- export type SyncClientStream<P, N, V = SyncVersion<P, N>> = ...;
317
- export type FullSyncPath<P, N, V> = `sync/${P}/${N & string}/${V & string}`;
318
-
319
- // Module augmentation merges the project's concrete maps into @luckystack/core
320
- // stub interfaces so framework code (apiRequest / syncRequest) gets the same
321
- // shapes without deep-relative imports.
322
- declare module '@luckystack/core' {
323
- interface ApiTypeMap extends _ProjectApiTypeMap {}
324
- interface SyncTypeMap extends _ProjectSyncTypeMap {}
325
- }
326
- ```
327
-
328
- 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.
329
-
330
- ### `schemasContent` -> `apiInputSchemas.generated.ts`
331
-
332
- Built by `buildSchemasContent`. Loops over the API map again, runs each input type through `typeTextToZodSource` (from `typeMap/zodEmitter.ts`), and emits:
333
-
334
- ```typescript
335
- import { z } from 'zod';
336
-
337
- export const apiInputSchemas: Record<string, Record<string, Record<string, z.ZodTypeAny>>> = {
338
- '<pagePath>': {
339
- '<apiName>': {
340
- '<version>': <z.object(...)>,
341
- },
342
- },
343
- };
344
-
345
- export const getApiInputSchema = (pagePath, apiName, version): z.ZodTypeAny | undefined =>
346
- apiInputSchemas[pagePath]?.[apiName]?.[version];
347
- ```
348
-
349
- `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.
350
-
351
- A sibling `syncInputSchemas` / `getSyncInputSchema` pair covers every server-backed sync route, built from its `clientInput` (the `data` the client sends) with the same converter and fallback. It exists so the test-runner sweeps (contract / fuzz) can reach sync routes the way they reach API routes. Sync input fallbacks to `z.any()` are recorded in `apiTypeDiagnostics.generated.json` exactly like API ones. Output and stream types never get a schema.
352
-
353
- ### `docsData` -> `apiDocs.generated.json`
354
-
355
- Pure JSON dump of every API and sync entry. Used by `@luckystack/docs-ui` to render an OpenAPI-like browser:
356
-
357
- ```json
358
- {
359
- "apis": {
360
- "<pagePath>": [
361
- { "page", "name", "version", "method", "input", "output", "stream", "rateLimit", "auth", "path" }
362
- ]
363
- },
364
- "syncs": {
365
- "<pagePath>": [
366
- { "page", "name", "version", "clientInput", "serverOutput", "clientOutput", "serverStream", "clientStream", "path" }
367
- ]
368
- }
369
- }
370
- ```
371
-
372
- Where each file is written:
373
-
374
- - `apiTypes.generated.ts` -> `getGeneratedSocketTypesPath()` (configurable via `@luckystack/core`).
375
- - `apiInputSchemas.generated.ts` -> `getGeneratedApiSchemasPath()`.
376
- - `apiDocs.generated.json` -> `getGeneratedApiDocsPath()`.
377
-
378
- `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).
379
-
380
- ---
381
-
382
- ## `quiet` option
383
-
384
- ```typescript
385
- generateTypeMapFile({ quiet: true });
386
- ```
387
-
388
- Used by `setupWatchers()` for both the initial-boot regeneration and every subsequent hot-reload regeneration (see `hot-reload.md`). Suppresses:
389
-
390
- - The two banner lines (`═══════…`).
391
- - The `[TypeMapGenerator] Found N API files` / `Sync server/client files` headers.
392
- - Per-API and per-sync logs.
393
-
394
- The final per-file write logs (`Generated apiTypes.generated.ts`, etc.) still fire, but only when content changed.
395
-
396
- ---
397
-
398
- ## Public extractor surface
399
-
400
- Re-exported from `@luckystack/devkit`:
401
-
402
- ```typescript
403
- export { getInputTypeFromFile, getSyncClientDataType } from './typeMap/extractors';
404
- ```
405
-
406
- 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`).
407
-
408
- ---
409
-
410
- ## Failure modes
411
-
412
- | Symptom | Cause |
413
- |---|---|
414
- | `[TypeProgram] tsconfig.server.json not found` | Project doesn't have a `tsconfig.server.json` next to `package.json`. See `ts-program-cache.md`. |
415
- | `assertValidRouteNaming` throw | A `_api/`/`_sync/` file fails the version regex. Fix the filename. |
416
- | `assertNoDuplicateNormalizedRouteKeys` throw | Two files (often case-differing) normalize to the same route key. Rename one. |
417
- | `[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. |
418
- | `[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`. |
419
- | `[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. |
420
- | `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. |
421
- | `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. |
422
- | Stale generated types after a hot reload | `invalidateProgramCache()` not called (shouldn't happen — the generator always invalidates at the top). |
9
+ **Server-backed sync routes get the same metadata and input schemas as API routes.** Their auth and rate-limit settings are read with the same extractors as the API side, and their client input runs through the same schema converter and diagnostics, so the test-runner's contract and fuzz sweeps can reach sync routes the way they reach API routes (ADR 0064). A client-only sync is not a callable route and gets neither. Output and stream types never get a runtime schema. Source: `../src/typeMapGenerator.ts`, `../src/typeMap/emitterArtifacts.ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/devkit",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -45,7 +45,7 @@
45
45
  "files": [
46
46
  "dist",
47
47
  "README.md",
48
- "CLAUDE.md",
48
+ "AGENTS.md",
49
49
  "docs",
50
50
  "LICENSE",
51
51
  "CHANGELOG.md"
@@ -56,13 +56,13 @@
56
56
  "test": "vitest run"
57
57
  },
58
58
  "dependencies": {
59
- "@luckystack/core": "^0.10.2",
59
+ "@luckystack/core": "^0.11.0",
60
60
  "chokidar": "^5.0.0",
61
- "dotenv": "^17.0.0"
61
+ "dotenv": "^17.0.0",
62
+ "typescript-7-api": "npm:typescript@7.1.0-dev.20261013.1"
62
63
  },
63
64
  "peerDependencies": {
64
65
  "@prisma/client": "^6.19.0",
65
- "typescript": ">=5.7.3 <7.0.0",
66
66
  "zod": "^4.0.0"
67
67
  },
68
68
  "peerDependenciesMeta": {
package/CLAUDE.md DELETED
@@ -1,135 +0,0 @@
1
- # @luckystack/devkit
2
-
3
- > AI summary + function INDEX. For deep specs see `docs/` next to this file.
4
-
5
- ## What this package does
6
-
7
- Dev-time tooling for LuckyStack projects: file-based route discovery + in-memory loaders (`devApis`, `devSyncs`, `devFunctions`), chokidar-driven hot reload, TypeScript-program-backed type-map + Zod schema emission, a route filename validator, a deep runtime type resolver (consumed by `@luckystack/core` in dev), a process supervisor that restarts the server on core file changes, file templates injected on empty API/sync file creation, and the `luckystack-validate-deploy` CLI. Designed to be installed as a `devDependency` only; the production server bundle never imports it.
8
-
9
- ## When to USE this package
10
-
11
- - Running the framework locally (`npm run server` / `npm run dev`) — `setupWatchers` + `initializeAll` are the canonical dev wiring, and the prod runtime maps loader delegates to `devApis` / `devSyncs` when `NODE_ENV !== 'production'`.
12
- - Generating typed artifacts (`apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, `apiDocs.generated.json`) from `_api/` and `_sync/` files — call `generateTypeMapFile()` from a build script.
13
- - Gating a deploy on configuration correctness — `luckystack-validate-deploy` (the package `bin`) confirms every service binding, preset, and env key resolves before shipping a bundle.
14
- - Wrapping the server in a watcher that restarts on `config.ts` / `.env` / `server/bootstrap/**` changes — `supervisor.ts` is the entry point.
15
- - Extending the deep type resolver from `@luckystack/core/src/runtimeTypeValidation.ts` (lazy-imported in dev) without owning the TypeScript-program plumbing.
16
-
17
- ## When to NOT suggest this (yet)
18
-
19
- - **Production runtime.** Devkit is dev-only. Do not import it from code paths reachable in `NODE_ENV=production`; the supervisor, hot-reload watchers, and TypeScript Program have no place in a prod bundle and will not be present in consumer prod tarballs.
20
- - **Bundled / packaged consumer apps (`create-luckystack-app` templates that ship to end users).** Devkit belongs in `devDependencies` of the project, not in any shipped artifact.
21
- - **Replacing the runtime route registry.** `devApis` / `devSyncs` are dev-only maps that mirror the file tree. In production, `@luckystack/server` reads the generated runtime maps; do not call `initializeApis` / `initializeSyncs` from a prod code path.
22
- - **Running tests in CI without a TypeScript Program.** The type-map emitter expects `tsconfig.server.json`; in headless CI test harnesses that do not produce generated types, skip `generateTypeMapFile()` and rely on previously committed artifacts.
23
- - **Hot-reloading non-LuckyStack code.** The watchers know about LuckyStack's marker segments (`_api/`, `_sync/`) and the configured `srcDir` / `sharedDir` / `serverFunctionDirs` (legacy `serverFunctionsDir` singular form still honored when set). Generic file watching belongs in chokidar directly.
24
-
25
- ## Function Index
26
-
27
- | Function / Export | One-liner | Deep doc |
28
- |---|---|---|
29
- | `initializeAll()` | Runs route-naming validation, then `initializeApis` + `initializeSyncs` + `initializeFunctions` in parallel. | -> docs/loader-pipeline.md |
30
- | `initializeApis()` | Clears `devApis`, walks the configured `srcDir`, loads every `_api/<name>_v<n>.ts` via dynamic `import()` with a cachebust query, and populates the dev-side route registry. | -> docs/loader-pipeline.md |
31
- | `initializeSyncs()` | Clears `devSyncs`, walks the configured `srcDir`, loads every `_sync/<name>_server_v<n>.ts` and `<name>_client_v<n>.ts`, and populates the dev-side route registry. | -> docs/loader-pipeline.md |
32
- | `initializeFunctions()` | Clears `devFunctions`, resolves the module list via `collectFunctionModules()`, imports each one, and builds a nested `Record<string, unknown>` mirror of the on-disk tree from its key path. Dev passes `onConflict` so a duplicate warns + keeps the first claim instead of throwing. | -> docs/loader-pipeline.md |
33
- | `collectFunctionModules(options?)` | **Single source of truth for function discovery** (ADR 0046). Walks every configured `serverFunctionDirs` root (default `['functions', 'shared']`, legacy singular `serverFunctionsDir` still honored) and derives each module's `functions.*` key path from its location relative to its root — so `shared/rbac/engine.ts` is `functions.rbac.engine`. Skips `node_modules`, test files (`.tests.`/`.test.`/`.spec.` via core's `isTestFile`), `__tests__`/`__mocks__` folders, and paths the `RoutingRules.ignore` predicate rejects. Throws on a cross-root duplicate key or a module/namespace collision; pass `onConflict` to warn + skip instead (dev-loader mode). `roots` overrides the configured roots for tests. Consumed by the dev loader, `functionsMeta.ts`, and BOTH `scripts/generateServerRequests.ts` copies. | -> docs/loader-pipeline.md |
34
- | `renderFunctionsMap({ modules, importPrefix, varPrefix? })` | Renders discovered modules into the `functions` export of a generated production runtime-map file: `{ imports, source }`, with a NESTED object literal matching what the dev loader builds and `apiTypes.generated.ts` declares. A default-only export is aliased to the file name (`functions.sleep.sleep`). | -> docs/loader-pipeline.md |
35
- | `formatFunctionKeyPath(keyPath)` / `functionModuleFileName(keyPath)` / `formatFunctionKeyConflict(...)` / `formatFunctionNamespaceConflict(...)` | Key-path helpers + the two canonical collision diagnostics, shared so dev and build-time emit identical wording. | -> docs/loader-pipeline.md |
36
- | Types: `FunctionModule`, `RenderedFunctionsMap`, `CollectFunctionModulesOptions` | Public typing for the function registry. | -> docs/loader-pipeline.md |
37
- | `upsertApiFromFile(filePath)` | Hot-reload single API: invalidates the TS Program cache + runtime type resolver cache, re-imports the file with a fresh cachebust, and replaces the matching entry in `devApis`. | -> docs/loader-pipeline.md |
38
- | `removeApiFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devApis`. | -> docs/loader-pipeline.md |
39
- | `upsertSyncFromFile(filePath)` | Hot-reload single sync: handles both `_server_v<n>` and `_client_v<n>` kinds, invalidates caches, re-imports, replaces the matching entry in `devSyncs`. | -> docs/loader-pipeline.md |
40
- | `removeSyncFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devSyncs`. | -> docs/loader-pipeline.md |
41
- | `devApis` | In-memory map of `route key -> { main, auth, rateLimit, httpMethod, schema, inputType, inputTypeFilePath }`. Mirrored by the prod runtime maps loader in dev. | -> docs/loader-pipeline.md |
42
- | `devSyncs` | In-memory map of `route key (with _server / _client suffix) -> server entry record or client callback function`. | -> docs/loader-pipeline.md |
43
- | `devFunctions` | Nested mirror of `serverFunctionsDir`. Each leaf is a module's named + default exports, with later folder scans merging onto existing nodes. | -> docs/loader-pipeline.md |
44
- | `setupWatchers()` | Boots the chokidar watchers for `srcDir` / each `serverFunctionDirs` root / `sharedDir` (legacy singular `serverFunctionsDir` still honored when set), coalesces hot-reload + type-map regenerations, fires initial background type-map generation. No-op when `NODE_ENV === 'production'`. | -> docs/hot-reload.md |
45
- | `generateTypeMapFile(options?)` | Discovers all `_api/` and `_sync/` files, runs the TypeChecker-backed extractors, emits `apiTypes.generated.ts` (incl. `apiMetaMap` + `syncMetaMap`), `apiInputSchemas.generated.ts` (`apiInputSchemas` + `syncInputSchemas`), and `apiDocs.generated.json`. Aborts if any unresolved type symbol is found. | -> docs/type-map-generation.md |
46
- | `getInputTypeFromFile(filePath)` | Public extractor: returns the inline-expanded `data` input type text for one API file. Consumed by the dev loader to drive `runtimeTypeValidation`. | -> docs/type-map-generation.md |
47
- | `getSyncClientDataType(filePath)` | Public extractor: returns the inline-expanded `clientInput` (sync server) or client data (sync client) type text. | -> docs/type-map-generation.md |
48
- | `API_VERSION_TOKEN_REGEX` | Canonical `_v<number>` regex used to parse API filenames. | -> docs/loader-pipeline.md |
49
- | `SYNC_VERSION_TOKEN_REGEX` | Canonical `_(server\|client)_v<number>` regex used to parse sync filenames. | -> docs/loader-pipeline.md |
50
- | `assertValidRouteNaming({ srcDir, context })` | Throws if any `_api/` or `_sync/` file fails naming rules. Called by `initializeAll` and `generateTypeMapFile`. | -> docs/loader-pipeline.md |
51
- | `assertNoDuplicateNormalizedRouteKeys({ srcDir, context })` | Throws on collisions between two files that normalize to the same route key. Called by `generateTypeMapFile`. | -> docs/loader-pipeline.md |
52
- | `registerRoutingRules(rules)` | Override the marker segments (`_api`, `_sync`), version regexes, `privateFolderPrefix`, `scaffoldIgnoredFolders`, and the optional `disableTemplateInjection: (filePath) => boolean` predicate for opting parts of the tree out of scaffold injection. | -> docs/loader-pipeline.md |
53
- | `getRoutingRules()` | Read the current rules (defaults shipped by the package). | -> docs/loader-pipeline.md |
54
- | `registerTemplate(kind, content)` | Override the BODY of a template kind with a string. Content resolution order at injection: consumer file (`.luckystack/templates/<kind>.template.ts(x)`) -> this string override -> bundled disk template. `{{REL_PATH}}` / `{{PAGE_PATH}}` / `{{SYNC_NAME}}` placeholders still apply. | -> docs/template-customization.md |
55
- | `getRegisteredTemplate(kind)` / `clearTemplateOverrides()` / `listRegisteredTemplateKinds()` | Lookup, test-reset, and diagnostic helpers for the content-override registry. | -> docs/template-customization.md |
56
- | `registerTemplateRule({ kind, match, priority? })` | Add a SELECTION rule — decides which kind a classified file gets. Rules evaluate by descending `priority` (ties: newest first); first match wins. `match(ctx)` receives `{ filePath, fileKind: 'api'\|'sync_server'\|'sync_client'\|'page', hasPairedServer, srcRelativePath }`. | -> docs/template-customization.md |
57
- | `registerTemplateKind(kind, { match, content?, priority? })` | Register a brand-new template kind (predicate + optional inline content) in one call. Default priority 100 so custom kinds beat the built-ins. | -> docs/template-customization.md |
58
- | `resolveTemplateKind(ctx)` / `getTemplateRules()` / `clearTemplateRules()` / `registerDefaultTemplateRules()` | Evaluate rules to a kind, read the active rule set, drop all rules (consumer replace), and (re)arm the built-in defaults. | -> docs/template-customization.md |
59
- | `BUILT_IN_TEMPLATE_KINDS` / `BUILT_IN_TEMPLATE_FILENAMES` / `DEFAULT_DASHBOARD_PATH_PATTERN` | The 6 built-in kinds, their bundled filenames, and the page-dashboard heuristic regex (reused by the scaffolded consumer rules file). | -> docs/template-customization.md |
60
- | Consumer overlay: `.luckystack/templates/` | devkit auto-loads `.luckystack/templates/templateRules.ts` in DEV (once, before the first injection) so consumers can edit/remove/add selection rules + kinds as code. Per-kind `*.template.ts(x)` files in the same folder override content. Shipped by `create-luckystack-app`. | -> docs/template-customization.md |
61
- | `assertNoDuplicatePageRoutes({ srcDir, context })` | Build-time validator that throws when two `page.tsx` files compute the same URL after invisible-parent stripping. Called by `generateTypeMapFile`. | -> docs/loader-pipeline.md |
62
- | `collectDuplicatePageRoutes(srcDir)` / `formatDuplicatePageRouteIssues({...})` | Non-throwing pair behind the assert. `initializeAll()` uses these to soft-warn at dev startup. **Internal** — live in `routeNamingValidation.ts` but are NOT re-exported from `index.ts`. | -> docs/loader-pipeline.md |
63
- | Type: `TemplateKind`, `DuplicatePageRouteIssue` | Public types for the new registry + duplicate-detector. | -> docs/hot-reload.md |
64
- | `apiMarkerSegment` / `syncMarkerSegment` | Resolved marker strings (default `_api` / `_sync`). | -> docs/loader-pipeline.md |
65
- | `isApiFileName(name)` / `isSyncFileName(name)` / `isSyncServerFileName(name)` / `isSyncClientFileName(name)` | Filename predicates that respect `getRoutingRules()`. | -> docs/loader-pipeline.md |
66
- | `Type: RoutingRules` | Shape of the overrideable rules registry. | -> docs/loader-pipeline.md |
67
- | `resolveRuntimeTypeText(typeText)` | Recursively expand a stored TypeScript type text into fully inlined form using the cached server program. Used by `@luckystack/core`'s dev-only runtime validator. | -> docs/runtime-type-resolver.md |
68
- | `clearRuntimeTypeResolverCache()` | Drop the resolver's memoization map. Called on every hot-reload upsert / delete. | -> docs/runtime-type-resolver.md |
69
- | `validateDeploy(input)` | Library form of the deploy validator. Returns a list of `ValidationFinding` records (severity + message + slot). Finding codes include `service-unassigned`, `service-in-multiple-presets`, `preset-references-unknown-service`, `binding-references-unknown-service`, `binding-invalid-url` (error — binding URL doesn't parse), `binding-missing-port` (error — binding URL has no explicit port), `unknown-redis-resource`, `unknown-mongo-resource`, `unknown-fallback-env`, `fallback-redis-mismatch`, `fallback-mongo-mismatch`, `missing-resource-env-var` (warning), `missing-synchronized-env-var` (warning), `service-bound-in-no-environment` (warning). | -> docs/cli.md |
70
- | `Types: ValidateDeployInput`, `ValidateDeployResult`, `ValidationFinding`, `ValidationSeverity` | Public typing for the validator. | -> docs/cli.md |
71
- | `luckystack-validate-deploy` (`bin`) | CLI wrapper that imports the consumer's compiled `services.config.js` + `deploy.config.js`, runs `validateDeploy`, prints findings, exits non-zero on errors (and on warnings under `--strict`). | -> docs/cli.md |
72
- | Supervisor entry (`supervisor.ts`) | Standalone Node entry: watches `config.ts`, `.env`, `.env.local`, `server/server.ts`, `server/bootstrap/**`, `server/auth/**`, and key `server/functions/*.ts` files; debounces restarts; respawns crashed children with a delay; honors SIGINT / SIGTERM. | -> docs/supervisor.md |
73
-
74
- Internal modules (not exported from `index.ts`, but live in this package):
75
-
76
- | Module | Role | Deep doc |
77
- |---|---|---|
78
- | `typeMap/tsProgram.ts` (`getServerProgram`, `invalidateProgramCache`, `expandType`) | Cached `ts.Program` over `tsconfig.server.json`. Rebuilt on hot reload + before every type-map generation. | -> docs/ts-program-cache.md |
79
- | `typeMap/discovery.ts` | Recursively walks `srcDir` to find `_api/`, `_sync/server`, `_sync/client` files. | -> docs/type-map-generation.md |
80
- | `typeMap/extractors.ts` | TypeChecker-backed extractors for `data` / `serverOutput` / `clientOutput` / stream payload types. | -> docs/type-map-generation.md |
81
- | `typeMap/apiMeta.ts` | Extracts `httpMethod`, `rateLimit`, `auth` from API files. | -> docs/type-map-generation.md |
82
- | `typeMap/routeMeta.ts` | Filename -> `pagePath` / `apiName` / `apiVersion` / `syncName` / `syncVersion`. | -> docs/type-map-generation.md |
83
- | `typeMap/functionsMeta.ts` | Builds the `Functions` interface text emitted into `apiTypes.generated.ts`. Discovery comes from `collectFunctionModules()`; this module only parses each file's exports and places it at its key path. | -> docs/type-map-generation.md |
84
- | `typeMap/emitter.ts` + `emitterArtifacts.ts` | Renders generated files to disk via `@luckystack/core`-resolved paths. | -> docs/type-map-generation.md |
85
- | `typeMap/zodEmitter.ts` | Generates the runtime Zod schemas for API inputs. | -> docs/type-map-generation.md |
86
- | `templateInjector.ts` + `templates/*.ts` | Detects empty `_api/`, `_sync/`, AND `page.tsx` files and injects starter content. Sync server/client pairing is auto-resolved. Page files get the `dashboard` template when the path contains `admin\|dashboard\|settings\|billing\|account\|profile`, else the `plain` template. Pages in invalid placements (inside a reserved framework folder, or directly inside an `_<folder>` with no URL segment left) get a commented diagnostic block instead of a usable template so the placement issue is visible at creation time. | -> docs/hot-reload.md |
87
- | `importDependencyGraph.ts` | Tracks which `_api/` / `_sync/` files depend on each shared module so hot reload can fan out. | -> docs/hot-reload.md |
88
- | `runtimeTypeResolver.ts` | Cached recursive expander built on top of `tsProgram`. | -> docs/runtime-type-resolver.md |
89
-
90
- ## Codegen contract decisions (DD-DEVKIT-D1 / D2 / D3)
91
-
92
- **D1 — getSourceFile/Program miss → loud warning, never silent**
93
-
94
- When `getServerProgram().getSourceFile(filePath)` returns `undefined`, the extractor always emits a `console.warn` and falls back to `{ }` (empty input) or `{ status: string }` (empty output). This is a documented marker: it means the file is present on disk but absent from the TS Program (typically because `tsconfig.server.json` globs do not cover it, or the Program was not rebuilt after a new file was added). The affected route is also listed in `apiTypeDiagnostics.generated.json` under `reason: 'default-fallback'`. Never suppress this warning — a silent `{ }` input accepts any payload shape at runtime.
95
-
96
- **D2 — Zod strict-vs-loose policy**
97
-
98
- Object literal types → `.strict()` (unknown keys rejected, fail-closed). This is the posture for all typed API/sync inputs that the Zod emitter can structurally resolve. Types that fall outside the emitter's scope (intersections, unresolvable TypeReferences, mapped/conditional types) → `z.any()` fallback with a TODO comment. The fallback count is surfaced in `apiTypeDiagnostics.generated.json` (`reason: 'zod-any-fallback'`). Do not add `.passthrough()` to the emitter's default output — consumers who need to accept extra keys should widen their `ApiParams.data` type explicitly.
99
-
100
- **D3 — apiTypeDiagnostics.generated.json**
101
-
102
- Every `generateTypeMapFile()` run emits `apiTypeDiagnostics.generated.json` alongside `apiDocs.generated.json`. It lists every route whose type extraction degraded to a fallback (source file miss, thrown API/sync input/output/**stream** extraction, or `z.any()` in the Zod schema). Shape: `{ generatedAt, totalRoutes, fallbackCount, fallbacks: DiagnosticsEntry[] }`. CI can fail on non-zero `fallbackCount` to enforce type-fidelity. The file path is `<generatedApiDocs dir>/apiTypeDiagnostics.generated.json`.
103
-
104
- One case is deliberately NOT a fallback: a sync side that does not exist on disk. `_client` is optional by contract (`/docs/ARCHITECTURE_SYNC.md` — a pass-through sync must not have one, because a no-op handler costs an execution per recipient), and an absent side yields the same `{ }` text as a present side that declares no shape. The generator therefore carries per-entry presence flags so the emitter can tell the two apart, and suppresses only the field belonging to the missing side. A THROWN extraction is still checked first and always reported, so this can never hide a real failure — do not widen the exception past the missing side's own field.
105
-
106
- **D4 — transport-safe route contracts**
107
-
108
- Output extraction projects `JSON.stringify` semantics recursively: `Date` becomes `string`; `toJSON()` becomes its return type; omitted object values (`undefined`, symbol, every callable value) disappear or make a union-valued property optional; those values become `null` in arrays/tuples. Binary values are rejected because HTTP JSON and Socket.io binary delivery cannot share one truthful output type. Input extraction does not project declarations, but rejects `Date`: JSON can only deliver an ISO string, so route authors must declare `string`, validate it, and convert explicitly before using Date methods.
109
-
110
- ## Config keys (env vars + registerProjectConfig slots)
111
-
112
- - `NODE_ENV` (env, required for branching) — `setupWatchers()` is a no-op outside dev; `supervisor.ts` only watches files in non-prod mode.
113
- - `LUCKYSTACK_CORE_SUPERVISED` (env, set by the supervisor) — present in the child process so framework code can detect it is running under the supervisor.
114
- - `projectConfig.paths.srcDir` — root for route discovery + watcher subscriptions.
115
- - `projectConfig.paths.sharedDir` — additional watch root for shared modules that feed back into route hot reload via the dependency graph.
116
- - `projectConfig.paths.serverFunctionDirs` (array) — roots for `initializeFunctions()` + the functions watchers. (Legacy `projectConfig.paths.serverFunctionsDir` singular form still honored when set; values are merged into the array at config load.)
117
- - `projectConfig.dev.hotReloadDebounceMs` — debounce window for coalesced reload + type-map regeneration.
118
- - `projectConfig.dev.watcherStabilityThresholdMs` / `projectConfig.dev.watcherPollIntervalMs` — chokidar `awaitWriteFinish` tuning passed to the source watcher.
119
- - Generated artifact paths (`getGeneratedSocketTypesPath()`, `getGeneratedApiDocsPath()`, Zod schema path) — resolved through `@luckystack/core`. Override at the core level, not in devkit.
120
- - `tsconfig.server.json` (file, required) — the `ts.Program` is built from this config; `getServerProgram()` throws if it cannot be located.
121
-
122
- ## Peer dependencies
123
-
124
- - **Required peer (runtime)**: `typescript@>=5.7.3 <7.0.0` — the type-map emitter and runtime type resolver call into the TypeScript Compiler API. No optional fallback. The emitter's `checker.typeToString` output was verified byte-identical between TS 5.7.3 and 6.0.3, so both are supported; TS 7 (native port) is excluded until re-verified. Consumers on the React-eslint stack typically stay on 5.7.x (eslint-plugin-react / jsx-a11y have no ESLint-10 release yet, and ESLint 10 is what TS6-capable react-x requires); the framework itself builds on TS 6. If a consumer's `typescript` version drifts outside this range the emitter may produce different inlined output. Treat as a hard peer.
125
- - **Required peer**: `zod@^4.0.0` — the Zod schema emitter compiles consumer input types into runtime schemas via `zodEmitter.ts`.
126
- - **Optional peer**: `@prisma/client@^6.19.0` — required only when consumer route/config types actually reference Prisma models; non-Prisma projects generate without it.
127
- - **Direct dependency**: `chokidar@^5.0.0` (the file watcher used by both `hotReload.ts` and `supervisor.ts`).
128
- - **Direct dependency**: `@luckystack/core` (project config, root dir, generated artifact paths, `tryCatch`, locale reloader hook).
129
- - **Optional**: `tsx` — the supervisor spawns the child with `node node_modules/tsx/dist/cli.mjs server/server.ts`. Consumers who run the server differently need to spawn the supervisor's child themselves.
130
-
131
- ## Related
132
-
133
- - Architecture deep-dives: `/docs/ARCHITECTURE_ROUTING.md` (file conventions + version token rules), `/docs/ARCHITECTURE_PACKAGING.md` (generated artifact layout), `/docs/ARCHITECTURE_API.md`, `/docs/ARCHITECTURE_SYNC.md`.
134
- - README (consumer quickstart): `./README.md`.
135
- - Consumed by: `scripts/generate*.ts` in this repo; `server/dev/setupWatchers.ts`; `server/prod/runtimeMaps.ts` (dev branch); `@luckystack/core/src/runtimeTypeValidation.ts` (dev-only lazy import).