@luckystack/devkit 0.8.4 → 0.8.5

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
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - `collectFunctionModules()` / `renderFunctionsMap()` — one shared source of truth for server-function discovery and `functions.*` key derivation, consumed by the dev loader, the type-map generator and `scripts/generateServerRequests.ts`. A cross-root duplicate key or a module/namespace collision now fails the build with a diagnostic naming both source files, instead of silently dropping one module from the generated map.
13
+
14
+ ### Fixed
15
+
16
+ - Test files in a `serverFunctionDirs` root are excluded from function injection. `functions/db.tests.ts` used to be injected as `functions['db.tests']` — imported at boot by the dev loader, declared in the generated `Functions` interface, and baked into the production runtime map. `__tests__` / `__mocks__` folders are skipped too (ADR 0047).
17
+
18
+ - Server-function discovery no longer diverges between layers. The type-map generator now honours the `RoutingRules.ignore` predicate on function roots (it previously only did so for routes), and both it and the dev loader derive key paths from the same walk that feeds the production map generator.
19
+
10
20
  ## [0.8.4] - 2026-08-17
11
21
 
12
22
  ### Changed
package/CLAUDE.md CHANGED
@@ -29,7 +29,11 @@ Dev-time tooling for LuckyStack projects: file-based route discovery + in-memory
29
29
  | `initializeAll()` | Runs route-naming validation, then `initializeApis` + `initializeSyncs` + `initializeFunctions` in parallel. | -> docs/loader-pipeline.md |
30
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
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 |
32
+ | `initializeFunctions()` | Clears `devFunctions`, resolves the module list via `collectFunctionModules()`, imports each one, and builds a nested `Record<string, unknown>` mirror of the on-disk tree from its key path. Dev passes `onConflict` so a duplicate warns + keeps the first claim instead of throwing. | -> docs/loader-pipeline.md |
33
+ | `collectFunctionModules(options?)` | **Single source of truth for function discovery** (ADR 0046). Walks every configured `serverFunctionDirs` root (default `['functions', 'shared']`, legacy singular `serverFunctionsDir` still honored) and derives each module's `functions.*` key path from its location relative to its root — so `shared/rbac/engine.ts` is `functions.rbac.engine`. Skips `node_modules`, test files (`.tests.`/`.test.`/`.spec.` via core's `isTestFile`), `__tests__`/`__mocks__` folders, and paths the `RoutingRules.ignore` predicate rejects. Throws on a cross-root duplicate key or a module/namespace collision; pass `onConflict` to warn + skip instead (dev-loader mode). `roots` overrides the configured roots for tests. Consumed by the dev loader, `functionsMeta.ts`, and BOTH `scripts/generateServerRequests.ts` copies. | -> docs/loader-pipeline.md |
34
+ | `renderFunctionsMap({ modules, importPrefix, varPrefix? })` | Renders discovered modules into the `functions` export of a generated production runtime-map file: `{ imports, source }`, with a NESTED object literal matching what the dev loader builds and `apiTypes.generated.ts` declares. A default-only export is aliased to the file name (`functions.sleep.sleep`). | -> docs/loader-pipeline.md |
35
+ | `formatFunctionKeyPath(keyPath)` / `functionModuleFileName(keyPath)` / `formatFunctionKeyConflict(...)` / `formatFunctionNamespaceConflict(...)` | Key-path helpers + the two canonical collision diagnostics, shared so dev and build-time emit identical wording. | -> docs/loader-pipeline.md |
36
+ | Types: `FunctionModule`, `RenderedFunctionsMap`, `CollectFunctionModulesOptions` | Public typing for the function registry. | -> docs/loader-pipeline.md |
33
37
  | `upsertApiFromFile(filePath)` | Hot-reload single API: invalidates the TS Program cache + runtime type resolver cache, re-imports the file with a fresh cachebust, and replaces the matching entry in `devApis`. | -> docs/loader-pipeline.md |
34
38
  | `removeApiFromFile(filePath)` | Hot-reload delete: invalidates caches and deletes the matching entry from `devApis`. | -> docs/loader-pipeline.md |
35
39
  | `upsertSyncFromFile(filePath)` | Hot-reload single sync: handles both `_server_v<n>` and `_client_v<n>` kinds, invalidates caches, re-imports, replaces the matching entry in `devSyncs`. | -> docs/loader-pipeline.md |
@@ -76,7 +80,7 @@ Internal modules (not exported from `index.ts`, but live in this package):
76
80
  | `typeMap/extractors.ts` | TypeChecker-backed extractors for `data` / `serverOutput` / `clientOutput` / stream payload types. | -> docs/type-map-generation.md |
77
81
  | `typeMap/apiMeta.ts` | Extracts `httpMethod`, `rateLimit`, `auth` from API files. | -> docs/type-map-generation.md |
78
82
  | `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 |
83
+ | `typeMap/functionsMeta.ts` | Builds the `Functions` interface text emitted into `apiTypes.generated.ts`. Discovery comes from `collectFunctionModules()`; this module only parses each file's exports and places it at its key path. | -> docs/type-map-generation.md |
80
84
  | `typeMap/emitter.ts` + `emitterArtifacts.ts` | Renders generated files to disk via `@luckystack/core`-resolved paths. | -> docs/type-map-generation.md |
81
85
  | `typeMap/zodEmitter.ts` | Generates the runtime Zod schemas for API inputs. | -> docs/type-map-generation.md |
82
86
  | `templateInjector.ts` + `templates/*.ts` | Detects empty `_api/`, `_sync/`, AND `page.tsx` files and injects starter content. Sync server/client pairing is auto-resolved. Page files get the `dashboard` template when the path contains `admin\|dashboard\|settings\|billing\|account\|profile`, else the `plain` template. Pages in invalid placements (inside a reserved framework folder, or directly inside an `_<folder>` with no URL segment left) get a commented diagnostic block instead of a usable template so the placement issue is visible at creation time. | -> docs/hot-reload.md |
package/dist/index.d.ts CHANGED
@@ -20,6 +20,79 @@ declare const assertNoDuplicateNormalizedRouteKeys: ({ srcDir, context, }: {
20
20
  context: string;
21
21
  }) => void;
22
22
 
23
+ interface FunctionModule {
24
+ /**
25
+ * Key path under the injected `functions` object. `shared/rbac/engine.ts`
26
+ * with root `shared` yields `['rbac', 'engine']`, reachable as
27
+ * `functions.rbac.engine`.
28
+ */
29
+ keyPath: string[];
30
+ /** Workspace-relative, forward-slash path to the source file (with `.ts`). */
31
+ sourcePath: string;
32
+ /** Workspace-relative, forward-slash root directory that claimed this key path. */
33
+ rootPath: string;
34
+ }
35
+ /** Dotted rendering of a key path, e.g. `rbac.engine`. */
36
+ declare const formatFunctionKeyPath: (keyPath: string[]) => string;
37
+ /**
38
+ * Last segment of a key path — the module's file name, which is also the alias
39
+ * a default-only export is published under (`functions.sleep.sleep`).
40
+ *
41
+ * Discovery never produces an empty key path; the guard exists so the callers
42
+ * get a real string instead of threading `string | undefined` through every
43
+ * emitter.
44
+ */
45
+ declare const functionModuleFileName: (keyPath: string[]) => string;
46
+ declare const formatFunctionKeyConflict: (keyPath: string[], firstSourcePath: string, secondSourcePath: string) => string;
47
+ declare const formatFunctionNamespaceConflict: (moduleKeyPath: string[], moduleSourcePath: string, nestedKeyPath: string[], nestedSourcePath: string) => string;
48
+ interface CollectFunctionModulesOptions {
49
+ /**
50
+ * Override the configured roots (absolute paths). Tests pass this;
51
+ * production callers omit it and get `getServerFunctionDirs()`.
52
+ */
53
+ roots?: string[];
54
+ /**
55
+ * When provided, a collision REPORTS through this callback and the offending
56
+ * module is skipped instead of throwing. The dev loader passes it so a
57
+ * duplicate does not hard-crash a running dev server; the build-time
58
+ * generators omit it so the same duplicate fails the build loudly.
59
+ */
60
+ onConflict?: (message: string) => void;
61
+ }
62
+ /**
63
+ * Discover every server function module across the configured
64
+ * `paths.serverFunctionDirs` roots (default `['functions', 'shared']`), in
65
+ * config order, and derive its `functions.*` key path from its location
66
+ * relative to the root that owns it.
67
+ *
68
+ * Throws on a cross-root key collision or a module/namespace collision (unless
69
+ * `onConflict` is supplied), so a broken registry fails the BUILD instead of
70
+ * producing a map that is silently missing entries at runtime.
71
+ */
72
+ declare const collectFunctionModules: (options?: CollectFunctionModulesOptions) => FunctionModule[];
73
+ interface RenderedFunctionsMap {
74
+ /** `import * as fn0 from '...'` lines, in emission order. */
75
+ imports: string[];
76
+ /** The full `export const functions = { ... };` declaration. */
77
+ source: string;
78
+ }
79
+ /**
80
+ * Render discovered modules into the `functions` export of a generated
81
+ * production runtime-map file. The emitted object is NESTED, matching what the
82
+ * dev loader builds and what `apiTypes.generated.ts` declares.
83
+ *
84
+ * @param modules Output of {@link collectFunctionModules}.
85
+ * @param importPrefix Prefix that turns a workspace-relative source path into a
86
+ * specifier relative to the generated file (`'../../'` for
87
+ * the standard `server/prod/` output location).
88
+ * @param varPrefix Import alias prefix; aliases are `<varPrefix><n>`.
89
+ */
90
+ declare const renderFunctionsMap: ({ modules, importPrefix, varPrefix, }: {
91
+ modules: FunctionModule[];
92
+ importPrefix: string;
93
+ varPrefix?: string;
94
+ }) => RenderedFunctionsMap;
95
+
23
96
  interface RoutingRules {
24
97
  /** Folder name that marks an API directory. Default: `_api`. */
25
98
  apiMarker: string;
@@ -204,4 +277,4 @@ interface ValidateDeployResult {
204
277
  }
205
278
  declare const validateDeploy: ({ services, deploy, env, }: ValidateDeployInput) => ValidateDeployResult;
206
279
 
207
- export { API_VERSION_TOKEN_REGEX, BUILT_IN_TEMPLATE_FILENAMES, BUILT_IN_TEMPLATE_KINDS, type BuiltInTemplateKind, DEFAULT_DASHBOARD_PATH_PATTERN, type RegisterTemplateKindOptions, type RoutingRules, SYNC_VERSION_TOKEN_REGEX, type TemplateKind, type TemplateMatchContext, type TemplateRule, type ValidateDeployInput, type ValidateDeployResult, type ValidationFinding, type ValidationSeverity, apiMarkerSegment, assertNoDuplicateNormalizedRouteKeys, assertValidRouteNaming, clearRuntimeTypeResolverCache, clearTemplateOverrides, clearTemplateRules, devApis, devFunctions, devSyncs, generateTypeMapFile, getInputTypeFromFile, getRegisteredTemplate, getRoutingRules, getSyncClientDataType, getTemplateRules, initializeAll, initializeApis, initializeFunctions, initializeSyncs, isApiFileName, isSyncClientFileName, isSyncFileName, isSyncServerFileName, listRegisteredTemplateKinds, registerDefaultTemplateRules, registerRoutingRules, registerTemplate, registerTemplateKind, registerTemplateRule, removeApiFromFile, removeSyncFromFile, resolveRuntimeTypeText, resolveTemplateKind, setupWatchers, syncMarkerSegment, upsertApiFromFile, upsertSyncFromFile, validateDeploy };
280
+ export { API_VERSION_TOKEN_REGEX, BUILT_IN_TEMPLATE_FILENAMES, BUILT_IN_TEMPLATE_KINDS, type BuiltInTemplateKind, type CollectFunctionModulesOptions, DEFAULT_DASHBOARD_PATH_PATTERN, type FunctionModule, type RegisterTemplateKindOptions, type RenderedFunctionsMap, type RoutingRules, SYNC_VERSION_TOKEN_REGEX, type TemplateKind, type TemplateMatchContext, type TemplateRule, type ValidateDeployInput, type ValidateDeployResult, type ValidationFinding, type ValidationSeverity, apiMarkerSegment, assertNoDuplicateNormalizedRouteKeys, assertValidRouteNaming, clearRuntimeTypeResolverCache, clearTemplateOverrides, clearTemplateRules, collectFunctionModules, devApis, devFunctions, devSyncs, formatFunctionKeyConflict, formatFunctionKeyPath, formatFunctionNamespaceConflict, functionModuleFileName, generateTypeMapFile, getInputTypeFromFile, getRegisteredTemplate, getRoutingRules, getSyncClientDataType, getTemplateRules, initializeAll, initializeApis, initializeFunctions, initializeSyncs, isApiFileName, isSyncClientFileName, isSyncFileName, isSyncServerFileName, listRegisteredTemplateKinds, registerDefaultTemplateRules, registerRoutingRules, registerTemplate, registerTemplateKind, registerTemplateRule, removeApiFromFile, removeSyncFromFile, renderFunctionsMap, resolveRuntimeTypeText, resolveTemplateKind, setupWatchers, syncMarkerSegment, upsertApiFromFile, upsertSyncFromFile, validateDeploy };