@luckystack/devkit 0.2.7 → 0.3.1

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 CHANGED
@@ -55,7 +55,7 @@ Dev-time tooling for LuckyStack projects: file-based route discovery + in-memory
55
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
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
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 |
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
59
  | Type: `TemplateKind`, `DuplicatePageRouteIssue` | Public types for the new registry + duplicate-detector. | -> docs/hot-reload.md |
60
60
  | `apiMarkerSegment` / `syncMarkerSegment` | Resolved marker strings (default `_api` / `_sync`). | -> docs/loader-pipeline.md |
61
61
  | `isApiFileName(name)` / `isSyncFileName(name)` / `isSyncServerFileName(name)` / `isSyncClientFileName(name)` | Filename predicates that respect `getRoutingRules()`. | -> docs/loader-pipeline.md |
@@ -114,7 +114,7 @@ Every `generateTypeMapFile()` run emits `apiTypeDiagnostics.generated.json` alon
114
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
115
  - **Required peer**: `zod@^4.0.0` — the Zod schema emitter compiles consumer input types into runtime schemas via `zodEmitter.ts`.
116
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`).
117
+ - **Direct dependency**: `chokidar@^5.0.0` (the file watcher used by both `hotReload.ts` and `supervisor.ts`).
118
118
  - **Direct dependency**: `@luckystack/core` (project config, root dir, generated artifact paths, `tryCatch`, locale reloader hook).
119
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
120
 
@@ -144,7 +144,7 @@ var validateDeploy = ({
144
144
  }
145
145
  }
146
146
  for (const service of serviceNames) {
147
- const boundIn = Object.entries(environments).filter(([, env2]) => service in env2.bindings);
147
+ const boundIn = Object.entries(environments).filter(([, envDef]) => service in envDef.bindings);
148
148
  if (boundIn.length === 0) {
149
149
  findings.push({
150
150
  severity: "warning",
@@ -167,4 +167,4 @@ var validateDeploy = ({
167
167
  export {
168
168
  validateDeploy
169
169
  };
170
- //# sourceMappingURL=chunk-YYWCJ7VZ.js.map
170
+ //# sourceMappingURL=chunk-BZTCJ7JY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/validateDeploy.ts"],"sourcesContent":["//? Pure validator for `services.config.ts` + `deploy.config.ts` content.\n//? Catches the classes of misconfiguration that otherwise only surface at\n//? runtime, often silently (empty service map served, fallback target\n//? unreachable, env var typo).\n//?\n//? Pure module so it can be unit-tested and reused outside the CLI — the\n//? CLI in `cli/validateDeploy.ts` is a thin wrapper that loads the config\n//? files, calls `validateDeploy(...)`, and prints results.\n\nimport type {\n DeployConfigShape,\n ServicesConfigShape,\n} from '@luckystack/core';\n\nexport type ValidationSeverity = 'error' | 'warning';\n\nexport interface ValidationFinding {\n severity: ValidationSeverity;\n code: string;\n message: string;\n /** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */\n location?: string;\n}\n\nexport interface ValidateDeployInput {\n services: ServicesConfigShape;\n deploy: DeployConfigShape;\n /**\n * Environment values to consult for `synchronizedEnvKeys` / `urlEnvKey`\n * presence checks. Defaults to `process.env`. Pass a fixture for tests.\n */\n env?: Record<string, string | undefined>;\n}\n\nexport interface ValidateDeployResult {\n ok: boolean;\n findings: ValidationFinding[];\n errorCount: number;\n warningCount: number;\n}\n\nexport const validateDeploy = ({\n services,\n deploy,\n env = process.env,\n}: ValidateDeployInput): ValidateDeployResult => {\n const findings: ValidationFinding[] = [];\n const serviceNames = new Set(Object.keys(services.services));\n\n //? Rule 1: every service is assigned to exactly one preset.\n const servicesInPresets = new Map<string, string[]>();\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n const existing = servicesInPresets.get(service) ?? [];\n existing.push(presetKey);\n servicesInPresets.set(service, existing);\n }\n }\n\n for (const service of serviceNames) {\n const owners = servicesInPresets.get(service) ?? [];\n if (owners.length === 0) {\n findings.push({\n severity: 'error',\n code: 'service-unassigned',\n message: `Service \"${service}\" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n } else if (owners.length > 1) {\n findings.push({\n severity: 'error',\n code: 'service-in-multiple-presets',\n message: `Service \"${service}\" is in multiple presets: ${owners.join(', ')}. A service may belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n //? Rule 2: every preset references services that exist.\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n if (!serviceNames.has(service)) {\n findings.push({\n severity: 'error',\n code: 'preset-references-unknown-service',\n message: `Preset \"${presetKey}\" references unknown service \"${service}\". Add it to services.services or remove the reference.`,\n location: `services.config.ts > presets.${presetKey}.services`,\n });\n }\n }\n }\n\n //? Rule 3: every binding's service matches an actual service.\n //? Rule 4: fallback env key must exist.\n //? Rule 5: redis/mongo resource keys must exist.\n //? Rule 6: fallback shared-resource invariant.\n const environments = deploy.environments ?? {};\n const resourceNames = new Set(Object.keys(deploy.resources));\n\n for (const [envKey, envDef] of Object.entries(environments)) {\n for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {\n if (!serviceNames.has(bindingService)) {\n findings.push({\n severity: 'error',\n code: 'binding-references-unknown-service',\n message: `Environment \"${envKey}\" binds unknown service \"${bindingService}\". Either add it to services.services or remove the binding.`,\n location: `deploy.config.ts > environments.${envKey}.bindings`,\n });\n }\n\n //? Every binding MUST declare an explicit port. The URL-spec default\n //? (80/443) is almost never what a multi-instance deploy wants — a\n //? missing port is far more likely a typo than a deliberate \"route to\n //? port 80\". The router enforces this at boot too (resolveTarget.ts).\n let parsed: URL | null = null;\n try {\n parsed = new URL(bindingUrl);\n } catch {\n findings.push({\n severity: 'error',\n code: 'binding-invalid-url',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding is not a valid URL: \"${bindingUrl}\".`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n if (parsed && !parsed.port) {\n findings.push({\n severity: 'error',\n code: 'binding-missing-port',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding \"${bindingUrl}\" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n }\n\n if (!resourceNames.has(envDef.redis)) {\n findings.push({\n severity: 'error',\n code: 'unknown-redis-resource',\n message: `Environment \"${envKey}\" references unknown redis resource \"${envDef.redis}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.redis`,\n });\n }\n if (!resourceNames.has(envDef.mongo)) {\n findings.push({\n severity: 'error',\n code: 'unknown-mongo-resource',\n message: `Environment \"${envKey}\" references unknown mongo resource \"${envDef.mongo}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.mongo`,\n });\n }\n\n if (envDef.fallback) {\n const fallbackEnv = environments[envDef.fallback];\n //? `fallbackEnv` reads from `environments[key]` — TS treats this as\n //? always-defined (Record lookup), but the runtime can return undefined\n //? when the user names a fallback env that doesn't exist. The else\n //? branch reports that error.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Record lookup can return undefined at runtime\n if (fallbackEnv) {\n if (fallbackEnv.redis !== envDef.redis) {\n findings.push({\n severity: 'error',\n code: 'fallback-redis-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different redis resources (\"${envDef.redis}\" vs \"${fallbackEnv.redis}\"). Fallback envs must share the same redis resource key to keep the shared-Redis invariant.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n if (fallbackEnv.mongo !== envDef.mongo) {\n findings.push({\n severity: 'error',\n code: 'fallback-mongo-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different mongo resources (\"${envDef.mongo}\" vs \"${fallbackEnv.mongo}\"). Fallback envs must share the same mongo resource key.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n } else {\n findings.push({\n severity: 'error',\n code: 'unknown-fallback-env',\n message: `Environment \"${envKey}\" has fallback \"${envDef.fallback}\" which does not exist in deploy.environments.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n }\n }\n\n //? Rule 7+8: env var references must resolve at config time.\n for (const [resourceKey, resource] of Object.entries(deploy.resources)) {\n const urlValue = env[resource.urlEnvKey];\n if (urlValue === undefined || urlValue === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-resource-env-var',\n message: `Resource \"${resourceKey}\" expects env var \"${resource.urlEnvKey}\" but it is unset. The framework will still boot if the env is provided at runtime, but config-time tooling cannot verify the value.`,\n location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`,\n });\n }\n\n for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {\n const value = env[synchronizedKey];\n if (value === undefined || value === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-synchronized-env-var',\n message: `Resource \"${resourceKey}\" requires synchronized env var \"${synchronizedKey}\" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,\n location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`,\n });\n }\n }\n }\n\n //? Rule 9: services declared in services.config but not bound in any\n //? environment will never receive traffic. Warning, not error — that's a\n //? valid intermediate state during a rollout.\n for (const service of serviceNames) {\n const boundIn = Object.entries(environments).filter(([, env]) => service in env.bindings);\n if (boundIn.length === 0) {\n findings.push({\n severity: 'warning',\n code: 'service-bound-in-no-environment',\n message: `Service \"${service}\" is in services.services and assigned to a preset but never bound in any environment. Requests for this service will fall through to the missing-service handler.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const warningCount = findings.filter((f) => f.severity === 'warning').length;\n\n return {\n ok: errorCount === 0,\n findings,\n errorCount,\n warningCount,\n };\n};\n"],"mappings":"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","names":["env"]}
1
+ {"version":3,"sources":["../src/validateDeploy.ts"],"sourcesContent":["//? Pure validator for `services.config.ts` + `deploy.config.ts` content.\n//? Catches the classes of misconfiguration that otherwise only surface at\n//? runtime, often silently (empty service map served, fallback target\n//? unreachable, env var typo).\n//?\n//? Pure module so it can be unit-tested and reused outside the CLI — the\n//? CLI in `cli/validateDeploy.ts` is a thin wrapper that loads the config\n//? files, calls `validateDeploy(...)`, and prints results.\n\nimport type {\n DeployConfigShape,\n ServicesConfigShape,\n} from '@luckystack/core';\n\nexport type ValidationSeverity = 'error' | 'warning';\n\nexport interface ValidationFinding {\n severity: ValidationSeverity;\n code: string;\n message: string;\n /** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */\n location?: string;\n}\n\nexport interface ValidateDeployInput {\n services: ServicesConfigShape;\n deploy: DeployConfigShape;\n /**\n * Environment values to consult for `synchronizedEnvKeys` / `urlEnvKey`\n * presence checks. Defaults to `process.env`. Pass a fixture for tests.\n */\n env?: Record<string, string | undefined>;\n}\n\nexport interface ValidateDeployResult {\n ok: boolean;\n findings: ValidationFinding[];\n errorCount: number;\n warningCount: number;\n}\n\nexport const validateDeploy = ({\n services,\n deploy,\n env = process.env,\n}: ValidateDeployInput): ValidateDeployResult => {\n const findings: ValidationFinding[] = [];\n const serviceNames = new Set(Object.keys(services.services));\n\n //? Rule 1: every service is assigned to exactly one preset.\n const servicesInPresets = new Map<string, string[]>();\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n const existing = servicesInPresets.get(service) ?? [];\n existing.push(presetKey);\n servicesInPresets.set(service, existing);\n }\n }\n\n for (const service of serviceNames) {\n const owners = servicesInPresets.get(service) ?? [];\n if (owners.length === 0) {\n findings.push({\n severity: 'error',\n code: 'service-unassigned',\n message: `Service \"${service}\" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n } else if (owners.length > 1) {\n findings.push({\n severity: 'error',\n code: 'service-in-multiple-presets',\n message: `Service \"${service}\" is in multiple presets: ${owners.join(', ')}. A service may belong to exactly one preset.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n //? Rule 2: every preset references services that exist.\n for (const [presetKey, preset] of Object.entries(services.presets)) {\n for (const service of preset.services) {\n if (!serviceNames.has(service)) {\n findings.push({\n severity: 'error',\n code: 'preset-references-unknown-service',\n message: `Preset \"${presetKey}\" references unknown service \"${service}\". Add it to services.services or remove the reference.`,\n location: `services.config.ts > presets.${presetKey}.services`,\n });\n }\n }\n }\n\n //? Rule 3: every binding's service matches an actual service.\n //? Rule 4: fallback env key must exist.\n //? Rule 5: redis/mongo resource keys must exist.\n //? Rule 6: fallback shared-resource invariant.\n const environments = deploy.environments ?? {};\n const resourceNames = new Set(Object.keys(deploy.resources));\n\n for (const [envKey, envDef] of Object.entries(environments)) {\n for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {\n if (!serviceNames.has(bindingService)) {\n findings.push({\n severity: 'error',\n code: 'binding-references-unknown-service',\n message: `Environment \"${envKey}\" binds unknown service \"${bindingService}\". Either add it to services.services or remove the binding.`,\n location: `deploy.config.ts > environments.${envKey}.bindings`,\n });\n }\n\n //? Every binding MUST declare an explicit port. The URL-spec default\n //? (80/443) is almost never what a multi-instance deploy wants — a\n //? missing port is far more likely a typo than a deliberate \"route to\n //? port 80\". The router enforces this at boot too (resolveTarget.ts).\n let parsed: URL | null = null;\n try {\n parsed = new URL(bindingUrl);\n } catch {\n findings.push({\n severity: 'error',\n code: 'binding-invalid-url',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding is not a valid URL: \"${bindingUrl}\".`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n if (parsed && !parsed.port) {\n findings.push({\n severity: 'error',\n code: 'binding-missing-port',\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding \"${bindingUrl}\" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\n });\n }\n }\n\n if (!resourceNames.has(envDef.redis)) {\n findings.push({\n severity: 'error',\n code: 'unknown-redis-resource',\n message: `Environment \"${envKey}\" references unknown redis resource \"${envDef.redis}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.redis`,\n });\n }\n if (!resourceNames.has(envDef.mongo)) {\n findings.push({\n severity: 'error',\n code: 'unknown-mongo-resource',\n message: `Environment \"${envKey}\" references unknown mongo resource \"${envDef.mongo}\". Add it to deploy.resources.`,\n location: `deploy.config.ts > environments.${envKey}.mongo`,\n });\n }\n\n if (envDef.fallback) {\n const fallbackEnv = environments[envDef.fallback];\n //? `fallbackEnv` reads from `environments[key]` — TS treats this as\n //? always-defined (Record lookup), but the runtime can return undefined\n //? when the user names a fallback env that doesn't exist. The else\n //? branch reports that error.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Record lookup can return undefined at runtime\n if (fallbackEnv) {\n if (fallbackEnv.redis !== envDef.redis) {\n findings.push({\n severity: 'error',\n code: 'fallback-redis-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different redis resources (\"${envDef.redis}\" vs \"${fallbackEnv.redis}\"). Fallback envs must share the same redis resource key to keep the shared-Redis invariant.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n if (fallbackEnv.mongo !== envDef.mongo) {\n findings.push({\n severity: 'error',\n code: 'fallback-mongo-mismatch',\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different mongo resources (\"${envDef.mongo}\" vs \"${fallbackEnv.mongo}\"). Fallback envs must share the same mongo resource key.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n } else {\n findings.push({\n severity: 'error',\n code: 'unknown-fallback-env',\n message: `Environment \"${envKey}\" has fallback \"${envDef.fallback}\" which does not exist in deploy.environments.`,\n location: `deploy.config.ts > environments.${envKey}.fallback`,\n });\n }\n }\n }\n\n //? Rule 7+8: env var references must resolve at config time.\n for (const [resourceKey, resource] of Object.entries(deploy.resources)) {\n const urlValue = env[resource.urlEnvKey];\n if (urlValue === undefined || urlValue === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-resource-env-var',\n message: `Resource \"${resourceKey}\" expects env var \"${resource.urlEnvKey}\" but it is unset. The framework will still boot if the env is provided at runtime, but config-time tooling cannot verify the value.`,\n location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`,\n });\n }\n\n for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {\n const value = env[synchronizedKey];\n if (value === undefined || value === '') {\n findings.push({\n severity: 'warning',\n code: 'missing-synchronized-env-var',\n message: `Resource \"${resourceKey}\" requires synchronized env var \"${synchronizedKey}\" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,\n location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`,\n });\n }\n }\n }\n\n //? Rule 9: services declared in services.config but not bound in any\n //? environment will never receive traffic. Warning, not error — that's a\n //? valid intermediate state during a rollout.\n for (const service of serviceNames) {\n const boundIn = Object.entries(environments).filter(([, envDef]) => service in envDef.bindings);\n if (boundIn.length === 0) {\n findings.push({\n severity: 'warning',\n code: 'service-bound-in-no-environment',\n message: `Service \"${service}\" is in services.services and assigned to a preset but never bound in any environment. Requests for this service will fall through to the missing-service handler.`,\n location: `services.config.ts > services.${service}`,\n });\n }\n }\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const warningCount = findings.filter((f) => f.severity === 'warning').length;\n\n return {\n ok: errorCount === 0,\n findings,\n errorCount,\n warningCount,\n };\n};\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  validateDeploy
4
- } from "../chunk-YYWCJ7VZ.js";
4
+ } from "../chunk-BZTCJ7JY.js";
5
5
 
6
6
  // src/cli/validateDeploy.ts
7
7
  import { pathToFileURL } from "url";
@@ -97,7 +97,7 @@ var importConfig = async (file, label) => {
97
97
  );
98
98
  }
99
99
  try {
100
- await import(pathToFileURL(abs).href);
100
+ await import(pathToFileURL(resolved).href);
101
101
  } catch (error) {
102
102
  const message = error instanceof Error ? error.message : String(error);
103
103
  throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);
@@ -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(abs).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\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":[]}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  validateDeploy
3
- } from "./chunk-YYWCJ7VZ.js";
3
+ } from "./chunk-BZTCJ7JY.js";
4
4
 
5
5
  // src/typeMap/discovery.ts
6
6
  import fs from "fs";
@@ -66,6 +66,14 @@ var isSyncFileName = (fileName) => {
66
66
  var isRouteTestFile = (fileNameOrPath) => {
67
67
  return fileNameOrPath.endsWith(".tests.ts");
68
68
  };
69
+ var validatePagePath = (srcRelativePath) => {
70
+ const rules2 = getRoutingRules();
71
+ const pageRules = {
72
+ privateFolderPrefix: rules2.privateFolderPrefix,
73
+ scaffoldIgnoredFolders: rules2.scaffoldIgnoredFolders
74
+ };
75
+ return corePagePath(srcRelativePath, pageRules);
76
+ };
69
77
 
70
78
  // src/typeMap/discovery.ts
71
79
  var toForwardSlashRelative = (absolute) => {
@@ -153,6 +161,7 @@ var isVersionedSyncClientFileName = (fileName) => isSyncClientFileName(fileName)
153
161
 
154
162
  // src/typeMap/routeMeta.ts
155
163
  var VERSION_SUFFIX_REGEX = API_VERSION_TOKEN_REGEX;
164
+ var escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
156
165
  var stripVersionSuffix = (name) => {
157
166
  return name.replace(VERSION_SUFFIX_REGEX, "");
158
167
  };
@@ -168,24 +177,27 @@ var extractPagePath = (filePath) => {
168
177
  if (rel.startsWith("..")) {
169
178
  throw new Error(`[routeMeta] file is outside srcDir \u2014 cannot extract page path: ${filePath}`);
170
179
  }
171
- const match = /^(?:(.+?)\/)_api\//.exec(rel);
180
+ const { apiMarker } = getRoutingRules();
181
+ const match = new RegExp(String.raw`^(?:(.+?)\/)${escapeRegExp(apiMarker)}\/`).exec(rel);
172
182
  if (match) {
173
183
  return match[1] ?? "system";
174
184
  }
175
- if (rel.startsWith("_api/")) {
185
+ if (rel.startsWith(`${apiMarker}/`)) {
176
186
  return "system";
177
187
  }
178
188
  return "";
179
189
  };
180
190
  var extractApiName = (filePath) => {
181
191
  const normalized = filePath.replaceAll("\\", "/");
182
- const match = /_api\/(.+)\.ts$/.exec(normalized);
192
+ const { apiMarker } = getRoutingRules();
193
+ const match = new RegExp(String.raw`${escapeRegExp(apiMarker)}\/(.+)\.ts$`).exec(normalized);
183
194
  const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
184
195
  return stripVersionSuffix(rawName);
185
196
  };
186
197
  var extractApiVersion = (filePath) => {
187
198
  const normalized = filePath.replaceAll("\\", "/");
188
- const match = /_api\/(.+)\.ts$/.exec(normalized);
199
+ const { apiMarker } = getRoutingRules();
200
+ const match = new RegExp(String.raw`${escapeRegExp(apiMarker)}\/(.+)\.ts$`).exec(normalized);
189
201
  const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
190
202
  return extractVersionFromName(rawName) ?? "v1";
191
203
  };
@@ -193,18 +205,20 @@ var extractSyncPagePath = (filePath) => {
193
205
  const normalized = filePath.replaceAll("\\", "/");
194
206
  const srcDirNormalized = getSrcDir().replaceAll("\\", "/");
195
207
  const rel = path2.posix.relative(srcDirNormalized, normalized);
196
- const match = /^(?:(.+?)\/)_sync\//.exec(rel);
208
+ const { syncMarker } = getRoutingRules();
209
+ const match = new RegExp(String.raw`^(?:(.+?)\/)${escapeRegExp(syncMarker)}\/`).exec(rel);
197
210
  if (match) {
198
211
  return match[1] ?? "system";
199
212
  }
200
- if (rel.startsWith("_sync/")) {
213
+ if (rel.startsWith(`${syncMarker}/`)) {
201
214
  return "system";
202
215
  }
203
216
  return "";
204
217
  };
205
218
  var extractSyncName = (filePath) => {
206
219
  const normalized = filePath.replaceAll("\\", "/");
207
- const match = /_sync\/(.+)\.ts$/.exec(normalized);
220
+ const { syncMarker } = getRoutingRules();
221
+ const match = new RegExp(String.raw`${escapeRegExp(syncMarker)}\/(.+)\.ts$`).exec(normalized);
208
222
  if (!match) {
209
223
  const basename = path2.basename(filePath, ".ts");
210
224
  return basename.replace(SYNC_VERSION_TOKEN_REGEX, "");
@@ -213,7 +227,8 @@ var extractSyncName = (filePath) => {
213
227
  };
214
228
  var extractSyncVersion = (filePath) => {
215
229
  const normalized = filePath.replaceAll("\\", "/");
216
- const match = /_sync\/(.+)\.ts$/.exec(normalized);
230
+ const { syncMarker } = getRoutingRules();
231
+ const match = new RegExp(String.raw`${escapeRegExp(syncMarker)}\/(.+)\.ts$`).exec(normalized);
217
232
  if (!match) {
218
233
  const basename = path2.basename(filePath, ".ts");
219
234
  const versionMatch2 = SYNC_VERSION_TOKEN_REGEX.exec(basename);
@@ -351,9 +366,10 @@ var extractAuth = (filePath) => {
351
366
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
352
367
  const decl = findExportedConst(sourceFile, "auth");
353
368
  const authInitializer = decl?.initializer ? unwrapExpression(decl.initializer) : void 0;
354
- if (!authInitializer || !ts.isObjectLiteralExpression(authInitializer)) return { login: false };
369
+ if (!authInitializer || !ts.isObjectLiteralExpression(authInitializer)) return { login: false, hasAdditional: false };
355
370
  let login = false;
356
371
  let additional;
372
+ let hasAdditional = false;
357
373
  for (const prop of authInitializer.properties) {
358
374
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
359
375
  const propInit = unwrapExpression(prop.initializer);
@@ -361,6 +377,7 @@ var extractAuth = (filePath) => {
361
377
  login = propInit.kind === ts.SyntaxKind.TrueKeyword;
362
378
  }
363
379
  if (prop.name.text === "additional" && ts.isArrayLiteralExpression(propInit)) {
380
+ hasAdditional = propInit.elements.length > 0;
364
381
  additional = [];
365
382
  for (const element of propInit.elements) {
366
383
  if (!ts.isObjectLiteralExpression(element)) continue;
@@ -369,9 +386,9 @@ var extractAuth = (filePath) => {
369
386
  }
370
387
  }
371
388
  }
372
- return additional && additional.length > 0 ? { login, additional } : { login };
389
+ return additional && additional.length > 0 ? { login, additional, hasAdditional } : { login, hasAdditional };
373
390
  });
374
- return auth ?? { login: false };
391
+ return auth ?? { login: false, hasAdditional: false };
375
392
  };
376
393
 
377
394
  // src/typeMap/emitterArtifacts.ts
@@ -699,9 +716,9 @@ export type StreamPayload = {
699
716
  export type ApiStreamEmitter<T extends StreamPayload = StreamPayload> = (payload?: T) => void | Promise<void>;
700
717
  export type SyncServerStreamEmitter<T extends StreamPayload = StreamPayload> = (payload?: T) => void | Promise<void>;
701
718
  export type SyncClientStreamEmitter<T extends StreamPayload = StreamPayload> = (payload?: T) => void | Promise<void>;
702
- //? Broadcast \xE2\u20AC\u201D fan-out to every socket in the receiver room (cross-instance via the Redis adapter).
719
+ //? Broadcast \u2014 fan-out to every socket in the receiver room (cross-instance via the Redis adapter).
703
720
  export type SyncBroadcastStreamEmitter<T extends StreamPayload = StreamPayload> = (payload?: T) => void;
704
- //? Targeted \xE2\u20AC\u201D emit only to the listed session tokens (each token is its own room).
721
+ //? Targeted \u2014 emit only to the listed session tokens (each token is its own room).
705
722
  export type SyncStreamToEmitter<T extends StreamPayload = StreamPayload> = (
706
723
  tokens: string | string[],
707
724
  payload?: T,
@@ -753,7 +770,11 @@ type _ProjectApiTypeMap = {
753
770
  stream: entry.stream,
754
771
  rateLimit: entry.rateLimit,
755
772
  auth: entry.auth,
756
- path: pagePath === "root" ? `api/${apiName}/${version}` : `api/${pagePath}/${apiName}/${version}`,
773
+ //? extractPagePath never yields 'root' for the src-root (it yields 'system'),
774
+ //? and a page folder literally named `root` must route as `api/root/...` — so
775
+ //? the old `pagePath === 'root'` special-case emitted a non-matching docs path.
776
+ //? Always use the full path (mirrors the already-fixed sync side).
777
+ path: `api/${pagePath}/${apiName}/${version}`,
757
778
  ...entry.meta ? { meta: entry.meta } : {}
758
779
  });
759
780
  content += ` '${version}': {
@@ -824,7 +845,7 @@ export const getApiMethod = (pagePath: string, apiName: string, version: string)
824
845
 
825
846
  export interface ApiMetaEntry {
826
847
  method: HttpMethod;
827
- auth: { login: boolean; additional?: Record<string, unknown>[] };
848
+ auth: { login: boolean; additional?: Record<string, unknown>[]; hasAdditional?: boolean };
828
849
  rateLimit?: number | false;
829
850
  }
830
851
 
@@ -844,10 +865,10 @@ export const apiMetaMap: Record<string, Record<string, Record<string, ApiMetaEnt
844
865
  content += ` '${apiName}': {
845
866
  `;
846
867
  for (const { version, entry } of mustGet(grouped, apiName, "grouped").toSorted((a, b) => a.version.localeCompare(b.version, void 0, { numeric: true }))) {
847
- const auth = entry.auth && typeof entry.auth === "object" ? entry.auth : { login: true };
868
+ const auth = entry.auth && typeof entry.auth === "object" ? entry.auth : { login: false, hasAdditional: false };
848
869
  const rateLimitPart = entry.rateLimit === void 0 ? "" : `, rateLimit: ${entry.rateLimit === false ? "false" : String(entry.rateLimit)}`;
849
870
  const additionalPart = auth.additional && auth.additional.length > 0 ? `, additional: ${JSON.stringify(auth.additional)}` : "";
850
- content += ` '${version}': { method: '${entry.method}', auth: { login: ${auth.login ? "true" : "false"}${additionalPart} }${rateLimitPart} },
871
+ content += ` '${version}': { method: '${entry.method}', auth: { login: ${auth.login ? "true" : "false"}${additionalPart}${auth.hasAdditional ? ", hasAdditional: true" : ""} }${rateLimitPart} },
851
872
  `;
852
873
  }
853
874
  content += ` },
@@ -948,7 +969,7 @@ export type SyncClientStream<P extends SyncPagePath, N extends SyncName<P>, V ex
948
969
  export type FullSyncPath<P extends SyncPagePath, N extends SyncName<P>, V extends SyncVersion<P, N>> = \`sync/\${P}/\${N & string}/\${V & string}\`;
949
970
 
950
971
  //
951
- // Type-level augmentation \xE2\u20AC\u201D merges the project's concrete ApiTypeMap / SyncTypeMap
972
+ // Type-level augmentation \u2014 merges the project's concrete ApiTypeMap / SyncTypeMap
952
973
  // into the @luckystack/core/typemap stub-declaration module so framework code
953
974
  // (apiRequest / syncRequest) sees the project routes without deep-relative
954
975
  // imports. Augmenting the module that DECLARES the stubs (not the re-exporting
@@ -1261,7 +1282,7 @@ var expandTypeDetailed = (type, checker, depth = 0, state) => {
1261
1282
  }
1262
1283
  if (objectType.objectFlags & ts4.ObjectFlags.Reference) {
1263
1284
  const refType = objectType;
1264
- const targetName = refType.target.symbol.name;
1285
+ const targetName = refType.target.symbol?.name ?? "";
1265
1286
  if (targetName === "Array" || targetName === "ReadonlyArray") {
1266
1287
  const typeArgs = checker.getTypeArguments(refType);
1267
1288
  const firstArg = typeArgs[0];
@@ -2133,7 +2154,7 @@ import { getSrcDir as getSrcDir2 } from "@luckystack/core";
2133
2154
  // src/routeNamingValidation.ts
2134
2155
  import fs5 from "fs";
2135
2156
  import path8 from "path";
2136
- import { ROOT_DIR as ROOT_DIR3, validatePagePath } from "@luckystack/core";
2157
+ import { ROOT_DIR as ROOT_DIR3 } from "@luckystack/core";
2137
2158
  var normalizePath = (value) => {
2138
2159
  return value.replaceAll("\\", "/");
2139
2160
  };
@@ -2656,7 +2677,7 @@ registerDefaultTemplateRules();
2656
2677
  import fs6 from "fs";
2657
2678
  import path9 from "path";
2658
2679
  import { pathToFileURL } from "url";
2659
- import { tryCatch, getServerFunctionDirs as getServerFunctionDirs2, getSrcDir as getSrcDir3 } from "@luckystack/core";
2680
+ import { tryCatch, getServerFunctionDirs as getServerFunctionDirs2, getSrcDir as getSrcDir3, ROOT_DIR as ROOT_DIR4 } from "@luckystack/core";
2660
2681
 
2661
2682
  // src/runtimeTypeResolver.ts
2662
2683
  import * as ts8 from "typescript";
@@ -3035,6 +3056,10 @@ var devApis = {};
3035
3056
  var devSyncs = {};
3036
3057
  var devFunctions = {};
3037
3058
  var normalizePath2 = (value) => value.replaceAll("\\", "/");
3059
+ var isIgnoredPath = (absolutePath) => {
3060
+ const relativeFromRoot = normalizePath2(path9.relative(ROOT_DIR4, absolutePath));
3061
+ return getRoutingRules().ignore(relativeFromRoot);
3062
+ };
3038
3063
  var mapApiPageLocation = (pageLocation) => {
3039
3064
  return pageLocation || "system";
3040
3065
  };
@@ -3124,6 +3149,7 @@ var collectTsFiles = (dir, relativeTo = "") => {
3124
3149
  for (const entry of entries) {
3125
3150
  if (entry === "node_modules") continue;
3126
3151
  const entryPath = path9.join(dir, entry);
3152
+ if (isIgnoredPath(entryPath)) continue;
3127
3153
  const relPath = relativeTo ? `${relativeTo}/${entry}` : entry;
3128
3154
  if (fs6.statSync(entryPath).isDirectory()) {
3129
3155
  results.push(...collectTsFiles(entryPath, relPath));
@@ -3162,6 +3188,7 @@ var initializeApis = async () => {
3162
3188
  }
3163
3189
  };
3164
3190
  var upsertApiFromFile = async (filePath) => {
3191
+ if (isIgnoredPath(path9.resolve(filePath))) return;
3165
3192
  const routeMeta = resolveApiRouteMetaFromPath(filePath);
3166
3193
  if (!routeMeta) {
3167
3194
  const normalized = normalizePath2(path9.resolve(filePath));
@@ -3213,6 +3240,7 @@ var removeApiFromFile = (filePath) => {
3213
3240
  };
3214
3241
  var scanApiFolder = async (file, basePath = "") => {
3215
3242
  const fullPath = path9.join(getSrcDir3(), basePath, file);
3243
+ if (isIgnoredPath(fullPath)) return;
3216
3244
  if (!fs6.statSync(fullPath).isDirectory()) return;
3217
3245
  if (!file.toLowerCase().endsWith("api")) {
3218
3246
  const subFolders = fs6.readdirSync(fullPath);
@@ -3273,6 +3301,7 @@ var initializeSyncs = async () => {
3273
3301
  }
3274
3302
  };
3275
3303
  var upsertSyncFromFile = async (filePath) => {
3304
+ if (isIgnoredPath(path9.resolve(filePath))) return;
3276
3305
  const routeMeta = resolveSyncRouteMetaFromPath(filePath);
3277
3306
  if (!routeMeta) {
3278
3307
  const normalized = normalizePath2(path9.resolve(filePath));
@@ -3329,6 +3358,7 @@ var removeSyncFromFile = (filePath) => {
3329
3358
  };
3330
3359
  var scanSyncFolder = async (file, basePath = "") => {
3331
3360
  const fullPath = path9.join(getSrcDir3(), basePath, file);
3361
+ if (isIgnoredPath(fullPath)) return;
3332
3362
  if (!fs6.statSync(fullPath).isDirectory()) return;
3333
3363
  if (!file.toLowerCase().endsWith("sync")) {
3334
3364
  const subFolders = fs6.readdirSync(fullPath);
@@ -3394,6 +3424,7 @@ var scanFunctionsFolder = async (dir, rootDir, basePath = []) => {
3394
3424
  const entries = fs6.readdirSync(dir);
3395
3425
  for (const entry of entries) {
3396
3426
  const fullPath = path9.join(dir, entry);
3427
+ if (isIgnoredPath(fullPath)) continue;
3397
3428
  const stat = fs6.statSync(fullPath);
3398
3429
  if (stat.isDirectory()) {
3399
3430
  await scanFunctionsFolder(fullPath, rootDir, [...basePath, entry]);
@@ -3446,11 +3477,11 @@ import fs7 from "fs";
3446
3477
  import path10 from "path";
3447
3478
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
3448
3479
  import * as ts9 from "typescript";
3449
- import { ROOT_DIR as ROOT_DIR4, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath6, getSrcDir as getSrcDir4, validatePagePath as validatePagePath2 } from "@luckystack/core";
3480
+ import { ROOT_DIR as ROOT_DIR5, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath6, getSrcDir as getSrcDir4 } from "@luckystack/core";
3450
3481
  var __filename = fileURLToPath(import.meta.url);
3451
3482
  var __dirname = path10.dirname(__filename);
3452
3483
  var templatesDir = path10.join(__dirname, "templates");
3453
- var getConsumerTemplatesDir = () => path10.join(ROOT_DIR4, ".luckystack", "templates");
3484
+ var getConsumerTemplatesDir = () => path10.join(ROOT_DIR5, ".luckystack", "templates");
3454
3485
  var templateContentFilenames = (kind) => {
3455
3486
  const builtIn = BUILT_IN_TEMPLATE_FILENAMES[kind];
3456
3487
  if (builtIn) return [builtIn];
@@ -3491,7 +3522,7 @@ var ensureConsumerTemplateConfigLoaded = () => {
3491
3522
  const rulesFile = path10.join(dir, fileName);
3492
3523
  if (!fs7.existsSync(rulesFile)) continue;
3493
3524
  const resolvedRulesFile = path10.resolve(rulesFile);
3494
- const resolvedRoot = path10.resolve(ROOT_DIR4);
3525
+ const resolvedRoot = path10.resolve(ROOT_DIR5);
3495
3526
  if (!resolvedRulesFile.startsWith(resolvedRoot + path10.sep) && resolvedRulesFile !== resolvedRoot) {
3496
3527
  console.warn(
3497
3528
  `[TemplateInjector] Template rules file resolves outside project root \u2014 skipping: ${rulesFile}`
@@ -3528,11 +3559,11 @@ var isCommentOnlyFile = (filePath) => {
3528
3559
  };
3529
3560
  var isInApiFolder = (filePath) => {
3530
3561
  const normalized = filePath.replaceAll("\\", "/");
3531
- return normalized.includes("/_api/") && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
3562
+ return normalized.includes(apiMarkerSegment()) && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
3532
3563
  };
3533
3564
  var isInSyncFolder = (filePath) => {
3534
3565
  const normalized = filePath.replaceAll("\\", "/");
3535
- return normalized.includes("/_sync/") && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
3566
+ return normalized.includes(syncMarkerSegment()) && filePath.endsWith(".ts") && !isRouteTestFile(filePath);
3536
3567
  };
3537
3568
  var isPageFile = (filePath) => {
3538
3569
  const normalized = filePath.replaceAll("\\", "/");
@@ -3773,7 +3804,7 @@ var calculateRelativePath = (filePath) => {
3773
3804
  const srcRelative = computeSrcRelativePath(filePath);
3774
3805
  if (srcRelative !== null) {
3775
3806
  try {
3776
- const srcDepthFromRoot = path10.relative(ROOT_DIR4, getSrcDir4()).replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0).length;
3807
+ const srcDepthFromRoot = path10.relative(ROOT_DIR5, getSrcDir4()).replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0).length;
3777
3808
  const dirSegmentsAfterSrc = Math.max(
3778
3809
  0,
3779
3810
  srcRelative.split("/").filter((segment) => segment.length > 0).length - 1
@@ -3812,7 +3843,7 @@ var classifyFile = (filePath) => {
3812
3843
  };
3813
3844
  var checkPagePlacement = (filePath, srcRelativePath) => {
3814
3845
  if (srcRelativePath === null) return null;
3815
- const placement = validatePagePath2(srcRelativePath);
3846
+ const placement = validatePagePath(srcRelativePath);
3816
3847
  if (!placement.valid) {
3817
3848
  return getInvalidPagePlacementMessage(filePath, placement.reason ?? "invalid placement", srcRelativePath);
3818
3849
  }
@@ -4035,12 +4066,21 @@ var removeTypeAliases = (content) => {
4035
4066
  result = result.replaceAll(/type PagePath = '[^']*';\s*\n?/g, "");
4036
4067
  return result.replaceAll(/type SyncName = '[^']*';\s*\n?/g, "");
4037
4068
  };
4069
+ var splitAtMainBody = (content) => {
4070
+ const paramsAnchor = content.indexOf(": SyncParams");
4071
+ if (paramsAnchor === -1) return [content, ""];
4072
+ const bodyOpener = content.indexOf("=> {", paramsAnchor);
4073
+ if (bodyOpener === -1) return [content, ""];
4074
+ const splitIndex = bodyOpener + "=> {".length;
4075
+ return [content.slice(0, splitIndex), content.slice(splitIndex)];
4076
+ };
4038
4077
  var updateClientFileForDeletedServer = async (clientFilePath, clientInputTypes) => {
4039
4078
  try {
4040
4079
  let content = fs7.readFileSync(clientFilePath, "utf8");
4041
4080
  content = inlineClientInputType(content, clientInputTypes);
4042
- content = removeServerOutputFromParams(content);
4043
- content = removeServerOutputFromDestructuring(content);
4081
+ const [header, body] = splitAtMainBody(content);
4082
+ const cleanedHeader = removeServerOutputFromDestructuring(removeServerOutputFromParams(header));
4083
+ content = `${cleanedHeader}${body}`;
4044
4084
  content = removeGeneratedTypeImports(content);
4045
4085
  content = removeTypeAliases(content);
4046
4086
  content = content.replaceAll(/\n{3,}/g, "\n\n");
@@ -4082,7 +4122,7 @@ var injectServerTemplateWithClientInput = async (serverFilePath, clientInputType
4082
4122
  // src/importDependencyGraph.ts
4083
4123
  import fs8 from "fs";
4084
4124
  import path11 from "path";
4085
- import { ROOT_DIR as ROOT_DIR5, getServerFunctionDirs as getServerFunctionDirs3, getSharedDir, getSrcDir as getSrcDir5 } from "@luckystack/core";
4125
+ import { ROOT_DIR as ROOT_DIR6, getServerFunctionDirs as getServerFunctionDirs3, getSharedDir, getSrcDir as getSrcDir5 } from "@luckystack/core";
4086
4126
  var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
4087
4127
  var normalizePath3 = (value) => path11.resolve(value).replaceAll("\\", "/");
4088
4128
  var isSupportedScriptFile = (value) => {
@@ -4194,7 +4234,7 @@ var resolveImportToFile = (importerPath, specifier) => {
4194
4234
  return null;
4195
4235
  }
4196
4236
  if (specifier === "config") {
4197
- return tryResolveWithExtensions(path11.join(ROOT_DIR5, "config"));
4237
+ return tryResolveWithExtensions(path11.join(ROOT_DIR6, "config"));
4198
4238
  }
4199
4239
  if (specifier.startsWith("./") || specifier.startsWith("../")) {
4200
4240
  const relativeBase = path11.resolve(path11.dirname(importerPath), specifier);
@@ -4223,7 +4263,7 @@ var collectScopedFiles = () => {
4223
4263
  for (const dir of getServerFunctionDirs3()) {
4224
4264
  collectScriptFiles(dir, files);
4225
4265
  }
4226
- const configFile = tryResolveWithExtensions(path11.join(ROOT_DIR5, "config"));
4266
+ const configFile = tryResolveWithExtensions(path11.join(ROOT_DIR6, "config"));
4227
4267
  if (configFile) {
4228
4268
  files.add(configFile);
4229
4269
  }
@@ -4274,19 +4314,24 @@ var findDependentRouteFiles = (changedFilePath) => {
4274
4314
  import fs9 from "fs";
4275
4315
  import path12 from "path";
4276
4316
  import { spawn } from "child_process";
4277
- import { ROOT_DIR as ROOT_DIR6, tryCatch as tryCatch2 } from "@luckystack/core";
4278
- var GENERATED_CLIENT_DIR = path12.join(ROOT_DIR6, "node_modules", ".prisma", "client");
4317
+ import { ROOT_DIR as ROOT_DIR7, tryCatch as tryCatch2 } from "@luckystack/core";
4318
+ var GENERATED_CLIENT_DIR = path12.join(ROOT_DIR7, "node_modules", ".prisma", "client");
4279
4319
  var GENERATED_CLIENT_MARKER = path12.join(GENERATED_CLIENT_DIR, "schema.prisma");
4280
- var DEFAULT_SCHEMA_PATH = path12.join(ROOT_DIR6, "prisma", "schema.prisma");
4320
+ var DEFAULT_SCHEMA_PATH = path12.join(ROOT_DIR7, "prisma", "schema.prisma");
4281
4321
  var isPrismaClientMissing = () => fs9.existsSync(DEFAULT_SCHEMA_PATH) && !fs9.existsSync(GENERATED_CLIENT_MARKER);
4282
4322
  var runPrismaGenerate = async () => tryCatch2(
4283
4323
  () => new Promise((resolve, reject) => {
4284
4324
  const localBinName = process.platform === "win32" ? "prisma.cmd" : "prisma";
4285
- const localBin = path12.join(ROOT_DIR6, "node_modules", ".bin", localBinName);
4325
+ const localBin = path12.join(ROOT_DIR7, "node_modules", ".bin", localBinName);
4286
4326
  const useLocal = fs9.existsSync(localBin);
4287
4327
  const [cmd, args] = useLocal ? [localBin, ["generate"]] : [process.platform === "win32" ? "npx.cmd" : "npx", ["prisma", "generate"]];
4288
- const child = spawn(cmd, args, {
4289
- cwd: ROOT_DIR6,
4328
+ const isWinShim = process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
4329
+ const child = isWinShim ? spawn(
4330
+ process.env.ComSpec ?? "cmd.exe",
4331
+ ["/d", "/s", "/c", `""${cmd}" ${args.join(" ")}"`],
4332
+ { cwd: ROOT_DIR7, stdio: "inherit", windowsVerbatimArguments: true }
4333
+ ) : spawn(cmd, args, {
4334
+ cwd: ROOT_DIR7,
4290
4335
  stdio: "inherit",
4291
4336
  shell: false
4292
4337
  });
@@ -4298,7 +4343,7 @@ var runPrismaGenerate = async () => tryCatch2(
4298
4343
  );
4299
4344
 
4300
4345
  // src/hotReload.ts
4301
- import { tryCatch as tryCatch3, getProjectConfig, getLocaleReloader } from "@luckystack/core";
4346
+ import { tryCatch as tryCatch3, getProjectConfig, getLocaleReloader, resolveEnvKey } from "@luckystack/core";
4302
4347
  var normalizeFsPath = (value) => path13.resolve(value).replaceAll("\\", "/");
4303
4348
  var isGeneratedPath = (normalizedPath) => {
4304
4349
  return normalizedPath.includes("apiTypes.generated.ts") || normalizedPath.includes("apiDocs.generated.json");
@@ -4409,7 +4454,7 @@ var mountWatchers = (onAdd, onChange, onDelete, onFunctionChange) => {
4409
4454
  watch(pathsConfig.sharedDir, { ignoreInitial: true }).on("add", onFunctionChange).on("change", onFunctionChange).on("unlink", onFunctionChange);
4410
4455
  };
4411
4456
  var setupWatchers = () => {
4412
- const isDevMode = process.env.NODE_ENV !== "production";
4457
+ const isDevMode = resolveEnvKey() !== "production";
4413
4458
  if (!isDevMode) return;
4414
4459
  const segments = buildPathSegments();
4415
4460
  const isInServerFunctionsDir = makeIsInServerFunctionsDir(segments);