@luckystack/devkit 0.6.7 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/devkit",
3
- "version": "0.6.7",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -39,8 +39,8 @@
39
39
  }
40
40
  },
41
41
  "bin": {
42
- "luckystack-validate-deploy": "./dist/cli/validateDeploy.js",
43
- "luckystack-dev": "./dist/supervisor.js"
42
+ "luckystack-validate-deploy": "dist/cli/validateDeploy.js",
43
+ "luckystack-dev": "dist/supervisor.js"
44
44
  },
45
45
  "files": [
46
46
  "dist",
@@ -56,7 +56,7 @@
56
56
  "test": "vitest run"
57
57
  },
58
58
  "dependencies": {
59
- "@luckystack/core": "^0.6.7",
59
+ "@luckystack/core": "^0.7.1",
60
60
  "chokidar": "^5.0.0",
61
61
  "dotenv": "^17.0.0"
62
62
  },
@@ -69,5 +69,11 @@
69
69
  "@prisma/client": {
70
70
  "optional": true
71
71
  }
72
+ },
73
+ "devDependencies": {
74
+ "@mikro-orm/core": "^6.6.14",
75
+ "@mikro-orm/mongodb": "^6.6.14",
76
+ "drizzle-orm": "^0.45.2",
77
+ "reflect-metadata": "^0.2.2"
72
78
  }
73
79
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/validateDeploy.ts"],"sourcesContent":["//? 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(([, envDef]) => service in envDef.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":[]}