@luckystack/devkit 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +125 -111
- package/LICENSE +21 -21
- package/README.md +44 -44
- package/dist/chunk-YYWCJ7VZ.js +170 -0
- package/dist/chunk-YYWCJ7VZ.js.map +1 -0
- package/dist/cli/validateDeploy.js +3 -168
- package/dist/cli/validateDeploy.js.map +1 -1
- package/dist/index.js +719 -599
- package/dist/index.js.map +1 -1
- package/dist/supervisor.js +187 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/templates/api.template.ts +54 -54
- package/dist/templates/page_dashboard.template.tsx +35 -35
- package/dist/templates/page_plain.template.tsx +32 -32
- package/dist/templates/sync_client.template.ts +40 -40
- package/dist/templates/sync_client_paired.template.ts +38 -38
- package/dist/templates/sync_client_standalone.template.ts +36 -36
- package/dist/templates/sync_server.template.ts +64 -64
- package/docs/cli.md +245 -245
- package/docs/hot-reload.md +365 -365
- package/docs/loader-pipeline.md +324 -324
- package/docs/runtime-type-resolver.md +258 -258
- package/docs/supervisor.md +292 -292
- package/docs/ts-program-cache.md +199 -199
- package/docs/type-map-generation.md +392 -392
- package/package.json +8 -5
|
@@ -0,0 +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,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
validateDeploy
|
|
4
|
+
} from "../chunk-YYWCJ7VZ.js";
|
|
2
5
|
|
|
3
6
|
// src/cli/validateDeploy.ts
|
|
4
7
|
import { pathToFileURL } from "url";
|
|
@@ -9,174 +12,6 @@ import {
|
|
|
9
12
|
isDeployConfigRegistered,
|
|
10
13
|
isServicesConfigRegistered
|
|
11
14
|
} from "@luckystack/core";
|
|
12
|
-
|
|
13
|
-
// src/validateDeploy.ts
|
|
14
|
-
var validateDeploy = ({
|
|
15
|
-
services,
|
|
16
|
-
deploy,
|
|
17
|
-
env = process.env
|
|
18
|
-
}) => {
|
|
19
|
-
const findings = [];
|
|
20
|
-
const serviceNames = new Set(Object.keys(services.services));
|
|
21
|
-
const servicesInPresets = /* @__PURE__ */ new Map();
|
|
22
|
-
for (const [presetKey, preset] of Object.entries(services.presets)) {
|
|
23
|
-
for (const service of preset.services) {
|
|
24
|
-
const existing = servicesInPresets.get(service) ?? [];
|
|
25
|
-
existing.push(presetKey);
|
|
26
|
-
servicesInPresets.set(service, existing);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
for (const service of serviceNames) {
|
|
30
|
-
const owners = servicesInPresets.get(service) ?? [];
|
|
31
|
-
if (owners.length === 0) {
|
|
32
|
-
findings.push({
|
|
33
|
-
severity: "error",
|
|
34
|
-
code: "service-unassigned",
|
|
35
|
-
message: `Service "${service}" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,
|
|
36
|
-
location: `services.config.ts > services.${service}`
|
|
37
|
-
});
|
|
38
|
-
} else if (owners.length > 1) {
|
|
39
|
-
findings.push({
|
|
40
|
-
severity: "error",
|
|
41
|
-
code: "service-in-multiple-presets",
|
|
42
|
-
message: `Service "${service}" is in multiple presets: ${owners.join(", ")}. A service may belong to exactly one preset.`,
|
|
43
|
-
location: `services.config.ts > services.${service}`
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
for (const [presetKey, preset] of Object.entries(services.presets)) {
|
|
48
|
-
for (const service of preset.services) {
|
|
49
|
-
if (!serviceNames.has(service)) {
|
|
50
|
-
findings.push({
|
|
51
|
-
severity: "error",
|
|
52
|
-
code: "preset-references-unknown-service",
|
|
53
|
-
message: `Preset "${presetKey}" references unknown service "${service}". Add it to services.services or remove the reference.`,
|
|
54
|
-
location: `services.config.ts > presets.${presetKey}.services`
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const environments = deploy.environments ?? {};
|
|
60
|
-
const resourceNames = new Set(Object.keys(deploy.resources));
|
|
61
|
-
for (const [envKey, envDef] of Object.entries(environments)) {
|
|
62
|
-
for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {
|
|
63
|
-
if (!serviceNames.has(bindingService)) {
|
|
64
|
-
findings.push({
|
|
65
|
-
severity: "error",
|
|
66
|
-
code: "binding-references-unknown-service",
|
|
67
|
-
message: `Environment "${envKey}" binds unknown service "${bindingService}". Either add it to services.services or remove the binding.`,
|
|
68
|
-
location: `deploy.config.ts > environments.${envKey}.bindings`
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
let parsed = null;
|
|
72
|
-
try {
|
|
73
|
-
parsed = new URL(bindingUrl);
|
|
74
|
-
} catch {
|
|
75
|
-
findings.push({
|
|
76
|
-
severity: "error",
|
|
77
|
-
code: "binding-invalid-url",
|
|
78
|
-
message: `Environment "${envKey}" service "${bindingService}" binding is not a valid URL: "${bindingUrl}".`,
|
|
79
|
-
location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (parsed && !parsed.port) {
|
|
83
|
-
findings.push({
|
|
84
|
-
severity: "error",
|
|
85
|
-
code: "binding-missing-port",
|
|
86
|
-
message: `Environment "${envKey}" service "${bindingService}" binding "${bindingUrl}" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,
|
|
87
|
-
location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (!resourceNames.has(envDef.redis)) {
|
|
92
|
-
findings.push({
|
|
93
|
-
severity: "error",
|
|
94
|
-
code: "unknown-redis-resource",
|
|
95
|
-
message: `Environment "${envKey}" references unknown redis resource "${envDef.redis}". Add it to deploy.resources.`,
|
|
96
|
-
location: `deploy.config.ts > environments.${envKey}.redis`
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
if (!resourceNames.has(envDef.mongo)) {
|
|
100
|
-
findings.push({
|
|
101
|
-
severity: "error",
|
|
102
|
-
code: "unknown-mongo-resource",
|
|
103
|
-
message: `Environment "${envKey}" references unknown mongo resource "${envDef.mongo}". Add it to deploy.resources.`,
|
|
104
|
-
location: `deploy.config.ts > environments.${envKey}.mongo`
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (envDef.fallback) {
|
|
108
|
-
const fallbackEnv = environments[envDef.fallback];
|
|
109
|
-
if (fallbackEnv) {
|
|
110
|
-
if (fallbackEnv.redis !== envDef.redis) {
|
|
111
|
-
findings.push({
|
|
112
|
-
severity: "error",
|
|
113
|
-
code: "fallback-redis-mismatch",
|
|
114
|
-
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.`,
|
|
115
|
-
location: `deploy.config.ts > environments.${envKey}.fallback`
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (fallbackEnv.mongo !== envDef.mongo) {
|
|
119
|
-
findings.push({
|
|
120
|
-
severity: "error",
|
|
121
|
-
code: "fallback-mongo-mismatch",
|
|
122
|
-
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.`,
|
|
123
|
-
location: `deploy.config.ts > environments.${envKey}.fallback`
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
findings.push({
|
|
128
|
-
severity: "error",
|
|
129
|
-
code: "unknown-fallback-env",
|
|
130
|
-
message: `Environment "${envKey}" has fallback "${envDef.fallback}" which does not exist in deploy.environments.`,
|
|
131
|
-
location: `deploy.config.ts > environments.${envKey}.fallback`
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
for (const [resourceKey, resource] of Object.entries(deploy.resources)) {
|
|
137
|
-
const urlValue = env[resource.urlEnvKey];
|
|
138
|
-
if (urlValue === void 0 || urlValue === "") {
|
|
139
|
-
findings.push({
|
|
140
|
-
severity: "warning",
|
|
141
|
-
code: "missing-resource-env-var",
|
|
142
|
-
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.`,
|
|
143
|
-
location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {
|
|
147
|
-
const value = env[synchronizedKey];
|
|
148
|
-
if (value === void 0 || value === "") {
|
|
149
|
-
findings.push({
|
|
150
|
-
severity: "warning",
|
|
151
|
-
code: "missing-synchronized-env-var",
|
|
152
|
-
message: `Resource "${resourceKey}" requires synchronized env var "${synchronizedKey}" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,
|
|
153
|
-
location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
for (const service of serviceNames) {
|
|
159
|
-
const boundIn = Object.entries(environments).filter(([, env2]) => service in env2.bindings);
|
|
160
|
-
if (boundIn.length === 0) {
|
|
161
|
-
findings.push({
|
|
162
|
-
severity: "warning",
|
|
163
|
-
code: "service-bound-in-no-environment",
|
|
164
|
-
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.`,
|
|
165
|
-
location: `services.config.ts > services.${service}`
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
170
|
-
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
171
|
-
return {
|
|
172
|
-
ok: errorCount === 0,
|
|
173
|
-
findings,
|
|
174
|
-
errorCount,
|
|
175
|
-
warningCount
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// src/cli/validateDeploy.ts
|
|
180
15
|
var parseArgs = (argv) => {
|
|
181
16
|
const args = {
|
|
182
17
|
deploy: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/validateDeploy.ts","../../src/validateDeploy.ts"],"sourcesContent":["#!/usr/bin/env node\r\n//? `luckystack-validate-deploy` CLI. Loads the consumer's compiled\r\n//? services.config.js and deploy.config.js as side-effects (they call\r\n//? `registerServicesConfig` / `registerDeployConfig` on import), then runs\r\n//? `validateDeploy()` and prints findings.\r\n//?\r\n//? Designed to be a pre-deploy gate: exit 1 on any error finding, 0\r\n//? otherwise. Warnings print to stderr but never fail the run.\r\n\r\nimport { pathToFileURL } from 'node:url';\r\nimport path from 'node:path';\r\nimport {\r\n getDeployConfig,\r\n getServicesConfig,\r\n isDeployConfigRegistered,\r\n isServicesConfigRegistered,\r\n} from '@luckystack/core';\r\n\r\nimport { validateDeploy, type ValidationFinding } from '../validateDeploy';\r\n\r\ninterface CliArgs {\r\n deploy: string | null;\r\n services: string | null;\r\n failOnWarning: boolean;\r\n}\r\n\r\nconst parseArgs = (argv: readonly string[]): CliArgs => {\r\n const args: CliArgs = {\r\n deploy: null,\r\n services: null,\r\n failOnWarning: false,\r\n };\r\n\r\n for (let i = 0; i < argv.length; i++) {\r\n const flag = argv[i];\r\n const next: string | undefined = i + 1 < argv.length ? argv[i + 1] : undefined;\r\n switch (flag) {\r\n case '--deploy':\r\n case '-d': {\r\n args.deploy = next ?? null;\r\n i++;\r\n break;\r\n }\r\n case '--services':\r\n case '-s': {\r\n args.services = next ?? null;\r\n i++;\r\n break;\r\n }\r\n case '--strict': {\r\n args.failOnWarning = true;\r\n break;\r\n }\r\n case '--help':\r\n case '-h': {\r\n printHelp();\r\n process.exit(0);\r\n break;\r\n }\r\n default: {\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return args;\r\n};\r\n\r\nconst printHelp = (): void => {\r\n process.stdout.write(`\r\nluckystack-validate-deploy — pre-deploy validator for services + deploy configs\r\n\r\nUSAGE\r\n luckystack-validate-deploy --deploy <file> --services <file> [options]\r\n\r\nREQUIRED\r\n --deploy, -d <file> Path to compiled deploy.config.js (registers DeployConfig).\r\n --services, -s <file> Path to compiled services.config.js (registers ServicesConfig).\r\n\r\nOPTIONS\r\n --strict Exit 1 on warnings as well as errors.\r\n --help, -h Show this help.\r\n\r\nWHAT IT CHECKS\r\n - Every service is assigned to exactly one preset.\r\n - Every preset references services that exist.\r\n - Every environment binding's service matches an actual service.\r\n - Every environment's redis/mongo resource keys exist.\r\n - Fallback envs must share the same redis/mongo resource keys.\r\n - synchronizedEnvKeys and urlEnvKey values resolve at config time (warning only).\r\n - Services bound in no environment (warning only — valid mid-rollout).\r\n\r\nEXAMPLES\r\n luckystack-validate-deploy --deploy dist/deploy.config.js --services dist/services.config.js\r\n npx tsx packages/devkit/dist/cli/validateDeploy.js --deploy ./deploy.config.ts --services ./services.config.ts\r\n`);\r\n};\r\n\r\nconst importConfig = async (file: string, label: string): Promise<void> => {\r\n const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);\r\n try {\r\n await import(pathToFileURL(abs).href);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);\r\n }\r\n};\r\n\r\nconst COLORS = process.stdout.isTTY\r\n ? {\r\n reset: '\u001b[0m',\r\n bold: '\u001b[1m',\r\n dim: '\u001b[2m',\r\n red: '\u001b[31m',\r\n green: '\u001b[32m',\r\n yellow: '\u001b[33m',\r\n cyan: '\u001b[36m',\r\n }\r\n : { reset: '', bold: '', dim: '', red: '', green: '', yellow: '', cyan: '' };\r\n\r\nconst printFinding = (finding: ValidationFinding): void => {\r\n const tag = finding.severity === 'error'\r\n ? `${COLORS.red}${COLORS.bold}ERROR${COLORS.reset}`\r\n : `${COLORS.yellow}${COLORS.bold}WARN ${COLORS.reset}`;\r\n const code = `${COLORS.dim}[${finding.code}]${COLORS.reset}`;\r\n const location = finding.location ? `\\n ${COLORS.dim}at ${finding.location}${COLORS.reset}` : '';\r\n const sink = finding.severity === 'error' ? process.stderr : process.stdout;\r\n sink.write(`${tag} ${code} ${finding.message}${location}\\n`);\r\n};\r\n\r\nconst main = async (): Promise<void> => {\r\n const args = parseArgs(process.argv.slice(2));\r\n\r\n if (!args.deploy || !args.services) {\r\n process.stderr.write('[luckystack-validate-deploy] --deploy and --services are required. Run with --help for usage.\\n');\r\n process.exit(2);\r\n }\r\n\r\n await importConfig(args.deploy, 'deploy config');\r\n await importConfig(args.services, 'services config');\r\n\r\n if (!isDeployConfigRegistered()) {\r\n process.stderr.write('[luckystack-validate-deploy] deploy config file did not call registerDeployConfig — nothing to validate.\\n');\r\n process.exit(2);\r\n }\r\n if (!isServicesConfigRegistered()) {\r\n process.stderr.write('[luckystack-validate-deploy] services config file did not call registerServicesConfig — nothing to validate.\\n');\r\n process.exit(2);\r\n }\r\n\r\n const result = validateDeploy({\r\n services: getServicesConfig(),\r\n deploy: getDeployConfig(),\r\n });\r\n\r\n for (const finding of result.findings) {\r\n printFinding(finding);\r\n }\r\n\r\n const summary = result.ok\r\n ? `${COLORS.green}${COLORS.bold}OK${COLORS.reset}`\r\n : `${COLORS.red}${COLORS.bold}FAILED${COLORS.reset}`;\r\n process.stdout.write(\r\n `\\n${summary} — ${String(result.errorCount)} error(s), ${String(result.warningCount)} warning(s)\\n`,\r\n );\r\n\r\n if (result.errorCount > 0) {\r\n process.exit(1);\r\n }\r\n if (args.failOnWarning && result.warningCount > 0) {\r\n process.exit(1);\r\n }\r\n process.exit(0);\r\n};\r\n\r\nmain().catch((error: unknown) => {\r\n const message = error instanceof Error ? error.stack ?? error.message : String(error);\r\n process.stderr.write(`[luckystack-validate-deploy] fatal: ${message}\\n`);\r\n process.exit(1);\r\n});\r\n","//? Pure validator for `services.config.ts` + `deploy.config.ts` content.\r\n//? Catches the classes of misconfiguration that otherwise only surface at\r\n//? runtime, often silently (empty service map served, fallback target\r\n//? unreachable, env var typo).\r\n//?\r\n//? Pure module so it can be unit-tested and reused outside the CLI — the\r\n//? CLI in `cli/validateDeploy.ts` is a thin wrapper that loads the config\r\n//? files, calls `validateDeploy(...)`, and prints results.\r\n\r\nimport type {\r\n DeployConfigShape,\r\n ServicesConfigShape,\r\n} from '@luckystack/core';\r\n\r\nexport type ValidationSeverity = 'error' | 'warning';\r\n\r\nexport interface ValidationFinding {\r\n severity: ValidationSeverity;\r\n code: string;\r\n message: string;\r\n /** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */\r\n location?: string;\r\n}\r\n\r\nexport interface ValidateDeployInput {\r\n services: ServicesConfigShape;\r\n deploy: DeployConfigShape;\r\n /**\r\n * Environment values to consult for `synchronizedEnvKeys` / `urlEnvKey`\r\n * presence checks. Defaults to `process.env`. Pass a fixture for tests.\r\n */\r\n env?: Record<string, string | undefined>;\r\n}\r\n\r\nexport interface ValidateDeployResult {\r\n ok: boolean;\r\n findings: ValidationFinding[];\r\n errorCount: number;\r\n warningCount: number;\r\n}\r\n\r\nexport const validateDeploy = ({\r\n services,\r\n deploy,\r\n env = process.env,\r\n}: ValidateDeployInput): ValidateDeployResult => {\r\n const findings: ValidationFinding[] = [];\r\n const serviceNames = new Set(Object.keys(services.services));\r\n\r\n //? Rule 1: every service is assigned to exactly one preset.\r\n const servicesInPresets = new Map<string, string[]>();\r\n for (const [presetKey, preset] of Object.entries(services.presets)) {\r\n for (const service of preset.services) {\r\n const existing = servicesInPresets.get(service) ?? [];\r\n existing.push(presetKey);\r\n servicesInPresets.set(service, existing);\r\n }\r\n }\r\n\r\n for (const service of serviceNames) {\r\n const owners = servicesInPresets.get(service) ?? [];\r\n if (owners.length === 0) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'service-unassigned',\r\n message: `Service \"${service}\" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n } else if (owners.length > 1) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'service-in-multiple-presets',\r\n message: `Service \"${service}\" is in multiple presets: ${owners.join(', ')}. A service may belong to exactly one preset.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n }\r\n }\r\n\r\n //? Rule 2: every preset references services that exist.\r\n for (const [presetKey, preset] of Object.entries(services.presets)) {\r\n for (const service of preset.services) {\r\n if (!serviceNames.has(service)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'preset-references-unknown-service',\r\n message: `Preset \"${presetKey}\" references unknown service \"${service}\". Add it to services.services or remove the reference.`,\r\n location: `services.config.ts > presets.${presetKey}.services`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 3: every binding's service matches an actual service.\r\n //? Rule 4: fallback env key must exist.\r\n //? Rule 5: redis/mongo resource keys must exist.\r\n //? Rule 6: fallback shared-resource invariant.\r\n const environments = deploy.environments ?? {};\r\n const resourceNames = new Set(Object.keys(deploy.resources));\r\n\r\n for (const [envKey, envDef] of Object.entries(environments)) {\r\n for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {\r\n if (!serviceNames.has(bindingService)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-references-unknown-service',\r\n message: `Environment \"${envKey}\" binds unknown service \"${bindingService}\". Either add it to services.services or remove the binding.`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings`,\r\n });\r\n }\r\n\r\n //? Every binding MUST declare an explicit port. The URL-spec default\r\n //? (80/443) is almost never what a multi-instance deploy wants — a\r\n //? missing port is far more likely a typo than a deliberate \"route to\r\n //? port 80\". The router enforces this at boot too (resolveTarget.ts).\r\n let parsed: URL | null = null;\r\n try {\r\n parsed = new URL(bindingUrl);\r\n } catch {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-invalid-url',\r\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding is not a valid URL: \"${bindingUrl}\".`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\r\n });\r\n }\r\n if (parsed && !parsed.port) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'binding-missing-port',\r\n message: `Environment \"${envKey}\" service \"${bindingService}\" binding \"${bindingUrl}\" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,\r\n location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`,\r\n });\r\n }\r\n }\r\n\r\n if (!resourceNames.has(envDef.redis)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-redis-resource',\r\n message: `Environment \"${envKey}\" references unknown redis resource \"${envDef.redis}\". Add it to deploy.resources.`,\r\n location: `deploy.config.ts > environments.${envKey}.redis`,\r\n });\r\n }\r\n if (!resourceNames.has(envDef.mongo)) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-mongo-resource',\r\n message: `Environment \"${envKey}\" references unknown mongo resource \"${envDef.mongo}\". Add it to deploy.resources.`,\r\n location: `deploy.config.ts > environments.${envKey}.mongo`,\r\n });\r\n }\r\n\r\n if (envDef.fallback) {\r\n const fallbackEnv = environments[envDef.fallback];\r\n //? `fallbackEnv` reads from `environments[key]` — TS treats this as\r\n //? always-defined (Record lookup), but the runtime can return undefined\r\n //? when the user names a fallback env that doesn't exist. The else\r\n //? branch reports that error.\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Record lookup can return undefined at runtime\r\n if (fallbackEnv) {\r\n if (fallbackEnv.redis !== envDef.redis) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'fallback-redis-mismatch',\r\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different redis resources (\"${envDef.redis}\" vs \"${fallbackEnv.redis}\"). Fallback envs must share the same redis resource key to keep the shared-Redis invariant.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n if (fallbackEnv.mongo !== envDef.mongo) {\r\n findings.push({\r\n severity: 'error',\r\n code: 'fallback-mongo-mismatch',\r\n message: `Environment \"${envKey}\" and its fallback \"${envDef.fallback}\" reference different mongo resources (\"${envDef.mongo}\" vs \"${fallbackEnv.mongo}\"). Fallback envs must share the same mongo resource key.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n } else {\r\n findings.push({\r\n severity: 'error',\r\n code: 'unknown-fallback-env',\r\n message: `Environment \"${envKey}\" has fallback \"${envDef.fallback}\" which does not exist in deploy.environments.`,\r\n location: `deploy.config.ts > environments.${envKey}.fallback`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 7+8: env var references must resolve at config time.\r\n for (const [resourceKey, resource] of Object.entries(deploy.resources)) {\r\n const urlValue = env[resource.urlEnvKey];\r\n if (urlValue === undefined || urlValue === '') {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'missing-resource-env-var',\r\n message: `Resource \"${resourceKey}\" expects env var \"${resource.urlEnvKey}\" but it is unset. The framework will still boot if the env is provided at runtime, but config-time tooling cannot verify the value.`,\r\n location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`,\r\n });\r\n }\r\n\r\n for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {\r\n const value = env[synchronizedKey];\r\n if (value === undefined || value === '') {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'missing-synchronized-env-var',\r\n message: `Resource \"${resourceKey}\" requires synchronized env var \"${synchronizedKey}\" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,\r\n location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`,\r\n });\r\n }\r\n }\r\n }\r\n\r\n //? Rule 9: services declared in services.config but not bound in any\r\n //? environment will never receive traffic. Warning, not error — that's a\r\n //? valid intermediate state during a rollout.\r\n for (const service of serviceNames) {\r\n const boundIn = Object.entries(environments).filter(([, env]) => service in env.bindings);\r\n if (boundIn.length === 0) {\r\n findings.push({\r\n severity: 'warning',\r\n code: 'service-bound-in-no-environment',\r\n message: `Service \"${service}\" is in services.services and assigned to a preset but never bound in any environment. Requests for this service will fall through to the missing-service handler.`,\r\n location: `services.config.ts > services.${service}`,\r\n });\r\n }\r\n }\r\n\r\n const errorCount = findings.filter((f) => f.severity === 'error').length;\r\n const warningCount = findings.filter((f) => f.severity === 'warning').length;\r\n\r\n return {\r\n ok: errorCount === 0,\r\n findings,\r\n errorCount,\r\n warningCount,\r\n };\r\n};\r\n"],"mappings":"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","names":["env"]}
|
|
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 importConfig = async (file: string, label: string): Promise<void> => {\n const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);\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":[]}
|