Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 13 additions & 35 deletions nix/scripts/canonicalize-node-modules.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
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 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()

Comment thread
jerome-benoit marked this conversation as resolved.
const versions = new Map<string, Entry[]>()

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)
Expand All @@ -33,32 +36,10 @@ for (const entry of directories) {
versions.set(parsed.name, list)
Comment thread
jerome-benoit marked this conversation as resolved.
}

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<string, Entry>()

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)
})
Comment on lines -36 to -61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, using the bun semver module is nice, but why remove the checks for non semver package versions? afaict, the behavior of semver.order is not specified for invalid semver strings and could lead to nondeterministic sorting: https://bun.com/docs/runtime/semver

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fec6dd2 - added isValidSemver() guard to handle invalid versions (latest, canary, git URLs) before calling Bun.semver.order.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure Bun.semver.order throws when provided invalid semver string? why not const isSemver = (s) => Bun.semver.satisfies(s, "x.x.x") which is documented to return false when s is not a semver string

@jerome-benoit jerome-benoit Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bun.semver.order throws on invalid versions (e.g. Invalid SemVer: latest), so the try/catch worked. Switched to Bun.semver.satisfies(v, "x.x.x") which is cleaner—returns false without exceptions. Fixed in bfb3a4a.

list.sort((a, b) => -Bun.semver.order(a.version, b.version))
selections.set(slug, list[0])
}

Expand All @@ -77,10 +58,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)
Expand Down
16 changes: 4 additions & 12 deletions nix/scripts/normalize-bun-binaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type PackageManifest = {

const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const bunEntries = (await safeReadDir(bunRoot)).sort()
const bunEntries = (await readdir(bunRoot)).sort()
let rewritten = 0
Comment thread
jerome-benoit marked this conversation as resolved.

for (const entry of bunEntries) {
Expand Down Expand Up @@ -45,11 +45,11 @@ 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[] = []
const topLevel = (await safeReadDir(modulesRoot)).sort()
const topLevel = (await readdir(modulesRoot)).sort()
for (const name of topLevel) {
if (name === ".bin" || name === ".bun") {
continue
Expand All @@ -59,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)) {
Expand Down Expand Up @@ -121,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) {
Expand Down
Loading