create-zerotal 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,27 @@ 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
+ - **The scaffolder says when it is not the published one.** `bun create zerotal` can serve a
16
+ copy cached from a previous run instead of fetching the current scaffolder, and a stale
17
+ scaffolder is worse than an old one: it stamps the dependency ranges *it* shipped with, so
18
+ the new project is created against versions that were current months ago — while the install
19
+ log shows today's framework resolving happily inside those ranges. Everything reads as
20
+ correct and the wrong packages are installed.
21
+
22
+ Observed with a cached 1.5.0, which pinned an Inertia major whose client-side DevTools hooks
23
+ do not exist, on a machine whose registry had 1.6.2. Nothing said so, because nothing was
24
+ looking.
25
+
26
+ The check now runs against the registry, and names the fix:
27
+ `bunx create-zerotal@latest <name>`. It costs no perceived time — the request goes out before
28
+ the banner and is read after the prompts — and it is advisory: an offline machine, a
29
+ firewalled registry and a slow network all mean "no answer", and no answer never stops
30
+ anyone creating an app.
31
+
11
32
  ## [1.6.1] — 2026-08-15
12
33
 
13
34
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zerotal",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Create a new Zerotal application",
5
5
  "license": "MIT",
6
6
  "maturity": "stable",
package/src/index.ts CHANGED
@@ -10,8 +10,18 @@
10
10
  import { resolve } from 'node:path';
11
11
  import { printBanner, ask, choose, log, info, warn, step, dim, c } from './prompts.ts';
12
12
  import { scaffold, install, type Template, type Database } from './scaffold.ts';
13
+ import { newerScaffolderVersion } from './staleness.ts';
14
+
15
+ /** This scaffolder's own version, read from the manifest that ships beside it. */
16
+ const ZT_SELF_VERSION: string = (
17
+ (await Bun.file(new URL('../package.json', import.meta.url)).json()) as { version: string }
18
+ ).version;
13
19
 
14
20
  async function main(): Promise<void> {
21
+ // Started before the banner and awaited after the prompts, so the check costs
22
+ // no perceived time at all — the answer arrives while a human is reading.
23
+ const newerScaffolder = newerScaffolderVersion(ZT_SELF_VERSION);
24
+
15
25
  printBanner();
16
26
 
17
27
  // ── Project name ────────────────────────────────────────────────────────────
@@ -89,6 +99,19 @@ async function main(): Promise<void> {
89
99
 
90
100
  // ── Scaffold ────────────────────────────────────────────────────────────────
91
101
  log('');
102
+
103
+ // Answered by now: the request went out before the first prompt, and the
104
+ // prompts take human time. Said before anything is written, so re-running with
105
+ // the current scaffolder costs nothing but a Ctrl-C.
106
+ const newer = await newerScaffolder;
107
+ if (newer) {
108
+ warn(`This scaffolder is ${ZT_SELF_VERSION}; ${newer} is published.`);
109
+ dim(`A cached copy stamps the dependency ranges it shipped with, so a new`);
110
+ dim(`project can be created against versions that are no longer current.`);
111
+ dim(`${c.bold}bunx create-zerotal@latest ${name}${c.reset}${c.gray} always fetches the published one.`);
112
+ log('');
113
+ }
114
+
92
115
  step(`Scaffolding ${c.bold}${name}${c.reset} (${template})…`);
93
116
 
94
117
  await scaffold({ name, template, db, target });
package/src/scaffold.ts CHANGED
@@ -13,7 +13,7 @@ export type Template = 'minimal' | 'api' | 'admin' | 'flow' | 'react' | 'vue';
13
13
  // "^1.1.0" found for specifier "zerotal"` — the first thing anyone trying the
14
14
  // framework saw. `scaffold.test.ts` now asserts the two agree, so CI fails rather
15
15
  // than the user's install.
16
- export const ZT_VERSION = "^1.6.2";
16
+ export const ZT_VERSION = "^1.6.3";
17
17
 
18
18
  export interface ScaffoldOptions {
19
19
  name: string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Noticing that this scaffolder is not the current one.
3
+ *
4
+ * `bun create zerotal` can serve a copy cached from a previous run rather than
5
+ * fetching the published scaffolder, and a stale scaffolder is worse than an old
6
+ * one: it stamps the dependency ranges *it* was built with, so a new project is
7
+ * created against versions that were current months ago while the install log
8
+ * shows today's framework resolving inside those ranges. Everything looks right
9
+ * and the wrong adapter is installed.
10
+ *
11
+ * That happened with a cached 1.5.0, which pinned an Inertia major the DevTools
12
+ * extension cannot read. Nothing said so, because nothing was looking.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ /** Where the published version is read from. */
18
+ export const REGISTRY_LATEST_URL = "https://registry.npmjs.org/create-zerotal/latest";
19
+
20
+ /** How long to wait before scaffolding without an answer. */
21
+ export const REGISTRY_TIMEOUT_MS = 2_000;
22
+
23
+ /**
24
+ * Compare two dot-separated versions numerically.
25
+ *
26
+ * Pre-release suffixes are ignored — `1.6.3-rc.1` compares as `1.6.3` — because
27
+ * the only decision this drives is whether to suggest re-running, and suggesting
28
+ * it once too often is cheaper than reimplementing SemVer here.
29
+ *
30
+ * @returns Negative when `a` is older, positive when newer, `0` when equal.
31
+ */
32
+ export function compareVersions(a: string, b: string): number {
33
+ const parts = (v: string): number[] =>
34
+ v
35
+ .split("-")[0]!
36
+ .split(".")
37
+ .map((n) => Number.parseInt(n, 10) || 0);
38
+ const left = parts(a);
39
+ const right = parts(b);
40
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
41
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
42
+ if (diff !== 0) return diff;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ /**
48
+ * The published version, when it is newer than the one running.
49
+ *
50
+ * @param current - This scaffolder's own version.
51
+ * @param options - Seams for tests, and for pointing at a private registry.
52
+ * @returns The newer version, or `null` — including whenever the check cannot be
53
+ * made. An offline machine, a firewalled registry and a slow network all mean
54
+ * "no answer", and no answer must never stop someone creating an app.
55
+ *
56
+ * @example
57
+ * const newer = await newerScaffolderVersion(ZT_VERSION);
58
+ * if (newer) warn(`A newer scaffolder exists (${newer}).`);
59
+ */
60
+ export async function newerScaffolderVersion(
61
+ current: string,
62
+ options: { fetchImpl?: typeof fetch; timeoutMs?: number; url?: string } = {},
63
+ ): Promise<string | null> {
64
+ const doFetch = options.fetchImpl ?? fetch;
65
+ const url = options.url ?? REGISTRY_LATEST_URL;
66
+ const timeoutMs = options.timeoutMs ?? REGISTRY_TIMEOUT_MS;
67
+
68
+ try {
69
+ const response = await doFetch(url, { signal: AbortSignal.timeout(timeoutMs) });
70
+ if (!response.ok) return null;
71
+ const { version } = (await response.json()) as { version?: unknown };
72
+ if (typeof version !== "string" || version === "") return null;
73
+ return compareVersions(version, current) > 0 ? version : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }