@luckystack/devkit 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/dist/{chunk-YYWCJ7VZ.js → chunk-BZTCJ7JY.js} +2 -2
- package/dist/{chunk-YYWCJ7VZ.js.map → chunk-BZTCJ7JY.js.map} +1 -1
- package/dist/cli/validateDeploy.js +2 -2
- package/dist/cli/validateDeploy.js.map +1 -1
- package/dist/index.js +87 -42
- package/dist/index.js.map +1 -1
- package/dist/supervisor.js +10 -4
- package/dist/supervisor.js.map +1 -1
- package/package.json +2 -2
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@^
|
|
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(([,
|
|
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-
|
|
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":";AAyCO,IAAM,iBAAiB,CAAC;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,MAAM,QAAQ;AAChB,MAAiD;AAC/C,QAAM,WAAgC,CAAC;AACvC,QAAM,eAAe,IAAI,IAAI,OAAO,KAAK,SAAS,QAAQ,CAAC;AAG3D,QAAM,oBAAoB,oBAAI,IAAsB;AACpD,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAClE,eAAW,WAAW,OAAO,UAAU;AACrC,YAAM,WAAW,kBAAkB,IAAI,OAAO,KAAK,CAAC;AACpD,eAAS,KAAK,SAAS;AACvB,wBAAkB,IAAI,SAAS,QAAQ;AAAA,IACzC;AAAA,EACF;AAEA,aAAW,WAAW,cAAc;AAClC,UAAM,SAAS,kBAAkB,IAAI,OAAO,KAAK,CAAC;AAClD,QAAI,OAAO,WAAW,GAAG;AACvB,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,YAAY,OAAO;AAAA,QAC5B,UAAU,iCAAiC,OAAO;AAAA,MACpD,CAAC;AAAA,IACH,WAAW,OAAO,SAAS,GAAG;AAC5B,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,YAAY,OAAO,6BAA6B,OAAO,KAAK,IAAI,CAAC;AAAA,QAC1E,UAAU,iCAAiC,OAAO;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AAGA,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAClE,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,CAAC,aAAa,IAAI,OAAO,GAAG;AAC9B,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,WAAW,SAAS,iCAAiC,OAAO;AAAA,UACrE,UAAU,gCAAgC,SAAS;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAMA,QAAM,eAAe,OAAO,gBAAgB,CAAC;AAC7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,KAAK,OAAO,SAAS,CAAC;AAE3D,aAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,YAAY,GAAG;AAC3D,eAAW,CAAC,gBAAgB,UAAU,KAAK,OAAO,QAAQ,OAAO,QAAQ,GAAG;AAC1E,UAAI,CAAC,aAAa,IAAI,cAAc,GAAG;AACrC,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,gBAAgB,MAAM,4BAA4B,cAAc;AAAA,UACzE,UAAU,mCAAmC,MAAM;AAAA,QACrD,CAAC;AAAA,MACH;AAMA,UAAI,SAAqB;AACzB,UAAI;AACF,iBAAS,IAAI,IAAI,UAAU;AAAA,MAC7B,QAAQ;AACN,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,gBAAgB,MAAM,cAAc,cAAc,kCAAkC,UAAU;AAAA,UACvG,UAAU,mCAAmC,MAAM,aAAa,cAAc;AAAA,QAChF,CAAC;AAAA,MACH;AACA,UAAI,UAAU,CAAC,OAAO,MAAM;AAC1B,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,gBAAgB,MAAM,cAAc,cAAc,cAAc,UAAU;AAAA,UACnF,UAAU,mCAAmC,MAAM,aAAa,cAAc;AAAA,QAChF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,IAAI,OAAO,KAAK,GAAG;AACpC,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,gBAAgB,MAAM,wCAAwC,OAAO,KAAK;AAAA,QACnF,UAAU,mCAAmC,MAAM;AAAA,MACrD,CAAC;AAAA,IACH;AACA,QAAI,CAAC,cAAc,IAAI,OAAO,KAAK,GAAG;AACpC,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,gBAAgB,MAAM,wCAAwC,OAAO,KAAK;AAAA,QACnF,UAAU,mCAAmC,MAAM;AAAA,MACrD,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,UAAU;AACnB,YAAM,cAAc,aAAa,OAAO,QAAQ;AAMhD,UAAI,aAAa;AACf,YAAI,YAAY,UAAU,OAAO,OAAO;AACtC,mBAAS,KAAK;AAAA,YACZ,UAAU;AAAA,YACV,MAAM;AAAA,YACN,SAAS,gBAAgB,MAAM,uBAAuB,OAAO,QAAQ,2CAA2C,OAAO,KAAK,SAAS,YAAY,KAAK;AAAA,YACtJ,UAAU,mCAAmC,MAAM;AAAA,UACrD,CAAC;AAAA,QACH;AACA,YAAI,YAAY,UAAU,OAAO,OAAO;AACtC,mBAAS,KAAK;AAAA,YACZ,UAAU;AAAA,YACV,MAAM;AAAA,YACN,SAAS,gBAAgB,MAAM,uBAAuB,OAAO,QAAQ,2CAA2C,OAAO,KAAK,SAAS,YAAY,KAAK;AAAA,YACtJ,UAAU,mCAAmC,MAAM;AAAA,UACrD,CAAC;AAAA,QACH;AAAA,MACF,OAAO;AACL,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,gBAAgB,MAAM,mBAAmB,OAAO,QAAQ;AAAA,UACjE,UAAU,mCAAmC,MAAM;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,aAAa,QAAQ,KAAK,OAAO,QAAQ,OAAO,SAAS,GAAG;AACtE,UAAM,WAAW,IAAI,SAAS,SAAS;AACvC,QAAI,aAAa,UAAa,aAAa,IAAI;AAC7C,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,aAAa,WAAW,sBAAsB,SAAS,SAAS;AAAA,QACzE,UAAU,gCAAgC,WAAW;AAAA,MACvD,CAAC;AAAA,IACH;AAEA,eAAW,mBAAmB,SAAS,uBAAuB,CAAC,GAAG;AAChE,YAAM,QAAQ,IAAI,eAAe;AACjC,UAAI,UAAU,UAAa,UAAU,IAAI;AACvC,iBAAS,KAAK;AAAA,UACZ,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,aAAa,WAAW,oCAAoC,eAAe;AAAA,UACpF,UAAU,gCAAgC,WAAW;AAAA,QACvD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAKA,aAAW,WAAW,cAAc;AAClC,UAAM,UAAU,OAAO,QAAQ,YAAY,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ;AAC9F,QAAI,QAAQ,WAAW,GAAG;AACxB,eAAS,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,YAAY,OAAO;AAAA,QAC5B,UAAU,iCAAiC,OAAO;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,aAAa,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE;AAClE,QAAM,eAAe,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,EAAE;AAEtE,SAAO;AAAA,IACL,IAAI,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
validateDeploy
|
|
4
|
-
} from "../chunk-
|
|
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(
|
|
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-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 \
|
|
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 \
|
|
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
|
-
|
|
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:
|
|
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 \
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
4043
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
4278
|
-
var GENERATED_CLIENT_DIR = path12.join(
|
|
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(
|
|
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(
|
|
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
|
|
4289
|
-
|
|
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 =
|
|
4457
|
+
const isDevMode = resolveEnvKey() !== "production";
|
|
4413
4458
|
if (!isDevMode) return;
|
|
4414
4459
|
const segments = buildPathSegments();
|
|
4415
4460
|
const isInServerFunctionsDir = makeIsInServerFunctionsDir(segments);
|