@warlock.js/core 5.24.0 → 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 (203) hide show
  1. package/CHANGELOG.md +30 -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/response.d.mts +5 -5
  113. package/esm/http/response.d.mts.map +1 -1
  114. package/esm/http/response.mjs.map +1 -1
  115. package/esm/http/server-rebuilt.d.mts +20 -0
  116. package/esm/http/server-rebuilt.d.mts.map +1 -0
  117. package/esm/http/server-rebuilt.mjs +21 -0
  118. package/esm/http/server-rebuilt.mjs.map +1 -0
  119. package/esm/http/tracing/index.mjs +1 -1
  120. package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
  121. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
  122. package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
  123. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
  124. package/esm/http/tracing/tracing.type.d.mts +2 -1
  125. package/esm/http/tracing/tracing.type.d.mts.map +1 -1
  126. package/esm/http/types.d.mts +9 -0
  127. package/esm/http/types.d.mts.map +1 -1
  128. package/esm/image/image.mjs.map +1 -1
  129. package/esm/index.d.mts +4 -2
  130. package/esm/index.mjs +4 -2
  131. package/esm/loader/app-module-importer.d.mts +17 -0
  132. package/esm/loader/app-module-importer.d.mts.map +1 -0
  133. package/esm/loader/app-module-importer.mjs +20 -0
  134. package/esm/loader/app-module-importer.mjs.map +1 -0
  135. package/esm/logger/logger.mjs.map +1 -1
  136. package/esm/mail/events.d.mts +7 -0
  137. package/esm/mail/events.d.mts.map +1 -1
  138. package/esm/mail/events.mjs +7 -0
  139. package/esm/mail/events.mjs.map +1 -1
  140. package/esm/mail/send-mail.d.mts.map +1 -1
  141. package/esm/mail/send-mail.mjs +23 -2
  142. package/esm/mail/send-mail.mjs.map +1 -1
  143. package/esm/mail/types.d.mts +5 -1
  144. package/esm/mail/types.d.mts.map +1 -1
  145. package/esm/mail/types.mjs.map +1 -1
  146. package/esm/manifest/manifest-manager.mjs.map +1 -1
  147. package/esm/production/build-contributions.mjs +19 -3
  148. package/esm/production/build-contributions.mjs.map +1 -1
  149. package/esm/production/production-builder.mjs +28 -8
  150. package/esm/production/production-builder.mjs.map +1 -1
  151. package/esm/production/production-supervisor.mjs +6 -0
  152. package/esm/production/production-supervisor.mjs.map +1 -1
  153. package/esm/production/resolve-build-config.d.mts +1 -1
  154. package/esm/production/resolve-build-config.mjs +15 -0
  155. package/esm/production/resolve-build-config.mjs.map +1 -1
  156. package/esm/production/route-registration-child.mjs +14 -2
  157. package/esm/production/route-registration-child.mjs.map +1 -1
  158. package/esm/production/route-registration-snapshot.mjs +9 -2
  159. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  160. package/esm/resource/resource-field-builder.mjs +1 -1
  161. package/esm/resource/resource-field-builder.mjs.map +1 -1
  162. package/esm/resource/resource.d.mts +5 -0
  163. package/esm/resource/resource.d.mts.map +1 -1
  164. package/esm/resource/resource.mjs +17 -6
  165. package/esm/resource/resource.mjs.map +1 -1
  166. package/esm/router/router.d.mts.map +1 -1
  167. package/esm/router/router.mjs +1 -0
  168. package/esm/router/router.mjs.map +1 -1
  169. package/esm/router/types.d.mts +2 -1
  170. package/esm/router/types.d.mts.map +1 -1
  171. package/esm/tests/index.d.mts +3 -2
  172. package/esm/tests/index.mjs +3 -2
  173. package/esm/tests/vitest-setup.d.mts +16 -1
  174. package/esm/tests/vitest-setup.d.mts.map +1 -1
  175. package/esm/tests/vitest-setup.mjs +20 -2
  176. package/esm/tests/vitest-setup.mjs.map +1 -1
  177. package/esm/use-cases/use-case-events.mjs.map +1 -1
  178. package/esm/utils/is-test-file.mjs +15 -0
  179. package/esm/utils/is-test-file.mjs.map +1 -0
  180. package/esm/utils/normalized-path.d.mts +3 -1
  181. package/esm/utils/normalized-path.d.mts.map +1 -1
  182. package/esm/utils/normalized-path.mjs +4 -2
  183. package/esm/utils/normalized-path.mjs.map +1 -1
  184. package/esm/utils/paths.mjs.map +1 -1
  185. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  186. package/esm/validation/validateAll.mjs +9 -3
  187. package/esm/validation/validateAll.mjs.map +1 -1
  188. package/esm/warlock-config/default-configurations.mjs +1 -0
  189. package/esm/warlock-config/default-configurations.mjs.map +1 -1
  190. package/esm/warlock-config/types.d.mts +7 -0
  191. package/esm/warlock-config/types.d.mts.map +1 -1
  192. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  193. package/llms-full.txt +264 -139
  194. package/package.json +15 -11
  195. package/skills/configure-app/SKILL.md +2 -0
  196. package/skills/define-resource/SKILL.md +4 -0
  197. package/skills/request-tracing/SKILL.md +38 -15
  198. package/skills/run-app/SKILL.md +71 -34
  199. package/skills/send-mail/SKILL.md +55 -25
  200. package/skills/test-http/SKILL.md +19 -8
  201. package/skills/test-service/SKILL.md +61 -40
  202. package/skills/warlock-conventions/SKILL.md +26 -18
  203. package/skills/write-cli-command/SKILL.md +50 -45
@@ -1,3 +1,4 @@
1
+ import { AppModuleImporter } from "../loader/app-module-importer.mjs";
1
2
  import { TestConnectorsSelection } from "./test-connectors-selection.mjs";
2
3
 
3
4
  //#region ../core/src/tests/vitest-setup.d.ts
@@ -11,7 +12,21 @@ type TestSetupOptions = {
11
12
  * it.
12
13
  */
13
14
  connectors?: TestConnectorsSelection;
15
+ /**
16
+ * Import hook for APPLICATION modules (config, routes, events, ...).
17
+ *
18
+ * Pass `(file) => import(file)` from your `src/test-setup.ts`: that file is
19
+ * app code, so Vitest handles the `import()` and app modules land in the same
20
+ * module graph as your test files. Without it Warlock imports them natively,
21
+ * one worker holds two copies of every class (`User`, ...), and `instanceof`
22
+ * fails. Defaults to native import.
23
+ */
24
+ importModule?: AppModuleImporter;
14
25
  };
26
+ /**
27
+ * Warn — once per process — that app modules are loaded outside Vitest's graph.
28
+ */
29
+ declare function warnMissingImportModule(importModule: AppModuleImporter | undefined): void;
15
30
  /**
16
31
  * Raised by the lifecycle itself, never by the runtime it manages — a conflict
17
32
  * between two calls, or a refusal to reuse a runtime that did not close.
@@ -66,5 +81,5 @@ declare function setupTest(options?: TestSetupOptions): Promise<void>;
66
81
  */
67
82
  declare function teardownTest(): Promise<void>;
68
83
  //#endregion
69
- export { TestLifecycleError, TestSetupOptions, setupTest, teardownTest };
84
+ export { TestLifecycleError, TestSetupOptions, setupTest, teardownTest, warnMissingImportModule };
70
85
  //# sourceMappingURL=vitest-setup.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;;KAiDY,gBAAA;EAAA;;;;AAS0B;AAOtC;;;EAPE,UAAA,GAAa,uBAAuB;AAAA;;;;AAQF;cADvB,kBAAA,SAA2B,KAAK;cACxB,OAAA;AAAA;;;;;;AAqC+C;AAgDpE;;;;AAA6C;;;;;;;;;;;;;;;;;iBAhDvB,SAAA,CAAU,OAAA,GAAU,gBAAA,GAAmB,OAAO;;;;;;;;;;;;;;;;;;iBAgD9C,YAAA,IAAgB,OAAO"}
1
+ {"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;;;KAmDY,gBAAA;EAAA;;;;;;;;EASV,UAAA,GAAa,uBAAA;EAUmB;AAQlC;;;;AAAmF;AAkBnF;;;EA1BE,YAAA,GAAe,iBAAiB;AAAA;;;;iBAQlB,uBAAA,CAAwB,YAA2C,EAA7B,iBAAiB;AAwDvE;;;;AAAA,cAtCa,kBAAA,SAA2B,KAAK;cACxB,OAAA;AAAA;;AAqC+C;AA0DpE;;;;AAA6C;;;;;;;;;;;;;;;;;;;;;iBA1DvB,SAAA,CAAU,OAAA,GAAU,gBAAA,GAAmB,OAAO;;;;;;;;;;;;;;;;;;iBA0D9C,YAAA,IAAgB,OAAO"}
@@ -3,6 +3,7 @@ import { bootstrap } from "../bootstrap.mjs";
3
3
  import { connectorsManager } from "../connectors/connectors-manager.mjs";
4
4
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
5
5
  import "../connectors/index.mjs";
6
+ import { setAppModuleImporter } from "../loader/app-module-importer.mjs";
6
7
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
7
8
  import { loadConfigFiles } from "../config/load-config-files.mjs";
8
9
  import { describeConnectorsSelection, isSameConnectorsSelection, normalizeConnectorNames, readRequestedConnectors, resolveEffectiveConnectors } from "./test-connectors-selection.mjs";
@@ -33,6 +34,17 @@ import { DEFAULT_TEST_SETUP_TIMEOUT, describeExpiredSetup, readConfiguredSetupTi
33
34
  * - HTTP global setup stays separately owned by `startHttpTestServer` /
34
35
  * `stopHttpTestServer`.
35
36
  */
37
+ const IMPORT_MODULE_WARNED_KEY = Symbol.for("warlock.core.importModuleWarned");
38
+ /**
39
+ * Warn — once per process — that app modules are loaded outside Vitest's graph.
40
+ */
41
+ function warnMissingImportModule(importModule) {
42
+ if (importModule || !process.env.VITEST) return;
43
+ const holder = globalThis;
44
+ if (holder[IMPORT_MODULE_WARNED_KEY]) return;
45
+ holder[IMPORT_MODULE_WARNED_KEY] = true;
46
+ console.warn("[warlock] Warlock loads app modules outside Vitest's module graph, so classes like User exist twice and instanceof fails. Pass importModule: (file) => import(file) to setupTest().");
47
+ }
36
48
  /**
37
49
  * Raised by the lifecycle itself, never by the runtime it manages — a conflict
38
50
  * between two calls, or a refusal to reuse a runtime that did not close.
@@ -72,6 +84,7 @@ const POISONED_MESSAGE = "setupTest() refuses to start a test runtime: an earlie
72
84
  * await setupTest({ connectors: false });
73
85
  */
74
86
  async function setupTest(options) {
87
+ warnMissingImportModule(options?.importModule);
75
88
  const registry = getTestLifecycleRegistry();
76
89
  const requested = readRequestedConnectors(options?.connectors);
77
90
  if (registry.state === "poisoned") throw new TestLifecycleError(POISONED_MESSAGE);
@@ -88,7 +101,12 @@ async function setupTest(options) {
88
101
  await assertPendingSetupMatches(attempt, requested);
89
102
  return attempt.completion;
90
103
  }
91
- return startTestRuntime(requested);
104
+ setAppModuleImporter(options?.importModule);
105
+ try {
106
+ return await startTestRuntime(requested);
107
+ } finally {
108
+ setAppModuleImporter(void 0);
109
+ }
92
110
  }
93
111
  /**
94
112
  * Shut the test runtime down and release the lifecycle.
@@ -338,5 +356,5 @@ function settled(attempt) {
338
356
  }
339
357
 
340
358
  //#endregion
341
- export { TestLifecycleError, setupTest, teardownTest };
359
+ export { TestLifecycleError, setupTest, teardownTest, warnMissingImportModule };
342
360
  //# sourceMappingURL=vitest-setup.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-setup.mjs","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"sourcesContent":["/**\r\n * Worker test lifecycle — `setupTest` / `teardownTest`.\r\n *\r\n * Implements `contracts/2026-08-12-test-worker-lifecycle.md`. The pair is\r\n * context-scoped, not \"once per worker\": Vitest runs `setupFiles` before EVERY\r\n * test file and rebuilds the module registry each time, so the harness that\r\n * calls `setupTest` owns pairing it with `teardownTest` in the same runtime\r\n * context.\r\n *\r\n * Ownership, stated once because teardown is wider than it looks:\r\n *\r\n * - the runtime assumes exclusive ownership of the process-local\r\n * `connectorsManager` for its lifetime;\r\n * - it owns the framework bootstrap, the application shutdown hooks, and the\r\n * connectors it starts — including the `connectors: false` case, where\r\n * bootstrap still registers hooks;\r\n * - teardown is MANAGER-WIDE. `connectorsManager.shutdown()` has no selective\r\n * ownership handle, so a lifecycle that started two connectors still closes\r\n * every connector the manager holds. Mixing `setupTest` with manual connector\r\n * startup in the same process is unsupported for exactly that reason.\r\n * - HTTP global setup stays separately owned by `startHttpTestServer` /\r\n * `stopHttpTestServer`.\r\n */\r\nimport { Application } from \"../application/application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport {\r\n describeConnectorsSelection,\r\n isSameConnectorsSelection,\r\n normalizeConnectorNames,\r\n readRequestedConnectors,\r\n resolveEffectiveConnectors,\r\n} from \"./test-connectors-selection\";\r\nimport type { RequestedTestConnectors, TestConnectorsSelection } from \"./test-connectors-selection\";\r\nimport { getTestLifecycleRegistry } from \"./test-lifecycle-state\";\r\nimport type { TestLifecycleRegistry, TestSetupAttempt } from \"./test-lifecycle-state\";\r\nimport {\r\n DEFAULT_TEST_SETUP_TIMEOUT,\r\n describeExpiredSetup,\r\n readConfiguredSetupTimeout,\r\n scheduleRealTimeout,\r\n} from \"./test-setup-timeout\";\r\nimport type { TestTimeoutHandle } from \"./test-setup-timeout\";\r\n\r\nexport type { TestConnectorsSelection } from \"./test-connectors-selection\";\r\n\r\nexport type TestSetupOptions = {\r\n /**\r\n * Which connectors to start.\r\n *\r\n * Precedence is `explicit non-undefined option > tests.connectors config >\r\n * true`. Omitting the property, or passing `undefined`, lets project config\r\n * apply — so an optional variable that happens to be `undefined` cannot erase\r\n * it.\r\n */\r\n connectors?: TestConnectorsSelection;\r\n};\r\n\r\n/**\r\n * Raised by the lifecycle itself, never by the runtime it manages — a conflict\r\n * between two calls, or a refusal to reuse a runtime that did not close.\r\n */\r\nexport class TestLifecycleError extends Error {\r\n public constructor(message: string) {\r\n super(message);\r\n\r\n this.name = \"TestLifecycleError\";\r\n }\r\n}\r\n\r\nconst POISONED_MESSAGE =\r\n \"setupTest() refuses to start a test runtime: an earlier teardownTest() reported a shutdown failure, so the connectors, ports and pools it owned are not known to be closed. Restart the Vitest worker before setting up again, or call teardownTest() to retry the shutdown — the lifecycle only returns to idle on a fully successful retry.\";\r\n\r\n/**\r\n * Bootstrap the test runtime and start the selected connectors.\r\n *\r\n * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for\r\n * every file and its module registry is rebuilt with it, measured across both\r\n * pools with isolation on and off. The lifecycle state lives on the runtime\r\n * context rather than in this module precisely so that a rebuild cannot hide a\r\n * runtime that is still up.\r\n *\r\n * Repeated calls:\r\n *\r\n * - concurrent calls with the same effective selection share one startup;\r\n * - while ready, the same effective selection is a no-op;\r\n * - while starting or ready, a DIFFERENT effective selection rejects and leaves\r\n * the live runtime untouched;\r\n * - a failed setup unwinds and returns to idle, so a clean retry is allowed;\r\n * - after a successful `teardownTest`, a later setup may use a different\r\n * selection.\r\n *\r\n * @example\r\n * // let project config decide, falling back to the default set\r\n * await setupTest();\r\n *\r\n * @example\r\n * // bootstrap only — no connectors, whatever config says\r\n * await setupTest({ connectors: false });\r\n */\r\nexport async function setupTest(options?: TestSetupOptions): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n const requested = readRequestedConnectors(options?.connectors);\r\n\r\n if (registry.state === \"poisoned\") {\r\n throw new TestLifecycleError(POISONED_MESSAGE);\r\n }\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n await settled(registry.teardownAttempt);\r\n\r\n return setupTest(options);\r\n }\r\n\r\n if (registry.state === \"ready\") {\r\n assertReadySelectionMatches(registry.activeSelection, requested);\r\n\r\n return;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n const attempt = registry.setupAttempt;\r\n\r\n await assertPendingSetupMatches(attempt, requested);\r\n\r\n return attempt.completion;\r\n }\r\n\r\n return startTestRuntime(requested);\r\n}\r\n\r\n/**\r\n * Shut the test runtime down and release the lifecycle.\r\n *\r\n * - idle is a no-op;\r\n * - concurrent calls share one attempt;\r\n * - a call made while setup is still running waits for that attempt to settle,\r\n * then closes the runtime if it succeeded;\r\n * - the local \"ready\" state is always cleared, including when shutdown rejects;\r\n * - a shutdown rejection is surfaced, never swallowed, and poisons the\r\n * lifecycle — a reported close failure is not proof that anything closed, so\r\n * later `setupTest` calls refuse until the worker is restarted or a retry\r\n * fully succeeds.\r\n *\r\n * ⚠ Individual connector shutdown failures are caught and logged inside\r\n * `connectorsManager.shutdown()`, so they never reach this function and never\r\n * poison anything. This lifecycle can only surface what that layer reports.\r\n */\r\nexport async function teardownTest(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n return registry.teardownAttempt;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n // A runtime that is still opening cannot be closed halfway. Wait for the\r\n // attempt to settle: a failed one has already unwound itself and left the\r\n // lifecycle idle, which the re-entry below then reads as \"nothing to do\".\r\n await settled(registry.setupAttempt.completion);\r\n\r\n return teardownTest();\r\n }\r\n\r\n if (registry.state === \"idle\") {\r\n return;\r\n }\r\n\r\n registry.state = \"stopping\";\r\n\r\n // Deferred by a microtask so the registry is fully published before the\r\n // attempt's first line runs — a synchronous throw would otherwise reach the\r\n // `finally` below, and reset the state, before this function had recorded\r\n // that a teardown was in flight at all.\r\n const attempt = Promise.resolve().then(() => runTestRuntimeShutdown());\r\n\r\n registry.teardownAttempt = attempt;\r\n\r\n return attempt;\r\n}\r\n\r\n/**\r\n * The setup attempt's hang guard, as the lifecycle consumes it.\r\n *\r\n * The bound's POLICY — the default, the config key, the message — lives in\r\n * `test-setup-timeout.ts`. What expiry MEANS lives here.\r\n *\r\n * @internal\r\n */\r\ntype SetupBound = {\r\n /**\r\n * Rejects with the expiry error. Never resolves: the attempt is the only side\r\n * of the race that can succeed.\r\n */\r\n readonly expiry: Promise<never>;\r\n\r\n /**\r\n * Re-aim the bound at `next` ms measured from when the ATTEMPT STARTED, once\r\n * config has made `tests.setupTimeout` readable.\r\n */\r\n rearm(next: number): void;\r\n\r\n /** The race is over — cancel whatever timer is still pending. */\r\n settle(): void;\r\n\r\n /** Whether the bound, rather than the attempt, won the race. */\r\n hasExpired(): boolean;\r\n};\r\n\r\n/**\r\n * Arm the hang guard for one setup attempt.\r\n *\r\n * Armed at the default BEFORE the attempt runs, because `tests.setupTimeout` is\r\n * not readable until the attempt has loaded config — and loading config is\r\n * itself one of the steps that can hang. `rearm` then narrows or widens it the\r\n * moment config arrives.\r\n */\r\nfunction armSetupBound(registry: TestLifecycleRegistry): SetupBound {\r\n const schedule = registry.scheduleTimeout ?? scheduleRealTimeout;\r\n const armedAt = Date.now();\r\n\r\n let handle: TestTimeoutHandle | undefined;\r\n let expired = false;\r\n let closed = false;\r\n let expire: (error: unknown) => void = () => undefined;\r\n\r\n const expiry = new Promise<never>((_resolve, reject) => {\r\n expire = reject;\r\n });\r\n\r\n // Only `awaitSetupWithinBound` observes this, and only until the attempt wins\r\n // — after that nobody is listening, and an unobserved rejection surfaces as an\r\n // unhandled rejection that fails an unrelated test file. Same reason as the\r\n // `effectiveSelection` barrier below.\r\n expiry.catch(() => undefined);\r\n\r\n const expireNow = (bound: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n closed = true;\r\n expired = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n\r\n expire(new TestLifecycleError(describeExpiredSetup(bound)));\r\n };\r\n\r\n const armFor = (bound: number, delay: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n handle?.cancel();\r\n\r\n // Already overdue. A bound the attempt has ALREADY exceeded must fire now,\r\n // not be rescheduled for time that was spent before it was known.\r\n if (delay <= 0) {\r\n expireNow(bound);\r\n\r\n return;\r\n }\r\n\r\n handle = schedule(delay, () => expireNow(bound));\r\n };\r\n\r\n const initialBound = registry.setupTimeoutOverride ?? DEFAULT_TEST_SETUP_TIMEOUT;\r\n\r\n armFor(initialBound, initialBound);\r\n\r\n return {\r\n expiry,\r\n\r\n // Measured from `armedAt`, never from now: adding a configured bound to the\r\n // time already spent would make the real bound default-plus-configured, and\r\n // a project that lowered the key would get a LONGER guard than the default.\r\n rearm: (next) => armFor(next, next - (Date.now() - armedAt)),\r\n\r\n settle: () => {\r\n closed = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n },\r\n\r\n hasExpired: () => expired,\r\n };\r\n}\r\n\r\n/**\r\n * Race one setup attempt against its bound.\r\n *\r\n * The losing attempt is NOT cancellable — nothing here can interrupt a bootstrap\r\n * stuck in a socket connect — so when the bound wins, the lifecycle is poisoned\r\n * and every later state write from that abandoned attempt is dropped. That\r\n * dropping happens in `runTestRuntimeStartup`, guarded on `hasExpired()`;\r\n * without it the still-running attempt would quietly overwrite `poisoned` with\r\n * `ready` or `idle` and hand the next caller a runtime nobody owns.\r\n */\r\nasync function awaitSetupWithinBound(\r\n attempt: Promise<void>,\r\n bound: SetupBound,\r\n registry: TestLifecycleRegistry,\r\n): Promise<void> {\r\n try {\r\n await Promise.race([attempt, bound.expiry]);\r\n } catch (error) {\r\n if (bound.hasExpired()) {\r\n registry.state = \"poisoned\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n throw error;\r\n } finally {\r\n bound.settle();\r\n }\r\n}\r\n\r\n/**\r\n * Open a new setup attempt and publish it before it runs.\r\n */\r\nfunction startTestRuntime(requested: RequestedTestConnectors): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n let publishEffectiveSelection: (selection: TestConnectorsSelection) => void = () => undefined;\r\n let failEffectiveSelection: (error: unknown) => void = () => undefined;\r\n\r\n const effectiveSelection = new Promise<TestConnectorsSelection>((resolve, reject) => {\r\n publishEffectiveSelection = resolve;\r\n failEffectiveSelection = reject;\r\n });\r\n\r\n // Only the mixed explicit/config comparison ever awaits this barrier, so its\r\n // rejection is usually unobserved — and an unobserved rejection would surface\r\n // as an unhandled rejection and fail an unrelated test file.\r\n effectiveSelection.catch(() => undefined);\r\n\r\n const bound = armSetupBound(registry);\r\n\r\n const attempt = Promise.resolve().then(() =>\r\n runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound),\r\n );\r\n\r\n // The attempt outlives a bound that beat it, and by then `completion` has\r\n // already rejected with the expiry error — so the attempt's own late rejection\r\n // has no observer. Same unhandled-rejection hazard as above.\r\n attempt.catch(() => undefined);\r\n\r\n // `teardownTest` waits on this same promise, which is how a teardown requested\r\n // during setup inherits the bound. There is deliberately no second timer and\r\n // no separate teardown deadline.\r\n const completion = awaitSetupWithinBound(attempt, bound, registry);\r\n\r\n registry.state = \"starting\";\r\n registry.setupAttempt = { requested, effectiveSelection, completion };\r\n\r\n return completion;\r\n}\r\n\r\n/**\r\n * The startup attempt itself.\r\n *\r\n * On failure: best-effort unwind, reset bookkeeping, rethrow the ORIGINAL error\r\n * unchanged. A cleanup failure is a secondary diagnostic and never replaces the\r\n * cause the caller needs to read.\r\n */\r\nasync function runTestRuntimeStartup(\r\n requested: RequestedTestConnectors,\r\n publishEffectiveSelection: (selection: TestConnectorsSelection) => void,\r\n failEffectiveSelection: (error: unknown) => void,\r\n bound: SetupBound,\r\n): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n try {\r\n Application.setEnvironment(\"test\");\r\n\r\n await warlockConfigManager.load();\r\n await bootstrap();\r\n\r\n await filesOrchestrator.init();\r\n await loadConfigFiles(true);\r\n\r\n // The first point at which `tests.setupTimeout` is readable at all. An\r\n // invalid value throws from here and takes the normal failure path — falling\r\n // back to the default would silently erase what the project configured.\r\n const configuredTimeout = readConfiguredSetupTimeout();\r\n\r\n if (configuredTimeout !== undefined) {\r\n bound.rearm(configuredTimeout);\r\n }\r\n\r\n // Resolved here and not at call time: the config layer is only readable once\r\n // the four steps above have run.\r\n const effectiveSelection = resolveEffectiveConnectors(requested);\r\n\r\n // Every state write below is conditional on this attempt still being the one\r\n // the lifecycle is waiting for. Once the bound has expired the lifecycle is\r\n // poisoned and this attempt is abandoned — it may still be running, but it\r\n // no longer speaks for the runtime.\r\n if (!bound.hasExpired()) {\r\n registry.activeSelection = effectiveSelection;\r\n }\r\n\r\n publishEffectiveSelection(effectiveSelection);\r\n\r\n await startSelectedConnectors(effectiveSelection);\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"ready\";\r\n registry.setupAttempt = undefined;\r\n }\r\n } catch (error) {\r\n failEffectiveSelection(error);\r\n\r\n await unwindPartialStartup();\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"idle\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n console.error(\"[vitest-setup] Failed to setup test environment:\", error);\r\n\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * `false` means none at all. Everything else starts something: an array starts\r\n * exactly those, and `true` starts the default set — all but http, which is the\r\n * global setup's job and shared across every worker.\r\n */\r\nasync function startSelectedConnectors(selection: TestConnectorsSelection): Promise<void> {\r\n if (selection === false) {\r\n return;\r\n }\r\n\r\n if (Array.isArray(selection)) {\r\n await connectorsManager.start(normalizeConnectorNames(selection));\r\n\r\n return;\r\n }\r\n\r\n await connectorsManager.startWithout([\"http\"]);\r\n}\r\n\r\n/**\r\n * Best-effort teardown of whatever a failed startup managed to start.\r\n *\r\n * Runs while an error is already in flight, so no step here may throw and every\r\n * step is attempted independently.\r\n */\r\nasync function unwindPartialStartup(): Promise<void> {\r\n // Two steps rather than one: `connectorsManager.shutdown()` runs the\r\n // application hooks itself, but it runs them FIRST and does not isolate their\r\n // rejection — so a hook that rejects would take the connector teardown down\r\n // with it and leave every connector up. Both calls are idempotent, so running\r\n // the hooks on their own first costs nothing and means neither failure can\r\n // skip the other step.\r\n await attemptCleanupStep(\"application shutdown hooks\", () => Application.runShutdownHooks());\r\n await attemptCleanupStep(\"connectors shutdown\", () => connectorsManager.shutdown());\r\n}\r\n\r\n/**\r\n * Run one cleanup step, isolating its failure so the next step still runs.\r\n */\r\nasync function attemptCleanupStep(step: string, run: () => Promise<void>): Promise<void> {\r\n try {\r\n await run();\r\n } catch (error) {\r\n // Reported, not thrown — it is secondary to the startup error about to be\r\n // rethrown, and losing that one to this would be the worse outcome.\r\n console.error(`[vitest-setup] Cleanup after a failed setup did not complete (${step}):`, error);\r\n }\r\n}\r\n\r\n/**\r\n * The teardown attempt itself.\r\n */\r\nasync function runTestRuntimeShutdown(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n let shutdownSucceeded = false;\r\n\r\n try {\r\n // Manager-wide, and deliberately called whatever the selection was: with\r\n // `connectors: false` nothing was started, but bootstrap still registered\r\n // application shutdown hooks, and the manager runs those first (see\r\n // `connectors-manager.ts` — `Application.runShutdownHooks()` is its first\r\n // line). Skipping the call to \"match\" an empty selection would leak them.\r\n await connectorsManager.shutdown();\r\n\r\n shutdownSucceeded = true;\r\n } finally {\r\n // In a `finally`, not after the `await`: a rejected shutdown must not leave\r\n // the lifecycle claiming a runtime it no longer owns. Poisoned rather than\r\n // idle, because a cleared flag does not prove that ports, sockets, pools or\r\n // timers closed.\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n registry.teardownAttempt = undefined;\r\n registry.state = shutdownSucceeded ? \"idle\" : \"poisoned\";\r\n }\r\n}\r\n\r\n/**\r\n * Reject when a ready runtime was set up with a different selection.\r\n */\r\nfunction assertReadySelectionMatches(\r\n activeSelection: TestConnectorsSelection | undefined,\r\n requested: RequestedTestConnectors,\r\n): void {\r\n if (activeSelection === undefined) {\r\n return;\r\n }\r\n\r\n // Config is loaded by the time anything is ready, so the config-derived layer\r\n // is resolvable right here.\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Reject when an in-flight setup attempt is starting a different selection.\r\n */\r\nasync function assertPendingSetupMatches(\r\n attempt: TestSetupAttempt,\r\n requested: RequestedTestConnectors,\r\n): Promise<void> {\r\n const pending = attempt.requested;\r\n\r\n // Two calls that both defer to project config resolve to the same thing by\r\n // construction — knowable without waiting for config to load.\r\n if (!pending.isExplicit && !requested.isExplicit) {\r\n return;\r\n }\r\n\r\n if (pending.isExplicit && requested.isExplicit) {\r\n assertSelectionsMatch(pending.selection, requested.selection);\r\n\r\n return;\r\n }\r\n\r\n // Mixed: one side is config-derived and config is not readable until the\r\n // running attempt has loaded it, so wait for that barrier first.\r\n const activeSelection = await attempt.effectiveSelection;\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Name both selections: a caller looking at this message has to be able to tell\r\n * which call to change without reading the framework's source.\r\n */\r\nfunction assertSelectionsMatch(\r\n activeSelection: TestConnectorsSelection,\r\n requestedSelection: TestConnectorsSelection,\r\n): void {\r\n if (isSameConnectorsSelection(activeSelection, requestedSelection)) {\r\n return;\r\n }\r\n\r\n throw new TestLifecycleError(\r\n `setupTest() is already using ${describeConnectorsSelection(activeSelection)}, and this call asked for ${describeConnectorsSelection(requestedSelection)}. One test runtime serves one connector selection — call teardownTest() before setting up a different one.`,\r\n );\r\n}\r\n\r\n/**\r\n * Await an attempt for its state transition only, not its result.\r\n */\r\nfunction settled(attempt: Promise<void>): Promise<void> {\r\n return attempt.catch(() => undefined);\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"vitest-setup.mjs","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"sourcesContent":["/**\n * Worker test lifecycle — `setupTest` / `teardownTest`.\n *\n * Implements `contracts/2026-08-12-test-worker-lifecycle.md`. The pair is\n * context-scoped, not \"once per worker\": Vitest runs `setupFiles` before EVERY\n * test file and rebuilds the module registry each time, so the harness that\n * calls `setupTest` owns pairing it with `teardownTest` in the same runtime\n * context.\n *\n * Ownership, stated once because teardown is wider than it looks:\n *\n * - the runtime assumes exclusive ownership of the process-local\n * `connectorsManager` for its lifetime;\n * - it owns the framework bootstrap, the application shutdown hooks, and the\n * connectors it starts — including the `connectors: false` case, where\n * bootstrap still registers hooks;\n * - teardown is MANAGER-WIDE. `connectorsManager.shutdown()` has no selective\n * ownership handle, so a lifecycle that started two connectors still closes\n * every connector the manager holds. Mixing `setupTest` with manual connector\n * startup in the same process is unsupported for exactly that reason.\n * - HTTP global setup stays separately owned by `startHttpTestServer` /\n * `stopHttpTestServer`.\n */\nimport { Application } from \"../application/application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { setAppModuleImporter } from \"../loader/app-module-importer\";\nimport type { AppModuleImporter } from \"../loader/app-module-importer\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport {\n describeConnectorsSelection,\n isSameConnectorsSelection,\n normalizeConnectorNames,\n readRequestedConnectors,\n resolveEffectiveConnectors,\n} from \"./test-connectors-selection\";\nimport type { RequestedTestConnectors, TestConnectorsSelection } from \"./test-connectors-selection\";\nimport { getTestLifecycleRegistry } from \"./test-lifecycle-state\";\nimport type { TestLifecycleRegistry, TestSetupAttempt } from \"./test-lifecycle-state\";\nimport {\n DEFAULT_TEST_SETUP_TIMEOUT,\n describeExpiredSetup,\n readConfiguredSetupTimeout,\n scheduleRealTimeout,\n} from \"./test-setup-timeout\";\nimport type { TestTimeoutHandle } from \"./test-setup-timeout\";\n\nexport type { TestConnectorsSelection } from \"./test-connectors-selection\";\n\nexport type TestSetupOptions = {\n /**\n * Which connectors to start.\n *\n * Precedence is `explicit non-undefined option > tests.connectors config >\n * true`. Omitting the property, or passing `undefined`, lets project config\n * apply — so an optional variable that happens to be `undefined` cannot erase\n * it.\n */\n connectors?: TestConnectorsSelection;\n /**\n * Import hook for APPLICATION modules (config, routes, events, ...).\n *\n * Pass `(file) => import(file)` from your `src/test-setup.ts`: that file is\n * app code, so Vitest handles the `import()` and app modules land in the same\n * module graph as your test files. Without it Warlock imports them natively,\n * one worker holds two copies of every class (`User`, ...), and `instanceof`\n * fails. Defaults to native import.\n */\n importModule?: AppModuleImporter;\n};\n\nconst IMPORT_MODULE_WARNED_KEY = Symbol.for(\"warlock.core.importModuleWarned\");\n\n/**\n * Warn — once per process — that app modules are loaded outside Vitest's graph.\n */\nexport function warnMissingImportModule(importModule: AppModuleImporter | undefined): void {\n if (importModule || !process.env.VITEST) return;\n\n const holder = globalThis as Record<symbol, unknown>;\n\n if (holder[IMPORT_MODULE_WARNED_KEY]) return;\n\n holder[IMPORT_MODULE_WARNED_KEY] = true;\n\n console.warn(\n \"[warlock] Warlock loads app modules outside Vitest's module graph, so classes like User exist twice and instanceof fails. Pass importModule: (file) => import(file) to setupTest().\",\n );\n}\n\n/**\n * Raised by the lifecycle itself, never by the runtime it manages — a conflict\n * between two calls, or a refusal to reuse a runtime that did not close.\n */\nexport class TestLifecycleError extends Error {\n public constructor(message: string) {\n super(message);\n\n this.name = \"TestLifecycleError\";\n }\n}\n\nconst POISONED_MESSAGE =\n \"setupTest() refuses to start a test runtime: an earlier teardownTest() reported a shutdown failure, so the connectors, ports and pools it owned are not known to be closed. Restart the Vitest worker before setting up again, or call teardownTest() to retry the shutdown — the lifecycle only returns to idle on a fully successful retry.\";\n\n/**\n * Bootstrap the test runtime and start the selected connectors.\n *\n * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for\n * every file and its module registry is rebuilt with it, measured across both\n * pools with isolation on and off. The lifecycle state lives on the runtime\n * context rather than in this module precisely so that a rebuild cannot hide a\n * runtime that is still up.\n *\n * Repeated calls:\n *\n * - concurrent calls with the same effective selection share one startup;\n * - while ready, the same effective selection is a no-op;\n * - while starting or ready, a DIFFERENT effective selection rejects and leaves\n * the live runtime untouched;\n * - a failed setup unwinds and returns to idle, so a clean retry is allowed;\n * - after a successful `teardownTest`, a later setup may use a different\n * selection.\n *\n * @example\n * // let project config decide, falling back to the default set\n * await setupTest();\n *\n * @example\n * // bootstrap only — no connectors, whatever config says\n * await setupTest({ connectors: false });\n */\nexport async function setupTest(options?: TestSetupOptions): Promise<void> {\n warnMissingImportModule(options?.importModule);\n\n const registry = getTestLifecycleRegistry();\n const requested = readRequestedConnectors(options?.connectors);\n\n if (registry.state === \"poisoned\") {\n throw new TestLifecycleError(POISONED_MESSAGE);\n }\n\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\n await settled(registry.teardownAttempt);\n\n return setupTest(options);\n }\n\n if (registry.state === \"ready\") {\n assertReadySelectionMatches(registry.activeSelection, requested);\n\n return;\n }\n\n if (registry.state === \"starting\" && registry.setupAttempt) {\n const attempt = registry.setupAttempt;\n\n await assertPendingSetupMatches(attempt, requested);\n\n return attempt.completion;\n }\n\n // Scoped to this startup: the loaders read it while app modules import, and\n // it is cleared whatever the outcome so it never leaks past setup.\n setAppModuleImporter(options?.importModule);\n\n try {\n return await startTestRuntime(requested);\n } finally {\n setAppModuleImporter(undefined);\n }\n}\n\n/**\n * Shut the test runtime down and release the lifecycle.\n *\n * - idle is a no-op;\n * - concurrent calls share one attempt;\n * - a call made while setup is still running waits for that attempt to settle,\n * then closes the runtime if it succeeded;\n * - the local \"ready\" state is always cleared, including when shutdown rejects;\n * - a shutdown rejection is surfaced, never swallowed, and poisons the\n * lifecycle — a reported close failure is not proof that anything closed, so\n * later `setupTest` calls refuse until the worker is restarted or a retry\n * fully succeeds.\n *\n * ⚠ Individual connector shutdown failures are caught and logged inside\n * `connectorsManager.shutdown()`, so they never reach this function and never\n * poison anything. This lifecycle can only surface what that layer reports.\n */\nexport async function teardownTest(): Promise<void> {\n const registry = getTestLifecycleRegistry();\n\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\n return registry.teardownAttempt;\n }\n\n if (registry.state === \"starting\" && registry.setupAttempt) {\n // A runtime that is still opening cannot be closed halfway. Wait for the\n // attempt to settle: a failed one has already unwound itself and left the\n // lifecycle idle, which the re-entry below then reads as \"nothing to do\".\n await settled(registry.setupAttempt.completion);\n\n return teardownTest();\n }\n\n if (registry.state === \"idle\") {\n return;\n }\n\n registry.state = \"stopping\";\n\n // Deferred by a microtask so the registry is fully published before the\n // attempt's first line runs — a synchronous throw would otherwise reach the\n // `finally` below, and reset the state, before this function had recorded\n // that a teardown was in flight at all.\n const attempt = Promise.resolve().then(() => runTestRuntimeShutdown());\n\n registry.teardownAttempt = attempt;\n\n return attempt;\n}\n\n/**\n * The setup attempt's hang guard, as the lifecycle consumes it.\n *\n * The bound's POLICY — the default, the config key, the message — lives in\n * `test-setup-timeout.ts`. What expiry MEANS lives here.\n *\n * @internal\n */\ntype SetupBound = {\n /**\n * Rejects with the expiry error. Never resolves: the attempt is the only side\n * of the race that can succeed.\n */\n readonly expiry: Promise<never>;\n\n /**\n * Re-aim the bound at `next` ms measured from when the ATTEMPT STARTED, once\n * config has made `tests.setupTimeout` readable.\n */\n rearm(next: number): void;\n\n /** The race is over — cancel whatever timer is still pending. */\n settle(): void;\n\n /** Whether the bound, rather than the attempt, won the race. */\n hasExpired(): boolean;\n};\n\n/**\n * Arm the hang guard for one setup attempt.\n *\n * Armed at the default BEFORE the attempt runs, because `tests.setupTimeout` is\n * not readable until the attempt has loaded config — and loading config is\n * itself one of the steps that can hang. `rearm` then narrows or widens it the\n * moment config arrives.\n */\nfunction armSetupBound(registry: TestLifecycleRegistry): SetupBound {\n const schedule = registry.scheduleTimeout ?? scheduleRealTimeout;\n const armedAt = Date.now();\n\n let handle: TestTimeoutHandle | undefined;\n let expired = false;\n let closed = false;\n let expire: (error: unknown) => void = () => undefined;\n\n const expiry = new Promise<never>((_resolve, reject) => {\n expire = reject;\n });\n\n // Only `awaitSetupWithinBound` observes this, and only until the attempt wins\n // — after that nobody is listening, and an unobserved rejection surfaces as an\n // unhandled rejection that fails an unrelated test file. Same reason as the\n // `effectiveSelection` barrier below.\n expiry.catch(() => undefined);\n\n const expireNow = (bound: number): void => {\n if (closed) {\n return;\n }\n\n closed = true;\n expired = true;\n\n handle?.cancel();\n handle = undefined;\n\n expire(new TestLifecycleError(describeExpiredSetup(bound)));\n };\n\n const armFor = (bound: number, delay: number): void => {\n if (closed) {\n return;\n }\n\n handle?.cancel();\n\n // Already overdue. A bound the attempt has ALREADY exceeded must fire now,\n // not be rescheduled for time that was spent before it was known.\n if (delay <= 0) {\n expireNow(bound);\n\n return;\n }\n\n handle = schedule(delay, () => expireNow(bound));\n };\n\n const initialBound = registry.setupTimeoutOverride ?? DEFAULT_TEST_SETUP_TIMEOUT;\n\n armFor(initialBound, initialBound);\n\n return {\n expiry,\n\n // Measured from `armedAt`, never from now: adding a configured bound to the\n // time already spent would make the real bound default-plus-configured, and\n // a project that lowered the key would get a LONGER guard than the default.\n rearm: (next) => armFor(next, next - (Date.now() - armedAt)),\n\n settle: () => {\n closed = true;\n\n handle?.cancel();\n handle = undefined;\n },\n\n hasExpired: () => expired,\n };\n}\n\n/**\n * Race one setup attempt against its bound.\n *\n * The losing attempt is NOT cancellable — nothing here can interrupt a bootstrap\n * stuck in a socket connect — so when the bound wins, the lifecycle is poisoned\n * and every later state write from that abandoned attempt is dropped. That\n * dropping happens in `runTestRuntimeStartup`, guarded on `hasExpired()`;\n * without it the still-running attempt would quietly overwrite `poisoned` with\n * `ready` or `idle` and hand the next caller a runtime nobody owns.\n */\nasync function awaitSetupWithinBound(\n attempt: Promise<void>,\n bound: SetupBound,\n registry: TestLifecycleRegistry,\n): Promise<void> {\n try {\n await Promise.race([attempt, bound.expiry]);\n } catch (error) {\n if (bound.hasExpired()) {\n registry.state = \"poisoned\";\n registry.activeSelection = undefined;\n registry.setupAttempt = undefined;\n }\n\n throw error;\n } finally {\n bound.settle();\n }\n}\n\n/**\n * Open a new setup attempt and publish it before it runs.\n */\nfunction startTestRuntime(requested: RequestedTestConnectors): Promise<void> {\n const registry = getTestLifecycleRegistry();\n\n let publishEffectiveSelection: (selection: TestConnectorsSelection) => void = () => undefined;\n let failEffectiveSelection: (error: unknown) => void = () => undefined;\n\n const effectiveSelection = new Promise<TestConnectorsSelection>((resolve, reject) => {\n publishEffectiveSelection = resolve;\n failEffectiveSelection = reject;\n });\n\n // Only the mixed explicit/config comparison ever awaits this barrier, so its\n // rejection is usually unobserved — and an unobserved rejection would surface\n // as an unhandled rejection and fail an unrelated test file.\n effectiveSelection.catch(() => undefined);\n\n const bound = armSetupBound(registry);\n\n const attempt = Promise.resolve().then(() =>\n runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound),\n );\n\n // The attempt outlives a bound that beat it, and by then `completion` has\n // already rejected with the expiry error — so the attempt's own late rejection\n // has no observer. Same unhandled-rejection hazard as above.\n attempt.catch(() => undefined);\n\n // `teardownTest` waits on this same promise, which is how a teardown requested\n // during setup inherits the bound. There is deliberately no second timer and\n // no separate teardown deadline.\n const completion = awaitSetupWithinBound(attempt, bound, registry);\n\n registry.state = \"starting\";\n registry.setupAttempt = { requested, effectiveSelection, completion };\n\n return completion;\n}\n\n/**\n * The startup attempt itself.\n *\n * On failure: best-effort unwind, reset bookkeeping, rethrow the ORIGINAL error\n * unchanged. A cleanup failure is a secondary diagnostic and never replaces the\n * cause the caller needs to read.\n */\nasync function runTestRuntimeStartup(\n requested: RequestedTestConnectors,\n publishEffectiveSelection: (selection: TestConnectorsSelection) => void,\n failEffectiveSelection: (error: unknown) => void,\n bound: SetupBound,\n): Promise<void> {\n const registry = getTestLifecycleRegistry();\n\n try {\n Application.setEnvironment(\"test\");\n\n await warlockConfigManager.load();\n await bootstrap();\n\n await filesOrchestrator.init();\n await loadConfigFiles(true);\n\n // The first point at which `tests.setupTimeout` is readable at all. An\n // invalid value throws from here and takes the normal failure path — falling\n // back to the default would silently erase what the project configured.\n const configuredTimeout = readConfiguredSetupTimeout();\n\n if (configuredTimeout !== undefined) {\n bound.rearm(configuredTimeout);\n }\n\n // Resolved here and not at call time: the config layer is only readable once\n // the four steps above have run.\n const effectiveSelection = resolveEffectiveConnectors(requested);\n\n // Every state write below is conditional on this attempt still being the one\n // the lifecycle is waiting for. Once the bound has expired the lifecycle is\n // poisoned and this attempt is abandoned — it may still be running, but it\n // no longer speaks for the runtime.\n if (!bound.hasExpired()) {\n registry.activeSelection = effectiveSelection;\n }\n\n publishEffectiveSelection(effectiveSelection);\n\n await startSelectedConnectors(effectiveSelection);\n\n if (!bound.hasExpired()) {\n registry.state = \"ready\";\n registry.setupAttempt = undefined;\n }\n } catch (error) {\n failEffectiveSelection(error);\n\n await unwindPartialStartup();\n\n if (!bound.hasExpired()) {\n registry.state = \"idle\";\n registry.activeSelection = undefined;\n registry.setupAttempt = undefined;\n }\n\n console.error(\"[vitest-setup] Failed to setup test environment:\", error);\n\n throw error;\n }\n}\n\n/**\n * `false` means none at all. Everything else starts something: an array starts\n * exactly those, and `true` starts the default set — all but http, which is the\n * global setup's job and shared across every worker.\n */\nasync function startSelectedConnectors(selection: TestConnectorsSelection): Promise<void> {\n if (selection === false) {\n return;\n }\n\n if (Array.isArray(selection)) {\n await connectorsManager.start(normalizeConnectorNames(selection));\n\n return;\n }\n\n await connectorsManager.startWithout([\"http\"]);\n}\n\n/**\n * Best-effort teardown of whatever a failed startup managed to start.\n *\n * Runs while an error is already in flight, so no step here may throw and every\n * step is attempted independently.\n */\nasync function unwindPartialStartup(): Promise<void> {\n // Two steps rather than one: `connectorsManager.shutdown()` runs the\n // application hooks itself, but it runs them FIRST and does not isolate their\n // rejection — so a hook that rejects would take the connector teardown down\n // with it and leave every connector up. Both calls are idempotent, so running\n // the hooks on their own first costs nothing and means neither failure can\n // skip the other step.\n await attemptCleanupStep(\"application shutdown hooks\", () => Application.runShutdownHooks());\n await attemptCleanupStep(\"connectors shutdown\", () => connectorsManager.shutdown());\n}\n\n/**\n * Run one cleanup step, isolating its failure so the next step still runs.\n */\nasync function attemptCleanupStep(step: string, run: () => Promise<void>): Promise<void> {\n try {\n await run();\n } catch (error) {\n // Reported, not thrown — it is secondary to the startup error about to be\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(`[vitest-setup] Cleanup after a failed setup did not complete (${step}):`, error);\n }\n}\n\n/**\n * The teardown attempt itself.\n */\nasync function runTestRuntimeShutdown(): Promise<void> {\n const registry = getTestLifecycleRegistry();\n let shutdownSucceeded = false;\n\n try {\n // Manager-wide, and deliberately called whatever the selection was: with\n // `connectors: false` nothing was started, but bootstrap still registered\n // application shutdown hooks, and the manager runs those first (see\n // `connectors-manager.ts` — `Application.runShutdownHooks()` is its first\n // line). Skipping the call to \"match\" an empty selection would leak them.\n await connectorsManager.shutdown();\n\n shutdownSucceeded = true;\n } finally {\n // In a `finally`, not after the `await`: a rejected shutdown must not leave\n // the lifecycle claiming a runtime it no longer owns. Poisoned rather than\n // idle, because a cleared flag does not prove that ports, sockets, pools or\n // timers closed.\n registry.activeSelection = undefined;\n registry.setupAttempt = undefined;\n registry.teardownAttempt = undefined;\n registry.state = shutdownSucceeded ? \"idle\" : \"poisoned\";\n }\n}\n\n/**\n * Reject when a ready runtime was set up with a different selection.\n */\nfunction assertReadySelectionMatches(\n activeSelection: TestConnectorsSelection | undefined,\n requested: RequestedTestConnectors,\n): void {\n if (activeSelection === undefined) {\n return;\n }\n\n // Config is loaded by the time anything is ready, so the config-derived layer\n // is resolvable right here.\n const requestedSelection = resolveEffectiveConnectors(requested);\n\n assertSelectionsMatch(activeSelection, requestedSelection);\n}\n\n/**\n * Reject when an in-flight setup attempt is starting a different selection.\n */\nasync function assertPendingSetupMatches(\n attempt: TestSetupAttempt,\n requested: RequestedTestConnectors,\n): Promise<void> {\n const pending = attempt.requested;\n\n // Two calls that both defer to project config resolve to the same thing by\n // construction — knowable without waiting for config to load.\n if (!pending.isExplicit && !requested.isExplicit) {\n return;\n }\n\n if (pending.isExplicit && requested.isExplicit) {\n assertSelectionsMatch(pending.selection, requested.selection);\n\n return;\n }\n\n // Mixed: one side is config-derived and config is not readable until the\n // running attempt has loaded it, so wait for that barrier first.\n const activeSelection = await attempt.effectiveSelection;\n const requestedSelection = resolveEffectiveConnectors(requested);\n\n assertSelectionsMatch(activeSelection, requestedSelection);\n}\n\n/**\n * Name both selections: a caller looking at this message has to be able to tell\n * which call to change without reading the framework's source.\n */\nfunction assertSelectionsMatch(\n activeSelection: TestConnectorsSelection,\n requestedSelection: TestConnectorsSelection,\n): void {\n if (isSameConnectorsSelection(activeSelection, requestedSelection)) {\n return;\n }\n\n throw new TestLifecycleError(\n `setupTest() is already using ${describeConnectorsSelection(activeSelection)}, and this call asked for ${describeConnectorsSelection(requestedSelection)}. One test runtime serves one connector selection — call teardownTest() before setting up a different one.`,\n );\n}\n\n/**\n * Await an attempt for its state transition only, not its result.\n */\nfunction settled(attempt: Promise<void>): Promise<void> {\n return attempt.catch(() => undefined);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-case-events.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-events.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\r\nimport type {\r\n UseCaseErrorResult,\r\n UseCaseEventsCallbacksMap,\r\n UseCaseOnExecutingContext,\r\n UseCaseResult,\r\n} from \"./types\";\r\n\r\n/**\r\n * Global event callbacks — shared across all use case instances.\r\n */\r\nexport const globalEventsCallbacksMap: UseCaseEventsCallbacksMap = {\r\n onExecuting: [],\r\n onCompleted: [],\r\n onError: [],\r\n};\r\n\r\n/**\r\n * Subscribe to lifecycle events fired by any use case.\r\n *\r\n * @example\r\n * globalUseCasesEvents.onCompleted((result) => metrics.track(result.name, result.benchmarkResult));\r\n * globalUseCasesEvents.onError((ctx) => logger.error(ctx.name, ctx.error));\r\n */\r\nexport const globalUseCasesEvents = {\r\n onExecuting(callback: (ctx: UseCaseOnExecutingContext) => void) {\r\n globalEventsCallbacksMap.onExecuting.push(callback);\r\n return {\r\n unsubscribe: () => {\r\n const idx = globalEventsCallbacksMap.onExecuting.indexOf(callback);\r\n if (idx !== -1) globalEventsCallbacksMap.onExecuting.splice(idx, 1);\r\n },\r\n };\r\n },\r\n onCompleted<Output>(callback: (result: UseCaseResult<Output>) => void) {\r\n globalEventsCallbacksMap.onCompleted.push(callback);\r\n return {\r\n unsubscribe: () => {\r\n const idx = globalEventsCallbacksMap.onCompleted.indexOf(callback);\r\n if (idx !== -1) globalEventsCallbacksMap.onCompleted.splice(idx, 1);\r\n },\r\n };\r\n },\r\n onError(callback: (ctx: UseCaseErrorResult) => void) {\r\n globalEventsCallbacksMap.onError.push(callback);\r\n return {\r\n unsubscribe: () => {\r\n const idx = globalEventsCallbacksMap.onError.indexOf(callback);\r\n if (idx !== -1) globalEventsCallbacksMap.onError.splice(idx, 1);\r\n },\r\n };\r\n },\r\n};\r\n\r\n/**\r\n * Dispatches a lifecycle event to invocation → use-case → global observers, in that order.\r\n *\r\n * Each observer is awaited sequentially and isolated in its own try/catch — a slow\r\n * observer can't stall the others, and a throwing one can't break the pipeline (e.g.\r\n * a successful use case must not fail because a metrics/broadcast observer threw).\r\n */\r\nexport async function fireLifecycleEvent<EventCtx>(\r\n ctx: EventCtx,\r\n observers: {\r\n invocation?: ((ctx: EventCtx) => void | Promise<void>)[];\r\n useCase?: ((ctx: EventCtx) => void | Promise<void>)[];\r\n global?: ((ctx: EventCtx) => void | Promise<void>)[];\r\n },\r\n) {\r\n const tiers = [observers.invocation, observers.useCase, observers.global];\r\n\r\n for (const tier of tiers) {\r\n if (!tier) {\r\n continue;\r\n }\r\n\r\n for (const observer of tier) {\r\n try {\r\n await observer(ctx);\r\n } catch (observerError) {\r\n log.error(\"use-cases\", \"lifecycle\", \"observer failed\", { error: observerError });\r\n }\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;AAWA,MAAa,2BAAsD;CACjE,aAAa,CAAC;CACd,aAAa,CAAC;CACd,SAAS,CAAC;AACZ;;;;;;;;AASA,MAAa,uBAAuB;CAClC,YAAY,UAAoD;EAC9D,yBAAyB,YAAY,KAAK,QAAQ;EAClD,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,YAAY,QAAQ,QAAQ;GACjE,IAAI,QAAQ,IAAI,yBAAyB,YAAY,OAAO,KAAK,CAAC;EACpE,EACF;CACF;CACA,YAAoB,UAAmD;EACrE,yBAAyB,YAAY,KAAK,QAAQ;EAClD,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,YAAY,QAAQ,QAAQ;GACjE,IAAI,QAAQ,IAAI,yBAAyB,YAAY,OAAO,KAAK,CAAC;EACpE,EACF;CACF;CACA,QAAQ,UAA6C;EACnD,yBAAyB,QAAQ,KAAK,QAAQ;EAC9C,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,QAAQ,QAAQ,QAAQ;GAC7D,IAAI,QAAQ,IAAI,yBAAyB,QAAQ,OAAO,KAAK,CAAC;EAChE,EACF;CACF;AACF;;;;;;;;AASA,eAAsB,mBACpB,KACA,WAKA;CACA,MAAM,QAAQ;EAAC,UAAU;EAAY,UAAU;EAAS,UAAU;CAAM;CAExE,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,MACH;EAGF,KAAK,MAAM,YAAY,MACrB,IAAI;GACF,MAAM,SAAS,GAAG;EACpB,SAAS,eAAe;GACtB,IAAI,MAAM,aAAa,aAAa,mBAAmB,EAAE,OAAO,cAAc,CAAC;EACjF;CAEJ;AACF"}
1
+ {"version":3,"file":"use-case-events.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-events.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type {\n UseCaseErrorResult,\n UseCaseEventsCallbacksMap,\n UseCaseOnExecutingContext,\n UseCaseResult,\n} from \"./types\";\n\n/**\n * Global event callbacks — shared across all use case instances.\n */\nexport const globalEventsCallbacksMap: UseCaseEventsCallbacksMap = {\n onExecuting: [],\n onCompleted: [],\n onError: [],\n};\n\n/**\n * Subscribe to lifecycle events fired by any use case.\n *\n * @example\n * globalUseCasesEvents.onCompleted((result) => metrics.track(result.name, result.benchmarkResult));\n * globalUseCasesEvents.onError((ctx) => logger.error(ctx.name, ctx.error));\n */\nexport const globalUseCasesEvents = {\n onExecuting(callback: (ctx: UseCaseOnExecutingContext) => void) {\n globalEventsCallbacksMap.onExecuting.push(callback);\n return {\n unsubscribe: () => {\n const idx = globalEventsCallbacksMap.onExecuting.indexOf(callback);\n if (idx !== -1) globalEventsCallbacksMap.onExecuting.splice(idx, 1);\n },\n };\n },\n onCompleted<Output>(callback: (result: UseCaseResult<Output>) => void) {\n globalEventsCallbacksMap.onCompleted.push(callback);\n return {\n unsubscribe: () => {\n const idx = globalEventsCallbacksMap.onCompleted.indexOf(callback);\n if (idx !== -1) globalEventsCallbacksMap.onCompleted.splice(idx, 1);\n },\n };\n },\n onError(callback: (ctx: UseCaseErrorResult) => void) {\n globalEventsCallbacksMap.onError.push(callback);\n return {\n unsubscribe: () => {\n const idx = globalEventsCallbacksMap.onError.indexOf(callback);\n if (idx !== -1) globalEventsCallbacksMap.onError.splice(idx, 1);\n },\n };\n },\n};\n\n/**\n * Dispatches a lifecycle event to invocation → use-case → global observers, in that order.\n *\n * Each observer is awaited sequentially and isolated in its own try/catch — a slow\n * observer can't stall the others, and a throwing one can't break the pipeline (e.g.\n * a successful use case must not fail because a metrics/broadcast observer threw).\n */\nexport async function fireLifecycleEvent<EventCtx>(\n ctx: EventCtx,\n observers: {\n invocation?: ((ctx: EventCtx) => void | Promise<void>)[];\n useCase?: ((ctx: EventCtx) => void | Promise<void>)[];\n global?: ((ctx: EventCtx) => void | Promise<void>)[];\n },\n) {\n const tiers = [observers.invocation, observers.useCase, observers.global];\n\n for (const tier of tiers) {\n if (!tier) {\n continue;\n }\n\n for (const observer of tier) {\n try {\n await observer(ctx);\n } catch (observerError) {\n log.error(\"use-cases\", \"lifecycle\", \"observer failed\", { error: observerError });\n }\n }\n }\n}\n"],"mappings":";;;;;;AAWA,MAAa,2BAAsD;CACjE,aAAa,CAAC;CACd,aAAa,CAAC;CACd,SAAS,CAAC;AACZ;;;;;;;;AASA,MAAa,uBAAuB;CAClC,YAAY,UAAoD;EAC9D,yBAAyB,YAAY,KAAK,QAAQ;EAClD,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,YAAY,QAAQ,QAAQ;GACjE,IAAI,QAAQ,IAAI,yBAAyB,YAAY,OAAO,KAAK,CAAC;EACpE,EACF;CACF;CACA,YAAoB,UAAmD;EACrE,yBAAyB,YAAY,KAAK,QAAQ;EAClD,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,YAAY,QAAQ,QAAQ;GACjE,IAAI,QAAQ,IAAI,yBAAyB,YAAY,OAAO,KAAK,CAAC;EACpE,EACF;CACF;CACA,QAAQ,UAA6C;EACnD,yBAAyB,QAAQ,KAAK,QAAQ;EAC9C,OAAO,EACL,mBAAmB;GACjB,MAAM,MAAM,yBAAyB,QAAQ,QAAQ,QAAQ;GAC7D,IAAI,QAAQ,IAAI,yBAAyB,QAAQ,OAAO,KAAK,CAAC;EAChE,EACF;CACF;AACF;;;;;;;;AASA,eAAsB,mBACpB,KACA,WAKA;CACA,MAAM,QAAQ;EAAC,UAAU;EAAY,UAAU;EAAS,UAAU;CAAM;CAExE,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,MACH;EAGF,KAAK,MAAM,YAAY,MACrB,IAAI;GACF,MAAM,SAAS,GAAG;EACpB,SAAS,eAAe;GACtB,IAAI,MAAM,aAAa,aAAa,mBAAmB,EAAE,OAAO,cAAc,CAAC;EACjF;CAEJ;AACF"}
@@ -0,0 +1,15 @@
1
+ //#region ../core/src/utils/is-test-file.ts
2
+ /** `foo.spec.ts`, `foo.test.tsx`, `foo.spec.mjs` … — a file only a test runner can load. */
3
+ const TEST_FILE = /\.(spec|test)\.[cm]?[jt]sx?$/;
4
+ /**
5
+ * Whether a path names a test file. Folder-scanning commands (`warlock seed`,
6
+ * `warlock migrate`) skip these: a spec that sits beside the seeder it tests
7
+ * calls `vi.mock()` at import time, which throws outside Vitest.
8
+ */
9
+ function isTestFile(filePath) {
10
+ return TEST_FILE.test(filePath);
11
+ }
12
+
13
+ //#endregion
14
+ export { isTestFile };
15
+ //# sourceMappingURL=is-test-file.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-test-file.mjs","names":[],"sources":["../../../../../../../core/src/utils/is-test-file.ts"],"sourcesContent":["/** `foo.spec.ts`, `foo.test.tsx`, `foo.spec.mjs` … — a file only a test runner can load. */\nconst TEST_FILE = /\\.(spec|test)\\.[cm]?[jt]sx?$/;\n\n/**\n * Whether a path names a test file. Folder-scanning commands (`warlock seed`,\n * `warlock migrate`) skip these: a spec that sits beside the seeder it tests\n * calls `vi.mock()` at import time, which throws outside Vitest.\n */\nexport function isTestFile(filePath: string): boolean {\n return TEST_FILE.test(filePath);\n}\n"],"mappings":";;AACA,MAAM,YAAY;;;;;;AAOlB,SAAgB,WAAW,UAA2B;CACpD,OAAO,UAAU,KAAK,QAAQ;AAChC"}
@@ -38,7 +38,9 @@ declare class Path {
38
38
  */
39
39
  static join(...paths: string[]): string;
40
40
  /**
41
- * Get directory name of a path
41
+ * Get directory name of a path. The input is normalized first: on POSIX a
42
+ * backslash is a filename character, so `a\b\c.ts` would otherwise have no
43
+ * directory at all.
42
44
  */
43
45
  static dirname(filePath: string): string;
44
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"normalized-path.d.mts","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"mappings":";;AAeA;;;;;;;;;;;;cAAa,IAAA;EAgCa;;;EAAA,OA5BV,UAAA,CAAW,YAAA;EA0CH;;;EAAA,OAnCR,QAAA,CAAS,YAAA;EAiDT;;;EAAA,OA1CA,oBAAA,CAAqB,YAAA;;;;SAOrB,UAAA,CAAW,YAAA;;;;SAOX,SAAA,CAAU,QAAA;;;;SAOV,IAAA,IAAQ,KAAA;;;;SAOR,OAAA,CAAQ,QAAA;;;;SAOR,QAAA,CAAS,QAAA,UAAkB,GAAA;;;;SAO3B,OAAA,CAAQ,QAAA;AAAA"}
1
+ {"version":3,"file":"normalized-path.d.mts","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"mappings":";;AAeA;;;;;;;;;;;;cAAa,IAAA;EAgCa;;;EAAA,OA5BV,UAAA,CAAW,YAAA;EA4CH;;;EAAA,OArCR,QAAA,CAAS,YAAA;EAmDT;;;EAAA,OA5CA,oBAAA,CAAqB,YAAA;;;;SAOrB,UAAA,CAAW,YAAA;;;;SAOX,SAAA,CAAU,QAAA;;;;SAOV,IAAA,IAAQ,KAAA;;;;;;SASR,OAAA,CAAQ,QAAA;;;;SAOR,QAAA,CAAS,QAAA,UAAkB,GAAA;;;;SAO3B,OAAA,CAAQ,QAAA;AAAA"}
@@ -52,10 +52,12 @@ var Path = class {
52
52
  return this.normalize(path.join(...paths));
53
53
  }
54
54
  /**
55
- * Get directory name of a path
55
+ * Get directory name of a path. The input is normalized first: on POSIX a
56
+ * backslash is a filename character, so `a\b\c.ts` would otherwise have no
57
+ * directory at all.
56
58
  */
57
59
  static dirname(filePath) {
58
- return this.normalize(path.dirname(filePath));
60
+ return this.normalize(path.dirname(this.normalize(filePath)));
59
61
  }
60
62
  /**
61
63
  * Get base name of a path
@@ -1 +1 @@
1
- {"version":3,"file":"normalized-path.mjs","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"sourcesContent":["import path from \"node:path\";\n\n/**\n * Path helpers that always hand back forward slashes.\n *\n * Node's `path` returns `\\` on Windows, and these values are compared against\n * module specifiers, glob results and map keys — all of which use `/`. A raw\n * `path.relative()` therefore produces a string that looks right in a log and\n * fails every lookup it is used for.\n *\n * This lived under `dev-server/` and was imported from there by production\n * modules (`connectors`, `database`, `config`), which put the whole dev-server\n * directory into the production module graph. The code was never dev-only —\n * only its location was.\n */\nexport class Path {\n /**\n * Convert the given absolute path to a relative path\n */\n public static toRelative(absolutePath: string) {\n return this.normalize(path.relative(process.cwd(), absolutePath));\n }\n\n /**\n * Get relative path of the given path\n */\n public static relative(relativePath: string) {\n return this.normalize(path.relative(process.cwd(), relativePath));\n }\n\n /**\n * Get normalized absolute path of the given path\n */\n public static toNormalizedAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Get absolute path of the given path\n */\n public static toAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Normalize the given path (convert backslashes to forward slashes)\n */\n public static normalize(filePath: string) {\n return filePath.replace(/\\\\/g, \"/\");\n }\n\n /**\n * Join paths and normalize\n */\n public static join(...paths: string[]) {\n return this.normalize(path.join(...paths));\n }\n\n /**\n * Get directory name of a path\n */\n public static dirname(filePath: string) {\n return this.normalize(path.dirname(filePath));\n }\n\n /**\n * Get base name of a path\n */\n public static basename(filePath: string, ext?: string) {\n return path.basename(filePath, ext);\n }\n\n /**\n * Get extension of a path\n */\n public static extname(filePath: string) {\n return path.extname(filePath);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,OAAb,MAAkB;;;;CAIhB,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,SAAS,cAAsB;EAC3C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,qBAAqB,cAAsB;EACvD,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,UAAU,UAAkB;EACxC,OAAO,SAAS,QAAQ,OAAO,GAAG;CACpC;;;;CAKA,OAAc,KAAK,GAAG,OAAiB;EACrC,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG,KAAK,CAAC;CAC3C;;;;CAKA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,CAAC;CAC9C;;;;CAKA,OAAc,SAAS,UAAkB,KAAc;EACrD,OAAO,KAAK,SAAS,UAAU,GAAG;CACpC;;;;CAKA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,QAAQ,QAAQ;CAC9B;AACF"}
1
+ {"version":3,"file":"normalized-path.mjs","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"sourcesContent":["import path from \"node:path\";\n\n/**\n * Path helpers that always hand back forward slashes.\n *\n * Node's `path` returns `\\` on Windows, and these values are compared against\n * module specifiers, glob results and map keys — all of which use `/`. A raw\n * `path.relative()` therefore produces a string that looks right in a log and\n * fails every lookup it is used for.\n *\n * This lived under `dev-server/` and was imported from there by production\n * modules (`connectors`, `database`, `config`), which put the whole dev-server\n * directory into the production module graph. The code was never dev-only —\n * only its location was.\n */\nexport class Path {\n /**\n * Convert the given absolute path to a relative path\n */\n public static toRelative(absolutePath: string) {\n return this.normalize(path.relative(process.cwd(), absolutePath));\n }\n\n /**\n * Get relative path of the given path\n */\n public static relative(relativePath: string) {\n return this.normalize(path.relative(process.cwd(), relativePath));\n }\n\n /**\n * Get normalized absolute path of the given path\n */\n public static toNormalizedAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Get absolute path of the given path\n */\n public static toAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Normalize the given path (convert backslashes to forward slashes)\n */\n public static normalize(filePath: string) {\n return filePath.replace(/\\\\/g, \"/\");\n }\n\n /**\n * Join paths and normalize\n */\n public static join(...paths: string[]) {\n return this.normalize(path.join(...paths));\n }\n\n /**\n * Get directory name of a path. The input is normalized first: on POSIX a\n * backslash is a filename character, so `a\\b\\c.ts` would otherwise have no\n * directory at all.\n */\n public static dirname(filePath: string) {\n return this.normalize(path.dirname(this.normalize(filePath)));\n }\n\n /**\n * Get base name of a path\n */\n public static basename(filePath: string, ext?: string) {\n return path.basename(filePath, ext);\n }\n\n /**\n * Get extension of a path\n */\n public static extname(filePath: string) {\n return path.extname(filePath);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,OAAb,MAAkB;;;;CAIhB,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,SAAS,cAAsB;EAC3C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,qBAAqB,cAAsB;EACvD,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,UAAU,UAAkB;EACxC,OAAO,SAAS,QAAQ,OAAO,GAAG;CACpC;;;;CAKA,OAAc,KAAK,GAAG,OAAiB;EACrC,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG,KAAK,CAAC;CAC3C;;;;;;CAOA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,UAAU,KAAK,QAAQ,KAAK,UAAU,QAAQ,CAAC,CAAC;CAC9D;;;;CAKA,OAAc,SAAS,UAAkB,KAAc;EACrD,OAAO,KAAK,SAAS,UAAU,GAAG;CACpC;;;;CAKA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,QAAQ,QAAQ;CAC9B;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAa,OAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport path from \"path\";\n\n/**\n * Get root path or join the given paths to the root path\n */\nexport function rootPath(...paths: string[]) {\n return path.resolve(process.cwd(), ...paths);\n}\n\n/**\n * Get src directory path or join the given paths to the src directory path\n */\nexport function srcPath(...paths: string[]) {\n return rootPath(\"src\", ...paths);\n}\n\n/**\n * Get the absolute path to the storage folder to the given path\n *\n * If no path is given, it will return the absolute path to the storage folder\n */\nexport function storagePath(relativePath = \"\") {\n return rootPath(\"storage\", relativePath);\n}\n\n/**\n * Get the absolute path to the uploads folder to the given path\n *\n * If no path is given, it will return the absolute path to the uploads folder\n */\nexport function uploadsPath(relativePath = \"\") {\n const configPath = config.get(\"uploads.root\");\n if (!configPath) {\n return rootPath(\"storage\", \"uploads\", relativePath);\n }\n\n return typeof configPath === \"function\"\n ? configPath(relativePath)\n : path.resolve(configPath, relativePath);\n}\n\n/**\n * Get the absolute path to the public folder to the given path\n *\n * If no path is given, it will return the absolute path to the public folder\n */\nexport function publicPath(relativePath = \"\") {\n return rootPath(\"public\", relativePath);\n}\n\n/**\n * Get the absolute path to the cache folder to the given path\n *\n * If no path is given, it will return the absolute path to the cache folder\n */\nexport function cachePath(relativePath = \"\") {\n return rootPath(\"storage\", \"cache\", relativePath);\n}\n\n/**\n * App path\n */\nexport function appPath(relativePath = \"\") {\n return rootPath(\"src/app\", relativePath);\n}\n\n/**\n * Get logs directory path\n */\nexport function logsPath(relativePath = \"\") {\n return rootPath(\"storage/logs\", relativePath);\n}\n\n/**\n * Get a temp path\n */\nexport function tempPath(relativePath = \"\") {\n return rootPath(\"storage/tmp\", relativePath);\n}\n\n/**\n * Remove any invalid characters from the file path using regex\n * It should accept any language character, numbers, and the following characters: _ - .\n */\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\nexport function sanitizePath(filePath: string) {\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\n}\n\n/**\n * Warlock path\n * PLEASE DO NOT add any files in this directory as it may be deleted\n */\nexport function warlockPath(...path: string[]) {\n return rootPath(\".warlock\", ...path);\n}\n\n/**\n * Get config directory path\n */\nexport function configPath(...path: string[]) {\n return rootPath(\"src/config\", ...path);\n}\n\nexport const paths = {\n root: rootPath,\n src: srcPath,\n storage: storagePath,\n logs: logsPath,\n uploads: uploadsPath,\n public: publicPath,\n cache: cachePath,\n app: appPath,\n temp: tempPath,\n warlock: warlockPath,\n config: configPath,\n sanitize: sanitizePath,\n};\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAa,OAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-plugin.mjs","names":[],"sources":["../../../../../../../../core/src/validation/plugins/database-plugin.ts"],"sourcesContent":["/**\r\n * Database Validation Plugin (core)\r\n *\r\n * Injects the request-aware database validation methods onto Seal scalar\r\n * validators:\r\n * - uniqueExceptCurrentUser / uniqueExceptCurrentId\r\n * - existsExceptCurrentUser / existsExceptCurrentId\r\n *\r\n * The base `unique()` / `exists()` methods are owned by\r\n * `@warlock.js/cascade` and registered by its own seal plugin.\r\n */\r\n\r\nimport { type ChildModel, type Model } from \"@warlock.js/cascade\";\r\nimport type { SealPlugin } from \"@warlock.js/seal\";\r\nimport { NumberValidator, ScalarValidator, StringValidator } from \"@warlock.js/seal\";\r\nimport {\r\n existsExceptCurrentIdRule,\r\n existsExceptCurrentUserRule,\r\n uniqueExceptCurrentIdRule,\r\n uniqueExceptCurrentUserRule,\r\n} from \"../database\";\r\nimport type {\r\n ExistsExceptCurrentIdRuleOptions,\r\n ExistsExceptCurrentUserRuleOptions,\r\n UniqueExceptCurrentIdRuleOptions,\r\n UniqueExceptCurrentUserRuleOptions,\r\n} from \"../database\";\r\n\r\n/**\r\n * Database validation plugin for Seal\r\n */\r\nexport const databasePlugin: SealPlugin = {\r\n name: \"database\",\r\n version: \"1.0.0\",\r\n description:\r\n \"Adds request-aware database validation methods (except-current-user/id) to validators\",\r\n\r\n install() {\r\n Object.assign(ScalarValidator.prototype, {\r\n uniqueExceptCurrentUser(\r\n this: ScalarValidator,\r\n model: ChildModel<Model> | string,\r\n optionsList?: Partial<UniqueExceptCurrentUserRuleOptions> & {\r\n errorMessage?: string;\r\n },\r\n ) {\r\n const { errorMessage, ...options } = optionsList || {};\r\n\r\n return this.addRule(uniqueExceptCurrentUserRule, errorMessage, {\r\n Model: model,\r\n ...options,\r\n });\r\n },\r\n\r\n uniqueExceptCurrentId(\r\n this: ScalarValidator,\r\n model: ChildModel<Model> | string,\r\n optionsList?: Partial<UniqueExceptCurrentIdRuleOptions> & {\r\n errorMessage?: string;\r\n },\r\n ) {\r\n const { errorMessage, ...options } = optionsList || {};\r\n\r\n return this.addRule(uniqueExceptCurrentIdRule, errorMessage, {\r\n Model: model,\r\n ...options,\r\n });\r\n },\r\n\r\n existsExceptCurrentUser(\r\n this: ScalarValidator,\r\n model: ChildModel<Model> | string,\r\n optionsList?: Partial<ExistsExceptCurrentUserRuleOptions> & {\r\n errorMessage?: string;\r\n },\r\n ) {\r\n const { errorMessage, ...options } = optionsList || {};\r\n\r\n return this.addRule(existsExceptCurrentUserRule, errorMessage, {\r\n Model: model,\r\n ...options,\r\n });\r\n },\r\n\r\n existsExceptCurrentId(\r\n this: ScalarValidator,\r\n model: ChildModel<Model> | string,\r\n optionsList?: Partial<ExistsExceptCurrentIdRuleOptions> & {\r\n errorMessage?: string;\r\n },\r\n ) {\r\n const { errorMessage, ...options } = optionsList || {};\r\n\r\n return this.addRule(existsExceptCurrentIdRule, errorMessage, {\r\n Model: model,\r\n ...options,\r\n });\r\n },\r\n });\r\n\r\n Object.assign(StringValidator.prototype, {\r\n uniqueExceptCurrentUser: ScalarValidator.prototype.uniqueExceptCurrentUser,\r\n uniqueExceptCurrentId: ScalarValidator.prototype.uniqueExceptCurrentId,\r\n existsExceptCurrentUser: ScalarValidator.prototype.existsExceptCurrentUser,\r\n existsExceptCurrentId: ScalarValidator.prototype.existsExceptCurrentId,\r\n });\r\n\r\n Object.assign(NumberValidator.prototype, {});\r\n },\r\n};\r\n"],"mappings":";;;;;;;;;;;AA+BA,MAAa,iBAA6B;CACxC,MAAM;CACN,SAAS;CACT,aACE;CAEF,UAAU;EACR,OAAO,OAAO,gBAAgB,WAAW;GACvC,wBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,6BAA6B,cAAc;KAC7D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,sBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,2BAA2B,cAAc;KAC3D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,wBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,6BAA6B,cAAc;KAC7D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,sBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,2BAA2B,cAAc;KAC3D,OAAO;KACP,GAAG;IACL,CAAC;GACH;EACF,CAAC;EAED,OAAO,OAAO,gBAAgB,WAAW;GACvC,yBAAyB,gBAAgB,UAAU;GACnD,uBAAuB,gBAAgB,UAAU;GACjD,yBAAyB,gBAAgB,UAAU;GACnD,uBAAuB,gBAAgB,UAAU;EACnD,CAAC;EAED,OAAO,OAAO,gBAAgB,WAAW,CAAC,CAAC;CAC7C;AACF"}
1
+ {"version":3,"file":"database-plugin.mjs","names":[],"sources":["../../../../../../../../core/src/validation/plugins/database-plugin.ts"],"sourcesContent":["/**\n * Database Validation Plugin (core)\n *\n * Injects the request-aware database validation methods onto Seal scalar\n * validators:\n * - uniqueExceptCurrentUser / uniqueExceptCurrentId\n * - existsExceptCurrentUser / existsExceptCurrentId\n *\n * The base `unique()` / `exists()` methods are owned by\n * `@warlock.js/cascade` and registered by its own seal plugin.\n */\n\nimport { type ChildModel, type Model } from \"@warlock.js/cascade\";\nimport type { SealPlugin } from \"@warlock.js/seal\";\nimport { NumberValidator, ScalarValidator, StringValidator } from \"@warlock.js/seal\";\nimport {\n existsExceptCurrentIdRule,\n existsExceptCurrentUserRule,\n uniqueExceptCurrentIdRule,\n uniqueExceptCurrentUserRule,\n} from \"../database\";\nimport type {\n ExistsExceptCurrentIdRuleOptions,\n ExistsExceptCurrentUserRuleOptions,\n UniqueExceptCurrentIdRuleOptions,\n UniqueExceptCurrentUserRuleOptions,\n} from \"../database\";\n\n/**\n * Database validation plugin for Seal\n */\nexport const databasePlugin: SealPlugin = {\n name: \"database\",\n version: \"1.0.0\",\n description:\n \"Adds request-aware database validation methods (except-current-user/id) to validators\",\n\n install() {\n Object.assign(ScalarValidator.prototype, {\n uniqueExceptCurrentUser(\n this: ScalarValidator,\n model: ChildModel<Model> | string,\n optionsList?: Partial<UniqueExceptCurrentUserRuleOptions> & {\n errorMessage?: string;\n },\n ) {\n const { errorMessage, ...options } = optionsList || {};\n\n return this.addRule(uniqueExceptCurrentUserRule, errorMessage, {\n Model: model,\n ...options,\n });\n },\n\n uniqueExceptCurrentId(\n this: ScalarValidator,\n model: ChildModel<Model> | string,\n optionsList?: Partial<UniqueExceptCurrentIdRuleOptions> & {\n errorMessage?: string;\n },\n ) {\n const { errorMessage, ...options } = optionsList || {};\n\n return this.addRule(uniqueExceptCurrentIdRule, errorMessage, {\n Model: model,\n ...options,\n });\n },\n\n existsExceptCurrentUser(\n this: ScalarValidator,\n model: ChildModel<Model> | string,\n optionsList?: Partial<ExistsExceptCurrentUserRuleOptions> & {\n errorMessage?: string;\n },\n ) {\n const { errorMessage, ...options } = optionsList || {};\n\n return this.addRule(existsExceptCurrentUserRule, errorMessage, {\n Model: model,\n ...options,\n });\n },\n\n existsExceptCurrentId(\n this: ScalarValidator,\n model: ChildModel<Model> | string,\n optionsList?: Partial<ExistsExceptCurrentIdRuleOptions> & {\n errorMessage?: string;\n },\n ) {\n const { errorMessage, ...options } = optionsList || {};\n\n return this.addRule(existsExceptCurrentIdRule, errorMessage, {\n Model: model,\n ...options,\n });\n },\n });\n\n Object.assign(StringValidator.prototype, {\n uniqueExceptCurrentUser: ScalarValidator.prototype.uniqueExceptCurrentUser,\n uniqueExceptCurrentId: ScalarValidator.prototype.uniqueExceptCurrentId,\n existsExceptCurrentUser: ScalarValidator.prototype.existsExceptCurrentUser,\n existsExceptCurrentId: ScalarValidator.prototype.existsExceptCurrentId,\n });\n\n Object.assign(NumberValidator.prototype, {});\n },\n};\n"],"mappings":";;;;;;;;;;;AA+BA,MAAa,iBAA6B;CACxC,MAAM;CACN,SAAS;CACT,aACE;CAEF,UAAU;EACR,OAAO,OAAO,gBAAgB,WAAW;GACvC,wBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,6BAA6B,cAAc;KAC7D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,sBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,2BAA2B,cAAc;KAC3D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,wBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,6BAA6B,cAAc;KAC7D,OAAO;KACP,GAAG;IACL,CAAC;GACH;GAEA,sBAEE,OACA,aAGA;IACA,MAAM,EAAE,cAAc,GAAG,YAAY,eAAe,CAAC;IAErD,OAAO,KAAK,QAAQ,2BAA2B,cAAc;KAC3D,OAAO;KACP,GAAG;IACL,CAAC;GACH;EACF,CAAC;EAED,OAAO,OAAO,gBAAgB,WAAW;GACvC,yBAAyB,gBAAgB,UAAU;GACnD,uBAAuB,gBAAgB,UAAU;GACjD,yBAAyB,gBAAgB,UAAU;GACnD,uBAAuB,gBAAgB,UAAU;EACnD,CAAC;EAED,OAAO,OAAO,gBAAgB,WAAW,CAAC,CAAC;CAC7C;AACF"}
@@ -5,8 +5,14 @@ import { log } from "@warlock.js/logger";
5
5
  import { merge } from "@mongez/reinforcements";
6
6
 
7
7
  //#region ../core/src/validation/validateAll.ts
8
- function resolveDataToParse(validating, request) {
9
- if (!validating || validating.length === 0) return request.allExceptParams();
8
+ function resolveDataToParse(validating, schema, request) {
9
+ if (!validating || validating.length === 0) {
10
+ const data = request.allExceptParams();
11
+ const shape = schema?.schema;
12
+ if (!shape || typeof shape !== "object") return data;
13
+ for (const [key, value] of Object.entries(request.params)) if (data[key] === void 0 && Object.hasOwn(shape, key)) data[key] = value;
14
+ return data;
15
+ }
10
16
  let data = {};
11
17
  for (const validatingType of validating) {
12
18
  if (validatingType === "body") data = merge(data, request.body);
@@ -25,7 +31,7 @@ async function validateAll(validation, request, response) {
25
31
  if (validation.schema) {
26
32
  log.info("validation", "schema", "Validating request schema");
27
33
  try {
28
- const data = resolveDataToParse(validation.validating, request);
34
+ const data = resolveDataToParse(validation.validating, validation.schema, request);
29
35
  const result = await v.validate(validation.schema, data);
30
36
  if (result.data && result.isValid) request.setValidatedData(result.data);
31
37
  if (!result.isValid) {
@@ -1 +1 @@
1
- {"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request } from \"../http\";\nimport { Response } from \"../http/response\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate({ request, response });\n\n // if there is a result, it means it failed\n if (result) {\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n // a returned Response already carries its own status; anything else is\n // sent with the configured failure status (400 by default)\n if (result instanceof Response) return result;\n\n return response.send(result, config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;;AAQA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS;GAAE;GAAS;EAAS,CAAC;EAG9D,IAAI,QAAQ;GACV,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAIpD,IAAI,kBAAkB,UAAU,OAAO;GAEvC,OAAO,SAAS,KAAK,QAAQ,OAAO,IAAI,6BAA6B,GAAG,CAAC;EAC3E;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
1
+ {"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request } from \"../http\";\nimport { Response } from \"../http/response\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(\n validating: RequestHandlerValidation[\"validating\"],\n schema: RequestHandlerValidation[\"schema\"],\n request: Request,\n) {\n if (!validating || validating.length === 0) {\n const data = request.allExceptParams();\n\n // Duck-typed rather than `instanceof ObjectValidator`: the app's schema can\n // come from a different copy of seal than core's, and `instanceof` would\n // then silently fall back to ignoring params.\n const shape = (schema as { schema?: unknown } | undefined)?.schema;\n\n if (!shape || typeof shape !== \"object\") return data;\n\n for (const [key, value] of Object.entries(request.params)) {\n if (data[key] === undefined && Object.hasOwn(shape, key)) {\n data[key] = value;\n }\n }\n\n return data;\n }\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, validation.schema, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate({ request, response });\n\n // if there is a result, it means it failed\n if (result) {\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n // a returned Response already carries its own status; anything else is\n // sent with the configured failure status (400 by default)\n if (result instanceof Response) return result;\n\n return response.send(result, config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;;AAQA,SAAS,mBACP,YACA,QACA,SACA;CACA,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG;EAC1C,MAAM,OAAO,QAAQ,gBAAgB;EAKrC,MAAM,QAAS,QAA6C;EAE5D,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;EAEhD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,MAAM,GACtD,IAAI,KAAK,SAAS,UAAa,OAAO,OAAO,OAAO,GAAG,GACrD,KAAK,OAAO;EAIhB,OAAO;CACT;CAEA,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,WAAW,QAAQ,OAAO;GACjF,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS;GAAE;GAAS;EAAS,CAAC;EAG9D,IAAI,QAAQ;GACV,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAIpD,IAAI,kBAAkB,UAAU,OAAO;GAEvC,OAAO,SAAS,KAAK,QAAQ,OAAO,IAAI,6BAA6B,GAAG,CAAC;EAC3E;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
@@ -2,6 +2,7 @@
2
2
  const defaultWarlockConfigurations = { build: {
3
3
  outdir: process.cwd() + "/dist",
4
4
  outFile: "app.js",
5
+ routeRegistrationTimeoutMs: 3e4,
5
6
  sourcemap: true,
6
7
  minify: true
7
8
  } };
@@ -1 +1 @@
1
- {"version":3,"file":"default-configurations.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/default-configurations.ts"],"sourcesContent":["import { type WarlockConfig } from \"./types\";\n\nexport const defaultWarlockConfigurations: WarlockConfig = {\n build: {\n outdir: process.cwd() + \"/dist\",\n outFile: \"app.js\",\n sourcemap: true,\n minify: true,\n },\n};\n"],"mappings":";AAEA,MAAa,+BAA8C,EACzD,OAAO;CACL,QAAQ,QAAQ,IAAI,IAAI;CACxB,SAAS;CACT,WAAW;CACX,QAAQ;AACV,EACF"}
1
+ {"version":3,"file":"default-configurations.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/default-configurations.ts"],"sourcesContent":["import { type WarlockConfig } from \"./types\";\n\nexport const defaultWarlockConfigurations: WarlockConfig = {\n build: {\n outdir: process.cwd() + \"/dist\",\n outFile: \"app.js\",\n routeRegistrationTimeoutMs: 30_000,\n sourcemap: true,\n minify: true,\n },\n};\n"],"mappings":";AAEA,MAAa,+BAA8C,EACzD,OAAO;CACL,QAAQ,QAAQ,IAAI,IAAI;CACxB,SAAS;CACT,4BAA4B;CAC5B,WAAW;CACX,QAAQ;AACV,EACF"}
@@ -63,6 +63,13 @@ type WarlockConfig = {
63
63
  * @default app.js
64
64
  */
65
65
  outFile?: string;
66
+ /**
67
+ * Maximum time to wait for the isolated route-registration process during
68
+ * `warlock build`. `WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS` overrides it.
69
+ *
70
+ * @default 30000
71
+ */
72
+ routeRegistrationTimeoutMs?: number;
66
73
  /**
67
74
  * Produce ONE self-contained file you can run with `node dist/app.js`.
68
75
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":";;;;;;;;;AAWA;;;KAAY,aAAA;EA8BF;;;EA1BR,MAAA;IA+NqB;;;IA3NnB,IAAA;IAJF;;;IAQE,IAAA;IAkBF;;;IAdE,cAAA;EAAA;EAqCA;;;;;;;;;;;EAvBF,KAAA,GAAQ,IAAA,CAAK,YAAA;IAqHT;;;;;IA/GF,MAAA;IAiJA;;;;;;;;;;IAtIA,YAAA;IA+MO;;;;;IAzMP,OAAA;;;;;;;;;;;;;;;;;IAiBA,YAAA;;;;;;;;;;;;;;;;IAgBA,OAAA;;;;;;IAMA,MAAA;;;;;;IAMA,SAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;EA2BF,UAAA,GAAa,SAAA;;;;EAKb,GAAA;IACE,QAAA,GAAW,UAAA;EAAA;;;;EAMb,SAAA;;;;IAIE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,OAAA;IAAA;;;;IAKF,cAAA,GAAiB,yBAAA;;;;;IAKjB,eAAA;;;;;;;;;IASA,eAAA;;;;;;;;;IASA,qBAAA;;;;;;;;;;IAUA,mBAAA;;;;;;;;;;;;IAYA,OAAA;EAAA;;;;EAMF,QAAA;;;;;;;;;;;;;;;;;;IAkBE,UAAA,GAAa,KAAA,CAAM,oBAAA;EAAA;;;;;;;EASrB,OAAA;;;;;;;;;IASE,OAAA;;;;;;;;IASA,OAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":";;;;;;;;;AAWA;;;KAAY,aAAA;EA8BF;;;EA1BR,MAAA;IAsOqB;;;IAlOnB,IAAA;IAJF;;;IAQE,IAAA;IAkBF;;;IAdE,cAAA;EAAA;EAqCA;;;;;;;;;;;EAvBF,KAAA,GAAQ,IAAA,CAAK,YAAA;IAsHX;;;;;IAhHA,MAAA;IA+IA;;;;;;;;;;IApIA,YAAA;IAsNO;AAAA;;;;IAhNP,OAAA;;;;;;;IAOA,0BAAA;;;;;;;;;;;;;;;;;IAiBA,YAAA;;;;;;;;;;;;;;;;IAgBA,OAAA;;;;;;IAMA,MAAA;;;;;;IAMA,SAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;EA2BF,UAAA,GAAa,SAAA;;;;EAKb,GAAA;IACE,QAAA,GAAW,UAAA;EAAA;;;;EAMb,SAAA;;;;IAIE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,OAAA;IAAA;;;;IAKF,cAAA,GAAiB,yBAAA;;;;;IAKjB,eAAA;;;;;;;;;IASA,eAAA;;;;;;;;;IASA,qBAAA;;;;;;;;;;IAUA,mBAAA;;;;;;;;;;;;IAYA,OAAA;EAAA;;;;EAMF,QAAA;;;;;;;;;;;;;;;;;;IAkBE,UAAA,GAAa,KAAA,CAAM,oBAAA;EAAA;;;;;;;EASrB,OAAA;;;;;;;;;IASE,OAAA;;;;;;;;IASA,OAAA;EAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { get } from \"@mongez/reinforcements\";\r\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"url\";\r\nimport { rootPath } from \"../utils\";\r\nimport { type WarlockConfig } from \"./types\";\r\nimport { getDeprecatedConfigWarnings } from \"./warn-deprecated-config\";\r\n\r\n/**\r\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\r\n *\r\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\r\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\r\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\r\n * the signal to fall back to transpiling the config ourselves.\r\n */\r\nexport function isUnknownTsExtensionError(error: unknown): boolean {\r\n return (\r\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\r\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\r\n );\r\n}\r\n\r\n/**\r\n * Warlock Config Manager\r\n *\r\n * Manages lazy loading of the pre-compiled warlock.config.js file\r\n * from the .warlock/cache directory.\r\n */\r\nexport class WarlockConfigManager {\r\n /**\r\n * Cached config instance\r\n */\r\n private config?: WarlockConfig;\r\n\r\n /**\r\n * Loading promise to prevent duplicate loads\r\n */\r\n private loading?: Promise<WarlockConfig | undefined>;\r\n\r\n /**\r\n * Load warlock.config.js (cached after first load)\r\n *\r\n * @returns The resolved Warlock configuration\r\n */\r\n public async load(): Promise<WarlockConfig | undefined> {\r\n // Already loaded\r\n if (this.config) {\r\n return this.config;\r\n }\r\n\r\n // Currently loading (prevent duplicate loads)\r\n if (this.loading) {\r\n return this.loading;\r\n }\r\n\r\n // Start loading\r\n this.loading = this.doLoad();\r\n this.config = await this.loading;\r\n this.loading = undefined;\r\n\r\n // Console, not the logger: this runs before any log channel exists.\r\n for (const warning of getDeprecatedConfigWarnings(this.config)) {\r\n console.warn(warning);\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Internal load implementation\r\n *\r\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\r\n * separate compile-to-disk step is needed.\r\n */\r\n private async doLoad(): Promise<WarlockConfig | undefined> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n // Written straight to the console, NOT through `@warlock.js/logger`.\r\n // This runs during CLI bootstrap, before `LoggerConnector.start()` has\r\n // configured a single channel, so a logger call here reaches nobody in\r\n // any application — the warning would be silently dropped exactly when\r\n // the user most needs it.\r\n console.warn(\r\n \"warlock.config.ts is missing — create it at the project root (see https://warlock.js.org/docs for the config reference)\",\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(configPath).href);\r\n return configModule.default;\r\n } catch (error) {\r\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\r\n // natively — in both cases the direct import above works. But `build` /\r\n // `start` register no hook, so on an older Node the import throws\r\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\r\n // so config loading is Node-version-independent rather than relying on an\r\n // experimental runtime feature.\r\n if (isUnknownTsExtensionError(error)) {\r\n return await this.loadViaEsbuild(configPath);\r\n }\r\n\r\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Fallback config loader for runtimes without native TypeScript support.\r\n *\r\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\r\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\r\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\r\n * the original location — imports it, then removes the temp file.\r\n */\r\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\r\n const { transformSync } = await import(\"esbuild\");\r\n const source = await readFile(configPath, \"utf8\");\r\n\r\n const { code } = transformSync(source, {\r\n loader: \"ts\",\r\n format: \"esm\",\r\n target: `node${process.versions.node.split(\".\")[0]}`,\r\n sourcefile: configPath,\r\n });\r\n\r\n // Unique sibling name so concurrent processes never clobber each other.\r\n const compiledPath = configPath.replace(\r\n /warlock\\.config\\.ts$/,\r\n `.warlock.config.${process.pid}.mjs`,\r\n );\r\n\r\n await writeFile(compiledPath, code, \"utf8\");\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(compiledPath).href);\r\n return configModule.default;\r\n } finally {\r\n await unlink(compiledPath).catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Get config value by key (dot notation supported)\r\n *\r\n * @example\r\n * config.get(\"server.port\") // Returns 3000\r\n * config.get(\"cli.commands\") // Returns array of commands\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @returns The config value\r\n * @throws Error if config is not loaded\r\n */\r\n public get<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): WarlockConfig[Key] {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return get(this.config, key as string, defaultValue);\r\n }\r\n\r\n /**\r\n * Lazy get - loads config if not already loaded\r\n *\r\n * @example\r\n * const port = await config.lazyGet(\"server\");\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @param defaultValue - Default value if config key is undefined\r\n * @returns The config value\r\n */\r\n async lazyGet<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): Promise<WarlockConfig[Key]> {\r\n await this.load();\r\n return this.get(key, defaultValue);\r\n }\r\n\r\n /**\r\n * Check if config is loaded\r\n */\r\n public get isLoaded(): boolean {\r\n return this.config !== undefined;\r\n }\r\n\r\n /**\r\n * Get the entire config object\r\n *\r\n * @throws Error if config is not loaded\r\n */\r\n public getAll(): WarlockConfig {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Reload config (useful for HMR/development)\r\n */\r\n public async reload(): Promise<void> {\r\n this.config = undefined;\r\n this.loading = undefined;\r\n await this.load();\r\n }\r\n}\r\n\r\n/**\r\n * Exported singleton instance\r\n *\r\n * @example\r\n * import { warlockConfig } from \"@warlock.js/core\";\r\n *\r\n * // Lazy load and get value\r\n * const port = await warlockConfig.lazyGet(\"server.port\");\r\n *\r\n * // Or load first, then get\r\n * await warlockConfig.load();\r\n * const commands = warlockConfig.get(\"cli.commands\");\r\n */\r\nexport const warlockConfigManager = new WarlockConfigManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAGf,KAAK,MAAM,WAAW,4BAA4B,KAAK,MAAM,GAC3D,QAAQ,KAAK,OAAO;EAGtB,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GAMxC,QAAQ,KACN,yHACF;GACA;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,CAAC,CAAC,MACzC,CAAC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,CAAC,CAAC,MAC3C,CAAC;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}
1
+ {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { get } from \"@mongez/reinforcements\";\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { pathToFileURL } from \"url\";\nimport { rootPath } from \"../utils\";\nimport { type WarlockConfig } from \"./types\";\nimport { getDeprecatedConfigWarnings } from \"./warn-deprecated-config\";\n\n/**\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\n *\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\n * the signal to fall back to transpiling the config ourselves.\n */\nexport function isUnknownTsExtensionError(error: unknown): boolean {\n return (\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\n );\n}\n\n/**\n * Warlock Config Manager\n *\n * Manages lazy loading of the pre-compiled warlock.config.js file\n * from the .warlock/cache directory.\n */\nexport class WarlockConfigManager {\n /**\n * Cached config instance\n */\n private config?: WarlockConfig;\n\n /**\n * Loading promise to prevent duplicate loads\n */\n private loading?: Promise<WarlockConfig | undefined>;\n\n /**\n * Load warlock.config.js (cached after first load)\n *\n * @returns The resolved Warlock configuration\n */\n public async load(): Promise<WarlockConfig | undefined> {\n // Already loaded\n if (this.config) {\n return this.config;\n }\n\n // Currently loading (prevent duplicate loads)\n if (this.loading) {\n return this.loading;\n }\n\n // Start loading\n this.loading = this.doLoad();\n this.config = await this.loading;\n this.loading = undefined;\n\n // Console, not the logger: this runs before any log channel exists.\n for (const warning of getDeprecatedConfigWarnings(this.config)) {\n console.warn(warning);\n }\n\n return this.config;\n }\n\n /**\n * Internal load implementation\n *\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\n * separate compile-to-disk step is needed.\n */\n private async doLoad(): Promise<WarlockConfig | undefined> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n // Written straight to the console, NOT through `@warlock.js/logger`.\n // This runs during CLI bootstrap, before `LoggerConnector.start()` has\n // configured a single channel, so a logger call here reaches nobody in\n // any application — the warning would be silently dropped exactly when\n // the user most needs it.\n console.warn(\n \"warlock.config.ts is missing — create it at the project root (see https://warlock.js.org/docs for the config reference)\",\n );\n return;\n }\n\n try {\n const configModule = await import(pathToFileURL(configPath).href);\n return configModule.default;\n } catch (error) {\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\n // natively — in both cases the direct import above works. But `build` /\n // `start` register no hook, so on an older Node the import throws\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\n // so config loading is Node-version-independent rather than relying on an\n // experimental runtime feature.\n if (isUnknownTsExtensionError(error)) {\n return await this.loadViaEsbuild(configPath);\n }\n\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\n }\n }\n\n /**\n * Fallback config loader for runtimes without native TypeScript support.\n *\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\n * the original location — imports it, then removes the temp file.\n */\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\n const { transformSync } = await import(\"esbuild\");\n const source = await readFile(configPath, \"utf8\");\n\n const { code } = transformSync(source, {\n loader: \"ts\",\n format: \"esm\",\n target: `node${process.versions.node.split(\".\")[0]}`,\n sourcefile: configPath,\n });\n\n // Unique sibling name so concurrent processes never clobber each other.\n const compiledPath = configPath.replace(\n /warlock\\.config\\.ts$/,\n `.warlock.config.${process.pid}.mjs`,\n );\n\n await writeFile(compiledPath, code, \"utf8\");\n\n try {\n const configModule = await import(pathToFileURL(compiledPath).href);\n return configModule.default;\n } finally {\n await unlink(compiledPath).catch(() => {});\n }\n }\n\n /**\n * Get config value by key (dot notation supported)\n *\n * @example\n * config.get(\"server.port\") // Returns 3000\n * config.get(\"cli.commands\") // Returns array of commands\n *\n * @param key - Config key (supports dot notation), autocompletes for first level only\n * @returns The config value\n * @throws Error if config is not loaded\n */\n public get<Key extends keyof WarlockConfig>(\n key: Key,\n defaultValue?: WarlockConfig[Key],\n ): WarlockConfig[Key] {\n if (!this.config) {\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\n }\n\n return get(this.config, key as string, defaultValue);\n }\n\n /**\n * Lazy get - loads config if not already loaded\n *\n * @example\n * const port = await config.lazyGet(\"server\");\n *\n * @param key - Config key (supports dot notation), autocompletes for first level only\n * @param defaultValue - Default value if config key is undefined\n * @returns The config value\n */\n async lazyGet<Key extends keyof WarlockConfig>(\n key: Key,\n defaultValue?: WarlockConfig[Key],\n ): Promise<WarlockConfig[Key]> {\n await this.load();\n return this.get(key, defaultValue);\n }\n\n /**\n * Check if config is loaded\n */\n public get isLoaded(): boolean {\n return this.config !== undefined;\n }\n\n /**\n * Get the entire config object\n *\n * @throws Error if config is not loaded\n */\n public getAll(): WarlockConfig {\n if (!this.config) {\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\n }\n\n return this.config;\n }\n\n /**\n * Reload config (useful for HMR/development)\n */\n public async reload(): Promise<void> {\n this.config = undefined;\n this.loading = undefined;\n await this.load();\n }\n}\n\n/**\n * Exported singleton instance\n *\n * @example\n * import { warlockConfig } from \"@warlock.js/core\";\n *\n * // Lazy load and get value\n * const port = await warlockConfig.lazyGet(\"server.port\");\n *\n * // Or load first, then get\n * await warlockConfig.load();\n * const commands = warlockConfig.get(\"cli.commands\");\n */\nexport const warlockConfigManager = new WarlockConfigManager();\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAGf,KAAK,MAAM,WAAW,4BAA4B,KAAK,MAAM,GAC3D,QAAQ,KAAK,OAAO;EAGtB,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GAMxC,QAAQ,KACN,yHACF;GACA;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,CAAC,CAAC,MACzC,CAAC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,CAAC,CAAC,MAC3C,CAAC;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}