@warlock.js/core 4.16.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.
- package/CHANGELOG.md +13 -0
- package/bin/warlock.js +154 -1
- package/esm/application/application.d.mts +65 -1
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +62 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -1
- package/esm/cli/cli-commands.manager.mjs +60 -10
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -0
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +40 -0
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/index.mjs +4 -2
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/types.d.mts +10 -2
- package/esm/commands/types.d.mts.map +1 -1
- package/esm/config/config-handlers.d.mts +1 -2
- package/esm/config/config-handlers.d.mts.map +1 -1
- package/esm/config/config-handlers.mjs +3 -2
- package/esm/config/config-handlers.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +29 -0
- package/esm/config/locale-configuration.mjs.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts +7 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs +44 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -0
- package/esm/connectors/assert-unique-connector-names.d.mts +7 -0
- package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-unique-connector-names.mjs +20 -0
- package/esm/connectors/assert-unique-connector-names.mjs.map +1 -0
- package/esm/connectors/connectors-manager.d.mts +38 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +45 -12
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +75 -0
- package/esm/connectors/describe-server-address.mjs.map +1 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +6 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/index.d.mts +4 -1
- package/esm/connectors/index.mjs +4 -1
- package/esm/connectors/register-configured-connectors.d.mts +14 -0
- package/esm/connectors/register-configured-connectors.d.mts.map +1 -0
- package/esm/connectors/register-configured-connectors.mjs +99 -0
- package/esm/connectors/register-configured-connectors.mjs.map +1 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +4 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +76 -1
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +50 -2
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/utils.d.mts +2 -2
- package/esm/database/utils.d.mts.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +2 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/read-config-ast.mjs +48 -0
- package/esm/dev-server/read-config-ast.mjs.map +1 -0
- package/esm/dev-server/run-typings-generation.mjs +18 -0
- package/esm/dev-server/run-typings-generation.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +30 -39
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +169 -1
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +93 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -3
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +11 -1
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/context/request-memo.d.mts +21 -0
- package/esm/http/context/request-memo.d.mts.map +1 -0
- package/esm/http/context/request-memo.mjs +48 -0
- package/esm/http/context/request-memo.mjs.map +1 -0
- package/esm/http/database/RequestLog.d.mts +164 -2
- package/esm/http/database/RequestLog.d.mts.map +1 -1
- package/esm/http/index.d.mts +6 -5
- package/esm/http/index.mjs +2 -1
- package/esm/http/middleware/cache-response-middleware.d.mts +2 -2
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/index.d.mts +1 -1
- package/esm/http/middleware/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -8
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +9 -22
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts +0 -14
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +19 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request.d.mts +119 -30
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +98 -25
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.mjs +1 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +36 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploaded-file.d.mts +51 -19
- package/esm/http/uploaded-file.d.mts.map +1 -1
- package/esm/http/uploaded-file.mjs +36 -8
- package/esm/http/uploaded-file.mjs.map +1 -1
- package/esm/index.d.mts +17 -9
- package/esm/index.mjs +10 -4
- package/esm/production/build-contributions.mjs +115 -0
- package/esm/production/build-contributions.mjs.map +1 -0
- package/esm/production/production-builder.mjs +97 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +25 -0
- package/esm/production/resolve-build-config.d.mts.map +1 -0
- package/esm/production/resolve-build-config.mjs +43 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +20 -9
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/restful/restful.d.mts +29 -8
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +31 -13
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/describe-route-for-log.mjs +29 -0
- package/esm/router/describe-route-for-log.mjs.map +1 -0
- package/esm/router/index.d.mts +3 -1
- package/esm/router/index.mjs +2 -0
- package/esm/router/log-request-lifecycle.mjs +38 -0
- package/esm/router/log-request-lifecycle.mjs.map +1 -0
- package/esm/router/positional-handler-diagnostics.d.mts +90 -0
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -0
- package/esm/router/positional-handler-diagnostics.mjs +247 -0
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -0
- package/esm/router/route-registry.d.mts +34 -0
- package/esm/router/route-registry.d.mts.map +1 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +87 -63
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +23 -5
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +1 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts +11 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +51 -4
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/vitest-setup.mjs +1 -1
- package/esm/utils/load-environment.mjs +33 -2
- package/esm/utils/load-environment.mjs.map +1 -1
- package/esm/utils/types.d.mts +4 -0
- package/esm/utils/types.d.mts.map +1 -1
- package/esm/validation/validateAll.mjs +4 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/validation/validators/file-validator.d.mts +25 -1
- package/esm/validation/validators/file-validator.d.mts.map +1 -1
- package/esm/warlock-config/index.mjs +1 -1
- package/esm/warlock-config/normalize-build-config.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +36 -2
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/package.json +15 -12
- package/skills/request-memo/SKILL.md +142 -0
- package/skills/use-request-locals/SKILL.md +144 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ 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
|
+
## 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
|
+
|
|
9
22
|
## 4.16.0 - 2026-08-18
|
|
10
23
|
|
|
11
24
|
### Security
|
package/bin/warlock.js
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import "
|
|
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;
|
|
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 (
|
|
280
|
-
|
|
281
|
-
|
|
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.
|
|
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)
|
|
307
|
-
|
|
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
|
|