create-zfb 0.1.0-next.74 → 0.1.0-next.76

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.
@@ -3,88 +3,15 @@ import { spawnSync } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { constants as osConstants } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
+ import { checkForNewerVersion } from "./version-check.mjs";
6
7
 
7
8
  const require = createRequire(import.meta.url);
8
9
 
9
- // Returns -1 if a < b, 0 if a === b, 1 if a > b.
10
- // Handles prerelease suffixes: 0.1.0-next.6 < 0.1.0-next.8 < 0.1.0
11
- function compareSemver(a, b) {
12
- const splitRelease = (v) => {
13
- // Drop SemVer build metadata (`+...`) before splitting — it is ignored
14
- // for precedence and would otherwise yield NaN numeric parts.
15
- const [main, pre] = v.split("+")[0].split(/-(.+)/, 2);
16
- return { parts: main.split(".").map(Number), pre: pre ?? null };
17
- };
18
- const ra = splitRelease(a);
19
- const rb = splitRelease(b);
20
- for (let i = 0; i < Math.max(ra.parts.length, rb.parts.length); i++) {
21
- const pa = ra.parts[i] ?? 0;
22
- const pb = rb.parts[i] ?? 0;
23
- if (pa !== pb) return pa < pb ? -1 : 1;
24
- }
25
- // same numeric parts — prerelease is less than no prerelease (semver rule)
26
- if (ra.pre !== null && rb.pre === null) return -1;
27
- if (ra.pre === null && rb.pre !== null) return 1;
28
- if (ra.pre !== null && rb.pre !== null) {
29
- // Compare dot-separated prerelease identifiers per semver: numeric
30
- // identifiers compare numerically (so next.9 < next.10), numeric ranks
31
- // below alphanumeric, and a shorter prefix ranks lower. Plain string
32
- // `<`/`>` would order next.10 before next.9 lexicographically.
33
- const sa = ra.pre.split(".");
34
- const sb = rb.pre.split(".");
35
- for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
36
- if (sa[i] === undefined) return -1;
37
- if (sb[i] === undefined) return 1;
38
- const na = /^\d+$/.test(sa[i]);
39
- const nb = /^\d+$/.test(sb[i]);
40
- if (na && nb) {
41
- const da = Number(sa[i]);
42
- const db = Number(sb[i]);
43
- if (da !== db) return da < db ? -1 : 1;
44
- } else if (na !== nb) {
45
- return na ? -1 : 1;
46
- } else if (sa[i] !== sb[i]) {
47
- return sa[i] < sb[i] ? -1 : 1;
48
- }
49
- }
50
- }
51
- return 0;
52
- }
53
-
54
10
  try {
55
11
  const ownPkg = require("../package.json");
56
- const version = ownPkg.version;
57
- const controller = new AbortController();
58
- const timer = setTimeout(() => controller.abort(), 1500);
59
- let res;
60
- try {
61
- res = await fetch("https://registry.npmjs.org/create-zfb/latest", {
62
- signal: controller.signal,
63
- headers: {
64
- "User-Agent": `create-zfb/${version}`,
65
- Accept: "application/json",
66
- },
67
- });
68
- if (res.ok) {
69
- const data = await res.json();
70
- const latest = data?.version;
71
- if (typeof latest === "string" && typeof version === "string") {
72
- if (compareSemver(version, latest) < 0) {
73
- process.stderr.write(
74
- `[create-zfb] You are running create-zfb@${version} but @latest is ${latest}.\n` +
75
- `[create-zfb] pnpm 11's minimumReleaseAge may have downgraded the install.\n` +
76
- `[create-zfb] Re-run with: pnpm create zfb@latest --config.minimumReleaseAge=0\n` +
77
- `[create-zfb] Or use: npm create zfb@latest\n`,
78
- );
79
- }
80
- }
81
- }
82
- } finally {
83
- // Keep the abort signal armed across both the header and body reads.
84
- // Only cancel the timer once the body has been fully consumed (or on
85
- // any error path), so a proxy that stalls after headers cannot hang
86
- // the scaffolder indefinitely.
87
- clearTimeout(timer);
12
+ const staleWarning = await checkForNewerVersion({ version: ownPkg.version });
13
+ if (staleWarning) {
14
+ process.stderr.write(staleWarning);
88
15
  }
89
16
  } catch {
90
17
  // silently skip on any error: network failure, timeout (AbortError), parse error, etc.
@@ -0,0 +1,92 @@
1
+ // Extracted from create-zfb.mjs so compareSemver and checkForNewerVersion can
2
+ // be unit-tested directly. create-zfb.mjs has side-effecting top-level code
3
+ // (fetch, spawnSync), so importing it in a test would run the whole CLI —
4
+ // this module has none, and is safe to import.
5
+
6
+ // Returns -1 if a < b, 0 if a === b, 1 if a > b.
7
+ // Handles prerelease suffixes: 0.1.0-next.6 < 0.1.0-next.8 < 0.1.0
8
+ export function compareSemver(a, b) {
9
+ const splitRelease = (v) => {
10
+ // Drop SemVer build metadata (`+...`) before splitting — it is ignored
11
+ // for precedence and would otherwise yield NaN numeric parts.
12
+ const [main, pre] = v.split("+")[0].split(/-(.+)/, 2);
13
+ return { parts: main.split(".").map(Number), pre: pre ?? null };
14
+ };
15
+ const ra = splitRelease(a);
16
+ const rb = splitRelease(b);
17
+ for (let i = 0; i < Math.max(ra.parts.length, rb.parts.length); i++) {
18
+ const pa = ra.parts[i] ?? 0;
19
+ const pb = rb.parts[i] ?? 0;
20
+ if (pa !== pb) return pa < pb ? -1 : 1;
21
+ }
22
+ // same numeric parts — prerelease is less than no prerelease (semver rule)
23
+ if (ra.pre !== null && rb.pre === null) return -1;
24
+ if (ra.pre === null && rb.pre !== null) return 1;
25
+ if (ra.pre !== null && rb.pre !== null) {
26
+ // Compare dot-separated prerelease identifiers per semver: numeric
27
+ // identifiers compare numerically (so next.9 < next.10), numeric ranks
28
+ // below alphanumeric, and a shorter prefix ranks lower. Plain string
29
+ // `<`/`>` would order next.10 before next.9 lexicographically.
30
+ const sa = ra.pre.split(".");
31
+ const sb = rb.pre.split(".");
32
+ for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
33
+ if (sa[i] === undefined) return -1;
34
+ if (sb[i] === undefined) return 1;
35
+ const na = /^\d+$/.test(sa[i]);
36
+ const nb = /^\d+$/.test(sb[i]);
37
+ if (na && nb) {
38
+ const da = Number(sa[i]);
39
+ const db = Number(sb[i]);
40
+ if (da !== db) return da < db ? -1 : 1;
41
+ } else if (na !== nb) {
42
+ return na ? -1 : 1;
43
+ } else if (sa[i] !== sb[i]) {
44
+ return sa[i] < sb[i] ? -1 : 1;
45
+ }
46
+ }
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ /**
52
+ * Checks the npm registry for a newer create-zfb release than `version` and
53
+ * returns a warning message if the installed version is stale, or null if
54
+ * up to date. Never throws — network failure, timeout (AbortError), and
55
+ * parse errors are all treated as "no warning" so a registry hiccup can
56
+ * never block scaffolding. `fetchFn` and `timeoutMs` are injectable so tests
57
+ * don't need to hit the real network or wait out the real timeout.
58
+ */
59
+ export async function checkForNewerVersion({ version, fetchFn = fetch, timeoutMs = 1500 } = {}) {
60
+ try {
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
63
+ try {
64
+ const res = await fetchFn("https://registry.npmjs.org/create-zfb/latest", {
65
+ signal: controller.signal,
66
+ headers: {
67
+ "User-Agent": `create-zfb/${version}`,
68
+ Accept: "application/json",
69
+ },
70
+ });
71
+ if (!res.ok) return null;
72
+ const data = await res.json();
73
+ const latest = data?.version;
74
+ if (typeof latest !== "string" || typeof version !== "string") return null;
75
+ if (compareSemver(version, latest) >= 0) return null;
76
+ return (
77
+ `[create-zfb] You are running create-zfb@${version} but @latest is ${latest}.\n` +
78
+ `[create-zfb] pnpm 11's minimumReleaseAge may have downgraded the install.\n` +
79
+ `[create-zfb] Re-run with: pnpm create zfb@latest --config.minimumReleaseAge=0\n` +
80
+ `[create-zfb] Or use: npm create zfb@latest\n`
81
+ );
82
+ } finally {
83
+ // Keep the abort signal armed across both the header and body reads.
84
+ // Only cancel the timer once the body has been fully consumed (or on
85
+ // any error path), so a proxy that stalls after headers cannot hang
86
+ // the scaffolder indefinitely.
87
+ clearTimeout(timer);
88
+ }
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zfb",
3
- "version": "0.1.0-next.74",
3
+ "version": "0.1.0-next.76",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Scaffold a new zfb static-site project: `npm create zfb@latest my-site`",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "bin",
30
+ "!bin/__tests__",
30
31
  "README.md",
31
32
  "CHANGELOG.md",
32
33
  "LICENSE"
@@ -35,6 +36,13 @@
35
36
  "node": ">=18"
36
37
  },
37
38
  "dependencies": {
38
- "@takazudo/zfb": "0.1.0-next.74"
39
+ "@takazudo/zfb": "0.1.0-next.76"
40
+ },
41
+ "devDependencies": {
42
+ "vitest": "^2.1.9"
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run",
46
+ "test:watch": "vitest"
39
47
  }
40
48
  }