@luckystack/devkit 0.6.6 → 0.7.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.
- 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
package/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-07-16
|
|
11
|
+
|
|
12
|
+
### Changed — BREAKING
|
|
13
|
+
|
|
14
|
+
- **Route OUTPUT types now describe what the client RECEIVES, not what the handler
|
|
15
|
+
returns.** Everything on an output path crosses the wire as JSON, and JSON has no
|
|
16
|
+
`Date`: `Date.prototype.toJSON()` makes it an ISO string. So `createdAt: Date` was
|
|
17
|
+
a lie TypeScript endorsed — `user.createdAt.getTime()` compiled and threw at
|
|
18
|
+
runtime. Generated outputs now read `createdAt: string`.
|
|
19
|
+
|
|
20
|
+
**Migration:** frontend code doing `user.createdAt.getTime()` no longer compiles.
|
|
21
|
+
Parse it instead: `new Date(user.createdAt).getTime()`. That code was already
|
|
22
|
+
broken at runtime; this converts a 3am crash into a red `tsc`. For hand-written
|
|
23
|
+
types that cross the wire, `@luckystack/core` now exports `Jsonify<T>` as the
|
|
24
|
+
type-level mirror (`type ClientUser = Jsonify<User>`).
|
|
25
|
+
|
|
26
|
+
Generic `JSON.stringify` rules apply without ORM name lists: a type with
|
|
27
|
+
`toJSON()` becomes its return type (covers `Date`, MikroORM's `Collection`,
|
|
28
|
+
Prisma's `Decimal`); every function-valued property is dropped; an
|
|
29
|
+
`undefined`/function/symbol union makes an object property optional and becomes
|
|
30
|
+
`null` in an array slot. Binary types (`Buffer`, `ArrayBuffer`, typed arrays,
|
|
31
|
+
Blob/File) now abort generation with an actionable error because HTTP JSON and
|
|
32
|
+
Socket.io binary transport produce incompatible shapes.
|
|
33
|
+
|
|
34
|
+
Wire-safe **INPUTS remain unprojected** because their text feeds the fail-closed
|
|
35
|
+
runtime validator. A `Date` input annotation is now rejected during generation:
|
|
36
|
+
JSON delivers an ISO string, so keeping `Date` would make the handler contract
|
|
37
|
+
lie. Declare `string`, validate it, then convert explicitly.
|
|
38
|
+
|
|
39
|
+
This also means a route returning an ORM entity no longer aborts generation: the
|
|
40
|
+
projection never walks into `EntityProperty`/`EntityMetadata`, because those do not
|
|
41
|
+
survive serialization. Measured on a real MikroORM entity: 44,000 chars with 5
|
|
42
|
+
unresolved symbols before, 149 chars with 0 after — matching what
|
|
43
|
+
`JSON.stringify` actually produces, field for field.
|
|
44
|
+
|
|
10
45
|
### Fixed
|
|
11
46
|
|
|
47
|
+
- The Windows Bun-supervisor regression now uses Windows path semantics even
|
|
48
|
+
when the suite runs on Linux, so CI verifies the intended absolute-entry guard
|
|
49
|
+
instead of rejecting the Windows fixture because of the host OS.
|
|
50
|
+
- Inferred function-injection values containing checker-owned
|
|
51
|
+
`typeof import("C:/absolute/path")` types now emit a portable type query back
|
|
52
|
+
to the consumer's exported value. This prevents Drizzle's schema-parameterized
|
|
53
|
+
SQLite client from becoming malformed generated TypeScript on the second
|
|
54
|
+
artifact generation.
|
|
55
|
+
- Prisma outputs containing a `JsonValue` field no longer collapse the entire
|
|
56
|
+
model (including nested relations) to `JsonValue`; JSON-type recognition now
|
|
57
|
+
matches the type itself instead of any mention inside its rendered object.
|
|
58
|
+
Real Prisma `Result.GetResult`, Drizzle relational-query, and populated
|
|
59
|
+
MikroORM `EntityDTO<Loaded<...>>` graphs now pin three-level Date projection.
|
|
60
|
+
|
|
12
61
|
Codegen fixes surfaced by a MikroORM/MongoDB consumer (verified against a real
|
|
13
62
|
MikroORM project + `tsc`); consumers can drop the corresponding
|
|
14
63
|
`node_modules/@luckystack/devkit/dist` patches once on this version.
|
|
@@ -32,6 +81,47 @@ MikroORM project + `tsc`); consumers can drop the corresponding
|
|
|
32
81
|
- **DEVKIT-5** — private helper subtrees under a marker (`_api/_lib/*`,
|
|
33
82
|
`_sync/_lib/__tests__/*`) are skipped by route-naming validation + discovery
|
|
34
83
|
instead of being flagged as invalid route files.
|
|
84
|
+
- **DEVKIT-6** — `expandTypeDetailed` no longer throws on a tuple type, so a
|
|
85
|
+
route returning a MikroORM entity keeps its real payload shape instead of
|
|
86
|
+
silently degrading to `{ status: string }`. The tuple branch tested the
|
|
87
|
+
*instance* for `ObjectFlags.Tuple`, but TypeScript puts that flag on the
|
|
88
|
+
tuple's *target* (only the empty tuple `[]` is its own target) — so every
|
|
89
|
+
non-empty tuple fell through to an unguarded `type.symbol.name` read and threw
|
|
90
|
+
`TypeError: Cannot read properties of undefined (reading 'name')`.
|
|
91
|
+
`extractors.ts` caught it, so the entire `result` shape was lost with only a
|
|
92
|
+
`console.error` to show for it. MikroORM's
|
|
93
|
+
`EntityProperty.embedded?: [string, string]` made every entity-returning route
|
|
94
|
+
hit this. The branch now tests the target, and the symbol read is
|
|
95
|
+
optional-chained to match the existing guards.
|
|
96
|
+
**Note:** a route that leaks an ORM entity into its payload will now surface
|
|
97
|
+
MikroORM's own types (`EntityProperty`, `EntityMetadata`, …) as unresolved
|
|
98
|
+
symbols, which `generateTypeMapFile()` reports as a hard abort. That is the
|
|
99
|
+
intended posture (DD-DEVKIT-D1: never silent) — the fix is to return a plain
|
|
100
|
+
DTO from the route rather than the entity.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- **Extraction failures are now a first-class diagnostics reason.** A type
|
|
105
|
+
extraction that THREW is reported in `apiTypeDiagnostics.generated.json` as
|
|
106
|
+
`reason: 'extraction-error'`, with the thrown message in a new optional
|
|
107
|
+
`detail` field, instead of being indistinguishable from a route that simply
|
|
108
|
+
declares no shape (both previously emitted `default-fallback`). Per
|
|
109
|
+
DD-DEVKIT-D3 a CI gate can fail on a non-zero `fallbackCount`, so a
|
|
110
|
+
whole-shape loss is no longer invisible to it. API `stream` and sync
|
|
111
|
+
`serverStream` / `clientStream` fields now use the same diagnostics seam; a
|
|
112
|
+
thrown stream extraction can no longer hide behind the legitimate `never`
|
|
113
|
+
default.
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
|
|
117
|
+
- **`DEPTH_LIMIT` raised from 12 to 14** in the type inliner. Measured, not
|
|
118
|
+
guessed: 14 fully exhausts the deepest real graph we have (a decorator-based
|
|
119
|
+
MikroORM entity: `BaseEntity` + `Collection` + a `ManyToOne` cycle) with zero
|
|
120
|
+
depth bailouts in ~3ms, and a limit of 30 traverses identically — it is the
|
|
121
|
+
cycle guard, not the depth limit, that bounds the walk. At 12 that graph was
|
|
122
|
+
truncated in 491 places. Raising it also *shrinks* the emitted text (a
|
|
123
|
+
truncated node is re-rendered verbosely by `checker.typeToString`). Every
|
|
124
|
+
route type in this repo is byte-identical before and after.
|
|
35
125
|
|
|
36
126
|
## [0.1.0]
|
|
37
127
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,125 +1,129 @@
|
|
|
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`, walks every configured `serverFunctionDirs` root (legacy singular `serverFunctionsDir` still honored when set), builds a nested `Record<string, unknown>` mirror of the on-disk tree, and merges named + default exports per file. | -> docs/loader-pipeline.md |
|
|
33
|
-
| `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 |
|
|
34
|
-
| `removeApiFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devApis`. | -> docs/loader-pipeline.md |
|
|
35
|
-
| `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 |
|
|
36
|
-
| `removeSyncFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devSyncs`. | -> docs/loader-pipeline.md |
|
|
37
|
-
| `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 |
|
|
38
|
-
| `devSyncs` | In-memory map of `route key (with _server / _client suffix) -> server entry record or client callback function`. | -> docs/loader-pipeline.md |
|
|
39
|
-
| `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 |
|
|
40
|
-
| `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 |
|
|
41
|
-
| `generateTypeMapFile(options?)` | Discovers all `_api/` and `_sync/` files, runs the TypeChecker-backed extractors, emits `apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, and `apiDocs.generated.json`. Aborts if any unresolved type symbol is found. | -> docs/type-map-generation.md |
|
|
42
|
-
| `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 |
|
|
43
|
-
| `getSyncClientDataType(filePath)` | Public extractor: returns the inline-expanded `clientInput` (sync server) or client data (sync client) type text. | -> docs/type-map-generation.md |
|
|
44
|
-
| `API_VERSION_TOKEN_REGEX` | Canonical `_v<number>` regex used to parse API filenames. | -> docs/loader-pipeline.md |
|
|
45
|
-
| `SYNC_VERSION_TOKEN_REGEX` | Canonical `_(server\|client)_v<number>` regex used to parse sync filenames. | -> docs/loader-pipeline.md |
|
|
46
|
-
| `assertValidRouteNaming({ srcDir, context })` | Throws if any `_api/` or `_sync/` file fails naming rules. Called by `initializeAll` and `generateTypeMapFile`. | -> docs/loader-pipeline.md |
|
|
47
|
-
| `assertNoDuplicateNormalizedRouteKeys({ srcDir, context })` | Throws on collisions between two files that normalize to the same route key. Called by `generateTypeMapFile`. | -> docs/loader-pipeline.md |
|
|
48
|
-
| `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 |
|
|
49
|
-
| `getRoutingRules()` | Read the current rules (defaults shipped by the package). | -> docs/loader-pipeline.md |
|
|
50
|
-
| `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 |
|
|
51
|
-
| `getRegisteredTemplate(kind)` / `clearTemplateOverrides()` / `listRegisteredTemplateKinds()` | Lookup, test-reset, and diagnostic helpers for the content-override registry. | -> docs/template-customization.md |
|
|
52
|
-
| `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 |
|
|
53
|
-
| `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 |
|
|
54
|
-
| `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 |
|
|
55
|
-
| `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 |
|
|
56
|
-
| 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 |
|
|
57
|
-
| `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 |
|
|
58
|
-
| `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 |
|
|
59
|
-
| Type: `TemplateKind`, `DuplicatePageRouteIssue` | Public types for the new registry + duplicate-detector. | -> docs/hot-reload.md |
|
|
60
|
-
| `apiMarkerSegment` / `syncMarkerSegment` | Resolved marker strings (default `_api` / `_sync`). | -> docs/loader-pipeline.md |
|
|
61
|
-
| `isApiFileName(name)` / `isSyncFileName(name)` / `isSyncServerFileName(name)` / `isSyncClientFileName(name)` | Filename predicates that respect `getRoutingRules()`. | -> docs/loader-pipeline.md |
|
|
62
|
-
| `Type: RoutingRules` | Shape of the overrideable rules registry. | -> docs/loader-pipeline.md |
|
|
63
|
-
| `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 |
|
|
64
|
-
| `clearRuntimeTypeResolverCache()` | Drop the resolver's memoization map. Called on every hot-reload upsert / delete. | -> docs/runtime-type-resolver.md |
|
|
65
|
-
| `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 |
|
|
66
|
-
| `Types: ValidateDeployInput`, `ValidateDeployResult`, `ValidationFinding`, `ValidationSeverity` | Public typing for the validator. | -> docs/cli.md |
|
|
67
|
-
| `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 |
|
|
68
|
-
| 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 |
|
|
69
|
-
|
|
70
|
-
Internal modules (not exported from `index.ts`, but live in this package):
|
|
71
|
-
|
|
72
|
-
| Module | Role | Deep doc |
|
|
73
|
-
|---|---|---|
|
|
74
|
-
| `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 |
|
|
75
|
-
| `typeMap/discovery.ts` | Recursively walks `srcDir` to find `_api/`, `_sync/server`, `_sync/client` files. | -> docs/type-map-generation.md |
|
|
76
|
-
| `typeMap/extractors.ts` | TypeChecker-backed extractors for `data` / `serverOutput` / `clientOutput` / stream payload types. | -> docs/type-map-generation.md |
|
|
77
|
-
| `typeMap/apiMeta.ts` | Extracts `httpMethod`, `rateLimit`, `auth` from API files. | -> docs/type-map-generation.md |
|
|
78
|
-
| `typeMap/routeMeta.ts` | Filename -> `pagePath` / `apiName` / `apiVersion` / `syncName` / `syncVersion`. | -> docs/type-map-generation.md |
|
|
79
|
-
| `typeMap/functionsMeta.ts` | Builds the `Functions` interface text emitted into `apiTypes.generated.ts`. | -> docs/type-map-generation.md |
|
|
80
|
-
| `typeMap/emitter.ts` + `emitterArtifacts.ts` | Renders generated files to disk via `@luckystack/core`-resolved paths. | -> docs/type-map-generation.md |
|
|
81
|
-
| `typeMap/zodEmitter.ts` | Generates the runtime Zod schemas for API inputs. | -> docs/type-map-generation.md |
|
|
82
|
-
| `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 |
|
|
83
|
-
| `importDependencyGraph.ts` | Tracks which `_api/` / `_sync/` files depend on each shared module so hot reload can fan out. | -> docs/hot-reload.md |
|
|
84
|
-
| `runtimeTypeResolver.ts` | Cached recursive expander built on top of `tsProgram`. | -> docs/runtime-type-resolver.md |
|
|
85
|
-
|
|
86
|
-
## Codegen contract decisions (DD-DEVKIT-D1 / D2 / D3)
|
|
87
|
-
|
|
88
|
-
**D1 — getSourceFile/Program miss → loud warning, never silent**
|
|
89
|
-
|
|
90
|
-
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.
|
|
91
|
-
|
|
92
|
-
**D2 — Zod strict-vs-loose policy**
|
|
93
|
-
|
|
94
|
-
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.
|
|
95
|
-
|
|
96
|
-
**D3 — apiTypeDiagnostics.generated.json**
|
|
97
|
-
|
|
98
|
-
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 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`.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
108
|
-
- `projectConfig.
|
|
109
|
-
-
|
|
110
|
-
- `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- **
|
|
119
|
-
- **
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
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`, walks every configured `serverFunctionDirs` root (legacy singular `serverFunctionsDir` still honored when set), builds a nested `Record<string, unknown>` mirror of the on-disk tree, and merges named + default exports per file. | -> docs/loader-pipeline.md |
|
|
33
|
+
| `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 |
|
|
34
|
+
| `removeApiFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devApis`. | -> docs/loader-pipeline.md |
|
|
35
|
+
| `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 |
|
|
36
|
+
| `removeSyncFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devSyncs`. | -> docs/loader-pipeline.md |
|
|
37
|
+
| `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 |
|
|
38
|
+
| `devSyncs` | In-memory map of `route key (with _server / _client suffix) -> server entry record or client callback function`. | -> docs/loader-pipeline.md |
|
|
39
|
+
| `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 |
|
|
40
|
+
| `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 |
|
|
41
|
+
| `generateTypeMapFile(options?)` | Discovers all `_api/` and `_sync/` files, runs the TypeChecker-backed extractors, emits `apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, and `apiDocs.generated.json`. Aborts if any unresolved type symbol is found. | -> docs/type-map-generation.md |
|
|
42
|
+
| `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 |
|
|
43
|
+
| `getSyncClientDataType(filePath)` | Public extractor: returns the inline-expanded `clientInput` (sync server) or client data (sync client) type text. | -> docs/type-map-generation.md |
|
|
44
|
+
| `API_VERSION_TOKEN_REGEX` | Canonical `_v<number>` regex used to parse API filenames. | -> docs/loader-pipeline.md |
|
|
45
|
+
| `SYNC_VERSION_TOKEN_REGEX` | Canonical `_(server\|client)_v<number>` regex used to parse sync filenames. | -> docs/loader-pipeline.md |
|
|
46
|
+
| `assertValidRouteNaming({ srcDir, context })` | Throws if any `_api/` or `_sync/` file fails naming rules. Called by `initializeAll` and `generateTypeMapFile`. | -> docs/loader-pipeline.md |
|
|
47
|
+
| `assertNoDuplicateNormalizedRouteKeys({ srcDir, context })` | Throws on collisions between two files that normalize to the same route key. Called by `generateTypeMapFile`. | -> docs/loader-pipeline.md |
|
|
48
|
+
| `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 |
|
|
49
|
+
| `getRoutingRules()` | Read the current rules (defaults shipped by the package). | -> docs/loader-pipeline.md |
|
|
50
|
+
| `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 |
|
|
51
|
+
| `getRegisteredTemplate(kind)` / `clearTemplateOverrides()` / `listRegisteredTemplateKinds()` | Lookup, test-reset, and diagnostic helpers for the content-override registry. | -> docs/template-customization.md |
|
|
52
|
+
| `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 |
|
|
53
|
+
| `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 |
|
|
54
|
+
| `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 |
|
|
55
|
+
| `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 |
|
|
56
|
+
| 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 |
|
|
57
|
+
| `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 |
|
|
58
|
+
| `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 |
|
|
59
|
+
| Type: `TemplateKind`, `DuplicatePageRouteIssue` | Public types for the new registry + duplicate-detector. | -> docs/hot-reload.md |
|
|
60
|
+
| `apiMarkerSegment` / `syncMarkerSegment` | Resolved marker strings (default `_api` / `_sync`). | -> docs/loader-pipeline.md |
|
|
61
|
+
| `isApiFileName(name)` / `isSyncFileName(name)` / `isSyncServerFileName(name)` / `isSyncClientFileName(name)` | Filename predicates that respect `getRoutingRules()`. | -> docs/loader-pipeline.md |
|
|
62
|
+
| `Type: RoutingRules` | Shape of the overrideable rules registry. | -> docs/loader-pipeline.md |
|
|
63
|
+
| `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 |
|
|
64
|
+
| `clearRuntimeTypeResolverCache()` | Drop the resolver's memoization map. Called on every hot-reload upsert / delete. | -> docs/runtime-type-resolver.md |
|
|
65
|
+
| `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 |
|
|
66
|
+
| `Types: ValidateDeployInput`, `ValidateDeployResult`, `ValidationFinding`, `ValidationSeverity` | Public typing for the validator. | -> docs/cli.md |
|
|
67
|
+
| `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 |
|
|
68
|
+
| 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 |
|
|
69
|
+
|
|
70
|
+
Internal modules (not exported from `index.ts`, but live in this package):
|
|
71
|
+
|
|
72
|
+
| Module | Role | Deep doc |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `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 |
|
|
75
|
+
| `typeMap/discovery.ts` | Recursively walks `srcDir` to find `_api/`, `_sync/server`, `_sync/client` files. | -> docs/type-map-generation.md |
|
|
76
|
+
| `typeMap/extractors.ts` | TypeChecker-backed extractors for `data` / `serverOutput` / `clientOutput` / stream payload types. | -> docs/type-map-generation.md |
|
|
77
|
+
| `typeMap/apiMeta.ts` | Extracts `httpMethod`, `rateLimit`, `auth` from API files. | -> docs/type-map-generation.md |
|
|
78
|
+
| `typeMap/routeMeta.ts` | Filename -> `pagePath` / `apiName` / `apiVersion` / `syncName` / `syncVersion`. | -> docs/type-map-generation.md |
|
|
79
|
+
| `typeMap/functionsMeta.ts` | Builds the `Functions` interface text emitted into `apiTypes.generated.ts`. | -> docs/type-map-generation.md |
|
|
80
|
+
| `typeMap/emitter.ts` + `emitterArtifacts.ts` | Renders generated files to disk via `@luckystack/core`-resolved paths. | -> docs/type-map-generation.md |
|
|
81
|
+
| `typeMap/zodEmitter.ts` | Generates the runtime Zod schemas for API inputs. | -> docs/type-map-generation.md |
|
|
82
|
+
| `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 |
|
|
83
|
+
| `importDependencyGraph.ts` | Tracks which `_api/` / `_sync/` files depend on each shared module so hot reload can fan out. | -> docs/hot-reload.md |
|
|
84
|
+
| `runtimeTypeResolver.ts` | Cached recursive expander built on top of `tsProgram`. | -> docs/runtime-type-resolver.md |
|
|
85
|
+
|
|
86
|
+
## Codegen contract decisions (DD-DEVKIT-D1 / D2 / D3)
|
|
87
|
+
|
|
88
|
+
**D1 — getSourceFile/Program miss → loud warning, never silent**
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
**D2 — Zod strict-vs-loose policy**
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
**D3 — apiTypeDiagnostics.generated.json**
|
|
97
|
+
|
|
98
|
+
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`.
|
|
99
|
+
|
|
100
|
+
**D4 — transport-safe route contracts**
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
## Config keys (env vars + registerProjectConfig slots)
|
|
105
|
+
|
|
106
|
+
- `NODE_ENV` (env, required for branching) — `setupWatchers()` is a no-op outside dev; `supervisor.ts` only watches files in non-prod mode.
|
|
107
|
+
- `LUCKYSTACK_CORE_SUPERVISED` (env, set by the supervisor) — present in the child process so framework code can detect it is running under the supervisor.
|
|
108
|
+
- `projectConfig.paths.srcDir` — root for route discovery + watcher subscriptions.
|
|
109
|
+
- `projectConfig.paths.sharedDir` — additional watch root for shared modules that feed back into route hot reload via the dependency graph.
|
|
110
|
+
- `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.)
|
|
111
|
+
- `projectConfig.dev.hotReloadDebounceMs` — debounce window for coalesced reload + type-map regeneration.
|
|
112
|
+
- `projectConfig.dev.watcherStabilityThresholdMs` / `projectConfig.dev.watcherPollIntervalMs` — chokidar `awaitWriteFinish` tuning passed to the source watcher.
|
|
113
|
+
- Generated artifact paths (`getGeneratedSocketTypesPath()`, `getGeneratedApiDocsPath()`, Zod schema path) — resolved through `@luckystack/core`. Override at the core level, not in devkit.
|
|
114
|
+
- `tsconfig.server.json` (file, required) — the `ts.Program` is built from this config; `getServerProgram()` throws if it cannot be located.
|
|
115
|
+
|
|
116
|
+
## Peer dependencies
|
|
117
|
+
|
|
118
|
+
- **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.
|
|
119
|
+
- **Required peer**: `zod@^4.0.0` — the Zod schema emitter compiles consumer input types into runtime schemas via `zodEmitter.ts`.
|
|
120
|
+
- **Required peer**: `@prisma/client@^6.19.0` — type expansion may surface Prisma model types into emitted artifacts; missing the client breaks generation.
|
|
121
|
+
- **Direct dependency**: `chokidar@^5.0.0` (the file watcher used by both `hotReload.ts` and `supervisor.ts`).
|
|
122
|
+
- **Direct dependency**: `@luckystack/core` (project config, root dir, generated artifact paths, `tryCatch`, locale reloader hook).
|
|
123
|
+
- **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.
|
|
124
|
+
|
|
125
|
+
## Related
|
|
126
|
+
|
|
127
|
+
- 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`.
|
|
128
|
+
- README (consumer quickstart): `./README.md`.
|
|
129
|
+
- 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).
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Mathijs van Melick
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathijs van Melick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# @luckystack/devkit
|
|
2
|
-
|
|
3
|
-
> Dev-time tooling for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2): hot reload, route discovery, generated type-map emitter, Zod schema emitter, deep type resolver, plus the `luckystack-validate-deploy` CLI. Install as a `devDependency` in projects that build LuckyStack apps locally.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install --save-dev @luckystack/devkit
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
This is a build-time tool, not a runtime dependency — your production server bundle doesn't ship it. Project paths and the locale reloader hook in via the registries exported from `@luckystack/core` (`getProjectConfig().paths`, `registerLocaleReloader`), so devkit has no app-side relative imports.
|
|
12
|
-
|
|
13
|
-
## CLIs
|
|
14
|
-
|
|
15
|
-
| Bin | What |
|
|
16
|
-
| --- | --- |
|
|
17
|
-
| `luckystack-validate-deploy` | Reads compiled `deploy.config.js` + `services.config.js` and asserts every service is bound, every preset references real services, env keys resolve. Exits non-zero on errors; `--strict` also fails on warnings. |
|
|
18
|
-
|
|
19
|
-
## What it exports
|
|
20
|
-
|
|
21
|
-
| Group | Exports |
|
|
22
|
-
| --- | --- |
|
|
23
|
-
| Type generation | `generateTypeMapFile`, `getInputTypeFromFile`, `getSyncClientDataType` |
|
|
24
|
-
| Route conventions | `API_VERSION_TOKEN_REGEX`, `SYNC_VERSION_TOKEN_REGEX`, `assertNoDuplicateNormalizedRouteKeys`, `assertValidRouteNaming` |
|
|
25
|
-
| Routing rules registry | `registerRoutingRules`, `getRoutingRules`, `apiMarkerSegment`, `syncMarkerSegment`, `isApiFileName`, `isSyncFileName`, `isSyncServerFileName`, `isSyncClientFileName`. Type: `RoutingRules` |
|
|
26
|
-
| Dev runtime loaders | `devApis`, `devSyncs`, `devFunctions`, `initializeAll`, `initializeApis`, `initializeSyncs`, `initializeFunctions`, `upsertApiFromFile`, `removeApiFromFile`, `upsertSyncFromFile`, `removeSyncFromFile` |
|
|
27
|
-
| Hot reload | `setupWatchers` |
|
|
28
|
-
| Deep type resolver | `resolveRuntimeTypeText`, `clearRuntimeTypeResolverCache` (lazy-imported by `@luckystack/core`'s runtime validator in dev) |
|
|
29
|
-
|
|
30
|
-
## Consumed by
|
|
31
|
-
|
|
32
|
-
- `scripts/generate*.ts` — type-map + Zod schema emitters.
|
|
33
|
-
- `server/dev/setupWatchers.ts` (this repo's own dev server) — hot reload.
|
|
34
|
-
- `server/prod/runtimeMaps.ts` — when `NODE_ENV !== 'production'`, loads `devApis` / `devSyncs` for live route resolution.
|
|
35
|
-
- `@luckystack/core/src/runtimeTypeValidation.ts` — dev-only deep alias resolution.
|
|
36
|
-
|
|
37
|
-
## Related architecture docs
|
|
38
|
-
|
|
39
|
-
- [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — file conventions + version token rules.
|
|
40
|
-
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — generated artifact layout per preset.
|
|
41
|
-
|
|
42
|
-
## License
|
|
43
|
-
|
|
44
|
-
MIT — see [LICENSE](../../LICENSE).
|
|
1
|
+
# @luckystack/devkit
|
|
2
|
+
|
|
3
|
+
> Dev-time tooling for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2): hot reload, route discovery, generated type-map emitter, Zod schema emitter, deep type resolver, plus the `luckystack-validate-deploy` CLI. Install as a `devDependency` in projects that build LuckyStack apps locally.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @luckystack/devkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This is a build-time tool, not a runtime dependency — your production server bundle doesn't ship it. Project paths and the locale reloader hook in via the registries exported from `@luckystack/core` (`getProjectConfig().paths`, `registerLocaleReloader`), so devkit has no app-side relative imports.
|
|
12
|
+
|
|
13
|
+
## CLIs
|
|
14
|
+
|
|
15
|
+
| Bin | What |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `luckystack-validate-deploy` | Reads compiled `deploy.config.js` + `services.config.js` and asserts every service is bound, every preset references real services, env keys resolve. Exits non-zero on errors; `--strict` also fails on warnings. |
|
|
18
|
+
|
|
19
|
+
## What it exports
|
|
20
|
+
|
|
21
|
+
| Group | Exports |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Type generation | `generateTypeMapFile`, `getInputTypeFromFile`, `getSyncClientDataType` |
|
|
24
|
+
| Route conventions | `API_VERSION_TOKEN_REGEX`, `SYNC_VERSION_TOKEN_REGEX`, `assertNoDuplicateNormalizedRouteKeys`, `assertValidRouteNaming` |
|
|
25
|
+
| Routing rules registry | `registerRoutingRules`, `getRoutingRules`, `apiMarkerSegment`, `syncMarkerSegment`, `isApiFileName`, `isSyncFileName`, `isSyncServerFileName`, `isSyncClientFileName`. Type: `RoutingRules` |
|
|
26
|
+
| Dev runtime loaders | `devApis`, `devSyncs`, `devFunctions`, `initializeAll`, `initializeApis`, `initializeSyncs`, `initializeFunctions`, `upsertApiFromFile`, `removeApiFromFile`, `upsertSyncFromFile`, `removeSyncFromFile` |
|
|
27
|
+
| Hot reload | `setupWatchers` |
|
|
28
|
+
| Deep type resolver | `resolveRuntimeTypeText`, `clearRuntimeTypeResolverCache` (lazy-imported by `@luckystack/core`'s runtime validator in dev) |
|
|
29
|
+
|
|
30
|
+
## Consumed by
|
|
31
|
+
|
|
32
|
+
- `scripts/generate*.ts` — type-map + Zod schema emitters.
|
|
33
|
+
- `server/dev/setupWatchers.ts` (this repo's own dev server) — hot reload.
|
|
34
|
+
- `server/prod/runtimeMaps.ts` — when `NODE_ENV !== 'production'`, loads `devApis` / `devSyncs` for live route resolution.
|
|
35
|
+
- `@luckystack/core/src/runtimeTypeValidation.ts` — dev-only deep alias resolution.
|
|
36
|
+
|
|
37
|
+
## Related architecture docs
|
|
38
|
+
|
|
39
|
+
- [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — file conventions + version token rules.
|
|
40
|
+
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — generated artifact layout per preset.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validateDeploy.ts"],"sourcesContent":["//? Pure validator for `services.config.ts` + `deploy.config.ts` content.\n//? Catches the classes of misconfiguration that otherwise only surface at\n//? runtime, often silently (empty service map served, fallback target\n//? unreachable, env var typo).\n//?\n//? Pure module so it can be unit-tested and reused outside the CLI — the\n//? CLI in `cli/validateDeploy.ts` is a thin wrapper that loads the config\n//? files, calls `validateDeploy(...)`, and prints results.\n\nimport type {\n DeployConfigShape,\n ServicesConfigShape,\n} from '@luckystack/core';\n\nexport type ValidationSeverity = 'error' | 'warning';\n\nexport interface ValidationFinding {\n severity: ValidationSeverity;\n code: string;\n message: string;\n /** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */\n location?: string;\n}\n\nexport interface ValidateDeployInput {\n services: ServicesConfigShape;\n deploy: DeployConfigShape;\n /**\n * Environment values to consult for `synchronizedEnvKeys` / `urlEnvKey`\n * presence checks. Defaults to `process.env`. Pass a fixture for tests.\n */\n env?: Record<string, string | undefined>;\n}\n\nexport interface ValidateDeployResult {\n ok: boolean;\n findings: ValidationFinding[];\n errorCount: number;\n warningCount: number;\n}\n\nexport const validateDeploy = ({\n services,\n deploy,\n env = process.env,\n}: ValidateDeployInput): ValidateDeployResult => {\n const findings: ValidationFinding[] = [];\n const serviceNames = new Set(Object.keys(services.services));\n\n //? Rule 1: every service is assigned to exactly one preset.\n const servicesInPresets = new Map<string, string[]>();\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n const existing = servicesInPresets.get(service) ?? [];\n existing.push(presetKey);\n servicesInPresets.set(service, existing);\n }\n }\n\n for (const service of serviceNames) {\n const owners = servicesInPresets.get(service) ?? [];\n if (owners.length === 0) {\n findings.push({\n severity: 'error',\n code: 'service-unassigned',\n message: `Service \"${service}\" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n } else if (owners.length > 1) {\n findings.push({\n severity: 'error',\n code: 'service-in-multiple-presets',\n message: `Service \"${service}\" is in multiple presets: ${owners.join(', ')}. A service may belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n //? Rule 2: every preset references services that exist.\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n if (!serviceNames.has(service)) {\n findings.push({\n severity: 'error',\n code: 'preset-references-unknown-service',\n message: `Preset \"${presetKey}\" references unknown service \"${service}\". Add it to services.services or remove the reference.`,\n location: `services.config.ts > presets.${presetKey}.services`,\n });\n }\n }\n }\n\n //? Rule 3: every binding's service matches an actual service.\n //? Rule 4: fallback env key must exist.\n //? Rule 5: redis/mongo resource keys must exist.\n //? Rule 6: fallback shared-resource invariant.\n const environments = deploy.environments ?? {};\n const resourceNames = new Set(Object.keys(deploy.resources));\n\n for (const [envKey, envDef] of Object.entries(environments)) {\n for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {\n if (!serviceNames.has(bindingService)) {\n findings.push({\n severity: 'error',\n code: 'binding-references-unknown-service',\n message: `Environment \"${envKey}\" binds unknown service \"${bindingService}\". Either add it to services.services or remove the binding.`,\n location: `deploy.config.ts > environments.${envKey}.bindings`,\n });\n }\n\n //? Every binding MUST declare an explicit port. The URL-spec default\n //? (80/443) is almost never what a multi-instance deploy wants — a\n //? missing port is far more likely a typo than a deliberate \"route to\n //? port 80\". The router enforces this at boot too (resolveTarget.ts).\n let parsed: URL | null = null;\n try {\n parsed = new URL(bindingUrl);\n } catch {\n findings.push({\n severity: 'error',\n code: 'binding-invalid-url',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding is not a valid URL: \"${bindingUrl}\".`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n if (parsed && !parsed.port) {\n findings.push({\n severity: 'error',\n code: 'binding-missing-port',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding \"${bindingUrl}\" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n }\n\n if (!resourceNames.has(envDef.redis)) {\n findings.push({\n severity: 'error',\n code: 'unknown-redis-resource',\n message: `Environment \"${envKey}\" references unknown redis resource \"${envDef.redis}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.redis`,\n });\n }\n if (!resourceNames.has(envDef.mongo)) {\n findings.push({\n severity: 'error',\n code: 'unknown-mongo-resource',\n message: `Environment \"${envKey}\" references unknown mongo resource \"${envDef.mongo}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.mongo`,\n });\n }\n\n if (envDef.fallback) {\n const fallbackEnv = environments[envDef.fallback];\n //? `fallbackEnv` reads from `environments[key]` — TS treats this as\n //? always-defined (Record lookup), but the runtime can return undefined\n //? when the user names a fallback env that doesn't exist. The else\n //? branch reports that error.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Record lookup can return undefined at runtime\n if (fallbackEnv) {\n if (fallbackEnv.redis !== envDef.redis) {\n findings.push({\n severity: 'error',\n code: 'fallback-redis-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different redis resources (\"${envDef.redis}\" vs \"${fallbackEnv.redis}\"). Fallback envs must share the same redis resource key to keep the shared-Redis invariant.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n if (fallbackEnv.mongo !== envDef.mongo) {\n findings.push({\n severity: 'error',\n code: 'fallback-mongo-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different mongo resources (\"${envDef.mongo}\" vs \"${fallbackEnv.mongo}\"). Fallback envs must share the same mongo resource key.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n } else {\n findings.push({\n severity: 'error',\n code: 'unknown-fallback-env',\n message: `Environment \"${envKey}\" has fallback \"${envDef.fallback}\" which does not exist in deploy.environments.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n }\n }\n\n //? Rule 7+8: env var references must resolve at config time.\n for (const [resourceKey, resource] of Object.entries(deploy.resources)) {\n const urlValue = env[resource.urlEnvKey];\n if (urlValue === undefined || urlValue === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-resource-env-var',\n message: `Resource \"${resourceKey}\" expects env var \"${resource.urlEnvKey}\" but it is unset. The framework will still boot if the env is provided at runtime, but config-time tooling cannot verify the value.`,\n location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`,\n });\n }\n\n for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {\n const value = env[synchronizedKey];\n if (value === undefined || value === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-synchronized-env-var',\n message: `Resource \"${resourceKey}\" requires synchronized env var \"${synchronizedKey}\" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,\n location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`,\n });\n }\n }\n }\n\n //? Rule 9: services declared in services.config but not bound in any\n //? environment will never receive traffic. Warning, not error — that's a\n //? valid intermediate state during a rollout.\n for (const service of serviceNames) {\n const boundIn = Object.entries(environments).filter(([, envDef]) => service in envDef.bindings);\n if (boundIn.length === 0) {\n findings.push({\n severity: 'warning',\n code: 'service-bound-in-no-environment',\n message: `Service \"${service}\" is in services.services and assigned to a preset but never bound in any environment. Requests for this service will fall through to the missing-service handler.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const warningCount = findings.filter((f) => f.severity === 'warning').length;\n\n return {\n ok: errorCount === 0,\n findings,\n errorCount,\n warningCount,\n };\n};\n"],"mappings":"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","names":[]}
|