summaryrefslogtreecommitdiffhomepage
path: root/nix/scripts/canonicalize-node-modules.ts
blob: faa6f63402e2eae9f7de86d62a77d40e7c77c3f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
}

const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const linkRoot = join(bunRoot, "node_modules")
const directories = (await readdir(bunRoot)).sort()
const versions = new Map<string, Entry[]>()

for (const entry of directories) {
  const full = join(bunRoot, entry)
  const info = await lstat(full)
  if (!info.isDirectory()) {
    continue
  }
  const parsed = parseEntry(entry)
  if (!parsed) {
    continue
  }
  const list = versions.get(parsed.name) ?? []
  list.push({ dir: full, version: parsed.version, label: entry })
  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<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)
  })
  selections.set(slug, list[0])
}

await rm(linkRoot, { recursive: true, force: true })
await mkdir(linkRoot, { recursive: true })

const rewrites: string[] = []

for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
  const parts = slug.split("/")
  const leaf = parts.pop()
  if (!leaf) {
    continue
  }
  const parent = join(linkRoot, ...parts)
  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) {
    continue
  }
  const relativeTarget = relative(parent, desired)
  const resolved = relativeTarget.length === 0 ? "." : relativeTarget
  await rm(linkPath, { recursive: true, force: true })
  await symlink(resolved, linkPath)
  rewrites.push(slug + " -> " + resolved)
}

rewrites.sort()
console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links")
for (const line of rewrites.slice(0, 20)) {
  console.log("  ", line)
}
if (rewrites.length > 20) {
  console.log("  ...")
}

function parseEntry(label: string) {
  const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
  if (marker <= 0) {
    return null
  }
  const name = label.slice(0, marker).replace(/\+/g, "/")
  const version = label.slice(marker + 1)
  if (!name || !version) {
    return null
  }
  return { name, version }
}