chore(nix): add bun node_modules normalization scripts (#1256)
This commit is contained in:
parent
c1e11faa71
commit
af6ec6c903
4 changed files with 464 additions and 0 deletions
97
.nix/scripts/canonicalize-node-modules.ts
Normal file
97
.nix/scripts/canonicalize-node-modules.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Canonicalize bun's internal node_modules symlinks for reproducible FOD hashes.
|
||||||
|
*
|
||||||
|
* Isolated-install layout produced by `bun install --linker=isolated`:
|
||||||
|
*
|
||||||
|
* node_modules/
|
||||||
|
* ├── react → .bun/react@18.3.1/node_modules/react (symlink)
|
||||||
|
* ├── minimatch → .bun/minimatch@3.1.2/node_modules/minimatch (symlink)
|
||||||
|
* └── .bun/
|
||||||
|
* ├── react@18.3.1/
|
||||||
|
* │ └── node_modules/
|
||||||
|
* │ └── react/ ← real package content
|
||||||
|
* ├── minimatch@3.1.2/
|
||||||
|
* │ └── node_modules/
|
||||||
|
* │ └── minimatch/ ← real package content
|
||||||
|
* └── node_modules/ ← target of this script
|
||||||
|
* ├── react → ../react@18.3.1/node_modules/react
|
||||||
|
* ├── minimatch → ../minimatch@3.1.2/node_modules/minimatch
|
||||||
|
* └── @babel/
|
||||||
|
* └── core → ../../@babel+core@7.28.5+…/node_modules/@babel/core
|
||||||
|
*
|
||||||
|
* Real package content lives in .bun/<pkg>@<ver>/node_modules/<pkg>/.
|
||||||
|
* The .bun/node_modules/ directory (linkRoot) holds only symlinks — it acts
|
||||||
|
* as a fallback upward-resolution path for packages inside .bun/.
|
||||||
|
*
|
||||||
|
* Bun's creation order for those symlinks is not guaranteed to be stable
|
||||||
|
* across hosts or filesystems, which can break fixed-output derivation hashes.
|
||||||
|
* This script reads the existing symlinks, removes them, and recreates them
|
||||||
|
* in lexicographic order while preserving the exact targets bun picked.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lstat, mkdir, readdir, readlink, rm, symlink } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
type LinkEntry = {
|
||||||
|
slug: string;
|
||||||
|
target: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function isDirectory(path: string) {
|
||||||
|
try {
|
||||||
|
const info = await lstat(path);
|
||||||
|
return info.isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectLinks(dir: string, prefix: string): Promise<LinkEntry[]> {
|
||||||
|
const result: LinkEntry[] = [];
|
||||||
|
const names = await readdir(dir);
|
||||||
|
for (const name of names) {
|
||||||
|
const full = join(dir, name);
|
||||||
|
const info = await lstat(full);
|
||||||
|
if (info.isSymbolicLink()) {
|
||||||
|
const target = await readlink(full);
|
||||||
|
const slug = prefix ? `${prefix}/${name}` : name;
|
||||||
|
result.push({ slug, target });
|
||||||
|
} else if (info.isDirectory() && !prefix && name.startsWith("@")) {
|
||||||
|
result.push(...(await collectLinks(full, name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function canonicalizeNodeModules(): Promise<void> {
|
||||||
|
const root = process.cwd();
|
||||||
|
const linkRoot = join(root, "node_modules/.bun/node_modules");
|
||||||
|
|
||||||
|
if (!(await isDirectory(linkRoot))) {
|
||||||
|
console.log(
|
||||||
|
"[canonicalize-node-modules] no .bun/node_modules directory, skipping",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await collectLinks(linkRoot, "");
|
||||||
|
entries.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||||
|
|
||||||
|
await rm(linkRoot, { recursive: true, force: true });
|
||||||
|
await mkdir(linkRoot, { recursive: true });
|
||||||
|
|
||||||
|
for (const { slug, target } of entries) {
|
||||||
|
const parts = slug.split("/");
|
||||||
|
const leaf = parts.pop();
|
||||||
|
if (!leaf) continue;
|
||||||
|
const parent = join(linkRoot, ...parts);
|
||||||
|
await mkdir(parent, { recursive: true });
|
||||||
|
await symlink(target, join(parent, leaf));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[canonicalize-node-modules] rebuilt ${entries.length} links`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await canonicalizeNodeModules();
|
||||||
|
}
|
||||||
229
.nix/scripts/heal-peer-dep-bins.ts
Normal file
229
.nix/scripts/heal-peer-dep-bins.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/**
|
||||||
|
* Heal missing `.bin/<peer>` symlinks produced by bun's isolated installer.
|
||||||
|
*
|
||||||
|
* ## The bug
|
||||||
|
*
|
||||||
|
* Bun's `--linker=isolated` installer creates `.bin/<name>` symlinks inside
|
||||||
|
* each package's private node_modules/.bin/ for every dependency that has a
|
||||||
|
* `bin` field in its manifest — regular, optional, AND peer dependencies all
|
||||||
|
* go through the same code path (`Installer.zig::linkDependencyBins`), and
|
||||||
|
* the decision to link is made purely on whether the source file exists on
|
||||||
|
* disk at the moment the linker looks (`bin.zig`:
|
||||||
|
*
|
||||||
|
* if (!bun.sys.exists(abs_target)) {
|
||||||
|
* this.skipped_due_to_missing_bin = true;
|
||||||
|
* return;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* ). For most dependencies the installer blocks the consuming package on the
|
||||||
|
* provider via `isTaskBlocked`, so by the time `linkDependencyBins` runs for
|
||||||
|
* package A the provider's file is guaranteed to be in place. But for
|
||||||
|
* circular peer dependency pairs — A declares B as a peer, B (transitively)
|
||||||
|
* depends on A — that blocking would deadlock, so bun's `Store.isCycle`
|
||||||
|
* detector explicitly bypasses it and lets both sides run in parallel.
|
||||||
|
*
|
||||||
|
* The consequence is a plain timing race between two worker threads. Which
|
||||||
|
* side wins depends on anything that shifts the relative scheduling of the
|
||||||
|
* two workers — CPU load, thread-pool size, filesystem write latency and
|
||||||
|
* caching, the kernel scheduler, NICE / cgroup limits — so the same bun
|
||||||
|
* version with the same bun.lock and the same install flags can produce
|
||||||
|
* different `.bin/` sets not just between different hosts but in principle
|
||||||
|
* between two consecutive runs on the same host. In practice we have
|
||||||
|
* observed divergence between a local NixOS sandbox, a GitHub Actions
|
||||||
|
* ubuntu-latest runner, and a GitHub Actions macos-latest runner, which is
|
||||||
|
* enough to break any single-hash FOD.
|
||||||
|
*
|
||||||
|
* Concretely, the Handy install hits this with
|
||||||
|
* - update-browserslist-db/.bin/browserslist (update-browserslist-db
|
||||||
|
* declares browserslist as a peer, browserslist has
|
||||||
|
* update-browserslist-db in its regular dependencies → cycle), and
|
||||||
|
* - @eslint-community/eslint-utils/.bin/eslint (eslint has eslint-utils
|
||||||
|
* as a regular dep, eslint-utils declares eslint as a peer → cycle).
|
||||||
|
*
|
||||||
|
* There is no bun configuration flag or env var that makes the outcome
|
||||||
|
* deterministic, and no upstream issue yet tracks this specific symptom
|
||||||
|
* (oven-sh/bun#28147 is the closest family match, different project). See
|
||||||
|
* the header of `canonicalize-node-modules.ts` for our sibling normalization
|
||||||
|
* pass that rebuilds `.bun/node_modules/` in sorted order.
|
||||||
|
*
|
||||||
|
* ## The fix this script applies
|
||||||
|
*
|
||||||
|
* For every package under `node_modules/.bun/` we walk its declared
|
||||||
|
* `peerDependencies`, find each resolved peer inside the package's private
|
||||||
|
* `node_modules/`, and create any `.bin/<name> → ../<peerName>/<path>`
|
||||||
|
* symlinks that bun's installer "intended" to create but may have skipped.
|
||||||
|
* Entries that already exist are left alone (the script is idempotent).
|
||||||
|
*
|
||||||
|
* This is the "fix by adding" approach — we produce the complete `.bin/`
|
||||||
|
* set that bun would have produced without the race, rather than stripping
|
||||||
|
* the inconsistent subset. Advantages:
|
||||||
|
*
|
||||||
|
* - Matches bun's intended behavior; if bun ever fixes the race upstream,
|
||||||
|
* this script becomes a no-op (every entry it would add already exists)
|
||||||
|
* and the FOD hash is unchanged.
|
||||||
|
* - Preserves `.bin/` entries that real code might depend on. We don't
|
||||||
|
* rely on the (true but brittle) argument that peer-dep `.bin/` entries
|
||||||
|
* are dead code in Tauri apps.
|
||||||
|
* - Easy to explain in review: we're patching a known upstream race bug
|
||||||
|
* with the exact output the upstream code is trying to produce.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lstat, mkdir, readdir, readlink, symlink } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
type Manifest = {
|
||||||
|
name?: string;
|
||||||
|
bin?: string | Record<string, string>;
|
||||||
|
peerDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function isDirectory(path: string) {
|
||||||
|
try {
|
||||||
|
const info = await lstat(path);
|
||||||
|
return info.isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readManifest(path: string): Promise<Manifest | null> {
|
||||||
|
const file = Bun.file(path);
|
||||||
|
if (!(await file.exists())) return null;
|
||||||
|
return (await file.json()) as Manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a .bun entry directory name (e.g. "@babel+core@7.28.5+a1c3dd1b9adf390b")
|
||||||
|
// back into an npm package name ("@babel/core"). Returns null for entries that
|
||||||
|
// do not look like <name>@<version>[+<peer-hash>].
|
||||||
|
function parsePkgName(bunEntry: string): string | null {
|
||||||
|
const at = bunEntry.startsWith("@")
|
||||||
|
? bunEntry.indexOf("@", 1)
|
||||||
|
: bunEntry.indexOf("@");
|
||||||
|
if (at <= 0) return null;
|
||||||
|
return bunEntry.slice(0, at).replace(/\+/g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unscoped name used as the default bin name when `bin` is a bare string.
|
||||||
|
// For "@scope/pkg" returns "pkg"; for "pkg" returns "pkg".
|
||||||
|
function defaultBinName(pkgName: string): string {
|
||||||
|
const slash = pkgName.lastIndexOf("/");
|
||||||
|
return slash >= 0 ? pkgName.slice(slash + 1) : pkgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinSpec = { name: string; path: string };
|
||||||
|
|
||||||
|
function parseBinField(pkgName: string, binField: Manifest["bin"]): BinSpec[] {
|
||||||
|
if (!binField) return [];
|
||||||
|
if (typeof binField === "string") {
|
||||||
|
return [{ name: defaultBinName(pkgName), path: binField }];
|
||||||
|
}
|
||||||
|
return Object.entries(binField).map(([name, path]) => ({
|
||||||
|
name: defaultBinName(name),
|
||||||
|
path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the relative symlink target that bun itself uses for a .bin entry
|
||||||
|
// sitting inside `.bun/<containing>/node_modules/.bin/`, pointing to a file
|
||||||
|
// under `.bun/<containing>/node_modules/<peerName>/...`.
|
||||||
|
function binTarget(peerName: string, binPath: string): string {
|
||||||
|
const clean = binPath.replace(/^\.\//, "");
|
||||||
|
return `../${peerName}/${clean}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealedEntry = {
|
||||||
|
containingEntry: string;
|
||||||
|
containingPkg: string;
|
||||||
|
peerName: string;
|
||||||
|
binName: string;
|
||||||
|
target: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function healPeerDepBins(): Promise<void> {
|
||||||
|
const root = process.cwd();
|
||||||
|
const bunRoot = join(root, "node_modules/.bun");
|
||||||
|
|
||||||
|
if (!(await isDirectory(bunRoot))) {
|
||||||
|
console.log("[heal-peer-dep-bins] no .bun directory, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bunEntries = (await readdir(bunRoot)).sort();
|
||||||
|
const healed: HealedEntry[] = [];
|
||||||
|
|
||||||
|
for (const entry of bunEntries) {
|
||||||
|
const pkgName = parsePkgName(entry);
|
||||||
|
if (!pkgName) continue;
|
||||||
|
const containingNodeModules = join(bunRoot, entry, "node_modules");
|
||||||
|
if (!(await isDirectory(containingNodeModules))) continue;
|
||||||
|
|
||||||
|
const manifest = await readManifest(
|
||||||
|
join(containingNodeModules, pkgName, "package.json"),
|
||||||
|
);
|
||||||
|
if (!manifest) continue;
|
||||||
|
|
||||||
|
const peers = Object.keys(manifest.peerDependencies ?? {});
|
||||||
|
if (peers.length === 0) continue;
|
||||||
|
|
||||||
|
const binRoot = join(containingNodeModules, ".bin");
|
||||||
|
|
||||||
|
for (const peerName of peers) {
|
||||||
|
// Peer may be optional and unresolved, or may not even be a real package
|
||||||
|
// directory in this install layout (e.g. bundled peer). Skip anything we
|
||||||
|
// cannot verify as "the peer's package.json is reachable from here".
|
||||||
|
const peerManifest = await readManifest(
|
||||||
|
join(containingNodeModules, peerName, "package.json"),
|
||||||
|
);
|
||||||
|
if (!peerManifest) continue;
|
||||||
|
|
||||||
|
const bins = parseBinField(peerName, peerManifest.bin);
|
||||||
|
if (bins.length === 0) continue;
|
||||||
|
|
||||||
|
// Ensure the bin directory exists (it may be absent entirely if bun's
|
||||||
|
// race skipped every link it would have created for this package).
|
||||||
|
await mkdir(binRoot, { recursive: true });
|
||||||
|
|
||||||
|
for (const bin of bins) {
|
||||||
|
const linkPath = join(binRoot, bin.name);
|
||||||
|
const target = binTarget(peerName, bin.path);
|
||||||
|
|
||||||
|
// Idempotent: skip if anything already occupies this path. We do not
|
||||||
|
// overwrite entries bun created; if bun already wrote a .bin/<name>,
|
||||||
|
// that is either the correct target (race won) or a close variant we
|
||||||
|
// should not second-guess.
|
||||||
|
try {
|
||||||
|
await lstat(linkPath);
|
||||||
|
continue;
|
||||||
|
} catch {
|
||||||
|
// Does not exist — fall through to create.
|
||||||
|
}
|
||||||
|
|
||||||
|
await symlink(target, linkPath);
|
||||||
|
healed.push({
|
||||||
|
containingEntry: entry,
|
||||||
|
containingPkg: pkgName,
|
||||||
|
peerName,
|
||||||
|
binName: bin.name,
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (healed.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`[heal-peer-dep-bins] healed ${healed.length} missing peer .bin entries:`,
|
||||||
|
);
|
||||||
|
for (const h of healed) {
|
||||||
|
console.log(
|
||||||
|
` ${h.containingEntry}/node_modules/.bin/${h.binName} → ${h.target} (peer ${h.peerName} of ${h.containingPkg})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[heal-peer-dep-bins] nothing to heal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await healPeerDepBins();
|
||||||
|
}
|
||||||
102
.nix/scripts/normalize-bun-binaries.ts
Normal file
102
.nix/scripts/normalize-bun-binaries.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
/**
|
||||||
|
* Normalize .bin symlinks inside bun's internal module directories.
|
||||||
|
*
|
||||||
|
* In an isolated install, every package under .bun/ gets its own private
|
||||||
|
* node_modules/ with a .bin/ directory holding symlinks to its dependencies'
|
||||||
|
* executables:
|
||||||
|
*
|
||||||
|
* node_modules/.bun/
|
||||||
|
* ├── @vitejs+plugin-react@4.7.0+…/
|
||||||
|
* │ └── node_modules/
|
||||||
|
* │ ├── vite → ../../vite@6.4.1+…/node_modules/vite (peer symlink)
|
||||||
|
* │ └── .bin/ ← target of this script
|
||||||
|
* │ └── vite → ../vite/bin/vite.js
|
||||||
|
* ├── eslint@9.39.1+…/
|
||||||
|
* │ └── node_modules/
|
||||||
|
* │ └── .bin/
|
||||||
|
* │ └── eslint → ../eslint/bin/eslint.js
|
||||||
|
* └── vite@6.4.1+…/
|
||||||
|
* └── node_modules/
|
||||||
|
* └── vite/ ← real package content
|
||||||
|
* └── bin/vite.js
|
||||||
|
*
|
||||||
|
* Real executables live in .bun/<pkg>@<ver>/node_modules/<pkg>/…; every .bin/
|
||||||
|
* entry is just a relative symlink reached through the peer symlinks in the
|
||||||
|
* same node_modules/.
|
||||||
|
*
|
||||||
|
* Bun's creation order for those .bin/ symlinks is not guaranteed to be stable
|
||||||
|
* across hosts or filesystems, which can break fixed-output derivation hashes.
|
||||||
|
* This script reads the .bin/ symlinks bun produced, removes them, and
|
||||||
|
* recreates them in lexicographic order while preserving the exact targets
|
||||||
|
* bun picked.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lstat, mkdir, readdir, readlink, rm, symlink } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
type BinEntry = {
|
||||||
|
name: string;
|
||||||
|
target: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function isDirectory(path: string) {
|
||||||
|
try {
|
||||||
|
const info = await lstat(path);
|
||||||
|
return info.isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectBinLinks(binRoot: string): Promise<BinEntry[]> {
|
||||||
|
const entries: BinEntry[] = [];
|
||||||
|
let names: string[];
|
||||||
|
try {
|
||||||
|
names = await readdir(binRoot);
|
||||||
|
} catch {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
for (const name of names) {
|
||||||
|
const full = join(binRoot, name);
|
||||||
|
const info = await lstat(full);
|
||||||
|
if (!info.isSymbolicLink()) continue;
|
||||||
|
entries.push({ name, target: await readlink(full) });
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeBunBinaries(): Promise<void> {
|
||||||
|
const root = process.cwd();
|
||||||
|
const bunRoot = join(root, "node_modules/.bun");
|
||||||
|
|
||||||
|
if (!(await isDirectory(bunRoot))) {
|
||||||
|
console.log("[normalize-bun-binaries] no .bun directory, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bunEntries = (await readdir(bunRoot)).sort();
|
||||||
|
let rewritten = 0;
|
||||||
|
|
||||||
|
for (const entry of bunEntries) {
|
||||||
|
const binRoot = join(bunRoot, entry, "node_modules", ".bin");
|
||||||
|
if (!(await isDirectory(binRoot))) continue;
|
||||||
|
|
||||||
|
const bins = await collectBinLinks(binRoot);
|
||||||
|
if (bins.length === 0) continue;
|
||||||
|
bins.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
await rm(binRoot, { recursive: true, force: true });
|
||||||
|
await mkdir(binRoot, { recursive: true });
|
||||||
|
|
||||||
|
for (const { name, target } of bins) {
|
||||||
|
await symlink(target, join(binRoot, name));
|
||||||
|
rewritten++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await normalizeBunBinaries();
|
||||||
|
}
|
||||||
36
.nix/scripts/normalize-install.ts
Normal file
36
.nix/scripts/normalize-install.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Make `bun install --linker=isolated` output bit-reproducible.
|
||||||
|
*
|
||||||
|
* Entry point for the nixpkgs FOD build — invoke this single script after
|
||||||
|
* `bun install` to get a `node_modules/` tree that is byte-identical
|
||||||
|
* across machines and runs with the same bun.lock.
|
||||||
|
*
|
||||||
|
* `bun install --linker=isolated` is not bit-reproducible out of the box,
|
||||||
|
* even with a frozen lockfile, a pinned bun version, `--ignore-scripts`,
|
||||||
|
* and a clean `BUN_INSTALL_CACHE_DIR`. Running it across a local NixOS
|
||||||
|
* sandbox, a GitHub Actions ubuntu-latest runner, and a GitHub Actions
|
||||||
|
* macos-latest runner produces subtly different `node_modules/` trees
|
||||||
|
* from identical inputs. Two independent sources of drift show up:
|
||||||
|
*
|
||||||
|
* - Missing `.bin/<peer>` entries around circular peer dependencies,
|
||||||
|
* caused by a timing race inside bun's isolated installer thread
|
||||||
|
* pool. See `heal-peer-dep-bins.ts` for the full explanation and fix.
|
||||||
|
*
|
||||||
|
* - Non-deterministic symlink creation order in `.bun/node_modules/`
|
||||||
|
* and in each per-package `.bin/` directory. NAR hashing sorts
|
||||||
|
* entries during serialization, so this is usually harmless, but we
|
||||||
|
* defensively rebuild both trees in canonical sorted order.
|
||||||
|
* See `canonicalize-node-modules.ts` and `normalize-bun-binaries.ts`.
|
||||||
|
*
|
||||||
|
* Each sub-script is also runnable standalone for debugging:
|
||||||
|
*
|
||||||
|
* bun --bun .nix/scripts/<name>.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { canonicalizeNodeModules } from "./canonicalize-node-modules.ts";
|
||||||
|
import { healPeerDepBins } from "./heal-peer-dep-bins.ts";
|
||||||
|
import { normalizeBunBinaries } from "./normalize-bun-binaries.ts";
|
||||||
|
|
||||||
|
await canonicalizeNodeModules();
|
||||||
|
await healPeerDepBins();
|
||||||
|
await normalizeBunBinaries();
|
||||||
Loading…
Reference in a new issue