@luckystack/devkit 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to `@luckystack/devkit` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.12.0] - 2026-10-15
9
+
10
+ ### Changed
11
+
12
+ - Requires Node.js 22.13 or newer (was 20). Node 20 stopped receiving security updates in April 2026, and the scaffold's lint toolchain (ESLint 10) needs 22.13.
13
+ - `luckystack-validate-deploy` keeps the original import error as `cause` when a config file fails to load.
14
+
15
+ ### Fixed
16
+
17
+ - Page/sync template placeholders and the expanded types in the functions metadata are inserted verbatim. They were passed to `String#replaceAll` as strings, so a `$'` or `$&` in a path or in a type such as `{ currency: '$' }` was read as a replacement pattern.
18
+
8
19
  ## [0.11.0] - 2026-10-14
9
20
 
10
21
  ### Changed
@@ -203,4 +203,4 @@ var validateDeploy = ({
203
203
  export {
204
204
  validateDeploy
205
205
  };
206
- //# sourceMappingURL=chunk-IWGWJU4M.js.map
206
+ //# sourceMappingURL=chunk-BJN4XIUX.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 custom HTTP route prefix has a valid, known service owner.\n const normalizedCustomPrefixes = new Set<string>();\n for (const [rawPrefix, owner] of Object.entries(services.customRoutes ?? {})) {\n const prefix = rawPrefix.length > 1 && rawPrefix.endsWith('/')\n ? rawPrefix.slice(0, -1)\n : rawPrefix;\n if (!prefix.startsWith('/') || prefix === '/' || prefix.includes('?') || prefix.includes('#')) {\n findings.push({\n severity: 'error',\n code: 'custom-route-invalid-prefix',\n message: `Custom route ownership prefix \"${rawPrefix}\" must be an absolute non-root path prefix without query/hash.`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n } else if (prefix === '/api' || prefix.startsWith('/api/') || prefix === '/sync' || prefix.startsWith('/sync/')) {\n findings.push({\n severity: 'error',\n code: 'custom-route-overlaps-typed-transport',\n message: `Custom route ownership prefix \"${rawPrefix}\" overlaps /api or /sync. Typed transport ownership is derived from the service-first route name.`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n if (normalizedCustomPrefixes.has(prefix)) {\n findings.push({\n severity: 'error',\n code: 'custom-route-duplicate-prefix',\n message: `Custom route ownership prefix \"${rawPrefix}\" duplicates normalized prefix \"${prefix}\".`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n normalizedCustomPrefixes.add(prefix);\n if (!serviceNames.has(owner)) {\n findings.push({\n severity: 'error',\n code: 'custom-route-unknown-service',\n message: `Custom route ownership prefix \"${rawPrefix}\" references unknown service \"${owner}\".`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n }\n\n //? Rule 4: 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
+ {"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 custom HTTP route prefix has a valid, known service owner.\n const normalizedCustomPrefixes = new Set<string>();\n for (const [rawPrefix, owner] of Object.entries(services.customRoutes ?? {})) {\n const prefix = rawPrefix.length > 1 && rawPrefix.endsWith('/')\n ? rawPrefix.slice(0, -1)\n : rawPrefix;\n if (!prefix.startsWith('/') || prefix === '/' || prefix.includes('?') || prefix.includes('#')) {\n findings.push({\n severity: 'error',\n code: 'custom-route-invalid-prefix',\n message: `Custom route ownership prefix \"${rawPrefix}\" must be an absolute non-root path prefix without query/hash.`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n } else if (prefix === '/api' || prefix.startsWith('/api/') || prefix === '/sync' || prefix.startsWith('/sync/')) {\n findings.push({\n severity: 'error',\n code: 'custom-route-overlaps-typed-transport',\n message: `Custom route ownership prefix \"${rawPrefix}\" overlaps /api or /sync. Typed transport ownership is derived from the service-first route name.`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n if (normalizedCustomPrefixes.has(prefix)) {\n findings.push({\n severity: 'error',\n code: 'custom-route-duplicate-prefix',\n message: `Custom route ownership prefix \"${rawPrefix}\" duplicates normalized prefix \"${prefix}\".`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n normalizedCustomPrefixes.add(prefix);\n if (!serviceNames.has(owner)) {\n findings.push({\n severity: 'error',\n code: 'custom-route-unknown-service',\n message: `Custom route ownership prefix \"${rawPrefix}\" references unknown service \"${owner}\".`,\n location: `services.config.ts > customRoutes.${rawPrefix}`,\n });\n }\n }\n\n //? Rule 4: 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 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-IWGWJU4M.js";
4
+ } from "../chunk-BJN4XIUX.js";
5
5
 
6
6
  // src/cli/validateDeploy.ts
7
7
  import { pathToFileURL } from "url";
@@ -100,7 +100,7 @@ var importConfig = async (file, label) => {
100
100
  await import(pathToFileURL(resolved).href);
101
101
  } catch (error) {
102
102
  const message = error instanceof Error ? error.message : String(error);
103
- throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);
103
+ throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`, { cause: error });
104
104
  }
105
105
  };
106
106
  var COLORS = process.stdout.isTTY ? {
@@ -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":";;;;;;AASA,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,IAAM,YAAY,CAAC,SAAqC;AACtD,QAAM,OAAgB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAEA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,UAAM,OAA2B,IAAI,IAAI,KAAK,SAAS,KAAK,IAAI,CAAC,IAAI;AACrE,YAAQ,MAAM;AAAA,MACZ,KAAK;AAAA,MACL,KAAK,MAAM;AACT,aAAK,SAAS,QAAQ;AACtB;AACA;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK,MAAM;AACT,aAAK,WAAW,QAAQ;AACxB;AACA;AAAA,MACF;AAAA,MACA,KAAK,YAAY;AACf,aAAK,gBAAgB;AACrB;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK,MAAM;AACT,kBAAU;AACV,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AAAA,MACA,SAAS;AACP;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,YAAY,MAAY;AAC5B,UAAQ,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CA0BtB;AACD;AAEA,IAAM,4BAA4B,oBAAI,IAAI,CAAC,OAAO,QAAQ,QAAQ,OAAO,QAAQ,MAAM,CAAC;AAExF,IAAM,eAAe,OAAO,MAAc,UAAiC;AACzE,QAAM,MAAM,KAAK,WAAW,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,IAAI;AAIxE,QAAM,OAAO,KAAK,QAAQ,QAAQ,IAAI,CAAC;AACvC,QAAM,WAAW,KAAK,QAAQ,GAAG;AACjC,MAAI,CAAC,SAAS,WAAW,OAAO,KAAK,GAAG,KAAK,aAAa,MAAM;AAC9D,UAAM,IAAI;AAAA,MACR,gCAAgC,KAAK,UAAU,IAAI;AAAA,IACrD;AAAA,EACF;AAGA,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,MAAI,CAAC,0BAA0B,IAAI,GAAG,GAAG;AACvC,UAAM,IAAI;AAAA,MACR,gCAAgC,KAAK,UAAU,IAAI,+BAA+B,GAAG,4BAAuB,CAAC,GAAG,yBAAyB,EAAE,KAAK,IAAI,CAAC;AAAA,IACvJ;AAAA,EACF;AAEA,MAAI;AACF,UAAM,OAAO,cAAc,QAAQ,EAAE;AAAA,EACvC,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAM,IAAI,MAAM,iDAAiD,KAAK,OAAO,GAAG,KAAK,OAAO,EAAE;AAAA,EAChG;AACF;AAEA,IAAM,SAAS,QAAQ,OAAO,QAC1B;AAAA,EACE,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR,IACA,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,IAAI,OAAO,IAAI,QAAQ,IAAI,MAAM,GAAG;AAE7E,IAAM,eAAe,CAAC,YAAqC;AACzD,QAAM,MAAM,QAAQ,aAAa,UAC7B,GAAG,OAAO,GAAG,GAAG,OAAO,IAAI,QAAQ,OAAO,KAAK,KAC/C,GAAG,OAAO,MAAM,GAAG,OAAO,IAAI,QAAQ,OAAO,KAAK;AACtD,QAAM,OAAO,GAAG,OAAO,GAAG,IAAI,QAAQ,IAAI,IAAI,OAAO,KAAK;AAC1D,QAAM,WAAW,QAAQ,WAAW;AAAA,IAAO,OAAO,GAAG,MAAM,QAAQ,QAAQ,GAAG,OAAO,KAAK,KAAK;AAC/F,QAAM,OAAO,QAAQ,aAAa,UAAU,QAAQ,SAAS,QAAQ;AACrE,OAAK,MAAM,GAAG,GAAG,IAAI,IAAI,IAAI,QAAQ,OAAO,GAAG,QAAQ;AAAA,CAAI;AAC7D;AAEA,IAAM,OAAO,YAA2B;AACtC,QAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAE5C,MAAI,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU;AAClC,YAAQ,OAAO,MAAM,iGAAiG;AACtH,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAa,KAAK,QAAQ,eAAe;AAC/C,QAAM,aAAa,KAAK,UAAU,iBAAiB;AAEnD,MAAI,CAAC,yBAAyB,GAAG;AAC/B,YAAQ,OAAO,MAAM,iHAA4G;AACjI,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,2BAA2B,GAAG;AACjC,YAAQ,OAAO,MAAM,qHAAgH;AACrI,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,eAAe;AAAA,IAC5B,UAAU,kBAAkB;AAAA,IAC5B,QAAQ,gBAAgB;AAAA,EAC1B,CAAC;AAED,aAAW,WAAW,OAAO,UAAU;AACrC,iBAAa,OAAO;AAAA,EACtB;AAEA,QAAM,UAAU,OAAO,KACnB,GAAG,OAAO,KAAK,GAAG,OAAO,IAAI,KAAK,OAAO,KAAK,KAC9C,GAAG,OAAO,GAAG,GAAG,OAAO,IAAI,SAAS,OAAO,KAAK;AACpD,UAAQ,OAAO;AAAA,IACb;AAAA,EAAK,OAAO,WAAM,OAAO,OAAO,UAAU,CAAC,cAAc,OAAO,OAAO,YAAY,CAAC;AAAA;AAAA,EACtF;AAEA,MAAI,OAAO,aAAa,GAAG;AACzB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,KAAK,iBAAiB,OAAO,eAAe,GAAG;AACjD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,CAAC,UAAmB;AAC/B,QAAM,UAAU,iBAAiB,QAAQ,MAAM,SAAS,MAAM,UAAU,OAAO,KAAK;AACpF,UAAQ,OAAO,MAAM,uCAAuC,OAAO;AAAA,CAAI;AACvE,UAAQ,KAAK,CAAC;AAChB,CAAC;","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}`, { cause: error });\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-IWGWJU4M.js";
3
+ } from "./chunk-BJN4XIUX.js";
4
4
 
5
5
  // src/typeMap/discovery.ts
6
6
  import fs2 from "fs";
@@ -2758,7 +2758,7 @@ var resolveLocalExportedTypes = ({
2758
2758
  const declarationNode = isClassDeclaration3(declaration) ? declaration.name ?? declaration : isTypeAliasDeclaration4(declaration) ? declaration.type : declaration;
2759
2759
  const declarationType = checker.getTypeAtLocation(declarationNode);
2760
2760
  const expanded = normalizeInlineType(expandType(declarationType, checker));
2761
- resolved = resolved.replaceAll(new RegExp(String.raw`\b${exportName}\b`, "g"), expanded);
2761
+ resolved = resolved.replaceAll(new RegExp(String.raw`\b${exportName}\b`, "g"), () => expanded);
2762
2762
  }
2763
2763
  return resolved;
2764
2764
  };
@@ -4462,12 +4462,12 @@ var applyTemplatePlaceholders = (content, filePath, fileKind) => {
4462
4462
  const relPath = calculateRelativePath(filePath);
4463
4463
  const pragmaPattern = /\/\/\s*@ts-(?:ignore|expect-error).*\r?\n(.*)\{\{REL_PATH\}\}/g;
4464
4464
  let result = content.replaceAll(pragmaPattern, (_, prefix) => `${prefix}${relPath}`);
4465
- result = result.replaceAll("{{REL_PATH}}", relPath);
4465
+ result = result.replaceAll("{{REL_PATH}}", () => relPath);
4466
4466
  if (fileKind === "sync_client" || fileKind === "sync_server") {
4467
4467
  const pagePath = extractSyncPagePath2(filePath);
4468
4468
  const syncName = extractSyncName2(filePath);
4469
- result = result.replaceAll("{{PAGE_PATH}}", pagePath);
4470
- result = result.replaceAll("{{SYNC_NAME}}", syncName);
4469
+ result = result.replaceAll("{{PAGE_PATH}}", () => pagePath);
4470
+ result = result.replaceAll("{{SYNC_NAME}}", () => syncName);
4471
4471
  }
4472
4472
  return result;
4473
4473
  };
@@ -4694,7 +4694,7 @@ var injectServerTemplateWithClientInput = async (serverFilePath, clientInputType
4694
4694
  content = content.replaceAll(pragmaPattern, (_, prefix) => {
4695
4695
  return `${prefix}${relPath}`;
4696
4696
  });
4697
- content = content.replaceAll("{{REL_PATH}}", relPath);
4697
+ content = content.replaceAll("{{REL_PATH}}", () => relPath);
4698
4698
  content = content.replace(
4699
4699
  /clientInput:\s*\{[^}]*\}/s,
4700
4700
  (_match) => `clientInput: ${clientInputTypes}`
@@ -4763,7 +4763,7 @@ var specifiersCache = /* @__PURE__ */ new Map();
4763
4763
  var scopedFilesCache = null;
4764
4764
  var SCOPED_FILES_TTL_MS = 1e3;
4765
4765
  var extractImportSpecifiers = (filePath) => {
4766
- let mtimeMs = 0;
4766
+ let mtimeMs;
4767
4767
  try {
4768
4768
  mtimeMs = fs10.statSync(filePath).mtimeMs;
4769
4769
  } catch {