@zerotal/core 1.6.2 → 1.6.3

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,31 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.6.3] — 2026-08-15
12
+
13
+ ### Added
14
+
15
+ - **`serve --dev` says when the framework on disk is no longer the framework running.** A
16
+ running dev server holds the code it imported at boot: `bun add zerotal@latest` in another
17
+ terminal rewrites `node_modules` and nothing else, and a save restarts only the _worker_,
18
+ which re-executes your app against that same in-memory framework. So an upgrade taken
19
+ mid-session appears to do nothing — the fix is installed, the symptom persists, and the
20
+ reasonable conclusion is that the fix does not work.
21
+
22
+ The supervisor now compares the version it booted with against the one installed, on each
23
+ restart, and says which is which:
24
+
25
+ ```text
26
+ [zerotal:dev] ⚠ framework upgraded on disk: running v1.6.2, installed v1.6.3
27
+ [zerotal:dev] restart the dev server to pick it up (a save will not).
28
+ ```
29
+
30
+ Once per version, so a long session is not nagged, and silent where there is nothing to read
31
+ — a workspace checkout or a hoisted layout is not a finding.
32
+
33
+ - **The dev banner carries the version** — `Zerotal v1.6.3 › dev`. There was previously
34
+ nothing on screen naming the framework a running server was actually executing.
35
+
11
36
  ## [1.6.2] — 2026-08-15
12
37
 
13
38
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -24,6 +24,7 @@ import {
24
24
  type PortOwner,
25
25
  } from "../../support/port.ts";
26
26
  import { localNetworkAddress } from "../../support/network.ts";
27
+ import { ZEROTAL_VERSION } from "../../support/version.ts";
27
28
  import { createInterface } from "node:readline";
28
29
 
29
30
  /**
@@ -319,7 +320,11 @@ export class ServeCommand extends Command {
319
320
  const network = localNetworkAddress();
320
321
 
321
322
  this.newLine();
322
- this.line(` Zerotal › ${mode}`);
323
+ // The version is here because a running server holds the framework it booted
324
+ // with: an upgrade installed in another terminal is invisible until a full
325
+ // restart, and without this line there is nothing on screen to tell you which
326
+ // one you are looking at.
327
+ this.line(` Zerotal v${ZEROTAL_VERSION} › ${mode}`);
323
328
  this.newLine();
324
329
  this._route("Local", `http://localhost:${port}`);
325
330
  this._route(
@@ -6,6 +6,7 @@ import { watch } from "node:fs";
6
6
  import type { BuildHookFn } from "./DevBuildHook.ts";
7
7
  import { DEV_WORKER_ENV_VAR } from "../support/env.ts";
8
8
  import { requestGracefulStop } from "./devShutdown.ts";
9
+ import { ZEROTAL_VERSION, installedCoreVersion } from "../support/version.ts";
9
10
 
10
11
  /**
11
12
  * How the orchestrator reports to whatever is presenting dev mode.
@@ -81,6 +82,8 @@ export class DevOrchestrator {
81
82
  * queues a single follow-up instead of running in parallel.
82
83
  */
83
84
  private _restartInFlight: Promise<void> | null = null;
85
+ /** The installed version already warned about, so one upgrade warns once. */
86
+ private _warnedVersion: string | null = null;
84
87
  private _restartQueued = false;
85
88
 
86
89
  /**
@@ -282,11 +285,37 @@ export class DevOrchestrator {
282
285
  // browser refetches the updated CSS.
283
286
  await this._runBuild();
284
287
 
288
+ this._warnIfFrameworkUpgradedUnderneath();
289
+
285
290
  // Stop before spawning, always: the old server owns the port until it exits.
286
291
  await this._stopChild();
287
292
  await this._spawnServerWithRetry();
288
293
  }
289
294
 
295
+ /**
296
+ * Say so when the framework on disk is no longer the framework running.
297
+ *
298
+ * A `bun add zerotal@latest` in another terminal changes `node_modules` and
299
+ * nothing else: this process holds the code it imported at boot, and a restart
300
+ * only re-executes the *app* against that same in-memory framework. So the
301
+ * upgrade appears to have no effect — the fix is installed, the symptom
302
+ * persists, and the obvious conclusion is that the fix does not work.
303
+ *
304
+ * Checked on restart rather than at boot, because that is when it can have
305
+ * become true, and warned once per version so a long session is not nagged.
306
+ */
307
+ private _warnIfFrameworkUpgradedUnderneath(): void {
308
+ const onDisk = installedCoreVersion(this._cwd);
309
+ if (onDisk === null || onDisk === ZEROTAL_VERSION || onDisk === this._warnedVersion) return;
310
+
311
+ this._warnedVersion = onDisk;
312
+ this._say(
313
+ ` [zerotal:dev] ⚠ framework upgraded on disk: running v${ZEROTAL_VERSION}, installed v${onDisk}`,
314
+ "warn",
315
+ );
316
+ this._say(" [zerotal:dev] restart the dev server to pick it up (a save will not).", "warn");
317
+ }
318
+
290
319
  /** Stop the running server and wait for it to actually exit, releasing the port. */
291
320
  private async _stopChild(): Promise<void> {
292
321
  const child = this._child;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The framework version this build of `@zerotal/core` is.
3
+ *
4
+ * Read from the manifest rather than written down, for the reason `@zerotal/monitor`
5
+ * already learned: a hardcoded version is correct until the next release, and three
6
+ * separate literals across the monorepo once claimed a version that had never been
7
+ * published. The monorepo releases in lockstep, so this package's manifest carries
8
+ * the framework version, and it ships inside the tarball.
9
+ *
10
+ * @module
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+
14
+ const pkg: { version: string } = JSON.parse(
15
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
16
+ ) as { version: string };
17
+
18
+ /** Full version of the running framework, e.g. `1.6.2`. */
19
+ export const ZEROTAL_VERSION: string = pkg.version;
20
+
21
+ /**
22
+ * The version of `@zerotal/core` currently installed in a project, which is not
23
+ * necessarily {@link ZEROTAL_VERSION} — a long-running process holds the code it
24
+ * booted with, so an upgrade lands on disk without reaching it.
25
+ *
26
+ * @param cwd - Project root to look under.
27
+ * @returns The installed version, or `null` when there is nothing to read — a
28
+ * workspace checkout, a hoisted layout, a partial install. Absence is not a
29
+ * finding, so callers stay quiet on `null` rather than guessing.
30
+ */
31
+ export function installedCoreVersion(cwd: string): string | null {
32
+ try {
33
+ const raw = readFileSync(`${cwd}/node_modules/@zerotal/core/package.json`, "utf8");
34
+ const { version } = JSON.parse(raw) as { version?: string };
35
+ return version ?? null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }