From 9789de61255921bde95fda756a36a3c94ee997d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 7 Feb 2026 23:05:36 +0100 Subject: [PATCH 1/6] fix(nix): handle missing .bun directory in canonicalize-node-modules Use safeReadDir and isDirectory helpers (consistent with normalize-bun-binaries.ts) and replace dynamic semver import with Bun.semver.order. Fixes #12632, #12603, #12602 --- nix/scripts/canonicalize-node-modules.ts | 56 +++++++++--------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index faa6f63402e2..cc75448ce3cd 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -1,27 +1,38 @@ import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" import { join, relative } from "path" -type SemverLike = { - valid: (value: string) => string | null - rcompare: (left: string, right: string) => number -} - type Entry = { dir: string version: string label: string } +async function safeReadDir(path: string) { + try { + return await readdir(path) + } catch { + return [] + } +} + +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") -const directories = (await readdir(bunRoot)).sort() +const directories = (await safeReadDir(bunRoot)).sort() const versions = new Map() for (const entry of directories) { const full = join(bunRoot, entry) - const info = await lstat(full) - if (!info.isDirectory()) { + if (!(await isDirectory(full))) { continue } const parsed = parseEntry(entry) @@ -33,32 +44,10 @@ for (const entry of directories) { versions.set(parsed.name, list) } -const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as - | SemverLike - | { - default: SemverLike - } -const semver = "default" in semverModule ? semverModule.default : semverModule const selections = new Map() for (const [slug, list] of versions) { - list.sort((a, b) => { - const left = semver.valid(a.version) - const right = semver.valid(b.version) - if (left && right) { - const delta = semver.rcompare(left, right) - if (delta !== 0) { - return delta - } - } - if (left && !right) { - return -1 - } - if (!left && right) { - return 1 - } - return b.version.localeCompare(a.version) - }) + list.sort((a, b) => -Bun.semver.order(a.version, b.version)) selections.set(slug, list[0]) } @@ -77,10 +66,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0] await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) - const exists = await lstat(desired) - .then((info) => info.isDirectory()) - .catch(() => false) - if (!exists) { + if (!(await isDirectory(desired))) { continue } const relativeTarget = relative(parent, desired) From 3a2255166a184d2be6f705c3c1365fdc3b545a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 7 Feb 2026 23:42:05 +0100 Subject: [PATCH 2/6] fix(nix): skip canonicalization when .bun directory is empty --- nix/scripts/canonicalize-node-modules.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index cc75448ce3cd..7446ed51e906 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -28,6 +28,12 @@ const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") const directories = (await safeReadDir(bunRoot)).sort() + +if (directories.length === 0) { + console.log("[canonicalize-node-modules] no .bun directory, skipping") + process.exit(0) +} + const versions = new Map() for (const entry of directories) { From 3b8b5e8e764c11cc93a20127b520e95734bd4fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 7 Feb 2026 23:43:45 +0100 Subject: [PATCH 3/6] fix(nix): harmonize log messages and add early exit to normalize-bun-binaries --- nix/scripts/normalize-bun-binaries.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts index 531d8fd0567a..48299d6d1e78 100644 --- a/nix/scripts/normalize-bun-binaries.ts +++ b/nix/scripts/normalize-bun-binaries.ts @@ -9,6 +9,12 @@ type PackageManifest = { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const bunEntries = (await safeReadDir(bunRoot)).sort() + +if (bunEntries.length === 0) { + console.log("[normalize-bun-binaries] no .bun directory, skipping") + process.exit(0) +} + let rewritten = 0 for (const entry of bunEntries) { @@ -45,7 +51,7 @@ for (const entry of bunEntries) { } } -console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) +console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`) async function collectPackages(modulesRoot: string) { const found: string[] = [] From 3aeea9dce5e415d1f2b35f365a47ccbfb59051a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 8 Feb 2026 13:25:47 +0100 Subject: [PATCH 4/6] refactor(nix): use native Bun APIs and propagate errors --- nix/scripts/canonicalize-node-modules.ts | 16 +--------------- nix/scripts/normalize-bun-binaries.ts | 20 +++----------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index 7446ed51e906..dcccd2aefbdd 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -4,15 +4,6 @@ import { join, relative } from "path" type Entry = { dir: string version: string - label: string -} - -async function safeReadDir(path: string) { - try { - return await readdir(path) - } catch { - return [] - } } async function isDirectory(path: string) { @@ -27,12 +18,7 @@ async function isDirectory(path: string) { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") -const directories = (await safeReadDir(bunRoot)).sort() - -if (directories.length === 0) { - console.log("[canonicalize-node-modules] no .bun directory, skipping") - process.exit(0) -} +const directories = (await readdir(bunRoot)).sort() const versions = new Map() diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts index 48299d6d1e78..978ab325b7bb 100644 --- a/nix/scripts/normalize-bun-binaries.ts +++ b/nix/scripts/normalize-bun-binaries.ts @@ -8,13 +8,7 @@ type PackageManifest = { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") -const bunEntries = (await safeReadDir(bunRoot)).sort() - -if (bunEntries.length === 0) { - console.log("[normalize-bun-binaries] no .bun directory, skipping") - process.exit(0) -} - +const bunEntries = (await readdir(bunRoot)).sort() let rewritten = 0 for (const entry of bunEntries) { @@ -55,7 +49,7 @@ console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`) async function collectPackages(modulesRoot: string) { const found: string[] = [] - const topLevel = (await safeReadDir(modulesRoot)).sort() + const topLevel = (await readdir(modulesRoot)).sort() for (const name of topLevel) { if (name === ".bin" || name === ".bun") { continue @@ -65,7 +59,7 @@ async function collectPackages(modulesRoot: string) { continue } if (name.startsWith("@")) { - const scoped = (await safeReadDir(full)).sort() + const scoped = (await readdir(full)).sort() for (const child of scoped) { const scopedDir = join(full, child) if (await isDirectory(scopedDir)) { @@ -127,14 +121,6 @@ async function isDirectory(path: string) { } } -async function safeReadDir(path: string) { - try { - return await readdir(path) - } catch { - return [] - } -} - function normalizeBinName(name: string) { const slash = name.lastIndexOf("/") if (slash >= 0) { From fec6dd201fdeb843edbec2c4da40f7fe368aea8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 8 Feb 2026 20:56:36 +0100 Subject: [PATCH 5/6] fix(nix): handle invalid semver versions in canonicalize-node-modules --- nix/scripts/canonicalize-node-modules.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index dcccd2aefbdd..9ed7cf9539e8 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -15,6 +15,15 @@ async function isDirectory(path: string) { } } +function isValidSemver(v: string) { + try { + Bun.semver.order(v, "0.0.0") + return true + } catch { + return false + } +} + const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") @@ -32,15 +41,23 @@ for (const entry of directories) { continue } const list = versions.get(parsed.name) ?? [] - list.push({ dir: full, version: parsed.version, label: entry }) + list.push({ dir: full, version: parsed.version }) versions.set(parsed.name, list) } const selections = new Map() for (const [slug, list] of versions) { - list.sort((a, b) => -Bun.semver.order(a.version, b.version)) - selections.set(slug, list[0]) + list.sort((a, b) => { + const aValid = isValidSemver(a.version) + const bValid = isValidSemver(b.version) + if (aValid && bValid) return -Bun.semver.order(a.version, b.version) + if (aValid) return -1 + if (bValid) return 1 + return b.version.localeCompare(a.version) + }) + const first = list[0] + if (first) selections.set(slug, first) } await rm(linkRoot, { recursive: true, force: true }) From bfb3a4afc951fadb2df681872b531bdff15761d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 9 Feb 2026 00:02:13 +0100 Subject: [PATCH 6/6] refactor(nix): use Bun.semver.satisfies for cleaner semver validation --- nix/scripts/canonicalize-node-modules.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index 9ed7cf9539e8..7997a3cd2325 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -15,14 +15,7 @@ async function isDirectory(path: string) { } } -function isValidSemver(v: string) { - try { - Bun.semver.order(v, "0.0.0") - return true - } catch { - return false - } -} +const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x") const root = process.cwd() const bunRoot = join(root, "node_modules/.bun")