@zerotal/core 1.14.0 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/api-surface.md +2 -1
- package/package.json +1 -1
- package/src/application/Application.ts +34 -17
- package/src/command/CommandRunner.ts +50 -2
- package/src/command/startZerotal.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.14.1] — 2026-09-01
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A command that declares `needsApp = false` no longer boots the application.**
|
|
16
|
+
`CommandRunner.boot()` booted unconditionally, so `zt version`, `key:generate`, every
|
|
17
|
+
`make:*` scaffolder and the gate commands all paid for providers, a database connection
|
|
18
|
+
and a schedule registry they never touch — and could not run at all when booting was the
|
|
19
|
+
thing that was broken. A CLI you cannot reach when the app is down is missing exactly
|
|
20
|
+
when it is wanted.
|
|
21
|
+
|
|
22
|
+
Config is still bound, via the new `Application.bindConfig()`: `needsApp = false` is a
|
|
23
|
+
claim about the _application_, not about configuration, and every scaffolder reads config
|
|
24
|
+
for its output paths.
|
|
25
|
+
|
|
26
|
+
This also makes `zt version --json` pipeable, which previously needed the `--version`
|
|
27
|
+
form to avoid the boot log on stdout.
|
|
28
|
+
|
|
11
29
|
## [1.13.4] — 2026-09-01
|
|
12
30
|
|
|
13
31
|
### Fixed
|
package/api-surface.md
CHANGED
|
@@ -23,6 +23,7 @@ class Application = {
|
|
|
23
23
|
_userResolver: ((id: number) => Promise<AuthenticatedUser | null>) | undefined
|
|
24
24
|
adoptAsCurrent: () => Application
|
|
25
25
|
bind: (callback: (container: Container) => void) => Application
|
|
26
|
+
bindConfig: () => Promise<void>
|
|
26
27
|
boot: () => Promise<void>
|
|
27
28
|
bootAsWorker: () => Promise<void>
|
|
28
29
|
bootDurationMs: number | undefined
|
|
@@ -179,7 +180,7 @@ class CommandRan = {
|
|
|
179
180
|
|
|
180
181
|
class CommandRunner = {
|
|
181
182
|
new (_app: Application): CommandRunner
|
|
182
|
-
boot: () => Promise<void>
|
|
183
|
+
boot: (commandName?: string) => Promise<void>
|
|
183
184
|
callInProcess: (argv: string[], parameters?: Record<string, string | boolean | number>) => Promise<{ code: number; output: string;}>
|
|
184
185
|
command: (definition: CommandDefinition, aliases?: string[]) => CommandClass
|
|
185
186
|
discover: (dir: string) => Promise<string[]>
|
package/package.json
CHANGED
|
@@ -1157,23 +1157,7 @@ export class Application {
|
|
|
1157
1157
|
this.container._app = this;
|
|
1158
1158
|
|
|
1159
1159
|
// Auto-discover config when useConfig() was not called explicitly.
|
|
1160
|
-
|
|
1161
|
-
// Gracefully skips missing or unreadable files.
|
|
1162
|
-
if (this._configMap === undefined) {
|
|
1163
|
-
this._configMap = await _discoverConfig(process.cwd());
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// Built-in core singletons (overridable by providers via last-write-wins)
|
|
1167
|
-
this.container.singleton("config", () => new ConfigManager());
|
|
1168
|
-
this.container.singleton("events", () => new Emitter(this.container));
|
|
1169
|
-
|
|
1170
|
-
if (this._configMap !== undefined) {
|
|
1171
|
-
const configManager = new ConfigManager();
|
|
1172
|
-
for (const [key, value] of Object.entries(this._configMap)) {
|
|
1173
|
-
configManager.load(key, value);
|
|
1174
|
-
}
|
|
1175
|
-
this.container.value("config", configManager);
|
|
1176
|
-
}
|
|
1160
|
+
await this.bindConfig();
|
|
1177
1161
|
|
|
1178
1162
|
// Auto-discover app/providers/* BEFORE the register phase so they run the full lifecycle.
|
|
1179
1163
|
await this._discoverProviders(process.cwd());
|
|
@@ -1727,6 +1711,39 @@ export class Application {
|
|
|
1727
1711
|
process.on("SIGUSR2", () => void this._reloadRoutes());
|
|
1728
1712
|
}
|
|
1729
1713
|
|
|
1714
|
+
/**
|
|
1715
|
+
* Make `config` and `events` resolvable without booting the application.
|
|
1716
|
+
*
|
|
1717
|
+
* Split out of {@link boot} so a command that declares `needsApp = false` can
|
|
1718
|
+
* read configuration without paying for providers, a database connection and a
|
|
1719
|
+
* schedule registry it does not use. Every `make:*` scaffolder reads config for
|
|
1720
|
+
* its output paths, so "does not need the app" and "does not need config" are
|
|
1721
|
+
* different statements and only the first one is true of them.
|
|
1722
|
+
*
|
|
1723
|
+
* Idempotent: `boot()` calls it too, and calling it twice binds the same values.
|
|
1724
|
+
*
|
|
1725
|
+
* @category Configuration
|
|
1726
|
+
*/
|
|
1727
|
+
async bindConfig(): Promise<void> {
|
|
1728
|
+
// Scans <cwd>/config/*.ts and loads each file's default export.
|
|
1729
|
+
// Gracefully skips missing or unreadable files.
|
|
1730
|
+
if (this._configMap === undefined) {
|
|
1731
|
+
this._configMap = await _discoverConfig(process.cwd());
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Built-in core singletons (overridable by providers via last-write-wins)
|
|
1735
|
+
this.container.singleton("config", () => new ConfigManager());
|
|
1736
|
+
this.container.singleton("events", () => new Emitter(this.container));
|
|
1737
|
+
|
|
1738
|
+
if (this._configMap !== undefined) {
|
|
1739
|
+
const configManager = new ConfigManager();
|
|
1740
|
+
for (const [key, value] of Object.entries(this._configMap)) {
|
|
1741
|
+
configManager.load(key, value);
|
|
1742
|
+
}
|
|
1743
|
+
this.container.value("config", configManager);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1730
1747
|
/**
|
|
1731
1748
|
* Boot the application in the `worker` environment and block for queue/worker use.
|
|
1732
1749
|
*
|
|
@@ -330,9 +330,16 @@ export class CommandRunner {
|
|
|
330
330
|
* is dynamically imported. Application.create() reads it, so _env is already
|
|
331
331
|
* correct by the time boot() runs — no argv inspection needed here.
|
|
332
332
|
*/
|
|
333
|
-
async boot(): Promise<void> {
|
|
333
|
+
async boot(commandName?: string): Promise<void> {
|
|
334
334
|
this._app.container.value("commands", this);
|
|
335
|
-
|
|
335
|
+
|
|
336
|
+
// Config first, and separately from the application.
|
|
337
|
+
//
|
|
338
|
+
// `needsApp = false` is a claim about the *application* — providers, the
|
|
339
|
+
// database, the schedule registry — not about configuration. Every `make:*`
|
|
340
|
+
// scaffolder reads config for its output paths, so binding it here is what
|
|
341
|
+
// makes skipping the boot below safe rather than merely fast.
|
|
342
|
+
await this._app.bindConfig();
|
|
336
343
|
|
|
337
344
|
const {
|
|
338
345
|
ServeCommand,
|
|
@@ -409,6 +416,17 @@ export class CommandRunner {
|
|
|
409
416
|
// The `routes:types` alias was retired in 1.13.0; `zt upgrade` rewrites it.
|
|
410
417
|
this.register(RouteTypesCommand);
|
|
411
418
|
|
|
419
|
+
// Boot the application only when the command actually needs one.
|
|
420
|
+
//
|
|
421
|
+
// This used to be unconditional, so `zt version`, `zt key:generate`, every
|
|
422
|
+
// `make:*` scaffolder and the gate commands all paid for providers, a database
|
|
423
|
+
// connection and a schedule registry they never touch — and, worse, could not
|
|
424
|
+
// run at all when booting was the thing that was broken. A CLI you cannot reach
|
|
425
|
+
// when the app is down is missing exactly when it is wanted.
|
|
426
|
+
//
|
|
427
|
+
// Resolved against the built-ins registered above. A name that is not among
|
|
428
|
+
// them may still be an app command from `app/commands/`, which is discovered
|
|
429
|
+
// below and needs the boot — so an unknown name boots rather than assuming.
|
|
412
430
|
// Non-web commands (console, worker, test).
|
|
413
431
|
if (this._app._env !== "web") {
|
|
414
432
|
this.register(ReplCommand);
|
|
@@ -439,7 +457,20 @@ export class CommandRunner {
|
|
|
439
457
|
LintPackagesCommand,
|
|
440
458
|
MakePackageCommand,
|
|
441
459
|
]);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Everything above is a pure `register()` call, so it happens before the
|
|
463
|
+
// decision — which is what lets a scaffolder skip the boot too. Deciding
|
|
464
|
+
// earlier would leave `make:*` unregistered and unrecognised, and an
|
|
465
|
+
// unrecognised name boots.
|
|
466
|
+
const requested = commandName ? this._resolveStatic(commandName) : undefined;
|
|
467
|
+
const skipBoot = requested !== undefined && requested.needsApp === false;
|
|
468
|
+
|
|
469
|
+
if (skipBoot) return;
|
|
470
|
+
|
|
471
|
+
await this._app.boot();
|
|
442
472
|
|
|
473
|
+
if (this._app._env !== "web") {
|
|
443
474
|
// One `deploy:<target>` per environment this app releases to. Console-only,
|
|
444
475
|
// like the other release commands — a running web server has no business
|
|
445
476
|
// migrating a database.
|
|
@@ -497,6 +528,23 @@ export class CommandRunner {
|
|
|
497
528
|
// ── Private resolution ────────────────────────────────────────────────────
|
|
498
529
|
|
|
499
530
|
/** Resolve a command name to its class — handles both eager and lazy entries. */
|
|
531
|
+
/**
|
|
532
|
+
* Look a command up without resolving a lazy thunk.
|
|
533
|
+
*
|
|
534
|
+
* Used to decide whether to boot the application, which has to happen *before*
|
|
535
|
+
* anything is awaited that might need one. A thunk is `undefined` here rather
|
|
536
|
+
* than imported, so a lazily-registered command boots — the safe answer, since
|
|
537
|
+
* the only cost of booting unnecessarily is the cost this change avoids, while
|
|
538
|
+
* the cost of not booting when needed is a broken command.
|
|
539
|
+
*
|
|
540
|
+
* @internal
|
|
541
|
+
*/
|
|
542
|
+
private _resolveStatic(name: string): CommandClass | undefined {
|
|
543
|
+
const entry = this._registry.get(name);
|
|
544
|
+
if (typeof entry === "function" && "commandName" in entry) return entry as CommandClass;
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
500
548
|
private async _resolve(name: string): Promise<CommandClass | undefined> {
|
|
501
549
|
const entry = this._registry.get(name);
|
|
502
550
|
if (!entry) return undefined;
|