@luckystack/devkit 0.1.9 → 0.2.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 CHANGED
@@ -1,14 +1,14 @@
1
- # Changelog
2
-
3
- All notable changes to `@luckystack/devkit` are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.1.0]
11
-
12
- ### Added
13
-
14
- - Initial public release as part of the LuckyStack package split.
1
+ # Changelog
2
+
3
+ All notable changes to `@luckystack/devkit` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0]
11
+
12
+ ### Added
13
+
14
+ - Initial public release as part of the LuckyStack package split.
package/CLAUDE.md CHANGED
@@ -1,111 +1,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. | -> 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
- ## Config keys (env vars + registerProjectConfig slots)
87
-
88
- - `NODE_ENV` (env, required for branching) `setupWatchers()` is a no-op outside dev; `supervisor.ts` only watches files in non-prod mode.
89
- - `LUCKYSTACK_CORE_SUPERVISED` (env, set by the supervisor) — present in the child process so framework code can detect it is running under the supervisor.
90
- - `projectConfig.paths.srcDir` root for route discovery + watcher subscriptions.
91
- - `projectConfig.paths.sharedDir` — additional watch root for shared modules that feed back into route hot reload via the dependency graph.
92
- - `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.)
93
- - `projectConfig.dev.hotReloadDebounceMs` — debounce window for coalesced reload + type-map regeneration.
94
- - `projectConfig.dev.watcherStabilityThresholdMs` / `projectConfig.dev.watcherPollIntervalMs` chokidar `awaitWriteFinish` tuning passed to the source watcher.
95
- - Generated artifact paths (`getGeneratedSocketTypesPath()`, `getGeneratedApiDocsPath()`, Zod schema path) — resolved through `@luckystack/core`. Override at the core level, not in devkit.
96
- - `tsconfig.server.json` (file, required) the `ts.Program` is built from this config; `getServerProgram()` throws if it cannot be located.
97
-
98
- ## Peer dependencies
99
-
100
- - **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.
101
- - **Required peer**: `zod@^4.0.0` — the Zod schema emitter compiles consumer input types into runtime schemas via `zodEmitter.ts`.
102
- - **Required peer**: `@prisma/client@^6.19.0` type expansion may surface Prisma model types into emitted artifacts; missing the client breaks generation.
103
- - **Direct dependency**: `chokidar@^4.0.3` (the file watcher used by both `hotReload.ts` and `supervisor.ts`).
104
- - **Direct dependency**: `@luckystack/core` (project config, root dir, generated artifact paths, `tryCatch`, locale reloader hook).
105
- - **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.
106
-
107
- ## Related
108
-
109
- - 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`.
110
- - README (consumer quickstart): `./README.md`.
111
- - 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).
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. | -> 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
+ **D1getSourceFile/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
+ **D2Zod 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
+ **D3apiTypeDiagnostics.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
+ ## Config keys (env vars + registerProjectConfig slots)
101
+
102
+ - `NODE_ENV` (env, required for branching) `setupWatchers()` is a no-op outside dev; `supervisor.ts` only watches files in non-prod mode.
103
+ - `LUCKYSTACK_CORE_SUPERVISED` (env, set by the supervisor) present in the child process so framework code can detect it is running under the supervisor.
104
+ - `projectConfig.paths.srcDir` root for route discovery + watcher subscriptions.
105
+ - `projectConfig.paths.sharedDir` — additional watch root for shared modules that feed back into route hot reload via the dependency graph.
106
+ - `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.)
107
+ - `projectConfig.dev.hotReloadDebounceMs` — debounce window for coalesced reload + type-map regeneration.
108
+ - `projectConfig.dev.watcherStabilityThresholdMs` / `projectConfig.dev.watcherPollIntervalMs` — chokidar `awaitWriteFinish` tuning passed to the source watcher.
109
+ - Generated artifact paths (`getGeneratedSocketTypesPath()`, `getGeneratedApiDocsPath()`, Zod schema path) resolved through `@luckystack/core`. Override at the core level, not in devkit.
110
+ - `tsconfig.server.json` (file, required) — the `ts.Program` is built from this config; `getServerProgram()` throws if it cannot be located.
111
+
112
+ ## Peer dependencies
113
+
114
+ - **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.
115
+ - **Required peer**: `zod@^4.0.0` — the Zod schema emitter compiles consumer input types into runtime schemas via `zodEmitter.ts`.
116
+ - **Required peer**: `@prisma/client@^6.19.0` — type expansion may surface Prisma model types into emitted artifacts; missing the client breaks generation.
117
+ - **Direct dependency**: `chokidar@^4.0.3` (the file watcher used by both `hotReload.ts` and `supervisor.ts`).
118
+ - **Direct dependency**: `@luckystack/core` (project config, root dir, generated artifact paths, `tryCatch`, locale reloader hook).
119
+ - **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.
120
+
121
+ ## Related
122
+
123
+ - 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`.
124
+ - README (consumer quickstart): `./README.md`.
125
+ - 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,170 @@
1
+ // src/validateDeploy.ts
2
+ var validateDeploy = ({
3
+ services,
4
+ deploy,
5
+ env = process.env
6
+ }) => {
7
+ const findings = [];
8
+ const serviceNames = new Set(Object.keys(services.services));
9
+ const servicesInPresets = /* @__PURE__ */ new Map();
10
+ for (const [presetKey, preset] of Object.entries(services.presets)) {
11
+ for (const service of preset.services) {
12
+ const existing = servicesInPresets.get(service) ?? [];
13
+ existing.push(presetKey);
14
+ servicesInPresets.set(service, existing);
15
+ }
16
+ }
17
+ for (const service of serviceNames) {
18
+ const owners = servicesInPresets.get(service) ?? [];
19
+ if (owners.length === 0) {
20
+ findings.push({
21
+ severity: "error",
22
+ code: "service-unassigned",
23
+ message: `Service "${service}" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,
24
+ location: `services.config.ts > services.${service}`
25
+ });
26
+ } else if (owners.length > 1) {
27
+ findings.push({
28
+ severity: "error",
29
+ code: "service-in-multiple-presets",
30
+ message: `Service "${service}" is in multiple presets: ${owners.join(", ")}. A service may belong to exactly one preset.`,
31
+ location: `services.config.ts > services.${service}`
32
+ });
33
+ }
34
+ }
35
+ for (const [presetKey, preset] of Object.entries(services.presets)) {
36
+ for (const service of preset.services) {
37
+ if (!serviceNames.has(service)) {
38
+ findings.push({
39
+ severity: "error",
40
+ code: "preset-references-unknown-service",
41
+ message: `Preset "${presetKey}" references unknown service "${service}". Add it to services.services or remove the reference.`,
42
+ location: `services.config.ts > presets.${presetKey}.services`
43
+ });
44
+ }
45
+ }
46
+ }
47
+ const environments = deploy.environments ?? {};
48
+ const resourceNames = new Set(Object.keys(deploy.resources));
49
+ for (const [envKey, envDef] of Object.entries(environments)) {
50
+ for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {
51
+ if (!serviceNames.has(bindingService)) {
52
+ findings.push({
53
+ severity: "error",
54
+ code: "binding-references-unknown-service",
55
+ message: `Environment "${envKey}" binds unknown service "${bindingService}". Either add it to services.services or remove the binding.`,
56
+ location: `deploy.config.ts > environments.${envKey}.bindings`
57
+ });
58
+ }
59
+ let parsed = null;
60
+ try {
61
+ parsed = new URL(bindingUrl);
62
+ } catch {
63
+ findings.push({
64
+ severity: "error",
65
+ code: "binding-invalid-url",
66
+ message: `Environment "${envKey}" service "${bindingService}" binding is not a valid URL: "${bindingUrl}".`,
67
+ location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
68
+ });
69
+ }
70
+ if (parsed && !parsed.port) {
71
+ findings.push({
72
+ severity: "error",
73
+ code: "binding-missing-port",
74
+ message: `Environment "${envKey}" service "${bindingService}" binding "${bindingUrl}" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,
75
+ location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
76
+ });
77
+ }
78
+ }
79
+ if (!resourceNames.has(envDef.redis)) {
80
+ findings.push({
81
+ severity: "error",
82
+ code: "unknown-redis-resource",
83
+ message: `Environment "${envKey}" references unknown redis resource "${envDef.redis}". Add it to deploy.resources.`,
84
+ location: `deploy.config.ts > environments.${envKey}.redis`
85
+ });
86
+ }
87
+ if (!resourceNames.has(envDef.mongo)) {
88
+ findings.push({
89
+ severity: "error",
90
+ code: "unknown-mongo-resource",
91
+ message: `Environment "${envKey}" references unknown mongo resource "${envDef.mongo}". Add it to deploy.resources.`,
92
+ location: `deploy.config.ts > environments.${envKey}.mongo`
93
+ });
94
+ }
95
+ if (envDef.fallback) {
96
+ const fallbackEnv = environments[envDef.fallback];
97
+ if (fallbackEnv) {
98
+ if (fallbackEnv.redis !== envDef.redis) {
99
+ findings.push({
100
+ severity: "error",
101
+ code: "fallback-redis-mismatch",
102
+ 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.`,
103
+ location: `deploy.config.ts > environments.${envKey}.fallback`
104
+ });
105
+ }
106
+ if (fallbackEnv.mongo !== envDef.mongo) {
107
+ findings.push({
108
+ severity: "error",
109
+ code: "fallback-mongo-mismatch",
110
+ 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.`,
111
+ location: `deploy.config.ts > environments.${envKey}.fallback`
112
+ });
113
+ }
114
+ } else {
115
+ findings.push({
116
+ severity: "error",
117
+ code: "unknown-fallback-env",
118
+ message: `Environment "${envKey}" has fallback "${envDef.fallback}" which does not exist in deploy.environments.`,
119
+ location: `deploy.config.ts > environments.${envKey}.fallback`
120
+ });
121
+ }
122
+ }
123
+ }
124
+ for (const [resourceKey, resource] of Object.entries(deploy.resources)) {
125
+ const urlValue = env[resource.urlEnvKey];
126
+ if (urlValue === void 0 || urlValue === "") {
127
+ findings.push({
128
+ severity: "warning",
129
+ code: "missing-resource-env-var",
130
+ 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.`,
131
+ location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`
132
+ });
133
+ }
134
+ for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {
135
+ const value = env[synchronizedKey];
136
+ if (value === void 0 || value === "") {
137
+ findings.push({
138
+ severity: "warning",
139
+ code: "missing-synchronized-env-var",
140
+ message: `Resource "${resourceKey}" requires synchronized env var "${synchronizedKey}" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,
141
+ location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`
142
+ });
143
+ }
144
+ }
145
+ }
146
+ for (const service of serviceNames) {
147
+ const boundIn = Object.entries(environments).filter(([, env2]) => service in env2.bindings);
148
+ if (boundIn.length === 0) {
149
+ findings.push({
150
+ severity: "warning",
151
+ code: "service-bound-in-no-environment",
152
+ 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.`,
153
+ location: `services.config.ts > services.${service}`
154
+ });
155
+ }
156
+ }
157
+ const errorCount = findings.filter((f) => f.severity === "error").length;
158
+ const warningCount = findings.filter((f) => f.severity === "warning").length;
159
+ return {
160
+ ok: errorCount === 0,
161
+ findings,
162
+ errorCount,
163
+ warningCount
164
+ };
165
+ };
166
+
167
+ export {
168
+ validateDeploy
169
+ };
170
+ //# sourceMappingURL=chunk-YYWCJ7VZ.js.map