create-zerotal 1.6.1 → 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 +21 -0
- package/package.json +1 -1
- package/src/index.ts +23 -0
- package/src/scaffold.ts +1 -1
- package/src/staleness.ts +77 -0
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
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.
|
|
16
|
+
export const ZT_VERSION = "^1.6.3";
|
|
17
17
|
|
|
18
18
|
export interface ScaffoldOptions {
|
|
19
19
|
name: string;
|
package/src/staleness.ts
ADDED
|
@@ -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
|
+
}
|