94864cb5e4
- Add shared Nix node-modules derivation and Bun normalization scripts - Package the desktop and server apps from the normalized install tree
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Canonicalize Bun-managed install indirection without modifying package contents.
|
|
|
|
import {
|
|
lstat,
|
|
readdir,
|
|
readlink,
|
|
symlink,
|
|
unlink,
|
|
} from "node:fs/promises";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
|
|
const nodeModulesRoot = resolve(process.cwd(), process.argv[2] ?? "node_modules");
|
|
|
|
async function pathExists(path: string): Promise<boolean> {
|
|
try {
|
|
await lstat(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function lexicalSort(values: Iterable<string>): string[] {
|
|
return [...values].sort((left, right) => left.localeCompare(right, "en"));
|
|
}
|
|
|
|
function normalizeRelativeTarget(fromDir: string, targetPath: string): string {
|
|
const target = relative(fromDir, targetPath) || ".";
|
|
return target.replaceAll(/\/+/g, "/");
|
|
}
|
|
|
|
async function rewriteSymlink(linkPath: string, targetPath: string): Promise<void> {
|
|
const nextTarget = normalizeRelativeTarget(dirname(linkPath), targetPath);
|
|
const currentTarget = await readlink(linkPath).catch(() => null);
|
|
if (currentTarget === nextTarget) {
|
|
return;
|
|
}
|
|
|
|
await unlink(linkPath);
|
|
await symlink(nextTarget, linkPath);
|
|
}
|
|
|
|
async function canonicalizeManagedSymlink(linkPath: string): Promise<void> {
|
|
const linkDir = dirname(linkPath);
|
|
const currentTarget = await readlink(linkPath).catch(() => null);
|
|
if (currentTarget === null || currentTarget.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const resolvedTarget = resolve(linkDir, currentTarget);
|
|
if (await pathExists(resolvedTarget)) {
|
|
await rewriteSymlink(linkPath, resolvedTarget);
|
|
return;
|
|
}
|
|
|
|
const normalizedBrokenTarget = normalizeRelativeTarget(linkDir, resolvedTarget);
|
|
if (currentTarget !== normalizedBrokenTarget) {
|
|
await unlink(linkPath);
|
|
await symlink(normalizedBrokenTarget, linkPath);
|
|
}
|
|
}
|
|
|
|
async function readSortedDir(path: string): Promise<string[]> {
|
|
return lexicalSort(await readdir(path));
|
|
}
|
|
|
|
function isNodeModulesPackageEntry(path: string): boolean {
|
|
const parent = dirname(path);
|
|
const grandparent = dirname(parent);
|
|
return parent.endsWith("/node_modules") || grandparent.endsWith("/node_modules");
|
|
}
|
|
|
|
async function walkNodeModules(path: string): Promise<void> {
|
|
const entries = await readSortedDir(path);
|
|
for (const entry of entries) {
|
|
const entryPath = join(path, entry);
|
|
const stats = await lstat(entryPath);
|
|
|
|
if (stats.isSymbolicLink()) {
|
|
const relativePath = relative(nodeModulesRoot, entryPath);
|
|
const inBunStore = relativePath === ".bun" || relativePath.startsWith(".bun/");
|
|
const isWorkspaceLink = isNodeModulesPackageEntry(entryPath);
|
|
if (inBunStore || isWorkspaceLink) {
|
|
await canonicalizeManagedSymlink(entryPath);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!stats.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
if (entry === ".bin") {
|
|
continue;
|
|
}
|
|
|
|
await walkNodeModules(entryPath);
|
|
}
|
|
}
|
|
|
|
if (await pathExists(nodeModulesRoot)) {
|
|
await walkNodeModules(nodeModulesRoot);
|
|
}
|