@luckystack/devkit 0.4.0 → 0.5.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/CLAUDE.md +125 -125
- package/LICENSE +21 -21
- package/README.md +44 -44
- package/dist/{chunk-BZTCJ7JY.js → chunk-EU2RFSLO.js} +1 -1
- package/dist/chunk-EU2RFSLO.js.map +1 -0
- package/dist/cli/validateDeploy.js +1 -1
- package/dist/cli/validateDeploy.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/supervisor.js +0 -0
- 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 +199 -199
- package/docs/type-map-generation.md +392 -392
- package/package.json +7 -2
- package/dist/chunk-BZTCJ7JY.js.map +0 -1
package/CLAUDE.md
CHANGED
|
@@ -1,125 +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. **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
|
-
## 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@^5.0.0` (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).
|
|
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
|
+
## 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@^5.0.0` (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 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validateDeploy.ts"],"sourcesContent":["//? Pure validator for `services.config.ts` + `deploy.config.ts` content.\r\n//? Catches the classes of misconfiguration that otherwise only surface at\r\n//? runtime, often silently (empty service map served, fallback target\r\n//? unreachable, env var typo).\r\n//?\r\n//? Pure module so it can be unit-tested and reused outside the CLI — the\r\n//? CLI in `cli/validateDeploy.ts` is a thin wrapper that loads the config\r\n//? files, calls `validateDeploy(...)`, and prints results.\r\n\r\nimport type {\r\n DeployConfigShape,\r\n ServicesConfigShape,\r\n} from '@luckystack/core';\r\n\r\nexport type ValidationSeverity = 'error' | 'warning';\r\n\r\nexport interface ValidationFinding {\r\n severity: ValidationSeverity;\r\n code: string;\r\n message: string;\r\n /** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */\r\n location?: string;\r\n}\r\n\r\nexport interface ValidateDeployInput {\r\n services: ServicesConfigShape;\r\n deploy: DeployConfigShape;\r\n /**\r\n * Environment values to consult for `synchronizedEnvKeys` / `urlEnvKey`\r\n * presence checks. Defaults to `process.env`. Pass a fixture for tests.\r\n */\r\n env?: Record<string, string | undefined>;\r\n}\r\n\r\nexport interface ValidateDeployResult {\r\n ok: boolean;\r\n findings: ValidationFinding[];\r\n errorCount: number;\r\n warningCount: number;\r\n}\r\n\r\nexport const validateDeploy = ({\r\n services,\r\n deploy,\r\n env = process.env,\r\n}: ValidateDeployInput): ValidateDeployResult => {\r\n const findings: ValidationFinding[] = [];\r\n const serviceNames = new Set(Object.keys(services.services));\r\n\r\n //? Rule 1: every service is assigned to exactly one preset.\r\n const servicesInPresets = new Map<string, string[]>();\r\n for (const [presetKey, preset] of Object.entries(services.presets)) {\r\n for (const service of preset.services) {\r\n const existing = servicesInPresets.get(service) ?? [];\r\n existing.push(presetKey);\r\n servicesInPresets.set(service, existing);\r\n }\r\n }\r\n\r\n for (const service of serviceNames) {\r\n const owners = servicesInPresets.get(service) ?? [];\r\n if (owners.length === 0) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'service-unassigned',\r\n message: `Service \"${service}\" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n } else if (owners.length > 1) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'service-in-multiple-presets',\r\n message: `Service \"${service}\" is in multiple presets: ${owners.join(', ')}. A service may belong to exactly one preset.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n }\r\n }\r\n\r\n //? Rule 2: every preset references services that exist.\r\n for (const [presetKey, preset] of Object.entries(services.presets)) {\r\n for (const service of preset.services) {\r\n if (!serviceNames.has(service)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'preset-references-unknown-service',\r\n message: `Preset \"${presetKey}\" references unknown service \"${service}\". Add it to services.services or remove the reference.`,\r\n location: `services.config.ts > presets.${presetKey}.services`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 3: every binding's service matches an actual service.\r\n //? Rule 4: fallback env key must exist.\r\n //? Rule 5: redis/mongo resource keys must exist.\r\n //? Rule 6: fallback shared-resource invariant.\r\n const environments = deploy.environments ?? {};\r\n const resourceNames = new Set(Object.keys(deploy.resources));\r\n\r\n for (const [envKey, envDef] of Object.entries(environments)) {\r\n for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {\r\n if (!serviceNames.has(bindingService)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-references-unknown-service',\r\n message: `Environment \"${envKey}\" binds unknown service \"${bindingService}\". Either add it to services.services or remove the binding.`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings`,\r\n });\r\n }\r\n\r\n //? Every binding MUST declare an explicit port. The URL-spec default\r\n //? (80/443) is almost never what a multi-instance deploy wants — a\r\n //? missing port is far more likely a typo than a deliberate \"route to\r\n //? port 80\". The router enforces this at boot too (resolveTarget.ts).\r\n let parsed: URL | null = null;\r\n try {\r\n parsed = new URL(bindingUrl);\r\n } catch {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-invalid-url',\r\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding is not a valid URL: \"${bindingUrl}\".`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\r\n });\r\n }\r\n if (parsed && !parsed.port) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-missing-port',\r\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.`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\r\n });\r\n }\r\n }\r\n\r\n if (!resourceNames.has(envDef.redis)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-redis-resource',\r\n message: `Environment \"${envKey}\" references unknown redis resource \"${envDef.redis}\". Add it to deploy.resources.`,\r\n location: `deploy.config.ts > environments.${envKey}.redis`,\r\n });\r\n }\r\n if (!resourceNames.has(envDef.mongo)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-mongo-resource',\r\n message: `Environment \"${envKey}\" references unknown mongo resource \"${envDef.mongo}\". Add it to deploy.resources.`,\r\n location: `deploy.config.ts > environments.${envKey}.mongo`,\r\n });\r\n }\r\n\r\n if (envDef.fallback) {\r\n const fallbackEnv = environments[envDef.fallback];\r\n //? `fallbackEnv` reads from `environments[key]` — TS treats this as\r\n //? always-defined (Record lookup), but the runtime can return undefined\r\n //? when the user names a fallback env that doesn't exist. The else\r\n //? branch reports that error.\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Record lookup can return undefined at runtime\r\n if (fallbackEnv) {\r\n if (fallbackEnv.redis !== envDef.redis) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'fallback-redis-mismatch',\r\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.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n if (fallbackEnv.mongo !== envDef.mongo) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'fallback-mongo-mismatch',\r\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.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n } else {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-fallback-env',\r\n message: `Environment \"${envKey}\" has fallback \"${envDef.fallback}\" which does not exist in deploy.environments.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 7+8: env var references must resolve at config time.\r\n for (const [resourceKey, resource] of Object.entries(deploy.resources)) {\r\n const urlValue = env[resource.urlEnvKey];\r\n if (urlValue === undefined || urlValue === '') {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'missing-resource-env-var',\r\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.`,\r\n location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`,\r\n });\r\n }\r\n\r\n for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {\r\n const value = env[synchronizedKey];\r\n if (value === undefined || value === '') {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'missing-synchronized-env-var',\r\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.`,\r\n location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 9: services declared in services.config but not bound in any\r\n //? environment will never receive traffic. Warning, not error — that's a\r\n //? valid intermediate state during a rollout.\r\n for (const service of serviceNames) {\r\n const boundIn = Object.entries(environments).filter(([, envDef]) => service in envDef.bindings);\r\n if (boundIn.length === 0) {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'service-bound-in-no-environment',\r\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.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n }\r\n }\r\n\r\n const errorCount = findings.filter((f) => f.severity === 'error').length;\r\n const warningCount = findings.filter((f) => f.severity === 'warning').length;\r\n\r\n return {\r\n ok: errorCount === 0,\r\n findings,\r\n errorCount,\r\n warningCount,\r\n };\r\n};\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/validateDeploy.ts"],"sourcesContent":["#!/usr/bin/env node\n//? `luckystack-validate-deploy` CLI. Loads the consumer's compiled\n//? services.config.js and deploy.config.js as side-effects (they call\n//? `registerServicesConfig` / `registerDeployConfig` on import), then runs\n//? `validateDeploy()` and prints findings.\n//?\n//? Designed to be a pre-deploy gate: exit 1 on any error finding, 0\n//? otherwise. Warnings print to stderr but never fail the run.\n\nimport { pathToFileURL } from 'node:url';\nimport path from 'node:path';\nimport {\n getDeployConfig,\n getServicesConfig,\n isDeployConfigRegistered,\n isServicesConfigRegistered,\n} from '@luckystack/core';\n\nimport { validateDeploy, type ValidationFinding } from '../validateDeploy';\n\ninterface CliArgs {\n deploy: string | null;\n services: string | null;\n failOnWarning: boolean;\n}\n\nconst parseArgs = (argv: readonly string[]): CliArgs => {\n const args: CliArgs = {\n deploy: null,\n services: null,\n failOnWarning: false,\n };\n\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i];\n const next: string | undefined = i + 1 < argv.length ? argv[i + 1] : undefined;\n switch (flag) {\n case '--deploy':\n case '-d': {\n args.deploy = next ?? null;\n i++;\n break;\n }\n case '--services':\n case '-s': {\n args.services = next ?? null;\n i++;\n break;\n }\n case '--strict': {\n args.failOnWarning = true;\n break;\n }\n case '--help':\n case '-h': {\n printHelp();\n process.exit(0);\n break;\n }\n default: {\n break;\n }\n }\n }\n\n return args;\n};\n\nconst printHelp = (): void => {\n process.stdout.write(`\nluckystack-validate-deploy — pre-deploy validator for services + deploy configs\n\nUSAGE\n luckystack-validate-deploy --deploy <file> --services <file> [options]\n\nREQUIRED\n --deploy, -d <file> Path to compiled deploy.config.js (registers DeployConfig).\n --services, -s <file> Path to compiled services.config.js (registers ServicesConfig).\n\nOPTIONS\n --strict Exit 1 on warnings as well as errors.\n --help, -h Show this help.\n\nWHAT IT CHECKS\n - Every service is assigned to exactly one preset.\n - Every preset references services that exist.\n - Every environment binding's service matches an actual service.\n - Every environment's redis/mongo resource keys exist.\n - Fallback envs must share the same redis/mongo resource keys.\n - synchronizedEnvKeys and urlEnvKey values resolve at config time (warning only).\n - Services bound in no environment (warning only — valid mid-rollout).\n\nEXAMPLES\n luckystack-validate-deploy --deploy dist/deploy.config.js --services dist/services.config.js\n npx tsx packages/devkit/dist/cli/validateDeploy.js --deploy ./deploy.config.ts --services ./services.config.ts\n`);\n};\n\nconst ALLOWED_CONFIG_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']);\n\nconst importConfig = async (file: string, label: string): Promise<void> => {\n const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);\n\n //? Guard: resolved path must stay inside cwd (or the resolved project root)\n //? to prevent `--deploy ../../../../etc/shadow` style path injection.\n const root = path.resolve(process.cwd());\n const resolved = path.resolve(abs);\n if (!resolved.startsWith(root + path.sep) && resolved !== root) {\n throw new Error(\n `[luckystack-validate-deploy] ${label} path \"${file}\" resolves outside the project root — aborting`,\n );\n }\n\n //? Guard: only load known JS/TS module extensions.\n const ext = path.extname(resolved).toLowerCase();\n if (!ALLOWED_CONFIG_EXTENSIONS.has(ext)) {\n throw new Error(\n `[luckystack-validate-deploy] ${label} path \"${file}\" has disallowed extension \"${ext}\" — expected one of ${[...ALLOWED_CONFIG_EXTENSIONS].join(', ')}`,\n );\n }\n\n try {\n await import(pathToFileURL(resolved).href);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);\n }\n};\n\nconst COLORS = process.stdout.isTTY\n ? {\n reset: '\u001b[0m',\n bold: '\u001b[1m',\n dim: '\u001b[2m',\n red: '\u001b[31m',\n green: '\u001b[32m',\n yellow: '\u001b[33m',\n cyan: '\u001b[36m',\n }\n : { reset: '', bold: '', dim: '', red: '', green: '', yellow: '', cyan: '' };\n\nconst printFinding = (finding: ValidationFinding): void => {\n const tag = finding.severity === 'error'\n ? `${COLORS.red}${COLORS.bold}ERROR${COLORS.reset}`\n : `${COLORS.yellow}${COLORS.bold}WARN ${COLORS.reset}`;\n const code = `${COLORS.dim}[${finding.code}]${COLORS.reset}`;\n const location = finding.location ? `\\n ${COLORS.dim}at ${finding.location}${COLORS.reset}` : '';\n const sink = finding.severity === 'error' ? process.stderr : process.stdout;\n sink.write(`${tag} ${code} ${finding.message}${location}\\n`);\n};\n\nconst main = async (): Promise<void> => {\n const args = parseArgs(process.argv.slice(2));\n\n if (!args.deploy || !args.services) {\n process.stderr.write('[luckystack-validate-deploy] --deploy and --services are required. Run with --help for usage.\\n');\n process.exit(2);\n }\n\n await importConfig(args.deploy, 'deploy config');\n await importConfig(args.services, 'services config');\n\n if (!isDeployConfigRegistered()) {\n process.stderr.write('[luckystack-validate-deploy] deploy config file did not call registerDeployConfig — nothing to validate.\\n');\n process.exit(2);\n }\n if (!isServicesConfigRegistered()) {\n process.stderr.write('[luckystack-validate-deploy] services config file did not call registerServicesConfig — nothing to validate.\\n');\n process.exit(2);\n }\n\n const result = validateDeploy({\n services: getServicesConfig(),\n deploy: getDeployConfig(),\n });\n\n for (const finding of result.findings) {\n printFinding(finding);\n }\n\n const summary = result.ok\n ? `${COLORS.green}${COLORS.bold}OK${COLORS.reset}`\n : `${COLORS.red}${COLORS.bold}FAILED${COLORS.reset}`;\n process.stdout.write(\n `\\n${summary} — ${String(result.errorCount)} error(s), ${String(result.warningCount)} warning(s)\\n`,\n );\n\n if (result.errorCount > 0) {\n process.exit(1);\n }\n if (args.failOnWarning && result.warningCount > 0) {\n process.exit(1);\n }\n process.exit(0);\n};\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.stack ?? error.message : String(error);\n process.stderr.write(`[luckystack-validate-deploy] fatal: ${message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/validateDeploy.ts"],"sourcesContent":["#!/usr/bin/env node\r\n//? `luckystack-validate-deploy` CLI. Loads the consumer's compiled\r\n//? services.config.js and deploy.config.js as side-effects (they call\r\n//? `registerServicesConfig` / `registerDeployConfig` on import), then runs\r\n//? `validateDeploy()` and prints findings.\r\n//?\r\n//? Designed to be a pre-deploy gate: exit 1 on any error finding, 0\r\n//? otherwise. Warnings print to stderr but never fail the run.\r\n\r\nimport { pathToFileURL } from 'node:url';\r\nimport path from 'node:path';\r\nimport {\r\n getDeployConfig,\r\n getServicesConfig,\r\n isDeployConfigRegistered,\r\n isServicesConfigRegistered,\r\n} from '@luckystack/core';\r\n\r\nimport { validateDeploy, type ValidationFinding } from '../validateDeploy';\r\n\r\ninterface CliArgs {\r\n deploy: string | null;\r\n services: string | null;\r\n failOnWarning: boolean;\r\n}\r\n\r\nconst parseArgs = (argv: readonly string[]): CliArgs => {\r\n const args: CliArgs = {\r\n deploy: null,\r\n services: null,\r\n failOnWarning: false,\r\n };\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const flag = argv[i];\r\n const next: string | undefined = i + 1 < argv.length ? argv[i + 1] : undefined;\r\n switch (flag) {\r\n case '--deploy':\r\n case '-d': {\r\n args.deploy = next ?? null;\r\n i++;\r\n break;\r\n }\r\n case '--services':\r\n case '-s': {\r\n args.services = next ?? null;\r\n i++;\r\n break;\r\n }\r\n case '--strict': {\r\n args.failOnWarning = true;\r\n break;\r\n }\r\n case '--help':\r\n case '-h': {\r\n printHelp();\r\n process.exit(0);\r\n break;\r\n }\r\n default: {\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return args;\r\n};\r\n\r\nconst printHelp = (): void => {\r\n process.stdout.write(`\r\nluckystack-validate-deploy — pre-deploy validator for services + deploy configs\r\n\r\nUSAGE\r\n luckystack-validate-deploy --deploy <file> --services <file> [options]\r\n\r\nREQUIRED\r\n --deploy, -d <file> Path to compiled deploy.config.js (registers DeployConfig).\r\n --services, -s <file> Path to compiled services.config.js (registers ServicesConfig).\r\n\r\nOPTIONS\r\n --strict Exit 1 on warnings as well as errors.\r\n --help, -h Show this help.\r\n\r\nWHAT IT CHECKS\r\n - Every service is assigned to exactly one preset.\r\n - Every preset references services that exist.\r\n - Every environment binding's service matches an actual service.\r\n - Every environment's redis/mongo resource keys exist.\r\n - Fallback envs must share the same redis/mongo resource keys.\r\n - synchronizedEnvKeys and urlEnvKey values resolve at config time (warning only).\r\n - Services bound in no environment (warning only — valid mid-rollout).\r\n\r\nEXAMPLES\r\n luckystack-validate-deploy --deploy dist/deploy.config.js --services dist/services.config.js\r\n npx tsx packages/devkit/dist/cli/validateDeploy.js --deploy ./deploy.config.ts --services ./services.config.ts\r\n`);\r\n};\r\n\r\nconst ALLOWED_CONFIG_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']);\r\n\r\nconst importConfig = async (file: string, label: string): Promise<void> => {\r\n const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);\r\n\r\n //? Guard: resolved path must stay inside cwd (or the resolved project root)\r\n //? to prevent `--deploy ../../../../etc/shadow` style path injection.\r\n const root = path.resolve(process.cwd());\r\n const resolved = path.resolve(abs);\r\n if (!resolved.startsWith(root + path.sep) && resolved !== root) {\r\n throw new Error(\r\n `[luckystack-validate-deploy] ${label} path \"${file}\" resolves outside the project root — aborting`,\r\n );\r\n }\r\n\r\n //? Guard: only load known JS/TS module extensions.\r\n const ext = path.extname(resolved).toLowerCase();\r\n if (!ALLOWED_CONFIG_EXTENSIONS.has(ext)) {\r\n throw new Error(\r\n `[luckystack-validate-deploy] ${label} path \"${file}\" has disallowed extension \"${ext}\" — expected one of ${[...ALLOWED_CONFIG_EXTENSIONS].join(', ')}`,\r\n );\r\n }\r\n\r\n try {\r\n await import(pathToFileURL(resolved).href);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);\r\n }\r\n};\r\n\r\nconst COLORS = process.stdout.isTTY\r\n ? {\r\n reset: '\u001b[0m',\r\n bold: '\u001b[1m',\r\n dim: '\u001b[2m',\r\n red: '\u001b[31m',\r\n green: '\u001b[32m',\r\n yellow: '\u001b[33m',\r\n cyan: '\u001b[36m',\r\n }\r\n : { reset: '', bold: '', dim: '', red: '', green: '', yellow: '', cyan: '' };\r\n\r\nconst printFinding = (finding: ValidationFinding): void => {\r\n const tag = finding.severity === 'error'\r\n ? `${COLORS.red}${COLORS.bold}ERROR${COLORS.reset}`\r\n : `${COLORS.yellow}${COLORS.bold}WARN ${COLORS.reset}`;\r\n const code = `${COLORS.dim}[${finding.code}]${COLORS.reset}`;\r\n const location = finding.location ? `\\n ${COLORS.dim}at ${finding.location}${COLORS.reset}` : '';\r\n const sink = finding.severity === 'error' ? process.stderr : process.stdout;\r\n sink.write(`${tag} ${code} ${finding.message}${location}\\n`);\r\n};\r\n\r\nconst main = async (): Promise<void> => {\r\n const args = parseArgs(process.argv.slice(2));\r\n\r\n if (!args.deploy || !args.services) {\r\n process.stderr.write('[luckystack-validate-deploy] --deploy and --services are required. Run with --help for usage.\\n');\r\n process.exit(2);\r\n }\r\n\r\n await importConfig(args.deploy, 'deploy config');\r\n await importConfig(args.services, 'services config');\r\n\r\n if (!isDeployConfigRegistered()) {\r\n process.stderr.write('[luckystack-validate-deploy] deploy config file did not call registerDeployConfig — nothing to validate.\\n');\r\n process.exit(2);\r\n }\r\n if (!isServicesConfigRegistered()) {\r\n process.stderr.write('[luckystack-validate-deploy] services config file did not call registerServicesConfig — nothing to validate.\\n');\r\n process.exit(2);\r\n }\r\n\r\n const result = validateDeploy({\r\n services: getServicesConfig(),\r\n deploy: getDeployConfig(),\r\n });\r\n\r\n for (const finding of result.findings) {\r\n printFinding(finding);\r\n }\r\n\r\n const summary = result.ok\r\n ? `${COLORS.green}${COLORS.bold}OK${COLORS.reset}`\r\n : `${COLORS.red}${COLORS.bold}FAILED${COLORS.reset}`;\r\n process.stdout.write(\r\n `\\n${summary} — ${String(result.errorCount)} error(s), ${String(result.warningCount)} warning(s)\\n`,\r\n );\r\n\r\n if (result.errorCount > 0) {\r\n process.exit(1);\r\n }\r\n if (args.failOnWarning && result.warningCount > 0) {\r\n process.exit(1);\r\n }\r\n process.exit(0);\r\n};\r\n\r\nmain().catch((error: unknown) => {\r\n const message = error instanceof Error ? error.stack ?? error.message : String(error);\r\n process.stderr.write(`[luckystack-validate-deploy] fatal: ${message}\\n`);\r\n process.exit(1);\r\n});\r\n"],"mappings":"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","names":[]}
|