@zerotal/core 1.5.0 → 1.5.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 CHANGED
@@ -8,6 +8,25 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.5.1] — 2026-08-15
12
+
13
+ ### Fixed
14
+
15
+ - **Every development-only surface switched itself off under `zt serve`.**
16
+ `devSurfacesEnabled()` asked `Bun.env["APP_ENV"]` whether this was a development
17
+ environment — but `setAppEnv()` replaces that with the runtime mode before the app is
18
+ created, so it was asking whether `"web"` is development. The answer is no. An app with
19
+ `APP_ENV=development` in its `.env` therefore got **production error pages** from a plain
20
+ `bun zt serve`: no stack trace, no dev overlay.
21
+
22
+ It reads `deployEnv()` now, which is where the deployment name survives. Production and
23
+ staging are unaffected — both still fail closed, and an unset value still fails closed.
24
+
25
+ This is the third instance of one mistake. The weak-`APP_KEY` refusal and the ORM's N+1
26
+ detector had exactly the same bug, both fixed in 1.5.0. Reading `APP_ENV` to decide
27
+ anything about the _deployment_ is wrong once the app has booted; `deployEnv()` is the
28
+ answer.
29
+
11
30
  ## [1.5.0] — 2026-08-15
12
31
 
13
32
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { join } from "node:path";
24
24
  import { ConfigError } from "../errors/ConfigError.ts";
25
- import { DEPLOY_ENV_VAR } from "../support/env.ts";
25
+ import { DEPLOY_ENV_VAR, RUNTIME_MODES as _RUNTIME_MODES } from "../support/env.ts";
26
26
 
27
27
  // ── basePath() ────────────────────────────────────────────────────────────────
28
28
 
@@ -65,9 +65,6 @@ export function basePath(...segments: string[]): string {
65
65
  * setAppEnv(process.argv[2]);
66
66
  * const { default: app } = await import('./bootstrap/app.ts');
67
67
  */
68
- /** Runtime-mode values that Application understands natively — never remapped. */
69
- const _RUNTIME_MODES = new Set(["web", "worker", "console", "test", "testing", "repl"]);
70
-
71
68
  export function setAppEnv(command?: string): void {
72
69
  const normalizedCommand = (command ?? "").toLowerCase();
73
70
  const current = Bun.env["APP_ENV"];
@@ -79,10 +76,11 @@ export function setAppEnv(command?: string): void {
79
76
  // quietly answering no — including the weak-`APP_KEY` refusal and the ORM's
80
77
  // N+1 detector. `deployEnv()` reads this back; see {@link DEPLOY_ENV_VAR}.
81
78
  //
82
- // `??=` so the first caller wins: a re-entrant `setAppEnv` (dev mode boots the
83
- // app twice) must not stamp the runtime mode over the real deployment name.
79
+ // The guard is what protects a re-entrant call: `current` is only ever written
80
+ // when it is a genuine deployment name, so a second `setAppEnv` which sees the
81
+ // runtime mode this one just wrote — cannot stamp `"web"` over `"production"`.
84
82
  if (current && !_RUNTIME_MODES.has(current.toLowerCase())) {
85
- environment[DEPLOY_ENV_VAR] ??= current;
83
+ environment[DEPLOY_ENV_VAR] = current;
86
84
  }
87
85
 
88
86
  if (["serve", "start", "s", "dev", "d"].includes(normalizedCommand)) {
@@ -53,6 +53,22 @@ export const DEV_WORKER_ENV_VAR = "ZT_DEV";
53
53
  */
54
54
  export const DEPLOY_ENV_VAR = "ZT_APP_ENV";
55
55
 
56
+ /**
57
+ * The values of `APP_ENV` that name a runtime *mode* rather than a deployment.
58
+ * `setAppEnv()` writes these; {@link deployEnv} recognises them to know whether
59
+ * `APP_ENV` still holds the deployment name.
60
+ *
61
+ * @internal
62
+ */
63
+ export const RUNTIME_MODES: ReadonlySet<string> = new Set([
64
+ "web",
65
+ "worker",
66
+ "console",
67
+ "test",
68
+ "testing",
69
+ "repl",
70
+ ]);
71
+
56
72
  /**
57
73
  * The deployment name this process was started with — `production`, `staging`,
58
74
  * `local`, whatever the operator set — as opposed to the runtime *mode*.
@@ -72,7 +88,16 @@ export const DEPLOY_ENV_VAR = "ZT_APP_ENV";
72
88
  * @internal
73
89
  */
74
90
  export function deployEnv(): string {
75
- return Bun.env[DEPLOY_ENV_VAR] ?? Bun.env["APP_ENV"] ?? "";
91
+ const current = Bun.env["APP_ENV"] ?? "";
92
+ // If `APP_ENV` still holds a deployment name, it has not been overwritten yet —
93
+ // or something set it deliberately since — so it is the freshest answer. Only
94
+ // once it holds a runtime mode is the preserved copy the better one.
95
+ //
96
+ // Preferring the preserved copy unconditionally made it sticky for the life of
97
+ // the process: anything that set `APP_ENV` afterwards was ignored, which is
98
+ // wrong in itself and which leaked between test files sharing one process.
99
+ if (current && !RUNTIME_MODES.has(current.toLowerCase())) return current;
100
+ return Bun.env[DEPLOY_ENV_VAR] ?? current;
76
101
  }
77
102
 
78
103
  /**
@@ -101,7 +126,11 @@ export function deployEnv(): string {
101
126
  */
102
127
  export function devSurfacesEnabled(): boolean {
103
128
  if (Bun.env[DEV_WORKER_ENV_VAR] === "1") return true;
104
- return isDevSurfaceAllowed(Bun.env["APP_ENV"] ?? "");
129
+ // `deployEnv()`, not `Bun.env["APP_ENV"]` by the time anything asks, `setAppEnv()`
130
+ // has replaced that with the runtime mode, and `isDevSurfaceAllowed("web")` is false.
131
+ // An app with `APP_ENV=development` in its `.env` was therefore getting production
132
+ // error pages from a plain `zt serve`.
133
+ return isDevSurfaceAllowed(deployEnv());
105
134
  }
106
135
 
107
136
  /**