@warlock.js/core 4.15.0 → 5.0.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 (187) hide show
  1. package/CHANGELOG.md +43 -2
  2. package/bin/warlock.js +154 -1
  3. package/esm/application/application.d.mts +65 -1
  4. package/esm/application/application.d.mts.map +1 -1
  5. package/esm/application/application.mjs +62 -0
  6. package/esm/application/application.mjs.map +1 -1
  7. package/esm/application/index.d.mts +1 -1
  8. package/esm/cli/cli-commands.manager.mjs +60 -10
  9. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  10. package/esm/cli/commands/dev-server.command.mjs +1 -0
  11. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  12. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +40 -0
  13. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -0
  14. package/esm/cli/commands/doctor/checks/index.mjs +4 -2
  15. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  16. package/esm/commands/cli-command.d.mts +1 -1
  17. package/esm/commands/cli-command.d.mts.map +1 -1
  18. package/esm/commands/cli-command.mjs.map +1 -1
  19. package/esm/commands/types.d.mts +10 -2
  20. package/esm/commands/types.d.mts.map +1 -1
  21. package/esm/config/config-handlers.d.mts +1 -2
  22. package/esm/config/config-handlers.d.mts.map +1 -1
  23. package/esm/config/config-handlers.mjs +3 -2
  24. package/esm/config/config-handlers.mjs.map +1 -1
  25. package/esm/config/locale-configuration.mjs +29 -0
  26. package/esm/config/locale-configuration.mjs.map +1 -0
  27. package/esm/connectors/assert-no-reserved-connector-names.d.mts +7 -0
  28. package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -0
  29. package/esm/connectors/assert-no-reserved-connector-names.mjs +44 -0
  30. package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -0
  31. package/esm/connectors/assert-unique-connector-names.d.mts +7 -0
  32. package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -0
  33. package/esm/connectors/assert-unique-connector-names.mjs +20 -0
  34. package/esm/connectors/assert-unique-connector-names.mjs.map +1 -0
  35. package/esm/connectors/connectors-manager.d.mts +38 -0
  36. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  37. package/esm/connectors/connectors-manager.mjs +45 -12
  38. package/esm/connectors/connectors-manager.mjs.map +1 -1
  39. package/esm/connectors/describe-server-address.mjs +75 -0
  40. package/esm/connectors/describe-server-address.mjs.map +1 -0
  41. package/esm/connectors/http-connector.d.mts.map +1 -1
  42. package/esm/connectors/http-connector.mjs +6 -3
  43. package/esm/connectors/http-connector.mjs.map +1 -1
  44. package/esm/connectors/index.d.mts +4 -1
  45. package/esm/connectors/index.mjs +4 -1
  46. package/esm/connectors/register-configured-connectors.d.mts +14 -0
  47. package/esm/connectors/register-configured-connectors.d.mts.map +1 -0
  48. package/esm/connectors/register-configured-connectors.mjs +99 -0
  49. package/esm/connectors/register-configured-connectors.mjs.map +1 -0
  50. package/esm/connectors/socket-connector.d.mts.map +1 -1
  51. package/esm/connectors/socket-connector.mjs +4 -1
  52. package/esm/connectors/socket-connector.mjs.map +1 -1
  53. package/esm/connectors/types.d.mts +76 -1
  54. package/esm/connectors/types.d.mts.map +1 -1
  55. package/esm/connectors/types.mjs.map +1 -1
  56. package/esm/database/models/database-log/database-log.d.mts +50 -2
  57. package/esm/database/models/database-log/database-log.d.mts.map +1 -1
  58. package/esm/database/utils.d.mts +2 -2
  59. package/esm/database/utils.d.mts.map +1 -1
  60. package/esm/dev-server/development-server.mjs +1 -0
  61. package/esm/dev-server/development-server.mjs.map +1 -1
  62. package/esm/dev-server/layer-executor.mjs +2 -1
  63. package/esm/dev-server/layer-executor.mjs.map +1 -1
  64. package/esm/dev-server/read-config-ast.mjs +48 -0
  65. package/esm/dev-server/read-config-ast.mjs.map +1 -0
  66. package/esm/dev-server/run-typings-generation.mjs +18 -0
  67. package/esm/dev-server/run-typings-generation.mjs.map +1 -0
  68. package/esm/dev-server/type-generator.mjs +30 -39
  69. package/esm/dev-server/type-generator.mjs.map +1 -1
  70. package/esm/generations/add-command.action.mjs +169 -1
  71. package/esm/generations/add-command.action.mjs.map +1 -1
  72. package/esm/generations/stubs.mjs +93 -1
  73. package/esm/generations/stubs.mjs.map +1 -1
  74. package/esm/http/context/request-context.d.mts +14 -3
  75. package/esm/http/context/request-context.d.mts.map +1 -1
  76. package/esm/http/context/request-context.mjs +11 -1
  77. package/esm/http/context/request-context.mjs.map +1 -1
  78. package/esm/http/context/request-memo.d.mts +21 -0
  79. package/esm/http/context/request-memo.d.mts.map +1 -0
  80. package/esm/http/context/request-memo.mjs +48 -0
  81. package/esm/http/context/request-memo.mjs.map +1 -0
  82. package/esm/http/database/RequestLog.d.mts +164 -2
  83. package/esm/http/database/RequestLog.d.mts.map +1 -1
  84. package/esm/http/index.d.mts +6 -5
  85. package/esm/http/index.mjs +2 -1
  86. package/esm/http/middleware/cache-response-middleware.d.mts +2 -2
  87. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  88. package/esm/http/middleware/cache-response-middleware.mjs +1 -1
  89. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  90. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  91. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  92. package/esm/http/middleware/idempotency.middleware.mjs +1 -1
  93. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  94. package/esm/http/middleware/index.d.mts +1 -1
  95. package/esm/http/middleware/index.mjs +1 -1
  96. package/esm/http/middleware/inject-request-context.d.mts +1 -8
  97. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  98. package/esm/http/middleware/inject-request-context.mjs +9 -22
  99. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  100. package/esm/http/middleware/ip-filter.middleware.d.mts +6 -3
  101. package/esm/http/middleware/ip-filter.middleware.d.mts.map +1 -1
  102. package/esm/http/middleware/ip-filter.middleware.mjs +7 -4
  103. package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
  104. package/esm/http/middleware/maintenance.middleware.mjs +1 -1
  105. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  106. package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
  107. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  108. package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
  109. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  110. package/esm/http/middleware/utils/idempotency-key.d.mts +0 -14
  111. package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
  112. package/esm/http/middleware/utils/idempotency-key.mjs +19 -1
  113. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  114. package/esm/http/request.d.mts +149 -44
  115. package/esm/http/request.d.mts.map +1 -1
  116. package/esm/http/request.mjs +132 -43
  117. package/esm/http/request.mjs.map +1 -1
  118. package/esm/http/response.mjs +1 -1
  119. package/esm/http/response.mjs.map +1 -1
  120. package/esm/http/server.d.mts.map +1 -1
  121. package/esm/http/server.mjs.map +1 -1
  122. package/esm/http/types.d.mts +58 -1
  123. package/esm/http/types.d.mts.map +1 -1
  124. package/esm/http/uploaded-file.d.mts +51 -19
  125. package/esm/http/uploaded-file.d.mts.map +1 -1
  126. package/esm/http/uploaded-file.mjs +36 -8
  127. package/esm/http/uploaded-file.mjs.map +1 -1
  128. package/esm/index.d.mts +17 -9
  129. package/esm/index.mjs +10 -4
  130. package/esm/production/build-contributions.mjs +115 -0
  131. package/esm/production/build-contributions.mjs.map +1 -0
  132. package/esm/production/production-builder.mjs +97 -2
  133. package/esm/production/production-builder.mjs.map +1 -1
  134. package/esm/production/resolve-build-config.d.mts +25 -0
  135. package/esm/production/resolve-build-config.d.mts.map +1 -0
  136. package/esm/production/resolve-build-config.mjs +43 -1
  137. package/esm/production/resolve-build-config.mjs.map +1 -1
  138. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +1 -1
  139. package/esm/repositories/adapters/cascade/filter-applicator.mjs +20 -9
  140. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  141. package/esm/restful/restful.d.mts +29 -8
  142. package/esm/restful/restful.d.mts.map +1 -1
  143. package/esm/restful/restful.mjs +31 -13
  144. package/esm/restful/restful.mjs.map +1 -1
  145. package/esm/router/describe-route-for-log.mjs +29 -0
  146. package/esm/router/describe-route-for-log.mjs.map +1 -0
  147. package/esm/router/index.d.mts +3 -1
  148. package/esm/router/index.mjs +2 -0
  149. package/esm/router/log-request-lifecycle.mjs +38 -0
  150. package/esm/router/log-request-lifecycle.mjs.map +1 -0
  151. package/esm/router/positional-handler-diagnostics.d.mts +90 -0
  152. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -0
  153. package/esm/router/positional-handler-diagnostics.mjs +247 -0
  154. package/esm/router/positional-handler-diagnostics.mjs.map +1 -0
  155. package/esm/router/route-registry.d.mts +34 -0
  156. package/esm/router/route-registry.d.mts.map +1 -0
  157. package/esm/router/router.d.mts.map +1 -1
  158. package/esm/router/router.mjs +87 -63
  159. package/esm/router/router.mjs.map +1 -1
  160. package/esm/router/types.d.mts +23 -5
  161. package/esm/router/types.d.mts.map +1 -1
  162. package/esm/storage/storage.d.mts.map +1 -1
  163. package/esm/storage/storage.mjs +1 -1
  164. package/esm/storage/storage.mjs.map +1 -1
  165. package/esm/storage/utils/safe-fetch.d.mts +11 -1
  166. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  167. package/esm/storage/utils/safe-fetch.mjs +51 -4
  168. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  169. package/esm/tests/vitest-setup.mjs +1 -1
  170. package/esm/utils/load-environment.mjs +33 -2
  171. package/esm/utils/load-environment.mjs.map +1 -1
  172. package/esm/utils/types.d.mts +4 -0
  173. package/esm/utils/types.d.mts.map +1 -1
  174. package/esm/validation/validateAll.mjs +4 -1
  175. package/esm/validation/validateAll.mjs.map +1 -1
  176. package/esm/validation/validators/file-validator.d.mts +25 -1
  177. package/esm/validation/validators/file-validator.d.mts.map +1 -1
  178. package/esm/warlock-config/index.mjs +1 -1
  179. package/esm/warlock-config/normalize-build-config.mjs.map +1 -1
  180. package/esm/warlock-config/types.d.mts +36 -2
  181. package/esm/warlock-config/types.d.mts.map +1 -1
  182. package/llms-full.txt +179 -23
  183. package/llms.txt +1 -1
  184. package/package.json +23 -20
  185. package/skills/request-memo/SKILL.md +142 -0
  186. package/skills/use-middleware/SKILL.md +12 -1
  187. package/skills/use-request-locals/SKILL.md +144 -0
package/CHANGELOG.md CHANGED
@@ -6,7 +6,48 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an *Upgrading* section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
- ## 4.14.0
9
+ ## 5.0.0 - 2026-08-25
10
+
11
+ ### Upgrading
12
+
13
+ - **BREAKING — `Request` no longer has a `[key: string]: any` index signature.** Attaching arbitrary properties (`request.post = post`) no longer compiles. Migrate per attachment: per-request data written by middleware → `request.locals` (augment `RequestLocals`); per-request memoized computation → `requestMemo(key, fn)`; new typed members → module augmentation of `Request` itself
14
+ - **BREAKING — `fromRequest(key, callback)` is removed.** It cached values as dynamic `Request` properties, which only compiled because of the deleted index signature. `requestMemo(key, fn)` is the drop-in successor: same per-request lifetime, single-flight (concurrent callers share one promise), and it never touches the `Request` object
15
+ - **BREAKING — client-supplied locales are validated against `app.localeCodes`.** A `locale`/`translation-locale-code` header or `locale` query value outside the configured list is treated as absent and falls through to the default locale — it previously won as-is, steering every translated string and every serialized Resource. Apps that declare no `app.localeCodes` keep the pass-through behavior
16
+ - **`request.trans()` / `request.t()` now resolve the locale at call time.** Previously the translator captured the locale once during request construction (before routing), so a locale set later — a path locale, `setLocaleCode()` — changed `request.locale` but not the language of translated strings
17
+
18
+ ### Added
19
+
20
+ - **`request.requireUser()`** — returns the authenticated user non-optionally, or throws `UnAuthorizedError` when no user is attached. For handlers behind an auth guard, where an absent user is a misconfigured route rather than a normal state; replaces `request.user!` assertions
21
+
22
+ ## 4.16.0 - 2026-08-18
23
+
24
+ ### Security
25
+
26
+ - **`request.detectIp()` no longer trusts `X-Real-IP` / `X-Forwarded-For` unless `http.trustProxy` is set.** Both headers are client-settable, and `detectIp()` honoured them unconditionally — bypassing the `trustProxy` opt-in the Fastify server itself is configured with. Any client could therefore spoof its IP to everything keyed on `detectIp()`: `ipFilter` allowlists/denylists, the default rate-limit bucket key, and anonymous idempotency scoping. Without the opt-in, `detectIp()` (and its `realIp` alias) now returns `baseRequest.ip` — the socket peer address, which cannot be forged
27
+
28
+ ⚠ **If your app runs behind a proxy and relied on `detectIp()` reading the forwarding headers without setting `http.trustProxy`, set `http.trustProxy: true`** (or a Fastify `trustProxy` value matching your edge). With `true` set, behaviour is unchanged: `X-Real-IP` first, then the leftmost `X-Forwarded-For` hop, then the peer address. Only enable `true` when your edge overwrites those headers — it trusts them wholesale
29
+
30
+ - **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that *appends* to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
31
+
32
+ | `http.trustProxy` | Client IP |
33
+ | --- | --- |
34
+ | `false` *(default)* | Socket peer address; forwarding headers ignored |
35
+ | `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
36
+ | `2` | Walks past the 2 rightmost hops — for an edge that appends |
37
+ | `"10.0.0.0/8"`, `"loopback, 10.0.0.0/8"`, `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy, stops at the first that isn't |
38
+ | `(address, hop) => boolean` | Your predicate |
39
+
40
+ Prefer the narrowest shape your topology allows: with `true`, any client that can reach the process directly picks its own IP, and an `ipFilter` allowlist in front of it is decorative
41
+
42
+ ⚠ **`X-Real-IP` is now honoured only under `trustProxy: true`.** It carries no chain, so there is nothing to check a hop count or proxy list against, and a trusted edge that forwards the client's own `X-Real-IP` verbatim would otherwise let any client escape the bound. Under a bounded `trustProxy` the value comes from the `X-Forwarded-For` chain instead — if your edge sets only `X-Real-IP`, have it set `X-Forwarded-For` as well
43
+
44
+ ### Dependencies
45
+
46
+ - Bumped `@mongez/*` deps to their 2026-08-17 security release specs: `concat-route` ^1.2.0, `config` ^1.2.1, `dotenv` ^1.3.2, `events` ^2.2.7, `http` ^3.5.0, `localization` ^3.4.7, `reinforcements` ^4.0.1, `supportive-is` ^2.1.4
47
+ - ⚠ **`@mongez/reinforcements` 4.0.1 is a major bump: `Random` is now CSPRNG-backed (WebCrypto) and `Random.seed()` was removed** — seeded/reproducible `Random.string/nanoid/id/token/uuid` calls now throw. Audited `core`'s `Random.string(...)` call sites (`use-case.ts`, `http/request.ts`, `dev-server/files-watcher.ts`, `http/uploaded-file.ts`) and its test suite: none rely on seeding or reproducible output, so no code changes were required
48
+ - `@mongez/encryption` 2.0.1 (async `encrypt`/`decrypt`, throws on failure) does not apply to this package — `core` is not a consumer; `src/encryption/encrypt.ts` uses Node's built-in `crypto` module directly and is unaffected
49
+
50
+ ## 4.14.0 - 2026-08-16
10
51
 
11
52
  ### ⚠ Upgrading from 4.13.0 — read this first
12
53
 
@@ -104,7 +145,7 @@ afterAll(teardownTest); // ← is new
104
145
 
105
146
  **The cost is a framework bootstrap per test file — which is exactly what 4.13.0 already paid**, since its module-level flag died with the module registry between files. **Nothing gets slower; an unearned speed-up is simply not being claimed.** A worker-scoped lifetime remains open, and gets taken when the real per-file cost has been measured on a real application and the runner integration is chosen deliberately rather than inherited from whatever the wiring happened to do
106
147
 
107
- ## 4.13.0
148
+ ## 4.13.0 - 2026-08-12
108
149
 
109
150
  ### ⚠ Upgrading from 4.12.0 — read this first
110
151
 
package/bin/warlock.js CHANGED
@@ -1,3 +1,156 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import "../esm/cli/start.mjs";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const binDir = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * The published package ships compiled ESM next to this file; a source
11
+ * checkout (this monorepo, or anyone consuming `@warlock.js/core` straight
12
+ * from git) has only TypeScript. `main` in package.json already points at
13
+ * `./src/index.ts`, so "consumed as source" is the supported layout — the bin
14
+ * has to honour it too instead of assuming build output exists.
15
+ */
16
+ const publishedEntry = path.join(binDir, "..", "esm", "cli", "start.mjs");
17
+
18
+ if (existsSync(publishedEntry)) {
19
+ // One `stat` is the entire cost the published path pays. No loader is
20
+ // registered, nothing is transpiled, esbuild is never even imported.
21
+ await import(pathToFileURL(publishedEntry).href);
22
+ } else {
23
+ await bootFromSource();
24
+ }
25
+
26
+ /**
27
+ * Register a TypeScript loader, then hand off to the TS CLI entry.
28
+ *
29
+ * Why a bootstrap loader at all, when core already owns one under
30
+ * `src/dev-server/loader/`? Because that loader is itself TypeScript and is
31
+ * registered by `registerLoader()`, which can only be reached through a TS
32
+ * import — the chicken-and-egg. It also wants things a bare `warlock --help`
33
+ * has no business producing: a `.warlock/` directory in the user's cwd, a
34
+ * bundled hook worker, a transpile cache and a MessageChannel for HMR bumps.
35
+ *
36
+ * So this is deliberately the *smaller* half of that machinery, and it reuses
37
+ * rather than reimplements the part that actually encodes framework
38
+ * behaviour: resolution. The sequence below is what makes that possible.
39
+ */
40
+ async function bootFromSource() {
41
+ const sourceEntry = path.join(binDir, "..", "src", "cli", "start.ts");
42
+
43
+ if (!existsSync(sourceEntry)) {
44
+ console.error(
45
+ "[warlock] Neither esm/cli/start.mjs nor src/cli/start.ts was found next to " +
46
+ `${binDir}. The @warlock.js/core installation looks incomplete.`,
47
+ );
48
+ process.exit(1);
49
+ }
50
+
51
+ const { registerHooks } = await import("node:module");
52
+ const { readFileSync } = await import("node:fs");
53
+ const { transformSync } = await import("esbuild");
54
+ const { getTsconfig, resolvePathAlias } = await import("get-tsconfig");
55
+
56
+ // Same target the dev loader and the production builder pin, so a file
57
+ // transpiled here lowers TC39 decorators to the same helper output it would
58
+ // get anywhere else in the toolchain. Drifting from it would make the CLI a
59
+ // third, subtly different dialect.
60
+ const TRANSPILE_TARGET = "node22";
61
+ const TS_FILE = /\.(ts|tsx|mts|cts)$/;
62
+ const VERSION_QUERY = /\?v=\d+$/;
63
+
64
+ // Read from cwd, not from core: `jsx`, `experimentalDecorators` and friends
65
+ // are the *app's* call, and this is the same source of truth the dev
66
+ // loader's resolve hook reads.
67
+ const tsconfig = getTsconfig(process.cwd());
68
+ const tsconfigRaw = JSON.stringify({
69
+ compilerOptions: tsconfig?.config?.compilerOptions ?? {},
70
+ });
71
+
72
+ /**
73
+ * Handover switch. Verified behaviour: in-thread hooks run *before*
74
+ * `register()` worker hooks and delegate down the chain. So once the dev
75
+ * server registers its own loader, these hooks would still intercept every
76
+ * `.ts` first and short-circuit — silently bypassing the dev loader's
77
+ * transpile cache and, worse, its `?v=N` HMR stamping.
78
+ *
79
+ * Setting `active = false` makes both hooks pure pass-throughs, handing the
80
+ * TypeScript path over intact. `registerLoader()` should flip it just before
81
+ * it calls `register()`; until it does, these hooks stay in charge, which is
82
+ * the correct default for every non-dev command.
83
+ */
84
+ const bootstrapLoader = { active: true };
85
+ globalThis.__warlockBootstrapLoader = bootstrapLoader;
86
+
87
+ /**
88
+ * `load` hook. In-thread (`registerHooks`) rather than the worker-thread
89
+ * `register()`, which is what lets stage 2 below exist at all: a worker hook
90
+ * could not hand a transpiled function back to this thread.
91
+ */
92
+ function load(url, context, nextLoad) {
93
+ const cleanUrl = url.replace(VERSION_QUERY, "");
94
+
95
+ // Not TypeScript → not ours. npm `.js`, `node:`, JSON all fall through.
96
+ if (!bootstrapLoader.active || !TS_FILE.test(cleanUrl)) return nextLoad(url, context);
97
+
98
+ const absolutePath = fileURLToPath(cleanUrl);
99
+ const { code } = transformSync(readFileSync(absolutePath, "utf8"), {
100
+ loader: absolutePath.endsWith(".tsx") ? "tsx" : "ts",
101
+ format: "esm",
102
+ target: TRANSPILE_TARGET,
103
+ sourcefile: absolutePath,
104
+ sourcemap: "inline",
105
+ tsconfigRaw,
106
+ });
107
+
108
+ return { format: "module", source: code, shortCircuit: true };
109
+ }
110
+
111
+ registerHooks({ load });
112
+
113
+ // Stage 2 — the reuse. With `load` live, core's own resolver can be pulled
114
+ // in as TypeScript. It is imported by exact absolute path on purpose: it is
115
+ // a leaf module whose only import is `node:url`, so it needs no extension
116
+ // probing to load, which is precisely the thing we don't have yet.
117
+ const { ownResolve } = await import(
118
+ pathToFileURL(
119
+ path.join(binDir, "..", "src", "dev-server", "loader", "own-resolver.ts"),
120
+ ).href
121
+ );
122
+
123
+ let pathsMatcher = null;
124
+ if (tsconfig) {
125
+ pathsMatcher = specifier => resolvePathAlias(tsconfig, specifier);
126
+ }
127
+
128
+ /**
129
+ * `resolve` hook — a thin sync wrapper over `ownResolve`, which owns
130
+ * tsconfig `paths` plus TS extension/index probing. Everything it returns
131
+ * `null` for (bare npm, `node:`, `file:`) falls through to Node's default,
132
+ * which keeps `exports`/`node_modules` semantics intact.
133
+ *
134
+ * The dev loader's version of this also stamps `?v=N` for HMR. That is
135
+ * deliberately absent: the CLI process has no watcher, and the dev server
136
+ * registers its own full loader when it boots.
137
+ */
138
+ function resolve(specifier, context, nextResolve) {
139
+ const owned = bootstrapLoader.active
140
+ ? ownResolve(specifier, context.parentURL, pathsMatcher, existsSync)
141
+ : null;
142
+
143
+ // Producing the URL ourselves means `nextResolve` was never called, so the
144
+ // chain has to be short-circuited explicitly or Node raises
145
+ // ERR_LOADER_CHAIN_INCOMPLETE.
146
+ return owned ? { url: owned, shortCircuit: true } : nextResolve(specifier, context);
147
+ }
148
+
149
+ registerHooks({ resolve });
150
+
151
+ // The transpile above emits inline maps; without this they'd be dead weight
152
+ // and every CLI stack trace would point at generated line numbers.
153
+ process.setSourceMapsEnabled(true);
154
+
155
+ await import(pathToFileURL(sourceEntry).href);
156
+ }
@@ -22,6 +22,17 @@ type BootContext = {
22
22
  * the other listeners.
23
23
  */
24
24
  type BootListener = (context: BootContext) => void | Promise<void>;
25
+ /**
26
+ * A callback registered through `Application.onValidateBoot`. May be async —
27
+ * unlike {@link BootListener}, its rejection is NOT caught: it aborts boot.
28
+ *
29
+ * Use this for configuration that makes the application WRONG rather than
30
+ * degraded when missing (a signing secret, a required integration key) — the
31
+ * kind of check that must fail closed. Use `onceBooted` for everything else;
32
+ * that contract stays fire-and-forget on purpose (it is the right behaviour
33
+ * for plugins reacting to boot, not for gating it).
34
+ */
35
+ type BootValidator = () => void | Promise<void>;
25
36
  /**
26
37
  * A callback registered through `Application.onShutdown`. Runs once, while the
27
38
  * connectors (db, cache, http) are still up, so it can release app-owned
@@ -50,6 +61,17 @@ declare class Application {
50
61
  * `markBooted`.
51
62
  */
52
63
  private static bootListeners;
64
+ /**
65
+ * Whether `runStartupValidators` has already run. Flipped once, so a
66
+ * validator registered afterwards is refused instead of silently skipped
67
+ * or run too late to matter.
68
+ */
69
+ private static validated;
70
+ /**
71
+ * Startup validators queued before `runStartupValidators` ran, drained in
72
+ * registration order.
73
+ */
74
+ private static bootValidators;
53
75
  /**
54
76
  * The http port this process actually bound, once it has. Reported in the
55
77
  * readiness signal so a supervisor knows where to send its health check
@@ -126,6 +148,48 @@ declare class Application {
126
148
  * the remaining listeners are affected.
127
149
  */
128
150
  private static runBootListener;
151
+ /**
152
+ * Register a startup validator — distinct from {@link onceBooted}. A
153
+ * rejecting validator aborts boot instead of being caught and logged, so
154
+ * this is where configuration that must fail closed belongs (a signing
155
+ * secret, a required integration key), not `onceBooted`.
156
+ *
157
+ * Must be registered before the framework calls `runStartupValidators` —
158
+ * typically at module scope in `src/app/main.ts`, the same place a
159
+ * `BootListener` would be registered. Registering one after validation has
160
+ * already run throws: by then a rejection could no longer stop the app
161
+ * from serving, so running it late would silently defeat the guarantee
162
+ * this hook exists to give.
163
+ *
164
+ * @example
165
+ * Application.onValidateBoot(() => {
166
+ * if (!env("JWT_SECRET")) {
167
+ * throw new Error(
168
+ * "JWT_SECRET is not set. Sessions cannot be signed without it. " +
169
+ * "Generate one with `yarn jwt` and put it in .env.",
170
+ * );
171
+ * }
172
+ * });
173
+ */
174
+ static onValidateBoot(validator: BootValidator): void;
175
+ /**
176
+ * Run every registered startup validator, in registration order, and abort
177
+ * on the first rejection. Idempotent — a second call is a no-op.
178
+ *
179
+ * Distinct from `runBootListener`: a listener's failure is isolated so it
180
+ * can never break boot; a validator's failure IS the boot failing — that is
181
+ * the entire point of registering one instead of the other. The thrown
182
+ * error names the failing validator and the original cause, so whatever
183
+ * catches it (the dev server's boot try/catch, the production entry's
184
+ * top-level await) reports which check failed and why, not a bare
185
+ * rejection.
186
+ *
187
+ * @internal Framework entry points call this once app code (main.ts et al)
188
+ * has loaded and BEFORE late-phase connectors (http, socket) start, so a
189
+ * failing validator runs before anything can bind a port or accept a
190
+ * request. Application code must not call this directly.
191
+ */
192
+ static runStartupValidators(): Promise<void>;
129
193
  /**
130
194
  * Whether the application has begun shutting down. Once true, readiness
131
195
  * checks report not-ready so a load balancer drains this instance before the
@@ -209,5 +273,5 @@ declare class Application {
209
273
  static get publicPath(): string;
210
274
  }
211
275
  //#endregion
212
- export { Application, BootContext, BootListener, ShutdownListener };
276
+ export { Application, BootContext, BootListener, BootValidator, ShutdownListener };
213
277
  //# sourceMappingURL=application.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"application.d.mts","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"mappings":";;;;;AAiBA;;;;KAAY,WAAA;EACV,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAe;EAAf;;;AAKH;EAAd,cAAA;AAAA;;;;;;KAQU,YAAA,IAAgB,OAAA,EAAS,WAAA,YAAuB,OAAO;AAAA;AAOnE;;;;AAPmE,KAOvD,gBAAA,gBAAgC,OAAO;AAAA,cAEtC,WAAA;EAAW;;;EAAA,gBAIC,SAAA,EAAS,IAAA;EAoDW;;;EAAA,OA/C7B,eAAA,EAAiB,eAAA;EA0HG;;;EAAA,eArHnB,MAAA;EAkPmB;;;EAAA,eA7OnB,WAAA;EAfiB;;;;EAAA,eAqBjB,aAAA;EAAA;;;;;;EAAA,eAQA,UAAA;EAuBkB;;;;EAAA,eAjBlB,YAAA;EAqDoB;;;EAAA,eAhDpB,iBAAA;EA+DqB;;;EAAA,WA1DlB,OAAA;EAiHG;;;EAAA,OA1GP,kBAAA,CAAmB,QAAA,EAAU,eAAA;EAyIlB;;;;;EAAA,WAhIP,QAAA;EAwLe;;;;;EAAA,OA/KnB,aAAA,CAAc,IAAA;EA2MV;;;;;;;;AA0CU;;;;;;EA1CV,OAzLJ,UAAA,CAAW,QAAA,EAAU,YAAA;;;;;SAerB,UAAA,IAAc,OAAA,CAAQ,WAAA;;;;;;;;;;;;;;;SAwBtB,UAAA,CAAW,OAAA,EAAS,WAAA;;;;;iBA+Bb,eAAA;;;;;;aAgBH,cAAA;;;;;;;;;;;;SAeJ,UAAA,CAAW,QAAA,EAAU,gBAAA;;;;;;;;;;SAmBf,gBAAA,IAAoB,OAAA;;;;;iBAmBnB,mBAAA;;;;aAWH,MAAA;;;;aAOA,WAAA,IAAe,WAAA;;;;SAOnB,cAAA,CAAe,GAAA,EAAK,WAAA;;;;aAOhB,YAAA;;;;aAOA,aAAA;;;;aAOA,MAAA;;;;aAOA,QAAA;;;;aAOA,OAAA;;;;aAOA,OAAA;;;;aAOA,WAAA;;;;aAOA,WAAA;;;;aAOA,UAAA;AAAA"}
1
+ {"version":3,"file":"application.d.mts","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"mappings":";;;;;AAiBA;;;;KAAY,WAAA;EACV,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAe;EAAf;;;AAKH;EAAd,cAAA;AAAA;;;;;;KAQU,YAAA,IAAgB,OAAA,EAAS,WAAA,YAAuB,OAAO;AAAA;AAYnE;;;;AAAgD;AAkBhD;;;;AA9BmE,KAYvD,aAAA,gBAA6B,OAAO;AAoBhD;;;;;AAAA,KAFY,gBAAA,gBAAgC,OAAO;AAAA,cAEtC,WAAA;EAwHyB;;;EAAA,gBApHb,SAAA,EAAS,IAAA;EA2OY;;;EAAA,OAtO9B,eAAA,EAAiB,eAAA;EAoVG;;;EAAA,eA/UnB,MAAA;EAViB;;;EAAA,eAejB,WAAA;EAAA;;;;EAAA,eAMA,aAAA;EA2BA;;;;;EAAA,eApBA,SAAA;EA8CG;;;;EAAA,eAxCH,cAAA;EAmEU;;;;;;EAAA,eA3DV,UAAA;EAiIM;;;;EAAA,eA3HN,YAAA;EA2L6B;;;EAAA,eAtL7B,iBAAA;EAqOU;;;EAAA,WAhOP,OAAA;EAiRA;;;EAAA,OA1QJ,kBAAA,CAAmB,QAAA,EAAU,eAAA;EAwRT;;;;;EAAA,WA/QhB,QAAA;EAkTA;;;;;EAAA,OAzSJ,aAAA,CAAc,IAAA;EAqUA;;;;;;;;;;;;;;EAAA,OAnTd,UAAA,CAAW,QAAA,EAAU,YAAA;;;;;SAerB,UAAA,IAAc,OAAA,CAAQ,WAAA;;;;;;;;;;;;;;;SAwBtB,UAAA,CAAW,OAAA,EAAS,WAAA;;;;;iBA+Bb,eAAA;;;;;;;;;;;;;;;;;;;;;;;;SAkCP,cAAA,CAAe,SAAA,EAAW,aAAA;;;;;;;;;;;;;;;;;;SA8BpB,oBAAA,IAAwB,OAAA;;;;;;aAgC1B,cAAA;;;;;;;;;;;;SAeJ,UAAA,CAAW,QAAA,EAAU,gBAAA;;;;;;;;;;SAmBf,gBAAA,IAAoB,OAAA;;;;;iBAmBnB,mBAAA;;;;aAWH,MAAA;;;;aAOA,WAAA,IAAe,WAAA;;;;SAOnB,cAAA,CAAe,GAAA,EAAK,WAAA;;;;aAOhB,YAAA;;;;aAOA,aAAA;;;;aAOA,MAAA;;;;aAOA,QAAA;;;;aAOA,OAAA;;;;aAOA,OAAA;;;;aAOA,WAAA;;;;aAOA,WAAA;;;;aAOA,UAAA;AAAA"}
@@ -5,6 +5,7 @@ import { getFrameworkVersion } from "../utils/framework-vesion.mjs";
5
5
  import { log } from "@warlock.js/logger";
6
6
 
7
7
  //#region ../core/src/application/application.ts
8
+ const ErrorWithCause = Error;
8
9
  var Application = class {
9
10
  static {
10
11
  this.startedAt = /* @__PURE__ */ new Date(Date.now() - process.uptime() * 1e3);
@@ -15,6 +16,12 @@ var Application = class {
15
16
  static {
16
17
  this.bootListeners = [];
17
18
  }
19
+ static {
20
+ this.validated = false;
21
+ }
22
+ static {
23
+ this.bootValidators = [];
24
+ }
18
25
  static {
19
26
  this.shuttingDown = false;
20
27
  }
@@ -125,6 +132,61 @@ var Application = class {
125
132
  }
126
133
  }
127
134
  /**
135
+ * Register a startup validator — distinct from {@link onceBooted}. A
136
+ * rejecting validator aborts boot instead of being caught and logged, so
137
+ * this is where configuration that must fail closed belongs (a signing
138
+ * secret, a required integration key), not `onceBooted`.
139
+ *
140
+ * Must be registered before the framework calls `runStartupValidators` —
141
+ * typically at module scope in `src/app/main.ts`, the same place a
142
+ * `BootListener` would be registered. Registering one after validation has
143
+ * already run throws: by then a rejection could no longer stop the app
144
+ * from serving, so running it late would silently defeat the guarantee
145
+ * this hook exists to give.
146
+ *
147
+ * @example
148
+ * Application.onValidateBoot(() => {
149
+ * if (!env("JWT_SECRET")) {
150
+ * throw new Error(
151
+ * "JWT_SECRET is not set. Sessions cannot be signed without it. " +
152
+ * "Generate one with `yarn jwt` and put it in .env.",
153
+ * );
154
+ * }
155
+ * });
156
+ */
157
+ static onValidateBoot(validator) {
158
+ if (this.validated) throw new Error("Application.onValidateBoot(): startup validators already ran — this validator was registered too late to block boot. Register it before the framework calls Application.runStartupValidators(), typically at module scope in `src/app/main.ts`.");
159
+ this.bootValidators.push(validator);
160
+ }
161
+ /**
162
+ * Run every registered startup validator, in registration order, and abort
163
+ * on the first rejection. Idempotent — a second call is a no-op.
164
+ *
165
+ * Distinct from `runBootListener`: a listener's failure is isolated so it
166
+ * can never break boot; a validator's failure IS the boot failing — that is
167
+ * the entire point of registering one instead of the other. The thrown
168
+ * error names the failing validator and the original cause, so whatever
169
+ * catches it (the dev server's boot try/catch, the production entry's
170
+ * top-level await) reports which check failed and why, not a bare
171
+ * rejection.
172
+ *
173
+ * @internal Framework entry points call this once app code (main.ts et al)
174
+ * has loaded and BEFORE late-phase connectors (http, socket) start, so a
175
+ * failing validator runs before anything can bind a port or accept a
176
+ * request. Application code must not call this directly.
177
+ */
178
+ static async runStartupValidators() {
179
+ if (this.validated) return;
180
+ this.validated = true;
181
+ const validators = this.bootValidators;
182
+ this.bootValidators = [];
183
+ for (const validator of validators) try {
184
+ await validator();
185
+ } catch (error) {
186
+ throw new ErrorWithCause(`Startup validator "${validator.name || "<anonymous>"}" rejected boot: ${error instanceof Error ? error.message : String(error)}. Fix the condition it checks, or remove the validator if it no longer applies — boot cannot proceed while it fails.`, { cause: error });
187
+ }
188
+ }
189
+ /**
128
190
  * Whether the application has begun shutting down. Once true, readiness
129
191
  * checks report not-ready so a load balancer drains this instance before the
130
192
  * HTTP server stops accepting requests.
@@ -1 +1 @@
1
- {"version":3,"file":"application.mjs","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { BOOT_SIGNAL_VERSION, sendBootSignal } from \"./boot-signal\";\nimport {\n environment,\n setEnvironment,\n type Environment,\n type RuntimeStrategy,\n} from \"../utils/environment\";\nimport { getFrameworkVersion } from \"../utils/framework-vesion\";\nimport { appPath, publicPath, rootPath, srcPath, storagePath, uploadsPath } from \"../utils/paths\";\n\n/**\n * Snapshot of the global facts known the moment the application finished\n * booting: every connector (early + late) is active and all app files\n * (locales, events, main, routes) are loaded. Passed to every\n * `Application.onceBooted` listener and resolved from `Application.whenBooted`.\n */\nexport type BootContext = {\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n /**\n * Wall-clock milliseconds the boot took. The dev server measures it; the\n * production entry omits it.\n */\n bootDurationMs?: number;\n};\n\n/**\n * A callback registered through `Application.onceBooted`. May be async — its\n * rejection is caught and logged so a single listener can never break boot or\n * the other listeners.\n */\nexport type BootListener = (context: BootContext) => void | Promise<void>;\n\n/**\n * A callback registered through `Application.onShutdown`. Runs once, while the\n * connectors (db, cache, http) are still up, so it can release app-owned\n * resources cleanly. May be async — its rejection is caught and logged.\n */\nexport type ShutdownListener = () => void | Promise<void>;\n\nexport class Application {\n /**\n * Project start time regarding the process start time\n */\n public static readonly startedAt = new Date(Date.now() - process.uptime() * 1000);\n\n /**\n * Runtime strategy\n */\n public static runtimeStrategy: RuntimeStrategy;\n\n /**\n * Whether the application has finished booting. Flipped once by `markBooted`.\n */\n private static booted = false;\n\n /**\n * The boot context, set once booted so it can be replayed to late subscribers.\n */\n private static bootContext: BootContext | undefined;\n\n /**\n * Listeners queued before boot completed, drained in registration order by\n * `markBooted`.\n */\n private static bootListeners: BootListener[] = [];\n\n /**\n * The http port this process actually bound, once it has. Reported in the\n * readiness signal so a supervisor knows where to send its health check\n * instead of re-deriving it from config it may not be able to read.\n * Undefined for an app with no http connector — a queue worker still boots.\n */\n private static servedPort: number | undefined;\n\n /**\n * Whether the application has begun (or finished) shutting down. Flipped once\n * by `runShutdownHooks`.\n */\n private static shuttingDown = false;\n\n /**\n * Teardown callbacks, run once when shutdown begins.\n */\n private static shutdownListeners: ShutdownListener[] = [];\n\n /**\n * Get framework version\n */\n public static get version() {\n return getFrameworkVersion();\n }\n\n /**\n * Set the runtime strategy\n */\n public static setRuntimeStrategy(strategy: RuntimeStrategy) {\n this.runtimeStrategy = strategy;\n }\n\n /**\n * Whether the application has finished booting — every connector in both\n * the early and late phases is active and all app files (locales, events,\n * main, routes) have been loaded.\n */\n public static get isBooted(): boolean {\n return this.booted;\n }\n\n /**\n * Record the http port this process bound.\n *\n * @internal The http connector calls this after a successful `listen`.\n */\n public static setServedPort(port: number): void {\n this.servedPort = port;\n }\n\n /**\n * Run a callback once the application is fully booted.\n *\n * App files (`main` / `events` / `routes` / locales) are imported BEFORE the\n * late-phase connectors (http, socket) start, so a listener registered at\n * import time waits for the whole sequence to finish before it runs. If the\n * application is already booted, the callback runs on the next microtask —\n * so a late subscriber never silently misses the event.\n *\n * @example\n * Application.onceBooted(({ environment }) => {\n * log.info(\"app\", \"booted\", `ready in ${environment}`);\n * });\n */\n public static onceBooted(listener: BootListener): void {\n if (this.booted && this.bootContext) {\n const context = this.bootContext;\n void this.runBootListener(listener, context);\n\n return;\n }\n\n this.bootListeners.push(listener);\n }\n\n /**\n * Promise form of {@link onceBooted} — resolves with the boot context once\n * the application is booted, or immediately if it already is.\n */\n public static whenBooted(): Promise<BootContext> {\n if (this.booted && this.bootContext) {\n return Promise.resolve(this.bootContext);\n }\n\n return new Promise<BootContext>((resolve) => {\n this.onceBooted((context) => resolve(context));\n });\n }\n\n /**\n * Flip the boot latch and drain every queued listener. The dev server and\n * the production entry call this once, right after the late phase starts. It\n * is idempotent — a second call is a no-op, so a double-invoke can never\n * double-fire listeners.\n *\n * Also reports readiness to a supervising parent process — this is the one\n * moment in the lifecycle where \"the application is serving\" is true, so it\n * is the only honest source for `warlock start`'s success banner. Sent before\n * the listeners run: a slow or hanging `onceBooted` listener must not delay\n * the parent's view of a server that is already accepting requests.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static markBooted(context: BootContext): void {\n if (this.booted) {\n return;\n }\n\n this.booted = true;\n this.bootContext = context;\n\n sendBootSignal({\n type: \"warlock:ready\",\n version: BOOT_SIGNAL_VERSION,\n pid: process.pid,\n at: new Date().toISOString(),\n environment: context.environment,\n runtimeStrategy: context.runtimeStrategy,\n bootDurationMs: context.bootDurationMs,\n port: this.servedPort,\n });\n\n const listeners = this.bootListeners;\n this.bootListeners = [];\n\n for (const listener of listeners) {\n void this.runBootListener(listener, context);\n }\n }\n\n /**\n * Invoke a single boot listener, isolating its failure so neither boot nor\n * the remaining listeners are affected.\n */\n private static async runBootListener(\n listener: BootListener,\n context: BootContext,\n ): Promise<void> {\n try {\n await listener(context);\n } catch (error) {\n log.error(\"application\", \"booted-listener\", error as Error);\n }\n }\n\n /**\n * Whether the application has begun shutting down. Once true, readiness\n * checks report not-ready so a load balancer drains this instance before the\n * HTTP server stops accepting requests.\n */\n public static get isShuttingDown(): boolean {\n return this.shuttingDown;\n }\n\n /**\n * Register a teardown callback, run once when the application shuts down —\n * before the connectors (db, cache, http) are torn down, so it can still use\n * them. The natural place to close an app-owned resource opened in\n * `onceBooted`. If shutdown has already begun, the callback runs immediately.\n *\n * @example\n * Application.onShutdown(async () => {\n * await myQueueConsumer.stop();\n * });\n */\n public static onShutdown(listener: ShutdownListener): void {\n if (this.shuttingDown) {\n void this.runShutdownListener(listener);\n\n return;\n }\n\n this.shutdownListeners.push(listener);\n }\n\n /**\n * Flip the shutdown latch and run every teardown callback. The connectors\n * manager calls this at the start of shutdown — before any connector is torn\n * down. Idempotent and error-isolated: each callback is awaited, a thrown one\n * is logged and does not stop the rest. Listeners run LIFO (reverse of\n * registration), mirroring resource-unwind order.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static async runShutdownHooks(): Promise<void> {\n if (this.shuttingDown) {\n return;\n }\n\n this.shuttingDown = true;\n\n const listeners = this.shutdownListeners.reverse();\n this.shutdownListeners = [];\n\n for (const listener of listeners) {\n await this.runShutdownListener(listener);\n }\n }\n\n /**\n * Invoke a single teardown callback, isolating its failure so the rest still\n * run and shutdown is never blocked by one bad hook.\n */\n private static async runShutdownListener(listener: ShutdownListener): Promise<void> {\n try {\n await listener();\n } catch (error) {\n log.error(\"application\", \"shutdown-listener\", error as Error);\n }\n }\n\n /**\n * Get project uptime in milliseconds\n */\n public static get uptime(): number {\n return process.uptime() * 1000;\n }\n\n /**\n * Get the current environment\n */\n public static get environment(): Environment {\n return environment();\n }\n\n /**\n * Set the current environment\n */\n public static setEnvironment(env: Environment) {\n setEnvironment(env);\n }\n\n /**\n * Check if the application is running in production environment\n */\n public static get isProduction(): boolean {\n return this.environment === \"production\";\n }\n\n /**\n * Check if the application is running in development environment\n */\n public static get isDevelopment(): boolean {\n return this.environment === \"development\";\n }\n\n /**\n * Check if the application is running in test environment\n */\n public static get isTest(): boolean {\n return this.environment === \"test\";\n }\n\n /**\n * Get the root path\n */\n public static get rootPath(): string {\n return rootPath();\n }\n\n /**\n * Get the src path\n */\n public static get srcPath(): string {\n return srcPath();\n }\n\n /**\n * Get the app path\n */\n public static get appPath(): string {\n return appPath();\n }\n\n /**\n * Get the storage path\n */\n public static get storagePath(): string {\n return storagePath();\n }\n\n /**\n * Get the uploads path\n */\n public static get uploadsPath(): string {\n return uploadsPath();\n }\n\n /**\n * Get the public path\n */\n public static get publicPath(): string {\n return publicPath();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"application.mjs","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { BOOT_SIGNAL_VERSION, sendBootSignal } from \"./boot-signal\";\nimport {\n environment,\n setEnvironment,\n type Environment,\n type RuntimeStrategy,\n} from \"../utils/environment\";\nimport { getFrameworkVersion } from \"../utils/framework-vesion\";\nimport { appPath, publicPath, rootPath, srcPath, storagePath, uploadsPath } from \"../utils/paths\";\n\n/**\n * Snapshot of the global facts known the moment the application finished\n * booting: every connector (early + late) is active and all app files\n * (locales, events, main, routes) are loaded. Passed to every\n * `Application.onceBooted` listener and resolved from `Application.whenBooted`.\n */\nexport type BootContext = {\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n /**\n * Wall-clock milliseconds the boot took. The dev server measures it; the\n * production entry omits it.\n */\n bootDurationMs?: number;\n};\n\n/**\n * A callback registered through `Application.onceBooted`. May be async — its\n * rejection is caught and logged so a single listener can never break boot or\n * the other listeners.\n */\nexport type BootListener = (context: BootContext) => void | Promise<void>;\n\n/**\n * A callback registered through `Application.onValidateBoot`. May be async —\n * unlike {@link BootListener}, its rejection is NOT caught: it aborts boot.\n *\n * Use this for configuration that makes the application WRONG rather than\n * degraded when missing (a signing secret, a required integration key) — the\n * kind of check that must fail closed. Use `onceBooted` for everything else;\n * that contract stays fire-and-forget on purpose (it is the right behaviour\n * for plugins reacting to boot, not for gating it).\n */\nexport type BootValidator = () => void | Promise<void>;\n\n/**\n * `new Error(message, { cause })` is ES2022, and this package still compiles\n * against the ES2020 lib, where `Error` is typed as taking a message only.\n * Node has supported the option since v16, so this is the type layer\n * catching up with the runtime, not a change in what the code does. Same\n * workaround as `image.ts` (see #16).\n */\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\n\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\n\n/**\n * A callback registered through `Application.onShutdown`. Runs once, while the\n * connectors (db, cache, http) are still up, so it can release app-owned\n * resources cleanly. May be async — its rejection is caught and logged.\n */\nexport type ShutdownListener = () => void | Promise<void>;\n\nexport class Application {\n /**\n * Project start time regarding the process start time\n */\n public static readonly startedAt = new Date(Date.now() - process.uptime() * 1000);\n\n /**\n * Runtime strategy\n */\n public static runtimeStrategy: RuntimeStrategy;\n\n /**\n * Whether the application has finished booting. Flipped once by `markBooted`.\n */\n private static booted = false;\n\n /**\n * The boot context, set once booted so it can be replayed to late subscribers.\n */\n private static bootContext: BootContext | undefined;\n\n /**\n * Listeners queued before boot completed, drained in registration order by\n * `markBooted`.\n */\n private static bootListeners: BootListener[] = [];\n\n /**\n * Whether `runStartupValidators` has already run. Flipped once, so a\n * validator registered afterwards is refused instead of silently skipped\n * or run too late to matter.\n */\n private static validated = false;\n\n /**\n * Startup validators queued before `runStartupValidators` ran, drained in\n * registration order.\n */\n private static bootValidators: BootValidator[] = [];\n\n /**\n * The http port this process actually bound, once it has. Reported in the\n * readiness signal so a supervisor knows where to send its health check\n * instead of re-deriving it from config it may not be able to read.\n * Undefined for an app with no http connector — a queue worker still boots.\n */\n private static servedPort: number | undefined;\n\n /**\n * Whether the application has begun (or finished) shutting down. Flipped once\n * by `runShutdownHooks`.\n */\n private static shuttingDown = false;\n\n /**\n * Teardown callbacks, run once when shutdown begins.\n */\n private static shutdownListeners: ShutdownListener[] = [];\n\n /**\n * Get framework version\n */\n public static get version() {\n return getFrameworkVersion();\n }\n\n /**\n * Set the runtime strategy\n */\n public static setRuntimeStrategy(strategy: RuntimeStrategy) {\n this.runtimeStrategy = strategy;\n }\n\n /**\n * Whether the application has finished booting — every connector in both\n * the early and late phases is active and all app files (locales, events,\n * main, routes) have been loaded.\n */\n public static get isBooted(): boolean {\n return this.booted;\n }\n\n /**\n * Record the http port this process bound.\n *\n * @internal The http connector calls this after a successful `listen`.\n */\n public static setServedPort(port: number): void {\n this.servedPort = port;\n }\n\n /**\n * Run a callback once the application is fully booted.\n *\n * App files (`main` / `events` / `routes` / locales) are imported BEFORE the\n * late-phase connectors (http, socket) start, so a listener registered at\n * import time waits for the whole sequence to finish before it runs. If the\n * application is already booted, the callback runs on the next microtask —\n * so a late subscriber never silently misses the event.\n *\n * @example\n * Application.onceBooted(({ environment }) => {\n * log.info(\"app\", \"booted\", `ready in ${environment}`);\n * });\n */\n public static onceBooted(listener: BootListener): void {\n if (this.booted && this.bootContext) {\n const context = this.bootContext;\n void this.runBootListener(listener, context);\n\n return;\n }\n\n this.bootListeners.push(listener);\n }\n\n /**\n * Promise form of {@link onceBooted} — resolves with the boot context once\n * the application is booted, or immediately if it already is.\n */\n public static whenBooted(): Promise<BootContext> {\n if (this.booted && this.bootContext) {\n return Promise.resolve(this.bootContext);\n }\n\n return new Promise<BootContext>((resolve) => {\n this.onceBooted((context) => resolve(context));\n });\n }\n\n /**\n * Flip the boot latch and drain every queued listener. The dev server and\n * the production entry call this once, right after the late phase starts. It\n * is idempotent — a second call is a no-op, so a double-invoke can never\n * double-fire listeners.\n *\n * Also reports readiness to a supervising parent process — this is the one\n * moment in the lifecycle where \"the application is serving\" is true, so it\n * is the only honest source for `warlock start`'s success banner. Sent before\n * the listeners run: a slow or hanging `onceBooted` listener must not delay\n * the parent's view of a server that is already accepting requests.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static markBooted(context: BootContext): void {\n if (this.booted) {\n return;\n }\n\n this.booted = true;\n this.bootContext = context;\n\n sendBootSignal({\n type: \"warlock:ready\",\n version: BOOT_SIGNAL_VERSION,\n pid: process.pid,\n at: new Date().toISOString(),\n environment: context.environment,\n runtimeStrategy: context.runtimeStrategy,\n bootDurationMs: context.bootDurationMs,\n port: this.servedPort,\n });\n\n const listeners = this.bootListeners;\n this.bootListeners = [];\n\n for (const listener of listeners) {\n void this.runBootListener(listener, context);\n }\n }\n\n /**\n * Invoke a single boot listener, isolating its failure so neither boot nor\n * the remaining listeners are affected.\n */\n private static async runBootListener(\n listener: BootListener,\n context: BootContext,\n ): Promise<void> {\n try {\n await listener(context);\n } catch (error) {\n log.error(\"application\", \"booted-listener\", error as Error);\n }\n }\n\n /**\n * Register a startup validator — distinct from {@link onceBooted}. A\n * rejecting validator aborts boot instead of being caught and logged, so\n * this is where configuration that must fail closed belongs (a signing\n * secret, a required integration key), not `onceBooted`.\n *\n * Must be registered before the framework calls `runStartupValidators` —\n * typically at module scope in `src/app/main.ts`, the same place a\n * `BootListener` would be registered. Registering one after validation has\n * already run throws: by then a rejection could no longer stop the app\n * from serving, so running it late would silently defeat the guarantee\n * this hook exists to give.\n *\n * @example\n * Application.onValidateBoot(() => {\n * if (!env(\"JWT_SECRET\")) {\n * throw new Error(\n * \"JWT_SECRET is not set. Sessions cannot be signed without it. \" +\n * \"Generate one with `yarn jwt` and put it in .env.\",\n * );\n * }\n * });\n */\n public static onValidateBoot(validator: BootValidator): void {\n if (this.validated) {\n throw new Error(\n \"Application.onValidateBoot(): startup validators already ran — \" +\n \"this validator was registered too late to block boot. Register it \" +\n \"before the framework calls Application.runStartupValidators(), \" +\n \"typically at module scope in `src/app/main.ts`.\",\n );\n }\n\n this.bootValidators.push(validator);\n }\n\n /**\n * Run every registered startup validator, in registration order, and abort\n * on the first rejection. Idempotent — a second call is a no-op.\n *\n * Distinct from `runBootListener`: a listener's failure is isolated so it\n * can never break boot; a validator's failure IS the boot failing — that is\n * the entire point of registering one instead of the other. The thrown\n * error names the failing validator and the original cause, so whatever\n * catches it (the dev server's boot try/catch, the production entry's\n * top-level await) reports which check failed and why, not a bare\n * rejection.\n *\n * @internal Framework entry points call this once app code (main.ts et al)\n * has loaded and BEFORE late-phase connectors (http, socket) start, so a\n * failing validator runs before anything can bind a port or accept a\n * request. Application code must not call this directly.\n */\n public static async runStartupValidators(): Promise<void> {\n if (this.validated) {\n return;\n }\n\n this.validated = true;\n\n const validators = this.bootValidators;\n this.bootValidators = [];\n\n for (const validator of validators) {\n try {\n await validator();\n } catch (error) {\n const name = validator.name || \"<anonymous>\";\n const cause = error instanceof Error ? error.message : String(error);\n\n throw new ErrorWithCause(\n `Startup validator \"${name}\" rejected boot: ${cause}. ` +\n `Fix the condition it checks, or remove the validator if it no ` +\n `longer applies — boot cannot proceed while it fails.`,\n { cause: error },\n );\n }\n }\n }\n\n /**\n * Whether the application has begun shutting down. Once true, readiness\n * checks report not-ready so a load balancer drains this instance before the\n * HTTP server stops accepting requests.\n */\n public static get isShuttingDown(): boolean {\n return this.shuttingDown;\n }\n\n /**\n * Register a teardown callback, run once when the application shuts down —\n * before the connectors (db, cache, http) are torn down, so it can still use\n * them. The natural place to close an app-owned resource opened in\n * `onceBooted`. If shutdown has already begun, the callback runs immediately.\n *\n * @example\n * Application.onShutdown(async () => {\n * await myQueueConsumer.stop();\n * });\n */\n public static onShutdown(listener: ShutdownListener): void {\n if (this.shuttingDown) {\n void this.runShutdownListener(listener);\n\n return;\n }\n\n this.shutdownListeners.push(listener);\n }\n\n /**\n * Flip the shutdown latch and run every teardown callback. The connectors\n * manager calls this at the start of shutdown — before any connector is torn\n * down. Idempotent and error-isolated: each callback is awaited, a thrown one\n * is logged and does not stop the rest. Listeners run LIFO (reverse of\n * registration), mirroring resource-unwind order.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static async runShutdownHooks(): Promise<void> {\n if (this.shuttingDown) {\n return;\n }\n\n this.shuttingDown = true;\n\n const listeners = this.shutdownListeners.reverse();\n this.shutdownListeners = [];\n\n for (const listener of listeners) {\n await this.runShutdownListener(listener);\n }\n }\n\n /**\n * Invoke a single teardown callback, isolating its failure so the rest still\n * run and shutdown is never blocked by one bad hook.\n */\n private static async runShutdownListener(listener: ShutdownListener): Promise<void> {\n try {\n await listener();\n } catch (error) {\n log.error(\"application\", \"shutdown-listener\", error as Error);\n }\n }\n\n /**\n * Get project uptime in milliseconds\n */\n public static get uptime(): number {\n return process.uptime() * 1000;\n }\n\n /**\n * Get the current environment\n */\n public static get environment(): Environment {\n return environment();\n }\n\n /**\n * Set the current environment\n */\n public static setEnvironment(env: Environment) {\n setEnvironment(env);\n }\n\n /**\n * Check if the application is running in production environment\n */\n public static get isProduction(): boolean {\n return this.environment === \"production\";\n }\n\n /**\n * Check if the application is running in development environment\n */\n public static get isDevelopment(): boolean {\n return this.environment === \"development\";\n }\n\n /**\n * Check if the application is running in test environment\n */\n public static get isTest(): boolean {\n return this.environment === \"test\";\n }\n\n /**\n * Get the root path\n */\n public static get rootPath(): string {\n return rootPath();\n }\n\n /**\n * Get the src path\n */\n public static get srcPath(): string {\n return srcPath();\n }\n\n /**\n * Get the app path\n */\n public static get appPath(): string {\n return appPath();\n }\n\n /**\n * Get the storage path\n */\n public static get storagePath(): string {\n return storagePath();\n }\n\n /**\n * Get the uploads path\n */\n public static get uploadsPath(): string {\n return uploadsPath();\n }\n\n /**\n * Get the public path\n */\n public static get publicPath(): string {\n return publicPath();\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { app } from "./app.mjs";
2
- import { Application, BootContext, BootListener, ShutdownListener } from "./application.mjs";
2
+ import { Application, BootContext, BootListener, BootValidator, ShutdownListener } from "./application.mjs";
3
3
  import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BootSignal, BootSignalType, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
4
4
  import { AppConfigurations } from "./application-config-types.mjs";
@@ -7,6 +7,7 @@ import { bootstrap } from "../bootstrap.mjs";
7
7
  import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
8
8
  import { connectorsManager } from "../connectors/connectors-manager.mjs";
9
9
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
10
+ import { registerConfiguredConnectors } from "../connectors/register-configured-connectors.mjs";
10
11
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
11
12
  import { loadConfigFiles } from "../config/load-config-files.mjs";
12
13
  import { manifestManager } from "../manifest/manifest-manager.mjs";
@@ -19,11 +20,38 @@ import { colors } from "@mongez/copper";
19
20
  import { fileExistsAsync } from "@warlock.js/fs";
20
21
 
21
22
  //#region ../core/src/cli/cli-commands.manager.ts
23
+ /**
24
+ * Best-effort budget (ms) for draining stdout/stderr before a forced exit.
25
+ * Bounded so a stuck stream can never hang the CLI.
26
+ */
27
+ const EXIT_FLUSH_BUDGET = 500;
28
+ /**
29
+ * Exit with `code` once stdout/stderr have drained.
30
+ *
31
+ * `process.exit()` truncates writes still queued on a non-TTY stdout — a pipe,
32
+ * a CI log — which is how the *reason* for a failure gets lost even when the
33
+ * exit code is right. `exitCode` is set up front so that a natural exit (should
34
+ * the drain outlive the process's remaining work) is non-zero too.
35
+ */
36
+ function exitAfterFlush(code) {
37
+ process.exitCode = code;
38
+ const timer = setTimeout(() => process.exit(code), EXIT_FLUSH_BUDGET);
39
+ timer.unref?.();
40
+ let pending = 2;
41
+ const drained = () => {
42
+ if (--pending > 0) return;
43
+ clearTimeout(timer);
44
+ process.exit(code);
45
+ };
46
+ process.stdout.write("", drained);
47
+ process.stderr.write("", drained);
48
+ }
22
49
  var CLICommandsManager = class {
23
50
  constructor() {
24
51
  this.commands = [];
25
52
  this.commandsMap = /* @__PURE__ */ new Map();
26
53
  this.aliasMap = /* @__PURE__ */ new Map();
54
+ this.fatalAsyncErrorReported = false;
27
55
  }
28
56
  /**
29
57
  * Register the given commands
@@ -117,10 +145,10 @@ var CLICommandsManager = class {
117
145
  process.exit(0);
118
146
  }
119
147
  const resolved = this.resolveCommandArgs(command);
120
- await this.execute(command, {
148
+ if (await this.execute(command, {
121
149
  options: resolved.options,
122
150
  args: resolved.args
123
- });
151
+ }) === "succeeded") exitAfterFlush(0);
124
152
  }
125
153
  /**
126
154
  * Re-parse argv against the resolved command's declared options.
@@ -256,7 +284,10 @@ var CLICommandsManager = class {
256
284
  return result;
257
285
  }
258
286
  /**
259
- * Execute the given command
287
+ * Execute the given command and report how it ended.
288
+ *
289
+ * Returning the verdict — rather than exiting on success from in here — is
290
+ * what makes the awaited result trustworthy: see {@link CommandRunVerdict}.
260
291
  */
261
292
  async execute(command, data) {
262
293
  const startTime = Date.now();
@@ -274,24 +305,40 @@ var CLICommandsManager = class {
274
305
  displayBootError(command.name, error);
275
306
  process.exit(1);
276
307
  }
308
+ this.captureFatalAsyncErrors(command);
277
309
  try {
278
310
  await command.execute(data);
279
- if (!command.isPersistent) {
280
- displayCommandSuccess(command.name, Date.now() - startTime);
281
- process.exit(0);
282
- }
311
+ if (command.isPersistent) return "running";
312
+ await new Promise((resolve) => setImmediate(resolve));
313
+ if (this.fatalAsyncErrorReported) return "failed";
314
+ displayCommandSuccess(command.name, Date.now() - startTime);
315
+ return "succeeded";
283
316
  } catch (error) {
284
317
  displayCommandError(command.name, error);
285
318
  if (!command.isPersistent) process.exit(1);
319
+ return "failed";
286
320
  }
287
321
  }
322
+ captureFatalAsyncErrors(command) {
323
+ const onFatalAsyncError = (reason) => {
324
+ const error = reason instanceof Error ? reason : new Error(String(reason));
325
+ this.fatalAsyncErrorReported = true;
326
+ displayCommandError(command.name, error);
327
+ if (!command.isPersistent) exitAfterFlush(1);
328
+ };
329
+ process.on("unhandledRejection", onFatalAsyncError);
330
+ process.on("uncaughtException", onFatalAsyncError);
331
+ }
288
332
  /**
289
333
  * Load preloaders
290
334
  */
291
335
  async loadPreloaders(command) {
292
336
  const preloaders = command.commandPreload || {};
293
337
  if (preloaders.runtimeStrategy) Application.setRuntimeStrategy(preloaders.runtimeStrategy);
294
- if (preloaders.environemnt) Application.setEnvironment(preloaders.environemnt);
338
+ if (preloaders.environment) {
339
+ Application.setEnvironment(preloaders.environment);
340
+ if (process.env.NODE_ENV !== preloaders.environment) throw new Error(`Command "${command.name}" set NODE_ENV to "${preloaders.environment}", but it read back as "${process.env.NODE_ENV}". The environment did not take, and every decision made downstream of it — env file selection, bundler dep optimisation — would be wrong.`);
341
+ }
295
342
  await loadEnvironmentFiles();
296
343
  await warlockConfigManager.load();
297
344
  if (preloaders.config || preloaders.bootstrap || preloaders.prestart) await filesOrchestrator.init();
@@ -303,8 +350,11 @@ var CLICommandsManager = class {
303
350
  if (preloaders.prestart) {
304
351
  if (await fileExistsAsync(appPath("prestart.ts"))) await filesOrchestrator.load("src/app/prestart.ts");
305
352
  }
306
- if (preloaders.connectors) if (preloaders.connectors === true) await connectorsManager.startPhase("early");
307
- else await connectorsManager.start(preloaders.connectors);
353
+ if (preloaders.connectors) {
354
+ registerConfiguredConnectors();
355
+ if (preloaders.connectors === true) await connectorsManager.startPhase("early");
356
+ else await connectorsManager.start(preloaders.connectors);
357
+ }
308
358
  }
309
359
  };
310
360