@warlock.js/core 5.23.2 → 5.25.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.
Files changed (207) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/bin/warlock.js +0 -0
  3. package/esm/application/application.d.mts +21 -0
  4. package/esm/application/application.d.mts.map +1 -1
  5. package/esm/application/application.mjs +29 -1
  6. package/esm/application/application.mjs.map +1 -1
  7. package/esm/application/boot-signal.d.mts +2 -1
  8. package/esm/application/boot-signal.d.mts.map +1 -1
  9. package/esm/application/boot-signal.mjs.map +1 -1
  10. package/esm/application/index.d.mts +1 -0
  11. package/esm/application/index.mjs +1 -0
  12. package/esm/application/roles.d.mts +72 -0
  13. package/esm/application/roles.d.mts.map +1 -0
  14. package/esm/application/roles.mjs +124 -0
  15. package/esm/application/roles.mjs.map +1 -0
  16. package/esm/cli/commands/add.command.mjs.map +1 -1
  17. package/esm/cli/commands/create-database.command.mjs.map +1 -1
  18. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  20. package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
  21. package/esm/cli/commands/generate/utils/path-resolver.mjs.map +1 -1
  22. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  23. package/esm/cli/commands/routes.command.mjs.map +1 -1
  24. package/esm/cli/commands/seed.command.mjs.map +1 -1
  25. package/esm/cli/commands/start-production.command.mjs +54 -5
  26. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  27. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  28. package/esm/cli/commands/storage-put.command.mjs.map +1 -1
  29. package/esm/cli/commands/typings-generator.command.mjs +17 -1
  30. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  31. package/esm/cli/commands/update.command.mjs.map +1 -1
  32. package/esm/cli/commands-loader.mjs.map +1 -1
  33. package/esm/config/config-loader.d.mts.map +1 -1
  34. package/esm/config/config-loader.mjs +3 -1
  35. package/esm/config/config-loader.mjs.map +1 -1
  36. package/esm/connectors/connectors-manager.mjs.map +1 -1
  37. package/esm/connectors/database-connector.mjs.map +1 -1
  38. package/esm/connectors/herald-connector.mjs.map +1 -1
  39. package/esm/connectors/http-connector.d.mts.map +1 -1
  40. package/esm/connectors/http-connector.mjs +7 -0
  41. package/esm/connectors/http-connector.mjs.map +1 -1
  42. package/esm/connectors/logger-connector.mjs.map +1 -1
  43. package/esm/connectors/mail-connector.mjs.map +1 -1
  44. package/esm/connectors/types.d.mts +8 -0
  45. package/esm/connectors/types.d.mts.map +1 -1
  46. package/esm/connectors/types.mjs.map +1 -1
  47. package/esm/database/create-database-action.mjs.map +1 -1
  48. package/esm/database/drop-tables-action.mjs.map +1 -1
  49. package/esm/database/migrate-action.mjs +2 -1
  50. package/esm/database/migrate-action.mjs.map +1 -1
  51. package/esm/database/models/database-log/database-log.mjs.map +1 -1
  52. package/esm/database/seed-command-action.mjs +2 -1
  53. package/esm/database/seed-command-action.mjs.map +1 -1
  54. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  55. package/esm/dev-server/development-server.mjs +2 -0
  56. package/esm/dev-server/development-server.mjs.map +1 -1
  57. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  58. package/esm/dev-server/layer-executor.mjs.map +1 -1
  59. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  60. package/esm/dev-server/module-loader.mjs +10 -5
  61. package/esm/dev-server/module-loader.mjs.map +1 -1
  62. package/esm/dev-server/register-installed-devtools.mjs +47 -0
  63. package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
  64. package/esm/dev-server/run-connector-typings.mjs +28 -0
  65. package/esm/dev-server/run-connector-typings.mjs.map +1 -0
  66. package/esm/dev-server/special-file-patterns.mjs +3 -1
  67. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  68. package/esm/dev-server/special-files-collector.d.mts +2 -2
  69. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  70. package/esm/dev-server/special-files-collector.mjs +7 -4
  71. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  72. package/esm/encryption/password.mjs.map +1 -1
  73. package/esm/generations/add-command.action.mjs +3 -1
  74. package/esm/generations/add-command.action.mjs.map +1 -1
  75. package/esm/generations/features/access.feature.mjs.map +1 -1
  76. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  77. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  78. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  79. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  80. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  81. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  82. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  83. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  84. package/esm/generations/features/ai.feature.mjs.map +1 -1
  85. package/esm/generations/features/devtools.feature.mjs +23 -0
  86. package/esm/generations/features/devtools.feature.mjs.map +1 -0
  87. package/esm/generations/features/herald.feature.mjs.map +1 -1
  88. package/esm/generations/features/image.feature.mjs.map +1 -1
  89. package/esm/generations/features/index.mjs +2 -0
  90. package/esm/generations/features/index.mjs.map +1 -1
  91. package/esm/generations/features/mail.feature.mjs.map +1 -1
  92. package/esm/generations/features/mongodb.feature.mjs.map +1 -1
  93. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  94. package/esm/generations/features/postgres.feature.mjs.map +1 -1
  95. package/esm/generations/features/react-email.feature.mjs.map +1 -1
  96. package/esm/generations/features/react.feature.mjs.map +1 -1
  97. package/esm/generations/features/redis.feature.mjs.map +1 -1
  98. package/esm/generations/features/s3.feature.mjs.map +1 -1
  99. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  100. package/esm/generations/features/ses.feature.mjs.map +1 -1
  101. package/esm/generations/features/socket.feature.mjs.map +1 -1
  102. package/esm/generations/features/test.feature.mjs +6 -2
  103. package/esm/generations/features/test.feature.mjs.map +1 -1
  104. package/esm/generations/features/types.mjs.map +1 -1
  105. package/esm/generations/features/web.feature.mjs.map +1 -1
  106. package/esm/http/database/RequestLog.mjs.map +1 -1
  107. package/esm/http/index.d.mts +2 -1
  108. package/esm/http/index.mjs +2 -1
  109. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  110. package/esm/http/middleware/inject-request-context.mjs +9 -1
  111. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  112. package/esm/http/request.d.mts +7 -0
  113. package/esm/http/request.d.mts.map +1 -1
  114. package/esm/http/request.mjs +8 -0
  115. package/esm/http/request.mjs.map +1 -1
  116. package/esm/http/response.d.mts +5 -5
  117. package/esm/http/response.d.mts.map +1 -1
  118. package/esm/http/response.mjs.map +1 -1
  119. package/esm/http/server-rebuilt.d.mts +20 -0
  120. package/esm/http/server-rebuilt.d.mts.map +1 -0
  121. package/esm/http/server-rebuilt.mjs +21 -0
  122. package/esm/http/server-rebuilt.mjs.map +1 -0
  123. package/esm/http/tracing/index.mjs +1 -1
  124. package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
  125. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
  126. package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
  127. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
  128. package/esm/http/tracing/tracing.type.d.mts +2 -1
  129. package/esm/http/tracing/tracing.type.d.mts.map +1 -1
  130. package/esm/http/types.d.mts +9 -0
  131. package/esm/http/types.d.mts.map +1 -1
  132. package/esm/image/image.mjs.map +1 -1
  133. package/esm/index.d.mts +4 -2
  134. package/esm/index.mjs +4 -2
  135. package/esm/loader/app-module-importer.d.mts +17 -0
  136. package/esm/loader/app-module-importer.d.mts.map +1 -0
  137. package/esm/loader/app-module-importer.mjs +20 -0
  138. package/esm/loader/app-module-importer.mjs.map +1 -0
  139. package/esm/logger/logger.mjs.map +1 -1
  140. package/esm/mail/events.d.mts +7 -0
  141. package/esm/mail/events.d.mts.map +1 -1
  142. package/esm/mail/events.mjs +7 -0
  143. package/esm/mail/events.mjs.map +1 -1
  144. package/esm/mail/send-mail.d.mts.map +1 -1
  145. package/esm/mail/send-mail.mjs +23 -2
  146. package/esm/mail/send-mail.mjs.map +1 -1
  147. package/esm/mail/types.d.mts +5 -1
  148. package/esm/mail/types.d.mts.map +1 -1
  149. package/esm/mail/types.mjs.map +1 -1
  150. package/esm/manifest/manifest-manager.mjs.map +1 -1
  151. package/esm/production/build-contributions.mjs +19 -3
  152. package/esm/production/build-contributions.mjs.map +1 -1
  153. package/esm/production/production-builder.mjs +28 -8
  154. package/esm/production/production-builder.mjs.map +1 -1
  155. package/esm/production/production-supervisor.mjs +6 -0
  156. package/esm/production/production-supervisor.mjs.map +1 -1
  157. package/esm/production/resolve-build-config.d.mts +1 -1
  158. package/esm/production/resolve-build-config.mjs +15 -0
  159. package/esm/production/resolve-build-config.mjs.map +1 -1
  160. package/esm/production/route-registration-child.mjs +14 -2
  161. package/esm/production/route-registration-child.mjs.map +1 -1
  162. package/esm/production/route-registration-snapshot.mjs +9 -2
  163. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  164. package/esm/resource/resource-field-builder.mjs +1 -1
  165. package/esm/resource/resource-field-builder.mjs.map +1 -1
  166. package/esm/resource/resource.d.mts +5 -0
  167. package/esm/resource/resource.d.mts.map +1 -1
  168. package/esm/resource/resource.mjs +17 -6
  169. package/esm/resource/resource.mjs.map +1 -1
  170. package/esm/router/router.d.mts.map +1 -1
  171. package/esm/router/router.mjs +1 -0
  172. package/esm/router/router.mjs.map +1 -1
  173. package/esm/router/types.d.mts +2 -1
  174. package/esm/router/types.d.mts.map +1 -1
  175. package/esm/tests/index.d.mts +3 -2
  176. package/esm/tests/index.mjs +3 -2
  177. package/esm/tests/vitest-setup.d.mts +16 -1
  178. package/esm/tests/vitest-setup.d.mts.map +1 -1
  179. package/esm/tests/vitest-setup.mjs +20 -2
  180. package/esm/tests/vitest-setup.mjs.map +1 -1
  181. package/esm/use-cases/use-case-events.mjs.map +1 -1
  182. package/esm/utils/is-test-file.mjs +15 -0
  183. package/esm/utils/is-test-file.mjs.map +1 -0
  184. package/esm/utils/normalized-path.d.mts +3 -1
  185. package/esm/utils/normalized-path.d.mts.map +1 -1
  186. package/esm/utils/normalized-path.mjs +4 -2
  187. package/esm/utils/normalized-path.mjs.map +1 -1
  188. package/esm/utils/paths.mjs.map +1 -1
  189. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  190. package/esm/validation/validateAll.mjs +9 -3
  191. package/esm/validation/validateAll.mjs.map +1 -1
  192. package/esm/warlock-config/default-configurations.mjs +1 -0
  193. package/esm/warlock-config/default-configurations.mjs.map +1 -1
  194. package/esm/warlock-config/types.d.mts +7 -0
  195. package/esm/warlock-config/types.d.mts.map +1 -1
  196. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  197. package/llms-full.txt +264 -139
  198. package/package.json +15 -11
  199. package/skills/configure-app/SKILL.md +2 -0
  200. package/skills/define-resource/SKILL.md +4 -0
  201. package/skills/request-tracing/SKILL.md +38 -15
  202. package/skills/run-app/SKILL.md +71 -34
  203. package/skills/send-mail/SKILL.md +55 -25
  204. package/skills/test-http/SKILL.md +19 -8
  205. package/skills/test-service/SKILL.md +61 -40
  206. package/skills/warlock-conventions/SKILL.md +26 -18
  207. package/skills/write-cli-command/SKILL.md +50 -45
@@ -1 +1 @@
1
- {"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { colors } from \"@mongez/copper\";\nimport { log } from \"@warlock.js/logger\";\nimport { Application } from \"../application\";\nimport { health } from \"../http/health\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { registerHttpPlugins } from \"../http/plugins\";\nimport { setHttpReadyReport } from \"../http/ready-report\";\nimport { isCanonicalPortValue, resolveBindPort } from \"../http/resolve-bind-port\";\nimport {\n closeServerWithTimeout,\n type FastifyInstance,\n getHttpServer,\n startHttpServer,\n} from \"../http/server\";\nimport { ensureFatalIsVisible } from \"../logger/fatal-visibility\";\nimport { router } from \"../router/router\";\nimport { type Environment } from \"../utils\";\nimport { setBaseUrl } from \"../utils/urls\";\nimport { container } from \"./../container\";\nimport { BaseConnector } from \"./base-connector\";\nimport { describeServerAddress } from \"./describe-server-address\";\nimport { readBoundPort } from \"./read-bound-port\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\nfunction environmentColor(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.magentaBright(environment);\n case \"test\":\n return colors.yellowBright(environment);\n case \"production\":\n return colors.greenBright(environment);\n default:\n return colors.white(environment);\n }\n}\n\n/**\n * Default time to wait for in-flight requests to drain on shutdown before the\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\n */\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\n\n/**\n * HTTP Connector\n * Manages HTTP server (Fastify) lifecycle\n */\nexport class HttpConnector extends BaseConnector {\n public readonly name = \"http\";\n public readonly priority = ConnectorPriority.HTTP;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger HTTP server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\n\n /**\n * Fastify Server instance\n */\n protected http?: FastifyInstance;\n\n /**\n * Whether someone downstream will render the combined ready block.\n *\n * Only the dev server does (`dev-server/ready-block.ts`), and only it can:\n * the block reports facts — boot duration, the web surface — that this\n * connector does not own and must not wait for. When it will, this connector\n * records and stays quiet; otherwise it narrates as it always has.\n *\n * Keyed on the runtime strategy rather than on `NODE_ENV`, because that is\n * what actually distinguishes \"a dev server is driving this boot\" from \"an\n * app happens to be running with development config\".\n *\n * `!isBooted` is the second half and it is not a detail. The block summarises\n * a BOOT. A `restart()` after boot — an edited `src/config/http.ts` — is an\n * EVENT, and there is no block coming to carry it, so suppressing the lines\n * there would rebind the port in total silence.\n */\n protected get rendersReadyBlock(): boolean {\n return Application.runtimeStrategy === \"development\" && !Application.isBooted;\n }\n\n /**\n * Boot the connector — construction only (create Fastify, register\n * plugins, populate container). Route scanning is deferred to\n * `start()` so it reads the router after app code has registered.\n */\n public async boot() {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) return;\n\n const port = httpConfig.port;\n log.info(\n `http`,\n \"connection\",\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\n );\n\n this.http = startHttpServer(httpConfig.serverOptions);\n\n container.set(\"http.server\", this.http);\n\n await registerHttpPlugins(this.http);\n\n const baseUrl = config.get(\"app.baseUrl\");\n\n // update base url\n setBaseUrl(baseUrl);\n\n // Liveness/readiness endpoints — registered on Fastify before start() scans\n // the app router, so they exist by the time the server listens.\n this.registerHealthRoutes();\n }\n\n /**\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\n * endpoints directly on the Fastify instance — infra routes, kept off the app\n * router so they're immune to HMR and never collide with route scanning. Opt\n * out via `http.health.enabled = false`; rename via `http.health.path` /\n * `http.health.readinessPath`.\n */\n private registerHealthRoutes(): void {\n if (!this.http) {\n return;\n }\n\n const healthConfig = config.get(\"http.health\");\n\n if (healthConfig?.enabled === false) {\n return;\n }\n\n const livenessPath = healthConfig?.path ?? \"/health\";\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\n\n this.http.get(livenessPath, async (_request: any, reply: any) => {\n const result = health.liveness();\n\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\n });\n\n this.http.get(readinessPath, async (_request: any, reply: any) => {\n const result = await health.readiness();\n\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\n });\n }\n\n /**\n * Initialize HTTP server — bind app-registered routes to Fastify\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\n * app code without losing routes.\n */\n public async start(): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig || !this.http) return;\n\n // Read once. `Application.isBooted` flips during the same boot this method\n // belongs to, and a predicate that answered differently before and after\n // `listen()` would print half a report.\n const deferToReadyBlock = this.rendersReadyBlock;\n\n if (Application.runtimeStrategy === \"development\") {\n router.scanDevServer(this.http);\n } else {\n router.scan(this.http);\n }\n\n // Surface the route table as a readiness signal + a boot log, so an empty\n // or partial route surface (e.g. a route module that failed to register) is\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\n // keyed by name, so re-running on restart is idempotent.\n health.addRoutesRegisteredCheck(() => router.routeCount());\n\n const routeCount = router.routeCount();\n\n // In development this fact is one line of the single ready block rendered\n // after boot completes (`dev-server/ready-block.ts`); printing it here as\n // well would put the same number on screen twice, in two formats. Outside\n // development the line is load-bearing — a supervisor or CI log greps it —\n // so it stays exactly as it was.\n if (!deferToReadyBlock) {\n log.info(\"http\", \"routes\", `${routeCount} route(s) registered`);\n }\n\n // Resolved ONCE, here, at the top of the bind sequence: `httpConfig.port`\n // may still be a string that round-tripped `env()`'s number coercion\n // (`\"03999\"`, `\" 3999\"`, `\"1e3\"`) — `net.Server.listen({ port })` binds\n // all of those without complaint, silently, sometimes to the WRONG port\n // (`\"1e3\"` -> 1000). Every downstream use of the port — the preflight\n // probe and `listen()` itself — must agree on the same resolved number.\n //\n // This is still the port ASKED for, not the port BOUND — `port: 0` asks\n // for \"any free port\" and this stays `0` for that case. What gets\n // published to `warlock:ready` is read back from `listen()`'s resolved\n // address further down, never this value.\n const configuredPort = resolveBindPort(httpConfig.port);\n\n if (!isCanonicalPortValue(httpConfig.port, configuredPort)) {\n log.info(\n \"http\",\n \"connection\",\n `Configured http.port ${JSON.stringify(httpConfig.port)} normalised to ${configuredPort}.`,\n );\n }\n\n try {\n // Preflight the bind before `listen()` so a collision surfaces as the\n // instruction in `PortInUseError.message` (which names the port) instead\n // of a raw `EADDRINUSE` from deep inside Fastify/libuv.\n //\n // Port 0 is exempt: it asks the OS for a free port, so probing it binds\n // some unrelated ephemeral port and \"passes\" while proving nothing about\n // the port this server will actually take.\n if (configuredPort !== 0) {\n await assertPortIsAvailable(configuredPort, httpConfig.host || \"localhost\");\n }\n\n // `listen()` RESOLVES with the address it actually bound — which is the\n // only address worth announcing. See `describe-server-address.ts` for why\n // announcing `app.baseUrl` here instead was a defect rather than a\n // shortcut.\n const boundAddress = await this.http.listen({\n port: configuredPort,\n host: httpConfig.host || \"localhost\",\n });\n\n // Read the ACTUALLY-bound port back out of what `listen()` resolved,\n // rather than republishing `configuredPort`. With `http.port: 0` the OS\n // picks the port, so `configuredPort` stays `0` forever — publishing it\n // instead of the real port is exactly the \"Server ready at a URL that\n // gives connection refused\" defect `describe-server-address.ts` already\n // fixed for the display string. `boundAddress` always carries an\n // explicit port (Fastify never omits one), so `configuredPort` is only\n // a defensive fallback for an address this parse cannot make sense of.\n const boundPort = readBoundPort(boundAddress, configuredPort);\n\n Application.setServedPort(boundPort);\n\n const address = describeServerAddress(\n boundAddress,\n config.get(\"app.baseUrl\"),\n // Passed because `listen()` does not report it back: Fastify resolves a\n // `0.0.0.0` bind to `http://127.0.0.1:<port>`, so the wildcard is only\n // recoverable from what we ASKED for.\n httpConfig.host || \"localhost\",\n );\n\n // Recorded unconditionally: the ready block reads it in development, and\n // in every other mode it is simply the one place that knows what was\n // bound, which is worth having whether or not anything renders it.\n setHttpReadyReport({\n boundAddress: address.boundAddress,\n url: address.url,\n wildcardBind: address.wildcardBind,\n port: boundPort,\n routeCount,\n publicUrl: address.publicUrl,\n });\n\n if (!deferToReadyBlock) {\n log.success(`http`, \"connection\", address.ready);\n\n if (address.publicUrl) {\n log.info(`http`, \"connection\", address.publicUrl);\n }\n }\n\n // NEVER conditional on the block. A base URL pointing at a port nothing\n // is listening on is the single most misleading state this connector can\n // be in, and a warning a status block swallows is a warning nobody reads\n // — so it is logged where it lands ABOVE the block, in both modes.\n if (address.warning) {\n log.warn(`http`, \"connection\", address.warning);\n }\n } catch (error) {\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\n // async channels before the process exits.\n await log.fatal(\"http\", \"connection\", error);\n\n // …and `log.fatal` reaches NOBODY when the app's log config declares no\n // terminal channel — the ordinary production shape (`production: {\n // channels: [new FileLog()] }`), and the shape of every app with no `log`\n // config at all, since `LoggerConnector.start()` then returns before\n // `log.configure`. That is how an `EADDRINUSE` at boot became a process\n // that exited 1 in total silence: the error was constructed, thrown and\n // caught exactly as designed, and then written to zero channels. stderr\n // is the one destination that is always there.\n ensureFatalIsVisible(error);\n\n await log.flush();\n\n process.exit(1); // stop the process, exit with error\n }\n\n this.active = true;\n }\n\n /**\n * Restart — needs a fresh Fastify instance since `start()` now\n * re-runs `router.scan()`, and re-scanning the same Fastify would\n * register duplicate route handlers.\n */\n public async restart(): Promise<void> {\n await this.shutdown();\n await this.boot();\n await this.start();\n }\n\n /**\n * Shutdown HTTP server\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n const server = getHttpServer();\n\n if (server) {\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\n const drained = await closeServerWithTimeout(server, timeout);\n\n if (!drained) {\n log.warn(\n \"http\",\n \"shutdown\",\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\n );\n }\n }\n\n this.active = false;\n }\n\n /**\n * Override shouldRestart to handle routes.ts specially\n * routes.ts changes should NOT restart the server (use HMR instead)\n * Now receives config file paths directly from layer-executor\n */\n public shouldRestart(changedFiles: string[]): boolean {\n // Only restart for config changes, not routes\n return changedFiles.some((file) => {\n const relativePath = file.replace(/\\\\/g, \"/\");\n return this.watchedFiles.includes(relativePath);\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { colors } from \"@mongez/copper\";\nimport { log } from \"@warlock.js/logger\";\nimport { Application, getRoles, servesHttp } from \"../application\";\nimport { health } from \"../http/health\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { registerHttpPlugins } from \"../http/plugins\";\nimport { notifyHttpServerRebuilt } from \"../http/server-rebuilt\";\nimport { setHttpReadyReport } from \"../http/ready-report\";\nimport { isCanonicalPortValue, resolveBindPort } from \"../http/resolve-bind-port\";\nimport {\n closeServerWithTimeout,\n type FastifyInstance,\n getHttpServer,\n startHttpServer,\n} from \"../http/server\";\nimport { ensureFatalIsVisible } from \"../logger/fatal-visibility\";\nimport { router } from \"../router/router\";\nimport { type Environment } from \"../utils\";\nimport { setBaseUrl } from \"../utils/urls\";\nimport { container } from \"./../container\";\nimport { BaseConnector } from \"./base-connector\";\nimport { describeServerAddress } from \"./describe-server-address\";\nimport { readBoundPort } from \"./read-bound-port\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\nfunction environmentColor(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.magentaBright(environment);\n case \"test\":\n return colors.yellowBright(environment);\n case \"production\":\n return colors.greenBright(environment);\n default:\n return colors.white(environment);\n }\n}\n\n/**\n * Default time to wait for in-flight requests to drain on shutdown before the\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\n */\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\n\n/**\n * HTTP Connector\n * Manages HTTP server (Fastify) lifecycle\n */\nexport class HttpConnector extends BaseConnector {\n public readonly name = \"http\";\n public readonly priority = ConnectorPriority.HTTP;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger HTTP server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\n\n /**\n * Fastify Server instance\n */\n protected http?: FastifyInstance;\n\n /**\n * Whether someone downstream will render the combined ready block.\n *\n * Only the dev server does (`dev-server/ready-block.ts`), and only it can:\n * the block reports facts — boot duration, the web surface — that this\n * connector does not own and must not wait for. When it will, this connector\n * records and stays quiet; otherwise it narrates as it always has.\n *\n * Keyed on the runtime strategy rather than on `NODE_ENV`, because that is\n * what actually distinguishes \"a dev server is driving this boot\" from \"an\n * app happens to be running with development config\".\n *\n * `!isBooted` is the second half and it is not a detail. The block summarises\n * a BOOT. A `restart()` after boot — an edited `src/config/http.ts` — is an\n * EVENT, and there is no block coming to carry it, so suppressing the lines\n * there would rebind the port in total silence.\n */\n protected get rendersReadyBlock(): boolean {\n return Application.runtimeStrategy === \"development\" && !Application.isBooted;\n }\n\n /**\n * Boot the connector — construction only (create Fastify, register\n * plugins, populate container). Route scanning is deferred to\n * `start()` so it reads the router after app code has registered.\n */\n public async boot() {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) return;\n\n const port = httpConfig.port;\n log.info(\n `http`,\n \"connection\",\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\n );\n\n this.http = startHttpServer(httpConfig.serverOptions);\n\n container.set(\"http.server\", this.http);\n\n await registerHttpPlugins(this.http);\n\n const baseUrl = config.get(\"app.baseUrl\");\n\n // update base url\n setBaseUrl(baseUrl);\n\n // Liveness/readiness endpoints — registered on Fastify before start() scans\n // the app router, so they exist by the time the server listens.\n this.registerHealthRoutes();\n }\n\n /**\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\n * endpoints directly on the Fastify instance — infra routes, kept off the app\n * router so they're immune to HMR and never collide with route scanning. Opt\n * out via `http.health.enabled = false`; rename via `http.health.path` /\n * `http.health.readinessPath`.\n */\n private registerHealthRoutes(): void {\n if (!this.http) {\n return;\n }\n\n const healthConfig = config.get(\"http.health\");\n\n if (healthConfig?.enabled === false) {\n return;\n }\n\n const livenessPath = healthConfig?.path ?? \"/health\";\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\n\n this.http.get(livenessPath, async (_request: any, reply: any) => {\n const result = health.liveness();\n\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\n });\n\n this.http.get(readinessPath, async (_request: any, reply: any) => {\n const result = await health.readiness();\n\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\n });\n }\n\n /**\n * Initialize HTTP server — bind app-registered routes to Fastify\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\n * app code without losing routes.\n */\n public async start(): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig || !this.http) return;\n\n // Neither `api` nor `web` is active — an http-only listener would serve\n // nothing this process is meant to answer, so it never binds. `worker`\n // is the only role that reaches here today; named generically so a\n // future role composition still reads correctly.\n if (!servesHttp()) {\n log.info(\"http\", \"connection\", `http: not started (role: ${[...getRoles()].join(\", \")})`);\n\n return;\n }\n\n // Read once. `Application.isBooted` flips during the same boot this method\n // belongs to, and a predicate that answered differently before and after\n // `listen()` would print half a report.\n const deferToReadyBlock = this.rendersReadyBlock;\n\n if (Application.runtimeStrategy === \"development\") {\n router.scanDevServer(this.http);\n } else {\n router.scan(this.http);\n }\n\n // Surface the route table as a readiness signal + a boot log, so an empty\n // or partial route surface (e.g. a route module that failed to register) is\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\n // keyed by name, so re-running on restart is idempotent.\n health.addRoutesRegisteredCheck(() => router.routeCount());\n\n const routeCount = router.routeCount();\n\n // In development this fact is one line of the single ready block rendered\n // after boot completes (`dev-server/ready-block.ts`); printing it here as\n // well would put the same number on screen twice, in two formats. Outside\n // development the line is load-bearing — a supervisor or CI log greps it —\n // so it stays exactly as it was.\n if (!deferToReadyBlock) {\n log.info(\"http\", \"routes\", `${routeCount} route(s) registered`);\n }\n\n // Resolved ONCE, here, at the top of the bind sequence: `httpConfig.port`\n // may still be a string that round-tripped `env()`'s number coercion\n // (`\"03999\"`, `\" 3999\"`, `\"1e3\"`) — `net.Server.listen({ port })` binds\n // all of those without complaint, silently, sometimes to the WRONG port\n // (`\"1e3\"` -> 1000). Every downstream use of the port — the preflight\n // probe and `listen()` itself — must agree on the same resolved number.\n //\n // This is still the port ASKED for, not the port BOUND — `port: 0` asks\n // for \"any free port\" and this stays `0` for that case. What gets\n // published to `warlock:ready` is read back from `listen()`'s resolved\n // address further down, never this value.\n const configuredPort = resolveBindPort(httpConfig.port);\n\n if (!isCanonicalPortValue(httpConfig.port, configuredPort)) {\n log.info(\n \"http\",\n \"connection\",\n `Configured http.port ${JSON.stringify(httpConfig.port)} normalised to ${configuredPort}.`,\n );\n }\n\n try {\n // Preflight the bind before `listen()` so a collision surfaces as the\n // instruction in `PortInUseError.message` (which names the port) instead\n // of a raw `EADDRINUSE` from deep inside Fastify/libuv.\n //\n // Port 0 is exempt: it asks the OS for a free port, so probing it binds\n // some unrelated ephemeral port and \"passes\" while proving nothing about\n // the port this server will actually take.\n if (configuredPort !== 0) {\n await assertPortIsAvailable(configuredPort, httpConfig.host || \"localhost\");\n }\n\n // `listen()` RESOLVES with the address it actually bound — which is the\n // only address worth announcing. See `describe-server-address.ts` for why\n // announcing `app.baseUrl` here instead was a defect rather than a\n // shortcut.\n const boundAddress = await this.http.listen({\n port: configuredPort,\n host: httpConfig.host || \"localhost\",\n });\n\n // Read the ACTUALLY-bound port back out of what `listen()` resolved,\n // rather than republishing `configuredPort`. With `http.port: 0` the OS\n // picks the port, so `configuredPort` stays `0` forever — publishing it\n // instead of the real port is exactly the \"Server ready at a URL that\n // gives connection refused\" defect `describe-server-address.ts` already\n // fixed for the display string. `boundAddress` always carries an\n // explicit port (Fastify never omits one), so `configuredPort` is only\n // a defensive fallback for an address this parse cannot make sense of.\n const boundPort = readBoundPort(boundAddress, configuredPort);\n\n Application.setServedPort(boundPort);\n\n const address = describeServerAddress(\n boundAddress,\n config.get(\"app.baseUrl\"),\n // Passed because `listen()` does not report it back: Fastify resolves a\n // `0.0.0.0` bind to `http://127.0.0.1:<port>`, so the wildcard is only\n // recoverable from what we ASKED for.\n httpConfig.host || \"localhost\",\n );\n\n // Recorded unconditionally: the ready block reads it in development, and\n // in every other mode it is simply the one place that knows what was\n // bound, which is worth having whether or not anything renders it.\n setHttpReadyReport({\n boundAddress: address.boundAddress,\n url: address.url,\n wildcardBind: address.wildcardBind,\n port: boundPort,\n routeCount,\n publicUrl: address.publicUrl,\n });\n\n if (!deferToReadyBlock) {\n log.success(`http`, \"connection\", address.ready);\n\n if (address.publicUrl) {\n log.info(`http`, \"connection\", address.publicUrl);\n }\n }\n\n // NEVER conditional on the block. A base URL pointing at a port nothing\n // is listening on is the single most misleading state this connector can\n // be in, and a warning a status block swallows is a warning nobody reads\n // — so it is logged where it lands ABOVE the block, in both modes.\n if (address.warning) {\n log.warn(`http`, \"connection\", address.warning);\n }\n } catch (error) {\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\n // async channels before the process exits.\n await log.fatal(\"http\", \"connection\", error);\n\n // …and `log.fatal` reaches NOBODY when the app's log config declares no\n // terminal channel — the ordinary production shape (`production: {\n // channels: [new FileLog()] }`), and the shape of every app with no `log`\n // config at all, since `LoggerConnector.start()` then returns before\n // `log.configure`. That is how an `EADDRINUSE` at boot became a process\n // that exited 1 in total silence: the error was constructed, thrown and\n // caught exactly as designed, and then written to zero channels. stderr\n // is the one destination that is always there.\n ensureFatalIsVisible(error);\n\n await log.flush();\n\n process.exit(1); // stop the process, exit with error\n }\n\n this.active = true;\n }\n\n /**\n * Restart — needs a fresh Fastify instance since `start()` now\n * re-runs `router.scan()`, and re-scanning the same Fastify would\n * register duplicate route handlers.\n */\n public async restart(): Promise<void> {\n await this.shutdown();\n await this.boot();\n\n // Subscribers (the web connector's Vite middleware) re-attach to the new\n // instance here: after it exists, before `start()` listens.\n if (this.http) notifyHttpServerRebuilt(this.http);\n\n await this.start();\n }\n\n /**\n * Shutdown HTTP server\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n const server = getHttpServer();\n\n if (server) {\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\n const drained = await closeServerWithTimeout(server, timeout);\n\n if (!drained) {\n log.warn(\n \"http\",\n \"shutdown\",\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\n );\n }\n }\n\n this.active = false;\n }\n\n /**\n * Override shouldRestart to handle routes.ts specially\n * routes.ts changes should NOT restart the server (use HMR instead)\n * Now receives config file paths directly from layer-executor\n */\n public shouldRestart(changedFiles: string[]): boolean {\n // Only restart for config changes, not routes\n return changedFiles.some((file) => {\n const relativePath = file.replace(/\\\\/g, \"/\");\n return this.watchedFiles.includes(relativePath);\n });\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"logger-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/logger-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { setLogConfigurations } from \"../logger\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\n/**\r\n * Logger Connector\r\n * Manages logger lifecycle and ensures synchronous flushing on termination\r\n */\r\nexport class LoggerConnector extends BaseConnector {\r\n public readonly name = \"logger\";\r\n public readonly priority = ConnectorPriority.LOGGER;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\r\n\r\n /**\r\n * Files that trigger logger restart\r\n */\r\n protected readonly watchedFiles = [\"src/config/log.ts\", \"src/config/log.tsx\"];\r\n\r\n /**\r\n * Initialize logger configurations\r\n */\r\n public async start(): Promise<void> {\r\n const logConfig = config.get(\"log\");\r\n\r\n if (!logConfig) {\r\n return;\r\n }\r\n\r\n try {\r\n setLogConfigurations(logConfig);\r\n this.active = true;\r\n } catch (error) {\r\n console.error(\"Failed to initialize logger:\", error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Shutdown logger and flush messages synchronously\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n try {\r\n log.flushSync();\r\n this.active = false;\r\n } catch (error) {\r\n console.error(\"Failed to flush logger:\", error);\r\n throw error;\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;AAUA,IAAa,kBAAb,cAAqC,cAAc;;;cAC1B;;;sBAOW,CAAC,qBAAqB,oBAAoB;;;;;CAK5E,MAAa,QAAuB;EAClC,MAAM,YAAY,OAAO,IAAI,KAAK;EAElC,IAAI,CAAC,WACH;EAGF,IAAI;GACF,qBAAqB,SAAS;GAC9B,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,IAAI;GACF,IAAI,UAAU;GACd,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,2BAA2B,KAAK;GAC9C,MAAM;EACR;CACF;AACF"}
1
+ {"version":3,"file":"logger-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/logger-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { setLogConfigurations } from \"../logger\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Logger Connector\n * Manages logger lifecycle and ensures synchronous flushing on termination\n */\nexport class LoggerConnector extends BaseConnector {\n public readonly name = \"logger\";\n public readonly priority = ConnectorPriority.LOGGER;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger logger restart\n */\n protected readonly watchedFiles = [\"src/config/log.ts\", \"src/config/log.tsx\"];\n\n /**\n * Initialize logger configurations\n */\n public async start(): Promise<void> {\n const logConfig = config.get(\"log\");\n\n if (!logConfig) {\n return;\n }\n\n try {\n setLogConfigurations(logConfig);\n this.active = true;\n } catch (error) {\n console.error(\"Failed to initialize logger:\", error);\n throw error;\n }\n }\n\n /**\n * Shutdown logger and flush messages synchronously\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n try {\n log.flushSync();\n this.active = false;\n } catch (error) {\n console.error(\"Failed to flush logger:\", error);\n throw error;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAUA,IAAa,kBAAb,cAAqC,cAAc;;;cAC1B;;;sBAOW,CAAC,qBAAqB,oBAAoB;;;;;CAK5E,MAAa,QAAuB;EAClC,MAAM,YAAY,OAAO,IAAI,KAAK;EAElC,IAAI,CAAC,WACH;EAGF,IAAI;GACF,qBAAqB,SAAS;GAC9B,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,IAAI;GACF,IAAI,UAAU;GACd,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,2BAA2B,KAAK;GAC9C,MAAM;EACR;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"mail-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/mail-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { closeAllMailers, setMailConfigurations } from \"../mail\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\n/**\r\n * Mailer Connector\r\n * Manages mailer lifecycle and ensures graceful pool shutdown\r\n */\r\nexport class MailerConnector extends BaseConnector {\r\n public readonly name = \"mailer\";\r\n public readonly priority = ConnectorPriority.MAILER;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\r\n\r\n /**\r\n * Files that trigger mailer restart\r\n */\r\n protected readonly watchedFiles = [\".env\", \"src/config/mail.ts\", \"src/config/mail.tsx\"];\r\n\r\n /**\r\n * Initialize mailer configurations\r\n */\r\n public async start(): Promise<void> {\r\n const mailConfig = config.get(\"mail\");\r\n\r\n if (!mailConfig) {\r\n return;\r\n }\r\n\r\n try {\r\n setMailConfigurations(mailConfig);\r\n this.active = true;\r\n } catch (error) {\r\n console.error(\"Failed to initialize mailer:\", error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Shutdown mailer pool\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n try {\r\n closeAllMailers();\r\n this.active = false;\r\n } catch (error) {\r\n console.error(\"Failed to close all mailers:\", error);\r\n throw error;\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;AASA,IAAa,kBAAb,cAAqC,cAAc;;;cAC1B;;;sBAOW;GAAC;GAAQ;GAAsB;EAAqB;;;;;CAKtF,MAAa,QAAuB;EAClC,MAAM,aAAa,OAAO,IAAI,MAAM;EAEpC,IAAI,CAAC,YACH;EAGF,IAAI;GACF,sBAAsB,UAAU;GAChC,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,IAAI;GACF,gBAAgB;GAChB,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;AACF"}
1
+ {"version":3,"file":"mail-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/mail-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { closeAllMailers, setMailConfigurations } from \"../mail\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Mailer Connector\n * Manages mailer lifecycle and ensures graceful pool shutdown\n */\nexport class MailerConnector extends BaseConnector {\n public readonly name = \"mailer\";\n public readonly priority = ConnectorPriority.MAILER;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger mailer restart\n */\n protected readonly watchedFiles = [\".env\", \"src/config/mail.ts\", \"src/config/mail.tsx\"];\n\n /**\n * Initialize mailer configurations\n */\n public async start(): Promise<void> {\n const mailConfig = config.get(\"mail\");\n\n if (!mailConfig) {\n return;\n }\n\n try {\n setMailConfigurations(mailConfig);\n this.active = true;\n } catch (error) {\n console.error(\"Failed to initialize mailer:\", error);\n throw error;\n }\n }\n\n /**\n * Shutdown mailer pool\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n try {\n closeAllMailers();\n this.active = false;\n } catch (error) {\n console.error(\"Failed to close all mailers:\", error);\n throw error;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AASA,IAAa,kBAAb,cAAqC,cAAc;;;cAC1B;;;sBAOW;GAAC;GAAQ;GAAsB;EAAqB;;;;;CAKtF,MAAa,QAAuB;EAClC,MAAM,aAAa,OAAO,IAAI,MAAM;EAEpC,IAAI,CAAC,YACH;EAGF,IAAI;GACF,sBAAsB,UAAU;GAChC,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,IAAI;GACF,gBAAgB;GAChB,KAAK,SAAS;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,KAAK;GACnD,MAAM;EACR;CACF;AACF"}
@@ -125,6 +125,14 @@ type ConnectorBuildContribution = {
125
125
  * Runs after esbuild and before `.warlock/production` is removed.
126
126
  */
127
127
  emit?(context: ConnectorBuildContext): Promise<void> | void;
128
+ /**
129
+ * Write the connector's generated `.d.ts` files under `.warlock/typings`.
130
+ *
131
+ * Runs from `warlock generate.typings` with the same context `generate`
132
+ * receives (`productionDir` is not written to), so a clean checkout can
133
+ * typecheck before any build has run.
134
+ */
135
+ typings?(context: ConnectorBuildContext): Promise<void> | void;
128
136
  };
129
137
  type ConnectorName = "logger" | "mailer" | "http" | "database" | "cache" | "storage" | "herald" | "socket" | "notifications" | "access" | "ai" | (string & {});
130
138
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"mappings":";;;;;;;AAQA;;UAAiB,SAAA;EAIA;;;EAAA,SAAN,IAAA,EAAM,aAAA;EAmCJ;;;;EAAA,SA7BF,QAAA;EANA;;;;EAAA,SAYA,cAAA,EAAgB,uBAAA;EAKzB;;;EAAA,QAAA;EAYS;;;;EANT,IAAA,WAAe,OAAA;EAyBf;;;;EAnBA,KAAA,IAAS,OAAA;EA8BkC;AAO7C;;;EA/BE,OAAA,IAAW,OAAA;EAiCX;;;;EA3BA,QAAA,IAAY,OAAA;EAiCuB;;AAAa;AAalD;;EAvCE,aAAA,CAAc,YAAA;EAuCoB;AACtB;AAOd;;;;;;;EARoC,SA5BzB,KAAA,GAAQ,0BAAA;AAAA;AA+DnB;;;;AAAA,KAxDY,qBAAA;EAgEP,uEA9DH,aAAA,UAqEe;EAnEf,OAAA,UAmE8C;EAjE9C,OAAA,EAAS,mBAAA,EAwDT;EAAA,SAtDS,cAAA,YAA0B,aAAa;AAAA;;;;;;;;;AA+DF;AAGhD;KArDY,qBAAA,GAAwB,IAAI,CACtC,YAAA;;;AAoDuB;KA7Cb,4BAAA;EAoEuB;;;AAE7B;AAMN;;;EApEE,YAAA,aAqEA;EAnEA,OAAA,GAAU,qBAAqB;AAAA;;;;;;;;;;AA6E7B;;;;;KA5DQ,0BAAA;;;;;;EAMV,QAAA,EACE,OAAA,EAAS,qBAAA,GACR,OAAA,CAAQ,4BAAA,WAAuC,4BAAA;;;;;;EAOlD,IAAA,EAAM,OAAA,EAAS,qBAAA,GAAwB,OAAA;AAAA;AAAA,KAG7B,aAAA;;;;;;;;;;aAuBA,uBAAA;EACV,KAAA;EACA,IAAI;AAAA;;;;aAMM,iBAAA;EACV,MAAA;EACA,MAAA;EACA,QAAA;EACA,YAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,aAAA;EACA,MAAA;EACA,EAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"mappings":";;;;;;;AAQA;;UAAiB,SAAA;EAIA;;;EAAA,SAAN,IAAA,EAAM,aAAA;EAmCJ;;;;EAAA,SA7BF,QAAA;EANA;;;;EAAA,SAYA,cAAA,EAAgB,uBAAA;EAKzB;;;EAAA,QAAA;EAYS;;;;EANT,IAAA,WAAe,OAAA;EAyBf;;;;EAnBA,KAAA,IAAS,OAAA;EA8BkC;AAO7C;;;EA/BE,OAAA,IAAW,OAAA;EAiCX;;;;EA3BA,QAAA,IAAY,OAAA;EAiCuB;;AAAa;AAalD;;EAvCE,aAAA,CAAc,YAAA;EAuCoB;AACtB;AAOd;;;;;;;EARoC,SA5BzB,KAAA,GAAQ,0BAAA;AAAA;AA+DnB;;;;AAAA,KAxDY,qBAAA;EAgEP,uEA9DH,aAAA,UAqEe;EAnEf,OAAA,UA4EkB;EA1ElB,OAAA,EAAS,mBAAA,EA0EwC;EAAA,SAxExC,cAAA,YAA0B,aAAa;AAAA;;;;;;;;;;;KAatC,qBAAA,GAAwB,IAAI,CACtC,YAAA;;;;KAOU,4BAAA;EAsDA;;;;AAAa;AAuBzB;;EArEE,YAAA,aAsEA;EApEA,OAAA,GAAU,qBAAqB;AAAA;;;;;;;;;;;;;;;KAiBrB,0BAAA;EAqER;;;;;EA/DF,QAAA,EACE,OAAA,EAAS,qBAAA,GACR,OAAA,CAAQ,4BAAA,WAAuC,4BAAA;;;;;;EAOlD,IAAA,EAAM,OAAA,EAAS,qBAAA,GAAwB,OAAA;;;;;;;;EASvC,OAAA,EAAS,OAAA,EAAS,qBAAA,GAAwB,OAAA;AAAA;AAAA,KAGhC,aAAA;;;;;;;;;;aAuBA,uBAAA;EACV,KAAA;EACA,IAAI;AAAA;;;;aAMM,iBAAA;EACV,MAAA;EACA,MAAA;EACA,QAAA;EACA,YAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,aAAA;EACA,MAAA;EACA,EAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"sourcesContent":["import type { BuildOptions } from \"esbuild\";\nimport type { NamedApiRoute } from \"../router/types\";\nimport type { ResolvedBuildConfig } from \"../production/resolve-build-config\";\n\n/**\n * Connector Interface\n * All service connectors (Database, HTTP, Cache, etc.) must implement this interface\n */\nexport interface Connector {\n /**\n * Unique name of the connector\n */\n readonly name: ConnectorName;\n\n /**\n * Priority for initialization order\n * Lower numbers initialize first (e.g., Database: 1, HTTP: 3)\n */\n readonly priority: number;\n\n /**\n * Phase the connector boots in, relative to user code.\n * See `ConnectorLifecyclePhase` for semantics.\n */\n readonly lifecyclePhase: ConnectorLifecyclePhase;\n\n /**\n * Whether the connector is currently active/connected\n */\n isActive(): boolean;\n\n /**\n * Boot the connector\n * Called during server startup before starting the connector\n */\n boot(): void | Promise<void>;\n\n /**\n * Initialize the connector\n * Called during server startup\n */\n start(): Promise<void>;\n\n /**\n * Restart the connector\n * Called when watched files change\n */\n restart(): Promise<void>;\n\n /**\n * Gracefully shutdown the connector\n * Called during server shutdown\n */\n shutdown(): Promise<void>;\n\n /**\n * Determine if this connector needs restart based on changed files\n * @param changedFiles Array of relative file paths that changed\n * @returns true if connector should restart\n */\n shouldRestart(changedFiles: string[]): boolean;\n\n /**\n * What this connector contributes to `warlock build`.\n *\n * Read STATICALLY from `warlock.config.ts > connectors` — `warlock build`\n * never calls `boot()`/`start()`, so nothing here may assume a running\n * connector. The same `connectors` key drives both halves of a connector's\n * life — build-time contribution and runtime boot; see\n * {@link ConnectorBuildContribution}.\n */\n readonly build?: ConnectorBuildContribution;\n}\n\n/**\n * What a build contribution is handed. Everything it needs to write files\n * and shape the bundle, and nothing that would let it reach the runtime.\n */\nexport type ConnectorBuildContext = {\n /** Absolute path to `.warlock/production` — deleted after the build */\n productionDir: string;\n /** Absolute path to the application root (where `package.json` lives) */\n appRoot: string;\n /** The build config after framework defaults and user overrides */\n options: ResolvedBuildConfig;\n /** Fresh registration-only snapshot, present only when the Web connector is configured. */\n readonly namedApiRoutes?: readonly NamedApiRoute[];\n};\n\n/**\n * The esbuild options a contributor may patch.\n *\n * Deliberately a narrow `Pick`, not `Partial<BuildOptions>`: `entryPoints`,\n * `tsconfig` and `tsconfigRaw` stay forbidden here for the same reason they\n * are forbidden in the user's `build` block — they re-compile every file in\n * the bundle, workspace packages included (see `ForbiddenBuildOptionError`).\n * A contributor reaching them would reintroduce the failure the user-level\n * guard exists to prevent.\n */\nexport type ConnectorEsbuildPatch = Pick<\n BuildOptions,\n \"jsx\" | \"jsxImportSource\" | \"define\" | \"external\" | \"loader\" | \"plugins\"\n>;\n\n/**\n * What `generate` may hand back to the builder.\n */\nexport type ConnectorBuildGenerateResult = {\n /**\n * Lines appended verbatim to section 4 of the generated entry, AFTER the\n * `./routes` import — page registration must see the routes app code\n * collected, mirroring dev's ordering.\n *\n * @example ['await import(\"./pages\");']\n */\n entryImports?: string[];\n /** Merged into the esbuild call: defaults < this patch < user `build` */\n esbuild?: ConnectorEsbuildPatch;\n};\n\n/**\n * A connector's build-time half: two hooks, both awaited, drained in\n * `connectors` array order. A hook that throws fails the build.\n *\n * **This type is closed on purpose**: exactly these two optional function properties and ZERO data-bearing\n * fields. There is deliberately no property where a Vite/babel plugin\n * instance, an alias map or a compiled pipeline could live, so\n * `build: { plugins: [...] }` is a type error rather than something a\n * reviewer has to catch. Anything heavy must be constructed INSIDE a hook,\n * after that hook dynamically imports it — that is what keeps a connector's\n * static import graph out of the runtime closure. The drain mirrors this at\n * read time by rejecting unknown enumerable keys (`assertClosedContribution`):\n * the type narrows, the runtime asserts.\n */\nexport type ConnectorBuildContribution = {\n /**\n * Write files into `ctx.productionDir` and/or patch the bundle.\n *\n * Runs after the generated-imports check and before esbuild bundles.\n */\n generate?(\n context: ConnectorBuildContext,\n ): Promise<ConnectorBuildGenerateResult | void> | ConnectorBuildGenerateResult | void;\n\n /**\n * Produce artifacts esbuild does not — e.g. a client bundle.\n *\n * Runs after esbuild and before `.warlock/production` is removed.\n */\n emit?(context: ConnectorBuildContext): Promise<void> | void;\n};\n\nexport type ConnectorName =\n | \"logger\"\n | \"mailer\"\n | \"http\"\n | \"database\"\n | \"cache\"\n | \"storage\"\n | \"herald\"\n | \"socket\"\n | \"notifications\"\n | \"access\"\n | \"ai\"\n | (string & {});\n\n/**\n * When a connector boots relative to user code (main, routes, events).\n *\n * - `Early`: before user code is imported. For services user code\n * needs at import time (database, cache, logger, storage, mailer).\n * - `Late`: after user code is imported. For services that bind to\n * things user code registered (http scans the router; socket\n * depends on http's instance).\n */\nexport enum ConnectorLifecyclePhase {\n Early = \"early\",\n Late = \"late\",\n}\n\n/**\n * Connector priority constants\n */\nexport enum ConnectorPriority {\n LOGGER = 0,\n MAILER = 1,\n DATABASE = 2,\n COMMUNICATOR = 3,\n CACHE = 4,\n HTTP = 5,\n STORAGE = 6,\n SOCKET = 7,\n NOTIFICATIONS = 8,\n ACCESS = 9,\n AI = 10,\n}\n"],"mappings":";;;;;;;;;;AA+KA,IAAY,0BAAL;CACL;CACA;;AACF;;;;AAKA,IAAY,oBAAL;CACL;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;AACF"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"sourcesContent":["import type { BuildOptions } from \"esbuild\";\nimport type { NamedApiRoute } from \"../router/types\";\nimport type { ResolvedBuildConfig } from \"../production/resolve-build-config\";\n\n/**\n * Connector Interface\n * All service connectors (Database, HTTP, Cache, etc.) must implement this interface\n */\nexport interface Connector {\n /**\n * Unique name of the connector\n */\n readonly name: ConnectorName;\n\n /**\n * Priority for initialization order\n * Lower numbers initialize first (e.g., Database: 1, HTTP: 3)\n */\n readonly priority: number;\n\n /**\n * Phase the connector boots in, relative to user code.\n * See `ConnectorLifecyclePhase` for semantics.\n */\n readonly lifecyclePhase: ConnectorLifecyclePhase;\n\n /**\n * Whether the connector is currently active/connected\n */\n isActive(): boolean;\n\n /**\n * Boot the connector\n * Called during server startup before starting the connector\n */\n boot(): void | Promise<void>;\n\n /**\n * Initialize the connector\n * Called during server startup\n */\n start(): Promise<void>;\n\n /**\n * Restart the connector\n * Called when watched files change\n */\n restart(): Promise<void>;\n\n /**\n * Gracefully shutdown the connector\n * Called during server shutdown\n */\n shutdown(): Promise<void>;\n\n /**\n * Determine if this connector needs restart based on changed files\n * @param changedFiles Array of relative file paths that changed\n * @returns true if connector should restart\n */\n shouldRestart(changedFiles: string[]): boolean;\n\n /**\n * What this connector contributes to `warlock build`.\n *\n * Read STATICALLY from `warlock.config.ts > connectors` — `warlock build`\n * never calls `boot()`/`start()`, so nothing here may assume a running\n * connector. The same `connectors` key drives both halves of a connector's\n * life — build-time contribution and runtime boot; see\n * {@link ConnectorBuildContribution}.\n */\n readonly build?: ConnectorBuildContribution;\n}\n\n/**\n * What a build contribution is handed. Everything it needs to write files\n * and shape the bundle, and nothing that would let it reach the runtime.\n */\nexport type ConnectorBuildContext = {\n /** Absolute path to `.warlock/production` — deleted after the build */\n productionDir: string;\n /** Absolute path to the application root (where `package.json` lives) */\n appRoot: string;\n /** The build config after framework defaults and user overrides */\n options: ResolvedBuildConfig;\n /** Fresh registration-only snapshot, present only when the Web connector is configured. */\n readonly namedApiRoutes?: readonly NamedApiRoute[];\n};\n\n/**\n * The esbuild options a contributor may patch.\n *\n * Deliberately a narrow `Pick`, not `Partial<BuildOptions>`: `entryPoints`,\n * `tsconfig` and `tsconfigRaw` stay forbidden here for the same reason they\n * are forbidden in the user's `build` block — they re-compile every file in\n * the bundle, workspace packages included (see `ForbiddenBuildOptionError`).\n * A contributor reaching them would reintroduce the failure the user-level\n * guard exists to prevent.\n */\nexport type ConnectorEsbuildPatch = Pick<\n BuildOptions,\n \"jsx\" | \"jsxImportSource\" | \"define\" | \"external\" | \"loader\" | \"plugins\"\n>;\n\n/**\n * What `generate` may hand back to the builder.\n */\nexport type ConnectorBuildGenerateResult = {\n /**\n * Lines appended verbatim to section 4 of the generated entry, AFTER the\n * `./routes` import — page registration must see the routes app code\n * collected, mirroring dev's ordering.\n *\n * @example ['await import(\"./pages\");']\n */\n entryImports?: string[];\n /** Merged into the esbuild call: defaults < this patch < user `build` */\n esbuild?: ConnectorEsbuildPatch;\n};\n\n/**\n * A connector's build-time half: two hooks, both awaited, drained in\n * `connectors` array order. A hook that throws fails the build.\n *\n * **This type is closed on purpose**: exactly these two optional function properties and ZERO data-bearing\n * fields. There is deliberately no property where a Vite/babel plugin\n * instance, an alias map or a compiled pipeline could live, so\n * `build: { plugins: [...] }` is a type error rather than something a\n * reviewer has to catch. Anything heavy must be constructed INSIDE a hook,\n * after that hook dynamically imports it — that is what keeps a connector's\n * static import graph out of the runtime closure. The drain mirrors this at\n * read time by rejecting unknown enumerable keys (`assertClosedContribution`):\n * the type narrows, the runtime asserts.\n */\nexport type ConnectorBuildContribution = {\n /**\n * Write files into `ctx.productionDir` and/or patch the bundle.\n *\n * Runs after the generated-imports check and before esbuild bundles.\n */\n generate?(\n context: ConnectorBuildContext,\n ): Promise<ConnectorBuildGenerateResult | void> | ConnectorBuildGenerateResult | void;\n\n /**\n * Produce artifacts esbuild does not — e.g. a client bundle.\n *\n * Runs after esbuild and before `.warlock/production` is removed.\n */\n emit?(context: ConnectorBuildContext): Promise<void> | void;\n\n /**\n * Write the connector's generated `.d.ts` files under `.warlock/typings`.\n *\n * Runs from `warlock generate.typings` with the same context `generate`\n * receives (`productionDir` is not written to), so a clean checkout can\n * typecheck before any build has run.\n */\n typings?(context: ConnectorBuildContext): Promise<void> | void;\n};\n\nexport type ConnectorName =\n | \"logger\"\n | \"mailer\"\n | \"http\"\n | \"database\"\n | \"cache\"\n | \"storage\"\n | \"herald\"\n | \"socket\"\n | \"notifications\"\n | \"access\"\n | \"ai\"\n | (string & {});\n\n/**\n * When a connector boots relative to user code (main, routes, events).\n *\n * - `Early`: before user code is imported. For services user code\n * needs at import time (database, cache, logger, storage, mailer).\n * - `Late`: after user code is imported. For services that bind to\n * things user code registered (http scans the router; socket\n * depends on http's instance).\n */\nexport enum ConnectorLifecyclePhase {\n Early = \"early\",\n Late = \"late\",\n}\n\n/**\n * Connector priority constants\n */\nexport enum ConnectorPriority {\n LOGGER = 0,\n MAILER = 1,\n DATABASE = 2,\n COMMUNICATOR = 3,\n CACHE = 4,\n HTTP = 5,\n STORAGE = 6,\n SOCKET = 7,\n NOTIFICATIONS = 8,\n ACCESS = 9,\n AI = 10,\n}\n"],"mappings":";;;;;;;;;;AAwLA,IAAY,0BAAL;CACL;CACA;;AACF;;;;AAKA,IAAY,oBAAL;CACL;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-database-action.mjs","names":[],"sources":["../../../../../../../core/src/database/create-database-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { createDatabase } from \"@warlock.js/cascade\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport { config } from \"../config\";\r\n\r\nexport async function createDatabaseAction(command: CommandActionData) {\r\n const name = command.args[0];\r\n const { connection = \"default\" } = command.options;\r\n\r\n const connectionName =\r\n connection === \"default\" || connection === true ? undefined : String(connection);\r\n\r\n let databaseName = name;\r\n\r\n if (!databaseName) {\r\n if (connection === \"default\") {\r\n databaseName = config.get(\"database\")?.database;\r\n }\r\n }\r\n\r\n if (!databaseName) {\r\n log.error(\r\n \"database\",\r\n \"create\",\r\n \"Database name is required. Please provide a name or configure it in the database config.\",\r\n );\r\n return;\r\n }\r\n\r\n log.info(\"database\", \"create\", `Creating database ${colors.cyan(databaseName)}...`);\r\n\r\n try {\r\n const { created } = await createDatabase(databaseName, { connection: connectionName });\r\n\r\n if (created) {\r\n log.success(\r\n \"database\",\r\n \"create\",\r\n `Database ${colors.green(databaseName)} created successfully.`,\r\n );\r\n } else {\r\n log.warn(\"database\", \"create\", `Database ${colors.yellow(databaseName)} already exists.`);\r\n }\r\n } catch (error: any) {\r\n log.error(\r\n \"database\",\r\n \"create\",\r\n `Failed to create database ${colors.red(databaseName)}: ${error.message}`,\r\n );\r\n }\r\n}\r\n"],"mappings":";;;;;;;AAMA,eAAsB,qBAAqB,SAA4B;CACrE,MAAM,OAAO,QAAQ,KAAK;CAC1B,MAAM,EAAE,aAAa,cAAc,QAAQ;CAE3C,MAAM,iBACJ,eAAe,aAAa,eAAe,OAAO,SAAY,OAAO,UAAU;CAEjF,IAAI,eAAe;CAEnB,IAAI,CAAC,cACH;MAAI,eAAe,WACjB,eAAe,OAAO,IAAI,UAAU,CAAC,EAAE;CACzC;CAGF,IAAI,CAAC,cAAc;EACjB,IAAI,MACF,YACA,UACA,0FACF;EACA;CACF;CAEA,IAAI,KAAK,YAAY,UAAU,qBAAqB,OAAO,KAAK,YAAY,EAAE,IAAI;CAElF,IAAI;EACF,MAAM,EAAE,YAAY,MAAM,eAAe,cAAc,EAAE,YAAY,eAAe,CAAC;EAErF,IAAI,SACF,IAAI,QACF,YACA,UACA,YAAY,OAAO,MAAM,YAAY,EAAE,uBACzC;OAEA,IAAI,KAAK,YAAY,UAAU,YAAY,OAAO,OAAO,YAAY,EAAE,iBAAiB;CAE5F,SAAS,OAAY;EACnB,IAAI,MACF,YACA,UACA,6BAA6B,OAAO,IAAI,YAAY,EAAE,IAAI,MAAM,SAClE;CACF;AACF"}
1
+ {"version":3,"file":"create-database-action.mjs","names":[],"sources":["../../../../../../../core/src/database/create-database-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { createDatabase } from \"@warlock.js/cascade\";\nimport { log } from \"@warlock.js/logger\";\nimport type { CommandActionData } from \"../commands/types\";\nimport { config } from \"../config\";\n\nexport async function createDatabaseAction(command: CommandActionData) {\n const name = command.args[0];\n const { connection = \"default\" } = command.options;\n\n const connectionName =\n connection === \"default\" || connection === true ? undefined : String(connection);\n\n let databaseName = name;\n\n if (!databaseName) {\n if (connection === \"default\") {\n databaseName = config.get(\"database\")?.database;\n }\n }\n\n if (!databaseName) {\n log.error(\n \"database\",\n \"create\",\n \"Database name is required. Please provide a name or configure it in the database config.\",\n );\n return;\n }\n\n log.info(\"database\", \"create\", `Creating database ${colors.cyan(databaseName)}...`);\n\n try {\n const { created } = await createDatabase(databaseName, { connection: connectionName });\n\n if (created) {\n log.success(\n \"database\",\n \"create\",\n `Database ${colors.green(databaseName)} created successfully.`,\n );\n } else {\n log.warn(\"database\", \"create\", `Database ${colors.yellow(databaseName)} already exists.`);\n }\n } catch (error: any) {\n log.error(\n \"database\",\n \"create\",\n `Failed to create database ${colors.red(databaseName)}: ${error.message}`,\n );\n }\n}\n"],"mappings":";;;;;;;AAMA,eAAsB,qBAAqB,SAA4B;CACrE,MAAM,OAAO,QAAQ,KAAK;CAC1B,MAAM,EAAE,aAAa,cAAc,QAAQ;CAE3C,MAAM,iBACJ,eAAe,aAAa,eAAe,OAAO,SAAY,OAAO,UAAU;CAEjF,IAAI,eAAe;CAEnB,IAAI,CAAC,cACH;MAAI,eAAe,WACjB,eAAe,OAAO,IAAI,UAAU,CAAC,EAAE;CACzC;CAGF,IAAI,CAAC,cAAc;EACjB,IAAI,MACF,YACA,UACA,0FACF;EACA;CACF;CAEA,IAAI,KAAK,YAAY,UAAU,qBAAqB,OAAO,KAAK,YAAY,EAAE,IAAI;CAElF,IAAI;EACF,MAAM,EAAE,YAAY,MAAM,eAAe,cAAc,EAAE,YAAY,eAAe,CAAC;EAErF,IAAI,SACF,IAAI,QACF,YACA,UACA,YAAY,OAAO,MAAM,YAAY,EAAE,uBACzC;OAEA,IAAI,KAAK,YAAY,UAAU,YAAY,OAAO,OAAO,YAAY,EAAE,iBAAiB;CAE5F,SAAS,OAAY;EACnB,IAAI,MACF,YACA,UACA,6BAA6B,OAAO,IAAI,YAAY,EAAE,IAAI,MAAM,SAClE;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"drop-tables-action.mjs","names":[],"sources":["../../../../../../../core/src/database/drop-tables-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { dataSourceRegistry, dropAllTables } from \"@warlock.js/cascade\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { confirm } from \"../cli/commands/generate/utils/prompt\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\n\r\nexport async function dropTablesAction(command: CommandActionData) {\r\n const { force } = command.options;\r\n\r\n if (force) {\r\n const { dropped } = await dropAllTables();\r\n log.success(\"database\", \"drop\", `Dropped ${colors.yellowBright(dropped)} tables successfully.`);\r\n return;\r\n }\r\n\r\n // Preview phase — we keep direct driver access so we can show per-table\r\n // row counts before asking for confirmation. The actual drop still goes\r\n // through the Cascade Operations API.\r\n const driver = dataSourceRegistry.get().driver;\r\n const tables = await driver.blueprint.listTables();\r\n\r\n if (tables.length === 0) {\r\n log.warn(\"database\", \"drop\", \"No tables found in the database.\");\r\n return;\r\n }\r\n\r\n log.info(\"database\", \"drop\", `Found ${colors.yellowBright(tables.length)} tables:`);\r\n\r\n for (const table of tables) {\r\n const count = await driver.queryBuilder(table).count();\r\n console.log(` - ${colors.cyan(table)}: ${colors.green(count)} rows`);\r\n }\r\n\r\n const confirmed = await confirm(\r\n `Are you sure you want to drop all ${colors.red(tables.length)} tables?`,\r\n );\r\n\r\n if (!confirmed) {\r\n log.info(\"database\", \"drop\", \"Operation cancelled.\");\r\n return;\r\n }\r\n\r\n const { dropped } = await dropAllTables();\r\n\r\n log.success(\"database\", \"drop\", `Dropped ${colors.yellowBright(dropped)} tables successfully.`);\r\n}\r\n"],"mappings":";;;;;;AAMA,eAAsB,iBAAiB,SAA4B;CACjE,MAAM,EAAE,UAAU,QAAQ;CAE1B,IAAI,OAAO;EACT,MAAM,EAAE,YAAY,MAAM,cAAc;EACxC,IAAI,QAAQ,YAAY,QAAQ,WAAW,OAAO,aAAa,OAAO,EAAE,sBAAsB;EAC9F;CACF;CAKA,MAAM,SAAS,mBAAmB,IAAI,CAAC,CAAC;CACxC,MAAM,SAAS,MAAM,OAAO,UAAU,WAAW;CAEjD,IAAI,OAAO,WAAW,GAAG;EACvB,IAAI,KAAK,YAAY,QAAQ,kCAAkC;EAC/D;CACF;CAEA,IAAI,KAAK,YAAY,QAAQ,SAAS,OAAO,aAAa,OAAO,MAAM,EAAE,SAAS;CAElF,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,OAAO,aAAa,KAAK,CAAC,CAAC,MAAM;EACrD,QAAQ,IAAI,OAAO,OAAO,KAAK,KAAK,EAAE,IAAI,OAAO,MAAM,KAAK,EAAE,MAAM;CACtE;CAMA,IAAI,CAAC,MAJmB,QACtB,qCAAqC,OAAO,IAAI,OAAO,MAAM,EAAE,SACjE,GAEgB;EACd,IAAI,KAAK,YAAY,QAAQ,sBAAsB;EACnD;CACF;CAEA,MAAM,EAAE,YAAY,MAAM,cAAc;CAExC,IAAI,QAAQ,YAAY,QAAQ,WAAW,OAAO,aAAa,OAAO,EAAE,sBAAsB;AAChG"}
1
+ {"version":3,"file":"drop-tables-action.mjs","names":[],"sources":["../../../../../../../core/src/database/drop-tables-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { dataSourceRegistry, dropAllTables } from \"@warlock.js/cascade\";\nimport { log } from \"@warlock.js/logger\";\nimport { confirm } from \"../cli/commands/generate/utils/prompt\";\nimport type { CommandActionData } from \"../commands/types\";\n\nexport async function dropTablesAction(command: CommandActionData) {\n const { force } = command.options;\n\n if (force) {\n const { dropped } = await dropAllTables();\n log.success(\"database\", \"drop\", `Dropped ${colors.yellowBright(dropped)} tables successfully.`);\n return;\n }\n\n // Preview phase — we keep direct driver access so we can show per-table\n // row counts before asking for confirmation. The actual drop still goes\n // through the Cascade Operations API.\n const driver = dataSourceRegistry.get().driver;\n const tables = await driver.blueprint.listTables();\n\n if (tables.length === 0) {\n log.warn(\"database\", \"drop\", \"No tables found in the database.\");\n return;\n }\n\n log.info(\"database\", \"drop\", `Found ${colors.yellowBright(tables.length)} tables:`);\n\n for (const table of tables) {\n const count = await driver.queryBuilder(table).count();\n console.log(` - ${colors.cyan(table)}: ${colors.green(count)} rows`);\n }\n\n const confirmed = await confirm(\n `Are you sure you want to drop all ${colors.red(tables.length)} tables?`,\n );\n\n if (!confirmed) {\n log.info(\"database\", \"drop\", \"Operation cancelled.\");\n return;\n }\n\n const { dropped } = await dropAllTables();\n\n log.success(\"database\", \"drop\", `Dropped ${colors.yellowBright(dropped)} tables successfully.`);\n}\n"],"mappings":";;;;;;AAMA,eAAsB,iBAAiB,SAA4B;CACjE,MAAM,EAAE,UAAU,QAAQ;CAE1B,IAAI,OAAO;EACT,MAAM,EAAE,YAAY,MAAM,cAAc;EACxC,IAAI,QAAQ,YAAY,QAAQ,WAAW,OAAO,aAAa,OAAO,EAAE,sBAAsB;EAC9F;CACF;CAKA,MAAM,SAAS,mBAAmB,IAAI,CAAC,CAAC;CACxC,MAAM,SAAS,MAAM,OAAO,UAAU,WAAW;CAEjD,IAAI,OAAO,WAAW,GAAG;EACvB,IAAI,KAAK,YAAY,QAAQ,kCAAkC;EAC/D;CACF;CAEA,IAAI,KAAK,YAAY,QAAQ,SAAS,OAAO,aAAa,OAAO,MAAM,EAAE,SAAS;CAElF,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,OAAO,aAAa,KAAK,CAAC,CAAC,MAAM;EACrD,QAAQ,IAAI,OAAO,OAAO,KAAK,KAAK,EAAE,IAAI,OAAO,MAAM,KAAK,EAAE,MAAM;CACtE;CAMA,IAAI,CAAC,MAJmB,QACtB,qCAAqC,OAAO,IAAI,OAAO,MAAM,EAAE,SACjE,GAEgB;EACd,IAAI,KAAK,YAAY,QAAQ,sBAAsB;EACnD;CACF;CAEA,MAAM,EAAE,YAAY,MAAM,cAAc;CAExC,IAAI,QAAQ,YAAY,QAAQ,WAAW,OAAO,aAAa,OAAO,EAAE,sBAAsB;AAChG"}
@@ -4,6 +4,7 @@ import "../utils/index.mjs";
4
4
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
5
5
  import { getFilesFromDirectory } from "../dev-server/utils.mjs";
6
6
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
7
+ import { isTestFile } from "../utils/is-test-file.mjs";
7
8
  import { PENDING_EXIT_CODE, exitCodeFor } from "./pending-exit-code.mjs";
8
9
  import { resolvePendingMigrations } from "./resolve-pending-migrations.mjs";
9
10
  import { exportMigrationsSQL, freshMigrate, listExecutedMigrations, migrationRunner, rollbackMigrations, runMigrations } from "@warlock.js/cascade";
@@ -134,7 +135,7 @@ async function loadMigrationFile(absPath) {
134
135
  async function migrationFiles() {
135
136
  const migrationFiles = await getFilesFromDirectory(srcPath("app"), "*/models/*/migrations/*");
136
137
  const separateMigrationsFolderFIles = await getFilesFromDirectory(srcPath("app"), "*/migrations/*");
137
- return [...migrationFiles, ...separateMigrationsFolderFIles];
138
+ return [...migrationFiles, ...separateMigrationsFolderFIles].filter((file) => !isTestFile(file));
138
139
  }
139
140
  async function loadAllMigrations() {
140
141
  const configMigrations = warlockConfigManager.get("database")?.migrations || [];
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n exportMigrationsSQL,\r\n freshMigrate,\r\n listExecutedMigrations,\r\n Migration,\r\n type MigrationConstructor,\r\n migrationRunner,\r\n rollbackMigrations,\r\n runMigrations,\r\n} from \"@warlock.js/cascade\";\r\nimport dayjs from \"dayjs\";\r\nimport path from \"path\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\r\nimport {\r\n resolvePendingMigrations,\r\n type PendingMigrationsResult,\r\n} from \"./resolve-pending-migrations\";\r\n\r\n/**\r\n * `--list` — print the migration state: what has run, then what will run next.\r\n *\r\n * The executed section is printed FIRST and unconditionally, before anything\r\n * touches disk or config. That ordering is the contract: reading migrations\r\n * from the table cannot fail because of a broken migration file, and `--list`\r\n * is the command an operator reaches for *during* an incident. The pending\r\n * section is best-effort on top of an answer that is already on screen.\r\n *\r\n * Always exits 0. It is a report; `--pending` is the gate.\r\n */\r\nasync function listMigrationsAction() {\r\n await printExecutedMigrations();\r\n\r\n const pending = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(pending);\r\n}\r\n\r\nasync function printExecutedMigrations() {\r\n const createdMigrations = await listExecutedMigrations();\r\n\r\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\r\n\r\n if (createdMigrations.length === 0) {\r\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\r\n return;\r\n }\r\n\r\n // Display each migration as a block\r\n for (const migration of createdMigrations) {\r\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\r\n const createdAt = migration.createdAt\r\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\r\n : null;\r\n\r\n // Migration name with checkmark icon\r\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\r\n\r\n // Executed date\r\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\r\n\r\n // Created date (if available)\r\n if (createdAt) {\r\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\r\n }\r\n\r\n console.log(\"\"); // Empty line between migrations\r\n }\r\n}\r\n\r\n/**\r\n * Print the pending section shared by `--list` and `--pending`.\r\n *\r\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\r\n * over a tree we failed to read is the one output this whole feature exists to\r\n * prevent.\r\n */\r\nfunction printPendingMigrations(result: PendingMigrationsResult) {\r\n if (result.type === \"unavailable\") {\r\n console.log(\r\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\r\n colors.gray(\" The executed list above is still accurate.\\n\"),\r\n );\r\n return;\r\n }\r\n\r\n const { migrations } = result;\r\n\r\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\r\n\r\n if (migrations.length === 0) {\r\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\r\n return;\r\n }\r\n\r\n // Numbered because the ORDER is the answer: this is what the next\r\n // `warlock migrate` will do, in the sequence it will do it.\r\n migrations.forEach((migration, index) => {\r\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\r\n });\r\n\r\n console.log(\"\");\r\n}\r\n\r\n/**\r\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\r\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\r\n */\r\nasync function pendingMigrationsAction() {\r\n const result = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(result);\r\n\r\n const exitCode = exitCodeFor(result);\r\n\r\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\r\n console.error(\r\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\r\n );\r\n }\r\n\r\n process.exit(exitCode);\r\n}\r\n\r\nasync function allMigrationsFilesAction() {\r\n // get all available migration files in the project\r\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\r\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\r\n\r\n for (const file of files) {\r\n console.log(colors.yellowBright(file));\r\n }\r\n}\r\n\r\n/**\r\n * If path is provided, then run the migration runner against that file only\r\n * If fresh is provided, then rollback all migrations and run all migrations\r\n * If rollback is provided, then run the migration runner against all files in reverse order\r\n */\r\nexport async function migrateAction(options: CommandActionData) {\r\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\r\n\r\n if (list) {\r\n return await listMigrationsAction();\r\n }\r\n\r\n if (pending) {\r\n return await pendingMigrationsAction();\r\n }\r\n\r\n if (all) {\r\n return await allMigrationsFilesAction();\r\n }\r\n\r\n if (path) {\r\n await loadMigrationFile(Path.toAbsolute(path as string));\r\n } else {\r\n await loadAllMigrations();\r\n }\r\n\r\n if (fresh && rollback) {\r\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\r\n process.exit(1);\r\n }\r\n\r\n if (rollback) {\r\n await rollbackMigrations({ all: true });\r\n return;\r\n }\r\n\r\n if (sql) {\r\n await exportMigrationsSQL({\r\n pendingOnly: pendingOnly as boolean,\r\n compact: compact as boolean,\r\n });\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await freshMigrate();\r\n return;\r\n }\r\n\r\n await runMigrations();\r\n}\r\n\r\nasync function loadMigrationFile(absPath: string) {\r\n const relativePath = Path.toRelative(absPath);\r\n\r\n // MigrationConstructor, not `typeof Migration`: a migration file default-exports a\r\n // CONCRETE subclass, while Migration itself is abstract and so cannot satisfy a\r\n // `new () => ...` parameter. The annotation described the base class rather than\r\n // what is actually loaded, and that is what migrationRunner.register() rejected.\r\n const loadedModule = await filesOrchestrator.load<{ default: MigrationConstructor }>(\r\n relativePath,\r\n );\r\n\r\n if (!loadedModule?.default) {\r\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\r\n }\r\n\r\n const MigrationClass = loadedModule.default;\r\n\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = path\r\n .basename(absPath)\r\n .split(\".\")[0]\r\n .replace(\"-migration\", \"\")\r\n .replace(\"_migration\", \"\");\r\n }\r\n\r\n // Extract createdAt timestamp from filename if not already set\r\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\r\n if (!MigrationClass.createdAt) {\r\n const filename = path.basename(absPath);\r\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\r\n if (timestampMatch) {\r\n MigrationClass.createdAt = timestampMatch[1];\r\n }\r\n }\r\n\r\n migrationRunner.register(MigrationClass);\r\n}\r\n\r\n/**\r\n *\r\n * @returns List of absolute paths to migration files\r\n */\r\nasync function migrationFiles() {\r\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\r\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\r\n srcPath(\"app\"),\r\n \"*/migrations/*\",\r\n );\r\n\r\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles];\r\n\r\n return migrations;\r\n}\r\n\r\nasync function loadAllMigrations() {\r\n // Load config-registered migrations (from packages like @warlock.js/auth)\r\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\r\n\r\n for (const MigrationClass of configMigrations) {\r\n // Use class name as migration name if not set\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = MigrationClass.name;\r\n }\r\n migrationRunner.register(MigrationClass);\r\n }\r\n\r\n // Always load file-based migrations from src/app, regardless of config\r\n const migrations = await migrationFiles();\r\n for (const migrationFile of migrations) {\r\n await loadMigrationFile(migrationFile);\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,eAAe,uBAAuB;CACpC,MAAM,wBAAwB;CAI9B,uBAAuB,MAFD,yBAAyB,iBAAiB,CAElC;AAChC;AAEA,eAAe,0BAA0B;CACvC,MAAM,oBAAoB,MAAM,uBAAuB;CAEvD,QAAQ,IAAI,gCAAgC,OAAO,MAAM,kBAAkB,MAAM,EAAE,GAAG;CAEtF,IAAI,kBAAkB,WAAW,GAAG;EAClC,QAAQ,IAAI,OAAO,KAAK,2CAA2C,CAAC;EACpE;CACF;CAGA,KAAK,MAAM,aAAa,mBAAmB;EACzC,MAAM,aAAa,MAAM,UAAU,UAAU,CAAC,CAAC,OAAO,uBAAuB;EAC7E,MAAM,YAAY,UAAU,YACxB,MAAM,UAAU,SAAS,CAAC,CAAC,OAAO,uBAAuB,IACzD;EAGJ,QAAQ,IAAI,KAAK,OAAO,MAAM,GAAG,EAAE,GAAG,OAAO,WAAW,UAAU,IAAI,GAAG;EAGzE,QAAQ,IAAI,OAAO,OAAO,KAAK,WAAW,EAAE,GAAG,OAAO,MAAM,UAAU,GAAG;EAGzE,IAAI,WACF,QAAQ,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,SAAS,GAAG;EAG3E,QAAQ,IAAI,EAAE;CAChB;AACF;;;;;;;;AASA,SAAS,uBAAuB,QAAiC;CAC/D,IAAI,OAAO,SAAS,eAAe;EACjC,QAAQ,IACN,GAAG,OAAO,aAAa,sBAAsB,EAAE,KAAK,OAAO,OAAO,MAChE,OAAO,KAAK,gDAAgD,CAChE;EACA;CACF;CAEA,MAAM,EAAE,eAAe;CAEvB,QAAQ,IAAI,6BAA6B,OAAO,MAAM,WAAW,MAAM,EAAE,GAAG;CAE5E,IAAI,WAAW,WAAW,GAAG;EAC3B,QAAQ,IAAI,OAAO,KAAK,mDAAmD,CAAC;EAC5E;CACF;CAIA,WAAW,SAAS,WAAW,UAAU;EACvC,QAAQ,IAAI,KAAK,OAAO,KAAK,GAAG,QAAQ,EAAE,EAAE,EAAE,GAAG,OAAO,WAAW,UAAU,IAAI,GAAG;CACtF,CAAC;CAED,QAAQ,IAAI,EAAE;AAChB;;;;;AAMA,eAAe,0BAA0B;CACvC,MAAM,SAAS,MAAM,yBAAyB,iBAAiB;CAE/D,uBAAuB,MAAM;CAE7B,MAAM,WAAW,YAAY,MAAM;CAEnC,IAAI,aAAa,kBAAkB,aACjC,QAAQ,MACN,OAAO,UAAU,0EAA0E,CAC7F;CAGF,QAAQ,KAAK,QAAQ;AACvB;AAEA,eAAe,2BAA2B;CAExC,MAAM,SAAS,MAAM,eAAe,EAAC,CAAE,KAAK,SAAS,KAAK,WAAW,IAAI,CAAC;CAC1E,QAAQ,IAAI,0BAA0B,OAAO,MAAM,MAAM,MAAM,GAAG;CAElE,KAAK,MAAM,QAAQ,OACjB,QAAQ,IAAI,OAAO,aAAa,IAAI,CAAC;AAEzC;;;;;;AAOA,eAAsB,cAAc,SAA4B;CAC9D,MAAM,EAAE,OAAO,MAAM,UAAU,KAAK,MAAM,SAAS,KAAK,aAAa,YAAY,QAAQ;CAEzF,IAAI,MACF,OAAO,MAAM,qBAAqB;CAGpC,IAAI,SACF,OAAO,MAAM,wBAAwB;CAGvC,IAAI,KACF,OAAO,MAAM,yBAAyB;CAGxC,IAAI,MACF,MAAM,kBAAkB,KAAK,WAAW,IAAc,CAAC;MAEvD,MAAM,kBAAkB;CAG1B,IAAI,SAAS,UAAU;EACrB,QAAQ,IAAI,OAAO,UAAU,+CAA+C,CAAC;EAC7E,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,UAAU;EACZ,MAAM,mBAAmB,EAAE,KAAK,KAAK,CAAC;EACtC;CACF;CAEA,IAAI,KAAK;EACP,MAAM,oBAAoB;GACX;GACJ;EACX,CAAC;EACD;CACF;CAEA,IAAI,OAAO;EACT,MAAM,aAAa;EACnB;CACF;CAEA,MAAM,cAAc;AACtB;AAEA,eAAe,kBAAkB,SAAiB;CAChD,MAAM,eAAe,KAAK,WAAW,OAAO;CAM5C,MAAM,eAAe,MAAM,kBAAkB,KAC3C,YACF;CAEA,IAAI,CAAC,cAAc,SACjB,MAAM,IAAI,MAAM,GAAG,KAAK,WAAW,OAAO,EAAE,4BAA4B;CAG1E,MAAM,iBAAiB,aAAa;CAEpC,IAAI,CAAC,eAAe,eAClB,eAAe,gBAAgB,KAC5B,SAAS,OAAO,CAAC,CACjB,MAAM,GAAG,CAAC,CAAC,EAAE,CACb,QAAQ,cAAc,EAAE,CAAC,CACzB,QAAQ,cAAc,EAAE;CAK7B,IAAI,CAAC,eAAe,WAAW;EAE7B,MAAM,iBADW,KAAK,SAAS,OACD,CAAC,CAAC,MAAM,wCAAwC;EAC9E,IAAI,gBACF,eAAe,YAAY,eAAe;CAE9C;CAEA,gBAAgB,SAAS,cAAc;AACzC;;;;;AAMA,eAAe,iBAAiB;CAC9B,MAAM,iBAAiB,MAAM,sBAAsB,QAAQ,KAAK,GAAG,yBAAyB;CAC5F,MAAM,gCAAgC,MAAM,sBAC1C,QAAQ,KAAK,GACb,gBACF;CAIA,OAAO,CAFa,GAAG,gBAAgB,GAAG,6BAE1B;AAClB;AAEA,eAAe,oBAAoB;CAEjC,MAAM,mBAAmB,qBAAqB,IAAI,UAAU,CAAC,EAAE,cAAc,CAAC;CAE9E,KAAK,MAAM,kBAAkB,kBAAkB;EAE7C,IAAI,CAAC,eAAe,eAClB,eAAe,gBAAgB,eAAe;EAEhD,gBAAgB,SAAS,cAAc;CACzC;CAGA,MAAM,aAAa,MAAM,eAAe;CACxC,KAAK,MAAM,iBAAiB,YAC1B,MAAM,kBAAkB,aAAa;AAEzC"}
1
+ {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n exportMigrationsSQL,\n freshMigrate,\n listExecutedMigrations,\n Migration,\n type MigrationConstructor,\n migrationRunner,\n rollbackMigrations,\n runMigrations,\n} from \"@warlock.js/cascade\";\nimport dayjs from \"dayjs\";\nimport path from \"path\";\nimport type { CommandActionData } from \"../commands/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { isTestFile } from \"../utils/is-test-file\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\nimport {\n resolvePendingMigrations,\n type PendingMigrationsResult,\n} from \"./resolve-pending-migrations\";\n\n/**\n * `--list` — print the migration state: what has run, then what will run next.\n *\n * The executed section is printed FIRST and unconditionally, before anything\n * touches disk or config. That ordering is the contract: reading migrations\n * from the table cannot fail because of a broken migration file, and `--list`\n * is the command an operator reaches for *during* an incident. The pending\n * section is best-effort on top of an answer that is already on screen.\n *\n * Always exits 0. It is a report; `--pending` is the gate.\n */\nasync function listMigrationsAction() {\n await printExecutedMigrations();\n\n const pending = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(pending);\n}\n\nasync function printExecutedMigrations() {\n const createdMigrations = await listExecutedMigrations();\n\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\n\n if (createdMigrations.length === 0) {\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\n return;\n }\n\n // Display each migration as a block\n for (const migration of createdMigrations) {\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\n const createdAt = migration.createdAt\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\n : null;\n\n // Migration name with checkmark icon\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\n\n // Executed date\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\n\n // Created date (if available)\n if (createdAt) {\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\n }\n\n console.log(\"\"); // Empty line between migrations\n }\n}\n\n/**\n * Print the pending section shared by `--list` and `--pending`.\n *\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\n * over a tree we failed to read is the one output this whole feature exists to\n * prevent.\n */\nfunction printPendingMigrations(result: PendingMigrationsResult) {\n if (result.type === \"unavailable\") {\n console.log(\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\n colors.gray(\" The executed list above is still accurate.\\n\"),\n );\n return;\n }\n\n const { migrations } = result;\n\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\n\n if (migrations.length === 0) {\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\n return;\n }\n\n // Numbered because the ORDER is the answer: this is what the next\n // `warlock migrate` will do, in the sequence it will do it.\n migrations.forEach((migration, index) => {\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\n });\n\n console.log(\"\");\n}\n\n/**\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\n */\nasync function pendingMigrationsAction() {\n const result = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(result);\n\n const exitCode = exitCodeFor(result);\n\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\n console.error(\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\n );\n }\n\n process.exit(exitCode);\n}\n\nasync function allMigrationsFilesAction() {\n // get all available migration files in the project\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\n\n for (const file of files) {\n console.log(colors.yellowBright(file));\n }\n}\n\n/**\n * If path is provided, then run the migration runner against that file only\n * If fresh is provided, then rollback all migrations and run all migrations\n * If rollback is provided, then run the migration runner against all files in reverse order\n */\nexport async function migrateAction(options: CommandActionData) {\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\n\n if (list) {\n return await listMigrationsAction();\n }\n\n if (pending) {\n return await pendingMigrationsAction();\n }\n\n if (all) {\n return await allMigrationsFilesAction();\n }\n\n if (path) {\n await loadMigrationFile(Path.toAbsolute(path as string));\n } else {\n await loadAllMigrations();\n }\n\n if (fresh && rollback) {\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\n process.exit(1);\n }\n\n if (rollback) {\n await rollbackMigrations({ all: true });\n return;\n }\n\n if (sql) {\n await exportMigrationsSQL({\n pendingOnly: pendingOnly as boolean,\n compact: compact as boolean,\n });\n return;\n }\n\n if (fresh) {\n await freshMigrate();\n return;\n }\n\n await runMigrations();\n}\n\nasync function loadMigrationFile(absPath: string) {\n const relativePath = Path.toRelative(absPath);\n\n // MigrationConstructor, not `typeof Migration`: a migration file default-exports a\n // CONCRETE subclass, while Migration itself is abstract and so cannot satisfy a\n // `new () => ...` parameter. The annotation described the base class rather than\n // what is actually loaded, and that is what migrationRunner.register() rejected.\n const loadedModule = await filesOrchestrator.load<{ default: MigrationConstructor }>(\n relativePath,\n );\n\n if (!loadedModule?.default) {\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\n }\n\n const MigrationClass = loadedModule.default;\n\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = path\n .basename(absPath)\n .split(\".\")[0]\n .replace(\"-migration\", \"\")\n .replace(\"_migration\", \"\");\n }\n\n // Extract createdAt timestamp from filename if not already set\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\n if (!MigrationClass.createdAt) {\n const filename = path.basename(absPath);\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\n if (timestampMatch) {\n MigrationClass.createdAt = timestampMatch[1];\n }\n }\n\n migrationRunner.register(MigrationClass);\n}\n\n/**\n *\n * @returns List of absolute paths to migration files\n */\nasync function migrationFiles() {\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\n srcPath(\"app\"),\n \"*/migrations/*\",\n );\n\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles].filter(\n (file) => !isTestFile(file),\n );\n\n return migrations;\n}\n\nasync function loadAllMigrations() {\n // Load config-registered migrations (from packages like @warlock.js/auth)\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\n\n for (const MigrationClass of configMigrations) {\n // Use class name as migration name if not set\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = MigrationClass.name;\n }\n migrationRunner.register(MigrationClass);\n }\n\n // Always load file-based migrations from src/app, regardless of config\n const migrations = await migrationFiles();\n for (const migrationFile of migrations) {\n await loadMigrationFile(migrationFile);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-log.mjs","names":[],"sources":["../../../../../../../../../core/src/database/models/database-log/database-log.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nconst schema = v.object({\r\n module: v.string(),\r\n action: v.string(),\r\n content: v.any(),\r\n stack: v.string().optional(),\r\n level: v.string(),\r\n date: v.string(),\r\n});\r\n\r\ntype LogSchema = Infer.Output<typeof schema>;\r\n\r\nexport class DatabaseLogModel extends Model<LogSchema> {\r\n /**\r\n * Table name\r\n */\r\n public static table = \"logs\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public static schema = schema;\r\n\r\n /**\r\n * Legacy rows stored `message`/`trace`; map them to `content`/`stack`\r\n * when the new fields are absent.\r\n */\r\n private static readonly legacyFields: Record<string, string> = {\r\n content: \"message\",\r\n stack: \"trace\",\r\n };\r\n\r\n /**\r\n * Log content, falling back to the legacy `message` field\r\n */\r\n public get content(): unknown {\r\n return this.get(\"content\");\r\n }\r\n\r\n /**\r\n * Error stack, falling back to the legacy `trace` field\r\n */\r\n public get stack(): string | undefined {\r\n return this.get(\"stack\");\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public get(field: string, defaultValue?: any): any {\r\n const value = super.get(field, undefined as any);\r\n\r\n if (value !== undefined && value !== null) return value;\r\n\r\n const legacy = DatabaseLogModel.legacyFields[field];\r\n\r\n if (legacy) {\r\n const legacyValue = super.get(legacy, undefined as any);\r\n\r\n if (legacyValue !== undefined && legacyValue !== null) return legacyValue;\r\n }\r\n\r\n return value ?? defaultValue;\r\n }\r\n}\r\n"],"mappings":";;;;AAGA,MAAM,SAAS,EAAE,OAAO;CACtB,QAAQ,EAAE,OAAO;CACjB,QAAQ,EAAE,OAAO;CACjB,SAAS,EAAE,IAAI;CACf,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,OAAO,EAAE,OAAO;CAChB,MAAM,EAAE,OAAO;AACjB,CAAC;AAID,IAAa,mBAAb,MAAa,yBAAyB,MAAiB;;eAI/B;;;gBAKC;;;sBAMwC;GAC7D,SAAS;GACT,OAAO;EACT;;;;;CAKA,IAAW,UAAmB;EAC5B,OAAO,KAAK,IAAI,SAAS;CAC3B;;;;CAKA,IAAW,QAA4B;EACrC,OAAO,KAAK,IAAI,OAAO;CACzB;;;;CAKA,AAAO,IAAI,OAAe,cAAyB;EACjD,MAAM,QAAQ,MAAM,IAAI,OAAO,MAAgB;EAE/C,IAAI,UAAU,UAAa,UAAU,MAAM,OAAO;EAElD,MAAM,SAAS,iBAAiB,aAAa;EAE7C,IAAI,QAAQ;GACV,MAAM,cAAc,MAAM,IAAI,QAAQ,MAAgB;GAEtD,IAAI,gBAAgB,UAAa,gBAAgB,MAAM,OAAO;EAChE;EAEA,OAAO,SAAS;CAClB;AACF"}
1
+ {"version":3,"file":"database-log.mjs","names":[],"sources":["../../../../../../../../../core/src/database/models/database-log/database-log.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\nconst schema = v.object({\n module: v.string(),\n action: v.string(),\n content: v.any(),\n stack: v.string().optional(),\n level: v.string(),\n date: v.string(),\n});\n\ntype LogSchema = Infer.Output<typeof schema>;\n\nexport class DatabaseLogModel extends Model<LogSchema> {\n /**\n * Table name\n */\n public static table = \"logs\";\n\n /**\n * {@inheritdoc}\n */\n public static schema = schema;\n\n /**\n * Legacy rows stored `message`/`trace`; map them to `content`/`stack`\n * when the new fields are absent.\n */\n private static readonly legacyFields: Record<string, string> = {\n content: \"message\",\n stack: \"trace\",\n };\n\n /**\n * Log content, falling back to the legacy `message` field\n */\n public get content(): unknown {\n return this.get(\"content\");\n }\n\n /**\n * Error stack, falling back to the legacy `trace` field\n */\n public get stack(): string | undefined {\n return this.get(\"stack\");\n }\n\n /**\n * {@inheritdoc}\n */\n public get(field: string, defaultValue?: any): any {\n const value = super.get(field, undefined as any);\n\n if (value !== undefined && value !== null) return value;\n\n const legacy = DatabaseLogModel.legacyFields[field];\n\n if (legacy) {\n const legacyValue = super.get(legacy, undefined as any);\n\n if (legacyValue !== undefined && legacyValue !== null) return legacyValue;\n }\n\n return value ?? defaultValue;\n }\n}\n"],"mappings":";;;;AAGA,MAAM,SAAS,EAAE,OAAO;CACtB,QAAQ,EAAE,OAAO;CACjB,QAAQ,EAAE,OAAO;CACjB,SAAS,EAAE,IAAI;CACf,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,OAAO,EAAE,OAAO;CAChB,MAAM,EAAE,OAAO;AACjB,CAAC;AAID,IAAa,mBAAb,MAAa,yBAAyB,MAAiB;;eAI/B;;;gBAKC;;;sBAMwC;GAC7D,SAAS;GACT,OAAO;EACT;;;;;CAKA,IAAW,UAAmB;EAC5B,OAAO,KAAK,IAAI,SAAS;CAC3B;;;;CAKA,IAAW,QAA4B;EACrC,OAAO,KAAK,IAAI,OAAO;CACzB;;;;CAKA,AAAO,IAAI,OAAe,cAAyB;EACjD,MAAM,QAAQ,MAAM,IAAI,OAAO,MAAgB;EAE/C,IAAI,UAAU,UAAa,UAAU,MAAM,OAAO;EAElD,MAAM,SAAS,iBAAiB,aAAa;EAE7C,IAAI,QAAQ;GACV,MAAM,cAAc,MAAM,IAAI,QAAQ,MAAgB;GAEtD,IAAI,gBAAgB,UAAa,gBAAgB,MAAM,OAAO;EAChE;EAEA,OAAO,SAAS;CAClB;AACF"}
@@ -3,6 +3,7 @@ import { srcPath } from "../utils/paths.mjs";
3
3
  import "../utils/index.mjs";
4
4
  import { getFilesFromDirectory } from "../dev-server/utils.mjs";
5
5
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
6
+ import { isTestFile } from "../utils/is-test-file.mjs";
6
7
  import { seedRecordsTableName, seedsTableName } from "./seeds/utils.mjs";
7
8
  import { SeedersManager } from "./seeds/seeders.manager.mjs";
8
9
  import { dataSourceRegistry, transaction } from "@warlock.js/cascade";
@@ -102,7 +103,7 @@ async function seedCommandAction(options, overrides) {
102
103
  await seedersManager.run(transaction);
103
104
  }
104
105
  async function listSeedsFiles() {
105
- const seedsFiles = await getFilesFromDirectory(srcPath("app"), "*/seeds/*.ts");
106
+ const seedsFiles = (await getFilesFromDirectory(srcPath("app"), "*/seeds/*.ts")).filter((file) => !isTestFile(file));
106
107
  const seeds = [];
107
108
  for (const seedFile of seedsFiles) {
108
109
  const seed = await loadSeedFile(seedFile);
@@ -1 +1 @@
1
- {"version":3,"file":"seed-command-action.mjs","names":["runInTransaction"],"sources":["../../../../../../../core/src/database/seed-command-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n DataSource,\r\n dataSourceRegistry,\r\n transaction as runInTransaction,\r\n} from \"@warlock.js/cascade\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport type { Seeder } from \"./seeds/seeder\";\r\nimport { SeedersManager } from \"./seeds/seeders.manager\";\r\nimport type { SeedClock, SeedRecordRef } from \"./seeds/types\";\r\nimport { seedRecordsTableName, seedsTableName } from \"./seeds/utils\";\r\n\r\n/**\r\n * Programmatic-only overrides for {@link seedCommandAction}.\r\n *\r\n * Not derived from CLI flags (the CLI passes only {@link CommandActionData}),\r\n * so a back-fill script or a test can inject behaviour the command line can't\r\n * express.\r\n */\r\nexport type SeedCommandOverrides = {\r\n /**\r\n * Clock the run reads for the `now` handed to each seeder and for the\r\n * seeds-log metadata timestamps. Defaults to `() => new Date()`. Forwarded to\r\n * {@link SeedersManager}'s `clock` option.\r\n */\r\n clock?: SeedClock;\r\n};\r\n\r\n/**\r\n * Truncate every table except cascade's migrations tracking table — wiping it\r\n * would make the next `warlock migrate` re-run every migration.\r\n */\r\nexport async function clearAllTables(datasource: DataSource) {\r\n const migrationsTable = datasource.migrations?.table ?? \"_migrations\";\r\n const tables = await datasource.driver.blueprint.listTables();\r\n\r\n for (const table of tables) {\r\n if (table === migrationsTable) continue;\r\n\r\n await datasource.driver.truncateTable(table, { cascade: true });\r\n }\r\n}\r\n\r\ntype SeedRecordRow = SeedRecordRef & { id: number };\r\n\r\n/**\r\n * Undo seeded data by deleting the records a seeder tracked via `track`.\r\n *\r\n * Reads `seed_records` (all seeders, or a single `seederName`), then deletes\r\n * the referenced records in REVERSE seed-order and, within each seed, in\r\n * reverse insertion-order. Insertion id is strictly increasing across the\r\n * whole run, so deleting by descending `id` satisfies both at once. The whole\r\n * operation runs in a single transaction, and the matching seeds-log rows are\r\n * reset afterwards so `once: true` seeds re-run on the next `warlock seed`.\r\n *\r\n * @returns the number of tracked records deleted.\r\n */\r\nexport async function dropSeedRecords(\r\n datasource: DataSource,\r\n seederName?: string,\r\n): Promise<number> {\r\n const driver = datasource.driver;\r\n\r\n if (!(await driver.blueprint.tableExists(seedRecordsTableName))) {\r\n return 0;\r\n }\r\n\r\n const query = driver.queryBuilder<SeedRecordRow>(seedRecordsTableName);\r\n\r\n if (seederName) {\r\n query.where(\"seeder\", seederName);\r\n }\r\n\r\n const rows = await query.get();\r\n\r\n if (rows.length === 0) {\r\n return 0;\r\n }\r\n\r\n // Descending insertion id == reverse seed-order + reverse within-seed order.\r\n const ordered = [...rows].sort((a, b) => b.id - a.id);\r\n\r\n await runInTransaction(async () => {\r\n for (const row of ordered) {\r\n await driver.deleteMany(row.table, { id: row.recordId });\r\n }\r\n\r\n // Clear the tracked refs we just acted on.\r\n if (seederName) {\r\n await driver.deleteMany(seedRecordsTableName, { seeder: seederName });\r\n } else {\r\n await driver.deleteMany(seedRecordsTableName, {});\r\n }\r\n\r\n // Reset the seeds-log rows so `once: true` seeds re-run.\r\n const seederNames = seederName ? [seederName] : [...new Set(ordered.map((row) => row.seeder))];\r\n\r\n for (const name of seederNames) {\r\n await driver.deleteMany(seedsTableName, { name });\r\n }\r\n });\r\n\r\n return ordered.length;\r\n}\r\n\r\n/**\r\n * Run database seeds.\r\n *\r\n * @example\r\n * ```ts\r\n * import { seedCommandAction } from \"@warlock.js/core\";\r\n *\r\n * await seedCommandAction({\r\n * command: \"seed\",\r\n * options: { fresh: true, list: false }\r\n * });\r\n * ```\r\n *\r\n * @param overrides Programmatic-only overrides (e.g. an injected `clock`); the\r\n * CLI never passes this, so the command-line signature stays unchanged.\r\n */\r\nexport async function seedCommandAction(\r\n options: CommandActionData,\r\n overrides?: SeedCommandOverrides,\r\n) {\r\n const { path, fresh, transaction, list, drop } = options.options;\r\n\r\n const datasource = dataSourceRegistry.get();\r\n\r\n if (drop) {\r\n // `--drop` (boolean) undoes every tracked record; `--drop=<name>` scopes to\r\n // a single seeder.\r\n const seederName = typeof drop === \"string\" ? drop : undefined;\r\n\r\n const deleted = await dropSeedRecords(datasource, seederName);\r\n\r\n const scope = seederName ? colors.cyan(seederName) : \"all seeders\";\r\n console.log(\r\n `🗑️ Dropped ${colors.redBright(deleted)} tracked record(s) for ${scope}; matching seeds-log entries reset.`,\r\n );\r\n return;\r\n }\r\n\r\n if (list) {\r\n const seedFiles = await listSeedsFiles();\r\n\r\n if (seedFiles.length === 0) {\r\n console.log(\"No seeds found.\");\r\n return;\r\n }\r\n\r\n const seedersManager = new SeedersManager();\r\n\r\n seedersManager.register(...seedFiles);\r\n\r\n const seeds = seedersManager.sort().seeders.map((seed) => {\r\n return {\r\n name: seed.name,\r\n order: seed.order,\r\n enabled: seed.enabled,\r\n };\r\n });\r\n\r\n console.table(seeds);\r\n\r\n console.log(\r\n `Total Seeds: ${colors.blueBright(seeds.length)}, enabled: ${colors.greenBright(seeds.filter((seed) => seed.enabled !== false).length)}, disabled: ${colors.redBright(seeds.filter((seed) => seed.enabled === false).length)}`,\r\n );\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await clearAllTables(datasource);\r\n }\r\n\r\n const seeds = path\r\n ? [await loadSeedFile(Path.toAbsolute(path as string))]\r\n : await listSeedsFiles();\r\n\r\n const seedersManager = new SeedersManager({ clock: overrides?.clock });\r\n\r\n seedersManager.register(...seeds);\r\n\r\n await seedersManager.run(transaction as boolean);\r\n}\r\n\r\nasync function listSeedsFiles() {\r\n const seedsFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/seeds/*.ts\");\r\n\r\n const seeds = [];\r\n\r\n for (const seedFile of seedsFiles) {\r\n const seed = await loadSeedFile(seedFile);\r\n seeds.push(seed);\r\n }\r\n\r\n return seeds;\r\n}\r\n\r\nasync function loadSeedFile(absPath: string): Promise<Seeder> {\r\n const relativePath = Path.toRelative(absPath);\r\n const seedImport = await filesOrchestrator.load<{ default: Seeder }>(relativePath);\r\n\r\n if (!seedImport || !seedImport.default) {\r\n throw new Error(`Seeder file ${relativePath} does not export a default seeder.`);\r\n }\r\n\r\n return seedImport.default;\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"seed-command-action.mjs","names":["runInTransaction"],"sources":["../../../../../../../core/src/database/seed-command-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n DataSource,\n dataSourceRegistry,\n transaction as runInTransaction,\n} from \"@warlock.js/cascade\";\nimport type { CommandActionData } from \"../commands/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { isTestFile } from \"../utils/is-test-file\";\nimport type { Seeder } from \"./seeds/seeder\";\nimport { SeedersManager } from \"./seeds/seeders.manager\";\nimport type { SeedClock, SeedRecordRef } from \"./seeds/types\";\nimport { seedRecordsTableName, seedsTableName } from \"./seeds/utils\";\n\n/**\n * Programmatic-only overrides for {@link seedCommandAction}.\n *\n * Not derived from CLI flags (the CLI passes only {@link CommandActionData}),\n * so a back-fill script or a test can inject behaviour the command line can't\n * express.\n */\nexport type SeedCommandOverrides = {\n /**\n * Clock the run reads for the `now` handed to each seeder and for the\n * seeds-log metadata timestamps. Defaults to `() => new Date()`. Forwarded to\n * {@link SeedersManager}'s `clock` option.\n */\n clock?: SeedClock;\n};\n\n/**\n * Truncate every table except cascade's migrations tracking table — wiping it\n * would make the next `warlock migrate` re-run every migration.\n */\nexport async function clearAllTables(datasource: DataSource) {\n const migrationsTable = datasource.migrations?.table ?? \"_migrations\";\n const tables = await datasource.driver.blueprint.listTables();\n\n for (const table of tables) {\n if (table === migrationsTable) continue;\n\n await datasource.driver.truncateTable(table, { cascade: true });\n }\n}\n\ntype SeedRecordRow = SeedRecordRef & { id: number };\n\n/**\n * Undo seeded data by deleting the records a seeder tracked via `track`.\n *\n * Reads `seed_records` (all seeders, or a single `seederName`), then deletes\n * the referenced records in REVERSE seed-order and, within each seed, in\n * reverse insertion-order. Insertion id is strictly increasing across the\n * whole run, so deleting by descending `id` satisfies both at once. The whole\n * operation runs in a single transaction, and the matching seeds-log rows are\n * reset afterwards so `once: true` seeds re-run on the next `warlock seed`.\n *\n * @returns the number of tracked records deleted.\n */\nexport async function dropSeedRecords(\n datasource: DataSource,\n seederName?: string,\n): Promise<number> {\n const driver = datasource.driver;\n\n if (!(await driver.blueprint.tableExists(seedRecordsTableName))) {\n return 0;\n }\n\n const query = driver.queryBuilder<SeedRecordRow>(seedRecordsTableName);\n\n if (seederName) {\n query.where(\"seeder\", seederName);\n }\n\n const rows = await query.get();\n\n if (rows.length === 0) {\n return 0;\n }\n\n // Descending insertion id == reverse seed-order + reverse within-seed order.\n const ordered = [...rows].sort((a, b) => b.id - a.id);\n\n await runInTransaction(async () => {\n for (const row of ordered) {\n await driver.deleteMany(row.table, { id: row.recordId });\n }\n\n // Clear the tracked refs we just acted on.\n if (seederName) {\n await driver.deleteMany(seedRecordsTableName, { seeder: seederName });\n } else {\n await driver.deleteMany(seedRecordsTableName, {});\n }\n\n // Reset the seeds-log rows so `once: true` seeds re-run.\n const seederNames = seederName ? [seederName] : [...new Set(ordered.map((row) => row.seeder))];\n\n for (const name of seederNames) {\n await driver.deleteMany(seedsTableName, { name });\n }\n });\n\n return ordered.length;\n}\n\n/**\n * Run database seeds.\n *\n * @example\n * ```ts\n * import { seedCommandAction } from \"@warlock.js/core\";\n *\n * await seedCommandAction({\n * command: \"seed\",\n * options: { fresh: true, list: false }\n * });\n * ```\n *\n * @param overrides Programmatic-only overrides (e.g. an injected `clock`); the\n * CLI never passes this, so the command-line signature stays unchanged.\n */\nexport async function seedCommandAction(\n options: CommandActionData,\n overrides?: SeedCommandOverrides,\n) {\n const { path, fresh, transaction, list, drop } = options.options;\n\n const datasource = dataSourceRegistry.get();\n\n if (drop) {\n // `--drop` (boolean) undoes every tracked record; `--drop=<name>` scopes to\n // a single seeder.\n const seederName = typeof drop === \"string\" ? drop : undefined;\n\n const deleted = await dropSeedRecords(datasource, seederName);\n\n const scope = seederName ? colors.cyan(seederName) : \"all seeders\";\n console.log(\n `🗑️ Dropped ${colors.redBright(deleted)} tracked record(s) for ${scope}; matching seeds-log entries reset.`,\n );\n return;\n }\n\n if (list) {\n const seedFiles = await listSeedsFiles();\n\n if (seedFiles.length === 0) {\n console.log(\"No seeds found.\");\n return;\n }\n\n const seedersManager = new SeedersManager();\n\n seedersManager.register(...seedFiles);\n\n const seeds = seedersManager.sort().seeders.map((seed) => {\n return {\n name: seed.name,\n order: seed.order,\n enabled: seed.enabled,\n };\n });\n\n console.table(seeds);\n\n console.log(\n `Total Seeds: ${colors.blueBright(seeds.length)}, enabled: ${colors.greenBright(seeds.filter((seed) => seed.enabled !== false).length)}, disabled: ${colors.redBright(seeds.filter((seed) => seed.enabled === false).length)}`,\n );\n return;\n }\n\n if (fresh) {\n await clearAllTables(datasource);\n }\n\n const seeds = path\n ? [await loadSeedFile(Path.toAbsolute(path as string))]\n : await listSeedsFiles();\n\n const seedersManager = new SeedersManager({ clock: overrides?.clock });\n\n seedersManager.register(...seeds);\n\n await seedersManager.run(transaction as boolean);\n}\n\nasync function listSeedsFiles() {\n const seedsFiles = (await getFilesFromDirectory(srcPath(\"app\"), \"*/seeds/*.ts\")).filter(\n (file) => !isTestFile(file),\n );\n\n const seeds = [];\n\n for (const seedFile of seedsFiles) {\n const seed = await loadSeedFile(seedFile);\n seeds.push(seed);\n }\n\n return seeds;\n}\n\nasync function loadSeedFile(absPath: string): Promise<Seeder> {\n const relativePath = Path.toRelative(absPath);\n const seedImport = await filesOrchestrator.load<{ default: Seeder }>(relativePath);\n\n if (!seedImport || !seedImport.default) {\n throw new Error(`Seeder file ${relativePath} does not export a default seeder.`);\n }\n\n return seedImport.default;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dependency-graph.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dependency-graph.ts"],"sourcesContent":["import type { FileManager } from \"./file-manager\";\r\n\r\n/**\r\n * Dependency Graph\r\n * Tracks bidirectional relationships between files:\r\n * - dependencies: files that this file imports\r\n * - dependents: files that import this file\r\n *\r\n * Each forward edge also carries an `isTypeOnly` flag so cycle detection\r\n * can distinguish runtime cycles from ones formed purely by `import type`\r\n * edges — the latter are erased by TypeScript and have no runtime effect.\r\n */\r\nexport class DependencyGraph {\r\n /**\r\n * Map of file -> files it depends on (imports).\r\n * Key: relative file path.\r\n * Value: relative dependency path → `{ isTypeOnly }` for that edge.\r\n */\r\n private dependencies = new Map<string, Map<string, { isTypeOnly: boolean }>>();\r\n\r\n /**\r\n * Map of file -> files that depend on it (importers)\r\n * Key: relative file path\r\n * Value: Set of relative file paths that import this file\r\n */\r\n private dependents = new Map<string, Set<string>>();\r\n\r\n /**\r\n * Reference to the files map for accessing FileManager instances\r\n * Used to read per-file typeOnlyDependencies during graph build.\r\n */\r\n private files: Map<string, FileManager> | null = null;\r\n\r\n /**\r\n * Build dependency graph from FileManager map\r\n */\r\n public build(files: Map<string, FileManager>) {\r\n this.files = files;\r\n\r\n this.dependencies.clear();\r\n this.dependents.clear();\r\n\r\n for (const [relativePath, fileManager] of files) {\r\n if (!this.dependencies.has(relativePath)) {\r\n this.dependencies.set(relativePath, new Map());\r\n }\r\n\r\n if (!this.dependents.has(relativePath)) {\r\n this.dependents.set(relativePath, new Set());\r\n }\r\n\r\n for (const dependency of fileManager.dependencies) {\r\n const isTypeOnly = fileManager.typeOnlyDependencies.has(dependency);\r\n\r\n this.addDependency(relativePath, dependency, isTypeOnly);\r\n }\r\n }\r\n\r\n const cycles = this.detectCircularDependencies();\r\n\r\n if (cycles.length > 0) {\r\n this.displayCircularDependencyWarnings(cycles);\r\n }\r\n }\r\n\r\n /**\r\n * Add a dependency relationship.\r\n * If the edge already exists, the type-only flag is ANDed: any runtime\r\n * statement between the two files makes the edge runtime permanently.\r\n *\r\n * @param file The file that has the dependency\r\n * @param dependency The file being depended upon\r\n * @param isTypeOnly Whether every reference from `file` to `dependency` is type-only\r\n */\r\n public addDependency(file: string, dependency: string, isTypeOnly: boolean = false) {\r\n if (!this.dependencies.has(file)) {\r\n this.dependencies.set(file, new Map());\r\n }\r\n\r\n if (!this.dependents.has(dependency)) {\r\n this.dependents.set(dependency, new Set());\r\n }\r\n\r\n const edges = this.dependencies.get(file)!;\r\n const existing = edges.get(dependency);\r\n\r\n if (existing) {\r\n existing.isTypeOnly = existing.isTypeOnly && isTypeOnly;\r\n } else {\r\n edges.set(dependency, { isTypeOnly });\r\n }\r\n\r\n this.dependents.get(dependency)!.add(file);\r\n }\r\n\r\n /**\r\n * Remove a dependency relationship\r\n * @param file The file that has the dependency\r\n * @param dependency The file being depended upon\r\n */\r\n public removeDependency(file: string, dependency: string) {\r\n this.dependencies.get(file)?.delete(dependency);\r\n this.dependents.get(dependency)?.delete(file);\r\n }\r\n\r\n /**\r\n * Remove all relationships for a file\r\n * @param file The file to remove\r\n */\r\n public removeFile(file: string) {\r\n const deps = this.dependencies.get(file);\r\n\r\n if (deps) {\r\n for (const dependency of deps.keys()) {\r\n this.dependents.get(dependency)?.delete(file);\r\n }\r\n }\r\n\r\n const dependents = this.dependents.get(file);\r\n\r\n if (dependents) {\r\n for (const dependent of dependents) {\r\n this.dependencies.get(dependent)?.delete(file);\r\n }\r\n }\r\n\r\n this.dependencies.delete(file);\r\n this.dependents.delete(file);\r\n }\r\n\r\n /**\r\n * Update dependencies for a file.\r\n *\r\n * Accepts the updated `Set<string>` of dependency paths; when the caller\r\n * has per-edge type-only info, pass `typeOnlyDependencies` so the new\r\n * edges get the correct flag. Missing flag defaults to runtime.\r\n *\r\n * @param file The file to update\r\n * @param newDependencies New set of dependency paths\r\n * @param typeOnlyDependencies Subset of `newDependencies` whose edges are type-only\r\n */\r\n public updateFile(\r\n file: string,\r\n newDependencies: Set<string>,\r\n typeOnlyDependencies: Set<string> = new Set(),\r\n ) {\r\n const oldEdges = this.dependencies.get(file) ?? new Map<string, { isTypeOnly: boolean }>();\r\n\r\n for (const oldDep of oldEdges.keys()) {\r\n if (!newDependencies.has(oldDep)) {\r\n this.removeDependency(file, oldDep);\r\n }\r\n }\r\n\r\n for (const newDep of newDependencies) {\r\n const isTypeOnly = typeOnlyDependencies.has(newDep);\r\n const existing = oldEdges.get(newDep);\r\n\r\n if (!existing) {\r\n this.addDependency(file, newDep, isTypeOnly);\r\n\r\n continue;\r\n }\r\n\r\n if (existing.isTypeOnly !== isTypeOnly) {\r\n existing.isTypeOnly = isTypeOnly;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Get files that this file depends on (imports)\r\n * @param file The file to check\r\n * @returns Set of files this file imports\r\n */\r\n public getDependencies(file: string): Set<string> {\r\n const edges = this.dependencies.get(file);\r\n\r\n if (!edges) {\r\n return new Set();\r\n }\r\n\r\n return new Set(edges.keys());\r\n }\r\n\r\n /**\r\n * Check whether the edge `file → dependency` is type-only.\r\n * Returns false if the edge does not exist.\r\n */\r\n public isEdgeTypeOnly(file: string, dependency: string): boolean {\r\n return this.dependencies.get(file)?.get(dependency)?.isTypeOnly === true;\r\n }\r\n\r\n /**\r\n * Get files that depend on this file (importers)\r\n * @param file The file to check\r\n * @returns Set of files that import this file\r\n */\r\n public getDependents(file: string): Set<string> {\r\n return this.dependents.get(file) || new Set();\r\n }\r\n\r\n /**\r\n * Get invalidation chain for a file\r\n * Returns all files that need to be reloaded when this file changes\r\n * Includes the file itself and all transitive dependents\r\n * @param file The file that changed\r\n * @returns Array of files to invalidate (in order)\r\n */\r\n public getInvalidationChain(file: string): string[] {\r\n const chain: string[] = [file];\r\n const visited = new Set([file]);\r\n\r\n const traverse = (current: string) => {\r\n const deps = this.getDependents(current);\r\n for (const dep of deps) {\r\n if (!visited.has(dep)) {\r\n visited.add(dep);\r\n chain.push(dep);\r\n traverse(dep); // Recursive traversal\r\n }\r\n }\r\n };\r\n\r\n traverse(file);\r\n return chain;\r\n }\r\n\r\n /**\r\n * Detect circular dependencies in the dependency graph.\r\n * Uses depth-first search to find cycles.\r\n *\r\n * Filtering rule (per-edge): a cycle is suppressed when **any** edge in\r\n * the chain is type-only. TypeScript erases type-only imports entirely,\r\n * so a single type-only edge breaks the cycle at runtime. Only warn when\r\n * every edge is a runtime binding — that's the cycle that actually loops\r\n * at load time.\r\n *\r\n * @returns Array of circular dependency chains (each chain is an array of file paths)\r\n */\r\n public detectCircularDependencies(): string[][] {\r\n const cycles: string[][] = [];\r\n const visited = new Set<string>();\r\n const recursionStack = new Set<string>();\r\n\r\n const dfs = (file: string, path: string[]): void => {\r\n visited.add(file);\r\n recursionStack.add(file);\r\n path.push(file);\r\n\r\n const edges = this.dependencies.get(file);\r\n\r\n if (edges) {\r\n for (const dep of edges.keys()) {\r\n if (!visited.has(dep)) {\r\n dfs(dep, [...path]);\r\n } else if (recursionStack.has(dep)) {\r\n const cycleStart = path.indexOf(dep);\r\n const cycle = [...path.slice(cycleStart), dep];\r\n\r\n cycles.push(cycle);\r\n }\r\n }\r\n }\r\n\r\n recursionStack.delete(file);\r\n };\r\n\r\n for (const file of this.dependencies.keys()) {\r\n if (!visited.has(file)) {\r\n dfs(file, []);\r\n }\r\n }\r\n\r\n return cycles.filter((cycle) => this.isRuntimeCycle(cycle));\r\n }\r\n\r\n /**\r\n * True iff every edge in the cycle is a runtime binding. The cycle array\r\n * ends with a duplicate of its start (`[A, B, C, A]`) so we walk the\r\n * consecutive pairs once — any type-only edge means TS erases the loop\r\n * and there is no runtime cycle to report.\r\n */\r\n private isRuntimeCycle(cycle: string[]): boolean {\r\n for (let index = 0; index < cycle.length - 1; index++) {\r\n const from = cycle[index];\r\n const to = cycle[index + 1];\r\n\r\n // `index < cycle.length - 1` bounds both reads. Skipping an unreadable\r\n // edge rather than defaulting: this decides whether a cycle is\r\n // TYPE-ONLY, and a `?? \"\"` would ask `isEdgeTypeOnly(\"\", \"\")`, whose\r\n // answer is about a pair of edges that do not exist. A cycle wrongly\r\n // classified as type-only is a real circular import reported as harmless.\r\n if (from === undefined || to === undefined) continue;\r\n\r\n if (this.isEdgeTypeOnly(from, to)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Display circular dependency warnings in a formatted, user-friendly way\r\n * Shows each cycle with visual tree structure and helpful recommendations\r\n */\r\n private displayCircularDependencyWarnings(cycles: string[][]): void {\r\n const colors = {\r\n yellow: (text: string) => `\\x1b[33m${text}\\x1b[0m`,\r\n cyan: (text: string) => `\\x1b[36m${text}\\x1b[0m`,\r\n dim: (text: string) => `\\x1b[2m${text}\\x1b[0m`,\r\n bold: (text: string) => `\\x1b[1m${text}\\x1b[0m`,\r\n green: (text: string) => `\\x1b[32m${text}\\x1b[0m`,\r\n };\r\n\r\n console.log(\"\");\r\n console.log(colors.yellow(\"⚠️ Circular Dependencies Detected\"));\r\n console.log(colors.dim(\"━\".repeat(60)));\r\n console.log(\"\");\r\n console.log(\r\n colors.dim(\r\n `Found ${colors.bold(cycles.length.toString())} circular dependency chain${cycles.length > 1 ? \"s\" : \"\"}`,\r\n ),\r\n );\r\n console.log(\"\");\r\n\r\n cycles.forEach((cycle, index) => {\r\n console.log(colors.cyan(` ${index + 1}. Cycle with ${cycle.length - 1} files:`));\r\n console.log(\"\");\r\n\r\n // Display the cycle chain\r\n cycle.forEach((file, fileIndex) => {\r\n const isLast = fileIndex === cycle.length - 1;\r\n const arrow = isLast ? colors.dim(\" └─→ \") : colors.dim(\" ├─→ \");\r\n const fileName = file.split(\"/\").pop() || file;\r\n const filePath = colors.dim(file.replace(fileName, \"\"));\r\n\r\n if (isLast) {\r\n // Last item is the same as first (completes the cycle)\r\n console.log(\r\n arrow + colors.yellow(`${filePath}${colors.bold(fileName)} (cycle completes)`),\r\n );\r\n } else {\r\n console.log(arrow + filePath + colors.bold(fileName));\r\n }\r\n });\r\n\r\n console.log(\"\");\r\n });\r\n\r\n console.log(colors.dim(\"━\".repeat(60)));\r\n console.log(colors.yellow(\"💡 How to Fix:\"));\r\n console.log(\"\");\r\n\r\n // This whole block prints \"How to Fix\" advice for a cycle that was just\r\n // reported, so there is always one — but this is diagnostic OUTPUT, and a\r\n // formatter that throws while explaining a problem replaces the problem\r\n // with itself. An empty list simply has no example to show.\r\n const exampleCycle = cycles[0] ?? [];\r\n const fileA = exampleCycle[0]?.split(\"/\").pop() || \"fileA.ts\";\r\n const fileB = exampleCycle[1]?.split(\"/\").pop() || \"fileB.ts\";\r\n const stripExt = (file: string) => file.replace(/\\.tsx?$/, \"\");\r\n\r\n const kind = classifyCycle(exampleCycle);\r\n\r\n if (kind === \"resource\" || kind === \"model\") {\r\n // Cascade's `lazy()` is purpose-built for circular relations between\r\n // resources and models — recommend it first, not as a footnote.\r\n console.log(colors.green(` ✨ Use lazy() for the back-reference`));\r\n console.log(\r\n colors.dim(\r\n ` ${kind === \"resource\" ? \"Resources\" : \"Models\"} that reference each other should declare`,\r\n ),\r\n );\r\n console.log(colors.dim(` one side with lazy() so the import resolves at access time:`));\r\n console.log(\"\");\r\n console.log(colors.dim(` import { lazy } from \"@warlock.js/cascade\";`));\r\n console.log(colors.dim(` // inside ${fileB}:`));\r\n console.log(\r\n colors.dim(\r\n ` other: lazy(() => ${kind === \"resource\" ? \"OtherResource\" : \"OtherModel\"})`,\r\n ),\r\n );\r\n console.log(\"\");\r\n } else {\r\n // Generic cycle — dynamic import is the most common reliable fix.\r\n console.log(colors.green(` ✨ Convert one edge to a dynamic import`));\r\n console.log(colors.dim(` In ${fileB}, change:`));\r\n console.log(colors.dim(` import { SomeClass } from \"./${stripExt(fileA)}\";`));\r\n console.log(colors.dim(` To:`));\r\n console.log(colors.dim(` const { SomeClass } = await import(\"./${stripExt(fileA)}\");`));\r\n console.log(\"\");\r\n }\r\n\r\n console.log(colors.dim(\" Other options:\"));\r\n console.log(colors.dim(\" • Extract the shared symbols into a third file both can import.\"));\r\n console.log(\r\n colors.dim(\" • Pass the dependency as a constructor/function parameter instead.\"),\r\n );\r\n console.log(\"\");\r\n\r\n console.log(colors.dim(\"━\".repeat(60)));\r\n console.log(colors.dim(\" Circular dependencies can cause HMR issues and hard-to-debug\"));\r\n console.log(colors.dim(\" initialization order problems. Consider refactoring.\"));\r\n console.log(\"\");\r\n }\r\n\r\n /**\r\n * Get statistics about the dependency graph\r\n */\r\n public getStats() {\r\n let totalDependencies = 0;\r\n let maxDependencies = 0;\r\n let maxDependents = 0;\r\n let mostDependedFile = \"\";\r\n let mostDependingFile = \"\";\r\n\r\n for (const [file, deps] of this.dependencies) {\r\n totalDependencies += deps.size;\r\n if (deps.size > maxDependencies) {\r\n maxDependencies = deps.size;\r\n mostDependingFile = file;\r\n }\r\n }\r\n\r\n for (const [file, deps] of this.dependents) {\r\n if (deps.size > maxDependents) {\r\n maxDependents = deps.size;\r\n mostDependedFile = file;\r\n }\r\n }\r\n\r\n return {\r\n totalFiles: this.dependencies.size,\r\n totalDependencies,\r\n avgDependenciesPerFile: totalDependencies / this.dependencies.size || 0,\r\n maxDependencies,\r\n maxDependents,\r\n mostDependingFile,\r\n mostDependedFile,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Pick the most useful \"fix-it\" hint for a cycle by inspecting the file\r\n * extensions involved. Cascade resources/models have a first-class fix\r\n * (`lazy()`); generic cycles just want a dynamic import.\r\n */\r\nfunction classifyCycle(cycle: string[]): \"resource\" | \"model\" | \"generic\" {\r\n const files = cycle.slice(0, -1); // last entry repeats the first\r\n if (files.every((file) => file.endsWith(\".resource.ts\") || file.endsWith(\".resource.tsx\"))) {\r\n return \"resource\";\r\n }\r\n if (files.every((file) => file.endsWith(\".model.ts\") || file.endsWith(\".model.tsx\"))) {\r\n return \"model\";\r\n }\r\n return \"generic\";\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"dependency-graph.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dependency-graph.ts"],"sourcesContent":["import type { FileManager } from \"./file-manager\";\n\n/**\n * Dependency Graph\n * Tracks bidirectional relationships between files:\n * - dependencies: files that this file imports\n * - dependents: files that import this file\n *\n * Each forward edge also carries an `isTypeOnly` flag so cycle detection\n * can distinguish runtime cycles from ones formed purely by `import type`\n * edges — the latter are erased by TypeScript and have no runtime effect.\n */\nexport class DependencyGraph {\n /**\n * Map of file -> files it depends on (imports).\n * Key: relative file path.\n * Value: relative dependency path → `{ isTypeOnly }` for that edge.\n */\n private dependencies = new Map<string, Map<string, { isTypeOnly: boolean }>>();\n\n /**\n * Map of file -> files that depend on it (importers)\n * Key: relative file path\n * Value: Set of relative file paths that import this file\n */\n private dependents = new Map<string, Set<string>>();\n\n /**\n * Reference to the files map for accessing FileManager instances\n * Used to read per-file typeOnlyDependencies during graph build.\n */\n private files: Map<string, FileManager> | null = null;\n\n /**\n * Build dependency graph from FileManager map\n */\n public build(files: Map<string, FileManager>) {\n this.files = files;\n\n this.dependencies.clear();\n this.dependents.clear();\n\n for (const [relativePath, fileManager] of files) {\n if (!this.dependencies.has(relativePath)) {\n this.dependencies.set(relativePath, new Map());\n }\n\n if (!this.dependents.has(relativePath)) {\n this.dependents.set(relativePath, new Set());\n }\n\n for (const dependency of fileManager.dependencies) {\n const isTypeOnly = fileManager.typeOnlyDependencies.has(dependency);\n\n this.addDependency(relativePath, dependency, isTypeOnly);\n }\n }\n\n const cycles = this.detectCircularDependencies();\n\n if (cycles.length > 0) {\n this.displayCircularDependencyWarnings(cycles);\n }\n }\n\n /**\n * Add a dependency relationship.\n * If the edge already exists, the type-only flag is ANDed: any runtime\n * statement between the two files makes the edge runtime permanently.\n *\n * @param file The file that has the dependency\n * @param dependency The file being depended upon\n * @param isTypeOnly Whether every reference from `file` to `dependency` is type-only\n */\n public addDependency(file: string, dependency: string, isTypeOnly: boolean = false) {\n if (!this.dependencies.has(file)) {\n this.dependencies.set(file, new Map());\n }\n\n if (!this.dependents.has(dependency)) {\n this.dependents.set(dependency, new Set());\n }\n\n const edges = this.dependencies.get(file)!;\n const existing = edges.get(dependency);\n\n if (existing) {\n existing.isTypeOnly = existing.isTypeOnly && isTypeOnly;\n } else {\n edges.set(dependency, { isTypeOnly });\n }\n\n this.dependents.get(dependency)!.add(file);\n }\n\n /**\n * Remove a dependency relationship\n * @param file The file that has the dependency\n * @param dependency The file being depended upon\n */\n public removeDependency(file: string, dependency: string) {\n this.dependencies.get(file)?.delete(dependency);\n this.dependents.get(dependency)?.delete(file);\n }\n\n /**\n * Remove all relationships for a file\n * @param file The file to remove\n */\n public removeFile(file: string) {\n const deps = this.dependencies.get(file);\n\n if (deps) {\n for (const dependency of deps.keys()) {\n this.dependents.get(dependency)?.delete(file);\n }\n }\n\n const dependents = this.dependents.get(file);\n\n if (dependents) {\n for (const dependent of dependents) {\n this.dependencies.get(dependent)?.delete(file);\n }\n }\n\n this.dependencies.delete(file);\n this.dependents.delete(file);\n }\n\n /**\n * Update dependencies for a file.\n *\n * Accepts the updated `Set<string>` of dependency paths; when the caller\n * has per-edge type-only info, pass `typeOnlyDependencies` so the new\n * edges get the correct flag. Missing flag defaults to runtime.\n *\n * @param file The file to update\n * @param newDependencies New set of dependency paths\n * @param typeOnlyDependencies Subset of `newDependencies` whose edges are type-only\n */\n public updateFile(\n file: string,\n newDependencies: Set<string>,\n typeOnlyDependencies: Set<string> = new Set(),\n ) {\n const oldEdges = this.dependencies.get(file) ?? new Map<string, { isTypeOnly: boolean }>();\n\n for (const oldDep of oldEdges.keys()) {\n if (!newDependencies.has(oldDep)) {\n this.removeDependency(file, oldDep);\n }\n }\n\n for (const newDep of newDependencies) {\n const isTypeOnly = typeOnlyDependencies.has(newDep);\n const existing = oldEdges.get(newDep);\n\n if (!existing) {\n this.addDependency(file, newDep, isTypeOnly);\n\n continue;\n }\n\n if (existing.isTypeOnly !== isTypeOnly) {\n existing.isTypeOnly = isTypeOnly;\n }\n }\n }\n\n /**\n * Get files that this file depends on (imports)\n * @param file The file to check\n * @returns Set of files this file imports\n */\n public getDependencies(file: string): Set<string> {\n const edges = this.dependencies.get(file);\n\n if (!edges) {\n return new Set();\n }\n\n return new Set(edges.keys());\n }\n\n /**\n * Check whether the edge `file → dependency` is type-only.\n * Returns false if the edge does not exist.\n */\n public isEdgeTypeOnly(file: string, dependency: string): boolean {\n return this.dependencies.get(file)?.get(dependency)?.isTypeOnly === true;\n }\n\n /**\n * Get files that depend on this file (importers)\n * @param file The file to check\n * @returns Set of files that import this file\n */\n public getDependents(file: string): Set<string> {\n return this.dependents.get(file) || new Set();\n }\n\n /**\n * Get invalidation chain for a file\n * Returns all files that need to be reloaded when this file changes\n * Includes the file itself and all transitive dependents\n * @param file The file that changed\n * @returns Array of files to invalidate (in order)\n */\n public getInvalidationChain(file: string): string[] {\n const chain: string[] = [file];\n const visited = new Set([file]);\n\n const traverse = (current: string) => {\n const deps = this.getDependents(current);\n for (const dep of deps) {\n if (!visited.has(dep)) {\n visited.add(dep);\n chain.push(dep);\n traverse(dep); // Recursive traversal\n }\n }\n };\n\n traverse(file);\n return chain;\n }\n\n /**\n * Detect circular dependencies in the dependency graph.\n * Uses depth-first search to find cycles.\n *\n * Filtering rule (per-edge): a cycle is suppressed when **any** edge in\n * the chain is type-only. TypeScript erases type-only imports entirely,\n * so a single type-only edge breaks the cycle at runtime. Only warn when\n * every edge is a runtime binding — that's the cycle that actually loops\n * at load time.\n *\n * @returns Array of circular dependency chains (each chain is an array of file paths)\n */\n public detectCircularDependencies(): string[][] {\n const cycles: string[][] = [];\n const visited = new Set<string>();\n const recursionStack = new Set<string>();\n\n const dfs = (file: string, path: string[]): void => {\n visited.add(file);\n recursionStack.add(file);\n path.push(file);\n\n const edges = this.dependencies.get(file);\n\n if (edges) {\n for (const dep of edges.keys()) {\n if (!visited.has(dep)) {\n dfs(dep, [...path]);\n } else if (recursionStack.has(dep)) {\n const cycleStart = path.indexOf(dep);\n const cycle = [...path.slice(cycleStart), dep];\n\n cycles.push(cycle);\n }\n }\n }\n\n recursionStack.delete(file);\n };\n\n for (const file of this.dependencies.keys()) {\n if (!visited.has(file)) {\n dfs(file, []);\n }\n }\n\n return cycles.filter((cycle) => this.isRuntimeCycle(cycle));\n }\n\n /**\n * True iff every edge in the cycle is a runtime binding. The cycle array\n * ends with a duplicate of its start (`[A, B, C, A]`) so we walk the\n * consecutive pairs once — any type-only edge means TS erases the loop\n * and there is no runtime cycle to report.\n */\n private isRuntimeCycle(cycle: string[]): boolean {\n for (let index = 0; index < cycle.length - 1; index++) {\n const from = cycle[index];\n const to = cycle[index + 1];\n\n // `index < cycle.length - 1` bounds both reads. Skipping an unreadable\n // edge rather than defaulting: this decides whether a cycle is\n // TYPE-ONLY, and a `?? \"\"` would ask `isEdgeTypeOnly(\"\", \"\")`, whose\n // answer is about a pair of edges that do not exist. A cycle wrongly\n // classified as type-only is a real circular import reported as harmless.\n if (from === undefined || to === undefined) continue;\n\n if (this.isEdgeTypeOnly(from, to)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Display circular dependency warnings in a formatted, user-friendly way\n * Shows each cycle with visual tree structure and helpful recommendations\n */\n private displayCircularDependencyWarnings(cycles: string[][]): void {\n const colors = {\n yellow: (text: string) => `\\x1b[33m${text}\\x1b[0m`,\n cyan: (text: string) => `\\x1b[36m${text}\\x1b[0m`,\n dim: (text: string) => `\\x1b[2m${text}\\x1b[0m`,\n bold: (text: string) => `\\x1b[1m${text}\\x1b[0m`,\n green: (text: string) => `\\x1b[32m${text}\\x1b[0m`,\n };\n\n console.log(\"\");\n console.log(colors.yellow(\"⚠️ Circular Dependencies Detected\"));\n console.log(colors.dim(\"━\".repeat(60)));\n console.log(\"\");\n console.log(\n colors.dim(\n `Found ${colors.bold(cycles.length.toString())} circular dependency chain${cycles.length > 1 ? \"s\" : \"\"}`,\n ),\n );\n console.log(\"\");\n\n cycles.forEach((cycle, index) => {\n console.log(colors.cyan(` ${index + 1}. Cycle with ${cycle.length - 1} files:`));\n console.log(\"\");\n\n // Display the cycle chain\n cycle.forEach((file, fileIndex) => {\n const isLast = fileIndex === cycle.length - 1;\n const arrow = isLast ? colors.dim(\" └─→ \") : colors.dim(\" ├─→ \");\n const fileName = file.split(\"/\").pop() || file;\n const filePath = colors.dim(file.replace(fileName, \"\"));\n\n if (isLast) {\n // Last item is the same as first (completes the cycle)\n console.log(\n arrow + colors.yellow(`${filePath}${colors.bold(fileName)} (cycle completes)`),\n );\n } else {\n console.log(arrow + filePath + colors.bold(fileName));\n }\n });\n\n console.log(\"\");\n });\n\n console.log(colors.dim(\"━\".repeat(60)));\n console.log(colors.yellow(\"💡 How to Fix:\"));\n console.log(\"\");\n\n // This whole block prints \"How to Fix\" advice for a cycle that was just\n // reported, so there is always one — but this is diagnostic OUTPUT, and a\n // formatter that throws while explaining a problem replaces the problem\n // with itself. An empty list simply has no example to show.\n const exampleCycle = cycles[0] ?? [];\n const fileA = exampleCycle[0]?.split(\"/\").pop() || \"fileA.ts\";\n const fileB = exampleCycle[1]?.split(\"/\").pop() || \"fileB.ts\";\n const stripExt = (file: string) => file.replace(/\\.tsx?$/, \"\");\n\n const kind = classifyCycle(exampleCycle);\n\n if (kind === \"resource\" || kind === \"model\") {\n // Cascade's `lazy()` is purpose-built for circular relations between\n // resources and models — recommend it first, not as a footnote.\n console.log(colors.green(` ✨ Use lazy() for the back-reference`));\n console.log(\n colors.dim(\n ` ${kind === \"resource\" ? \"Resources\" : \"Models\"} that reference each other should declare`,\n ),\n );\n console.log(colors.dim(` one side with lazy() so the import resolves at access time:`));\n console.log(\"\");\n console.log(colors.dim(` import { lazy } from \"@warlock.js/cascade\";`));\n console.log(colors.dim(` // inside ${fileB}:`));\n console.log(\n colors.dim(\n ` other: lazy(() => ${kind === \"resource\" ? \"OtherResource\" : \"OtherModel\"})`,\n ),\n );\n console.log(\"\");\n } else {\n // Generic cycle — dynamic import is the most common reliable fix.\n console.log(colors.green(` ✨ Convert one edge to a dynamic import`));\n console.log(colors.dim(` In ${fileB}, change:`));\n console.log(colors.dim(` import { SomeClass } from \"./${stripExt(fileA)}\";`));\n console.log(colors.dim(` To:`));\n console.log(colors.dim(` const { SomeClass } = await import(\"./${stripExt(fileA)}\");`));\n console.log(\"\");\n }\n\n console.log(colors.dim(\" Other options:\"));\n console.log(colors.dim(\" • Extract the shared symbols into a third file both can import.\"));\n console.log(\n colors.dim(\" • Pass the dependency as a constructor/function parameter instead.\"),\n );\n console.log(\"\");\n\n console.log(colors.dim(\"━\".repeat(60)));\n console.log(colors.dim(\" Circular dependencies can cause HMR issues and hard-to-debug\"));\n console.log(colors.dim(\" initialization order problems. Consider refactoring.\"));\n console.log(\"\");\n }\n\n /**\n * Get statistics about the dependency graph\n */\n public getStats() {\n let totalDependencies = 0;\n let maxDependencies = 0;\n let maxDependents = 0;\n let mostDependedFile = \"\";\n let mostDependingFile = \"\";\n\n for (const [file, deps] of this.dependencies) {\n totalDependencies += deps.size;\n if (deps.size > maxDependencies) {\n maxDependencies = deps.size;\n mostDependingFile = file;\n }\n }\n\n for (const [file, deps] of this.dependents) {\n if (deps.size > maxDependents) {\n maxDependents = deps.size;\n mostDependedFile = file;\n }\n }\n\n return {\n totalFiles: this.dependencies.size,\n totalDependencies,\n avgDependenciesPerFile: totalDependencies / this.dependencies.size || 0,\n maxDependencies,\n maxDependents,\n mostDependingFile,\n mostDependedFile,\n };\n }\n}\n\n/**\n * Pick the most useful \"fix-it\" hint for a cycle by inspecting the file\n * extensions involved. Cascade resources/models have a first-class fix\n * (`lazy()`); generic cycles just want a dynamic import.\n */\nfunction classifyCycle(cycle: string[]): \"resource\" | \"model\" | \"generic\" {\n const files = cycle.slice(0, -1); // last entry repeats the first\n if (files.every((file) => file.endsWith(\".resource.ts\") || file.endsWith(\".resource.tsx\"))) {\n return \"resource\";\n }\n if (files.every((file) => file.endsWith(\".model.ts\") || file.endsWith(\".model.tsx\"))) {\n return \"model\";\n }\n return \"generic\";\n}\n"],"mappings":"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"}
@@ -14,6 +14,7 @@ import { devServerShortcuts } from "./shortcuts.mjs";
14
14
  import { BootPreconditionError } from "./boot-precondition-error.mjs";
15
15
  import { LayerExecutor } from "./layer-executor.mjs";
16
16
  import { printReadyBlock } from "./ready-block.mjs";
17
+ import { registerInstalledDevtools } from "./register-installed-devtools.mjs";
17
18
  import { typeGenerator } from "./type-generator.mjs";
18
19
  import { colors } from "@mongez/copper";
19
20
  import events from "@mongez/events";
@@ -53,6 +54,7 @@ var DevelopmentServer = class {
53
54
  } catch (error) {
54
55
  throw new BootPreconditionError(error.message, { cause: error });
55
56
  }
57
+ await registerInstalledDevtools();
56
58
  await connectorsManager.startPhase("late");
57
59
  await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());
58
60
  this.layerExecutor = new LayerExecutor(filesOrchestrator.getDependencyGraph(), filesOrchestrator.specialFilesCollector, filesOrchestrator.moduleLoader, (absolutePath) => filesOrchestrator.bumpVersion(absolutePath), () => filesOrchestrator.flushVersionBumps());