@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +23 -3
  2. package/bin/stasis.js +375 -170
  3. package/package.json +44 -11
  4. package/src/audit-corrections.js +68 -0
  5. package/src/audit.js +243 -59
  6. package/src/babel-worker.cjs +65 -0
  7. package/src/babel.js +131 -0
  8. package/src/bundle-from-lockfile.js +12 -18
  9. package/src/bundle-reader.js +77 -0
  10. package/src/cmd/build.js +95 -128
  11. package/src/cmd/bundle.js +877 -641
  12. package/src/cmd/diff.js +2 -19
  13. package/src/cmd/github-bundle.js +39 -0
  14. package/src/cmd/output.js +36 -0
  15. package/src/cmd/sbom.js +4 -18
  16. package/src/diff.js +67 -124
  17. package/src/json-stream.js +283 -0
  18. package/src/loader-mock.js +8 -15
  19. package/src/loaders/bash.js +49 -116
  20. package/src/loaders/cargo.js +1699 -0
  21. package/src/loaders/foundry.js +923 -0
  22. package/src/loaders/paths.js +33 -0
  23. package/src/loaders/php.js +339 -394
  24. package/src/loaders/rust.js +3254 -242
  25. package/src/loaders/solidity-ownership.js +531 -0
  26. package/src/loaders/solidity.js +412 -162
  27. package/src/loaders/toml.js +68 -0
  28. package/src/metro-resolver.js +150 -0
  29. package/src/mock.js +57 -165
  30. package/src/parse.js +4 -16
  31. package/src/resolve-fields.js +331 -0
  32. package/src/resolve-node.js +416 -0
  33. package/src/resolve-typescript.js +427 -0
  34. package/src/rollup.js +5 -0
  35. package/src/sbom.js +64 -92
  36. package/src/scan.js +234 -156
  37. package/src/vfs-bundle/entries.js +140 -0
  38. package/src/vfs-bundle/github.js +193 -0
  39. package/src/vfs-bundle/soldeer.js +125 -0
  40. package/src/vfs-bundle/tree.js +606 -0
  41. package/src/vfs-bundle.js +74 -0
  42. package/src/why.js +434 -0
  43. package/src/apis/npm/index.js +0 -36
  44. package/src/apis/npm/semver.cjs +0 -44
  45. package/src/cmd/extract.js +0 -193
@@ -0,0 +1,606 @@
1
+ import { constants } from 'node:fs'
2
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path'
3
+
4
+ import { NO_ENTRY, packageJSONStat, packageJSONText, readJson } from '@exodus/stasis-core/bundle-util'
5
+ import { byName } from '@exodus/stasis-core/host'
6
+ import { hasNodeModulesSegment, isPlainObject } from '@exodus/stasis-core/util'
7
+ import { buildNpmTree, findNpmWorkspaces } from '@preventive/deptree/npm.js'
8
+ import { buildPnpmTree, findPnpmProjects } from '@preventive/deptree/pnpm.js'
9
+ import { LockfileError, TomlError, buildSoldeerTree } from '@preventive/deptree/soldeer.js'
10
+ import { buildYarn1Tree, findYarn1Workspaces } from '@preventive/deptree/yarn1.js'
11
+ import { VfsError } from '@preventive/vfs'
12
+ import { createNodeResolver } from '../resolve-node.js'
13
+ import { settleSoldeer } from './soldeer.js'
14
+ import { isDir, isFile } from '../resolve-typescript.js'
15
+
16
+ // A project's dependencies laid out in memory from its lockfile by @preventive/deptree, as `pnpm
17
+ // install --frozen-lockfile --ignore-scripts` lays out its node_modules with pnpm 9, 10, 11 or 12,
18
+ // `yarn install --frozen-lockfile --ignore-scripts` with yarn 1.22, `npm ci --ignore-scripts` with
19
+ // npm 10.9 or 11, or `soldeer install` its dependencies folder with Soldeer 0.12, and the host that
20
+ // reads the project through them. The project is read through a host it is given, and nothing else.
21
+
22
+ // cwd or the nearest of its ancestors that `holds`, or null.
23
+ function nearest(cwd, holds) {
24
+ for (let dir = cwd; ; dir = dirname(dir)) {
25
+ if (holds(dir)) return dir
26
+ if (dirname(dir) === dir) return null
27
+ }
28
+ }
29
+
30
+ // Whether a directory holds a file of one of `names`.
31
+ const holding = (host, ...names) => (dir) => names.some((name) => isFile(join(dir, name), host))
32
+
33
+ // The directory of the package cwd is in that is none of `projects` (their directories from
34
+ // `root`), or null: the nearest package.json with a name between cwd and `root`, one without being a
35
+ // `type` marker.
36
+ function outsider(host, root, cwd, projects) {
37
+ for (let dir = cwd; dir !== root; dir = dirname(dir)) {
38
+ const file = join(dir, 'package.json')
39
+ if (!isFile(file, host)) continue
40
+ if (projects.has(relative(root, dir))) return null
41
+ const json = readJson(file, host) // unreadable, a package all the same
42
+ if (json === null || json.name !== undefined) return dir
43
+ }
44
+ return null
45
+ }
46
+
47
+ // `promise`, a refusal of @preventive/lockfile's naming `file` (deptree names the files it reads but
48
+ // for yarn.lock and soldeer.lock).
49
+ const naming = (file, promise) => promise.catch((cause) => {
50
+ if (!(cause instanceof LockfileError || cause instanceof TomlError)) throw cause
51
+ throw new Error(`${file}: ${cause.message}`, { cause })
52
+ })
53
+
54
+ // The directory `root` as `host` holds it, by paths from `/`, as deptree reads a project.
55
+ function projectView(host, root) {
56
+ const at = (p) => (p === '/' ? root : join(root, p))
57
+ const typeOf = (st) => (st.isDirectory() ? 'directory' : st.isFile() ? 'file' : 'other')
58
+ const stat = (p) => {
59
+ const st = host.stat(at(p))
60
+ if (st === null) throw new VfsError('ENOENT', p)
61
+ return st
62
+ }
63
+ return {
64
+ readdir: (p) => host.readdir(at(p)).map((d) => d.name),
65
+ lstat(p) {
66
+ if (host.readlink(at(p)) !== null) return { type: 'symlink', mode: 0o777 }
67
+ const st = stat(p)
68
+ return { type: typeOf(st), mode: st.mode & 0o777 }
69
+ },
70
+ stat: (p) => ({ type: typeOf(stat(p)) }),
71
+ readFile: (p) => host.readFile(at(p)),
72
+ }
73
+ }
74
+
75
+ // What pnpm matches packages' `libc` against (as deptree's host.libc takes it).
76
+ const LIBCS = new Set(['glibc', 'musl', 'unknown'])
77
+
78
+ // pnpm's libc, detect-libc's familySync, by its check of Node's report alone (it reads
79
+ // /usr/bin/ldd first): unknown where that can't tell, which pnpm then installs everything for.
80
+ // The report leaves out the network, whose reverse DNS can stall.
81
+ let machineLibc
82
+ function currentLibc() {
83
+ if (machineLibc !== undefined) return machineLibc
84
+ machineLibc = 'unknown'
85
+ if (process.platform !== 'linux' || !process.report) return machineLibc
86
+ const { excludeNetwork } = process.report
87
+ try {
88
+ process.report.excludeNetwork = true
89
+ const report = process.report.getReport()
90
+ if (report.header?.glibcVersionRuntime) machineLibc = 'glibc'
91
+ else if (report.sharedObjects?.some((file) => file.includes('libc.musl-') || file.includes('ld-musl-'))) machineLibc = 'musl'
92
+ } catch { /* unknown */ } finally {
93
+ process.report.excludeNetwork = excludeNetwork
94
+ }
95
+ return machineLibc
96
+ }
97
+
98
+ const machine = () => ({ node: process.versions.node, os: process.platform, cpu: process.arch })
99
+
100
+ // The machine a tree is laid out for: this one, but for the `os`, `cpu` and `libc` given. A libc is
101
+ // detected only for this machine's os; for another, unless given, pnpm matches none ('unknown').
102
+ const target = ({ os, cpu, libc } = {}) => {
103
+ const here = machine()
104
+ const forOs = os ?? here.os
105
+ return { node: here.node, os: forOs, cpu: cpu ?? here.cpu, libc: libc ?? (forOs === here.os ? currentLibc() : 'unknown') }
106
+ }
107
+
108
+ // `os` and `cpu` as Node names them (process.platform, process.arch); `libc` is pnpm's alone, and
109
+ // Soldeer takes `os` alone: what a package manager matches nothing against changes nothing.
110
+ export function checkTarget(name, { os, cpu, libc }) {
111
+ for (const [key, value] of Object.entries({ os, cpu })) {
112
+ if (value !== undefined && (typeof value !== 'string' || value === '')) throw new TypeError(`${name}: ${key} must be a non-empty string`)
113
+ }
114
+ if (libc !== undefined && !LIBCS.has(libc)) throw new TypeError(`${name}: libc must be one of 'glibc', 'musl', 'unknown'`)
115
+ }
116
+
117
+ // yarn installs a workspace from the root that declares it, whatever yarn.lock is nearer, and any
118
+ // other package from the nearest yarn.lock.
119
+ function yarn1Root(host, cwd) {
120
+ const workspaces = (dir) => {
121
+ const declared = readJson(join(dir, 'package.json'), host)?.workspaces
122
+ return Boolean(Array.isArray(declared) ? declared : declared?.packages)
123
+ }
124
+ const root = nearest(cwd, workspaces)
125
+ if (root !== null && outsider(host, root, cwd, new Set(findYarn1Workspaces({ project: projectView(host, root) }))) === null) return root
126
+ return nearest(cwd, holding(host, 'yarn.lock'))
127
+ }
128
+
129
+ // Whether `dir` holds a package: a package.json naming one, or unreadable, which is one all the same;
130
+ // one with no name is a `type` marker, its files the package's above.
131
+ function holdsPackage(host, dir) {
132
+ if (!isFile(join(dir, 'package.json'), host)) return false
133
+ const json = readJson(join(dir, 'package.json'), host)
134
+ return json === null || json.name !== undefined
135
+ }
136
+
137
+ // The first directory above `dir` whose workspaces, as npm's glob finds them on `os`, take it, as npm
138
+ // finds its local prefix; else null. npm takes a falsy declaration for none, and fails on any other
139
+ // but a sequence of globs, or yarn's `{ packages: [...] }` of them.
140
+ function npmWorkspaceRoot(host, dir, os) {
141
+ for (let above = dir; dirname(above) !== above;) {
142
+ above = dirname(above)
143
+ const declared = readJson(join(above, 'package.json'), host)?.workspaces
144
+ if (!declared) continue
145
+ const globs = Array.isArray(declared.packages) ? declared.packages : declared
146
+ if (!Array.isArray(globs) || globs.some((glob) => typeof glob !== 'string')) throw new Error(`${join(above, 'package.json')}: workspaces: expected a sequence of globs, which npm fails without`)
147
+ if (findNpmWorkspaces({ project: projectView(host, above), os: target({ os }).os }).includes(relative(above, dir))) return above
148
+ }
149
+ return null
150
+ }
151
+
152
+ // npm installs the package cwd is in from npmWorkspaceRoot's, whatever package-lock.json is nearer,
153
+ // and past a workspace declaring workspaces of its own -- and any other package from the nearest
154
+ // package-lock.json beside a package.json, npm's prefix being no directory without one. A
155
+ // package.json with no name is a package where a workspace root takes it, which npm names by its
156
+ // directory, and else a `type` marker, its files the package's above.
157
+ function npmRoot(host, cwd, os) {
158
+ for (let dir = cwd; ; dir = dirname(dir)) {
159
+ if (isFile(join(dir, 'package.json'), host)) {
160
+ const root = npmWorkspaceRoot(host, dir, os)
161
+ if (root !== null) return root
162
+ if (holdsPackage(host, dir)) break
163
+ }
164
+ if (dirname(dir) === dir) break
165
+ }
166
+ return nearest(cwd, (dir) => holding(host, 'package-lock.json')(dir) && holding(host, 'package.json')(dir))
167
+ }
168
+
169
+ // npm's host: the machine's, but a libc npm finds only as glibc or musl, and none where it finds
170
+ // neither.
171
+ const npmHost = (npm, given) => {
172
+ const { libc, ...host } = target(given)
173
+ return { npm, ...host, ...(libc === 'glibc' || libc === 'musl' ? { libc } : {}) }
174
+ }
175
+
176
+ // What each package manager reproduced installs from: the kind of bundle it installs for; its
177
+ // lockfile; the name a root package.json's packageManager pins it by, where one does; the version
178
+ // reproduced where nothing pins one; the directory it installs cwd from, which holds the lockfile
179
+ // (for the os given, where that changes it); whether a directory holding the lockfile is installed
180
+ // from by itself, by the names in it and in each directory above it (`above()`, as listings); the
181
+ // projects it finds in a view of that directory, for the machine given; the directory it installs
182
+ // in each, and any it hides, as another package manager's; and the tree.
183
+ const PACKAGE_MANAGERS = {
184
+ pnpm: {
185
+ kind: 'js',
186
+ lockfile: 'pnpm-lock.yaml',
187
+ pin: 'pnpm',
188
+ version: '10.33.4',
189
+ // pnpm installs from the workspace's root, whatever pnpm-lock.yaml is nearer.
190
+ root: (host, cwd) => nearest(cwd, holding(host, 'pnpm-workspace.yaml')) ?? nearest(cwd, holding(host, 'pnpm-lock.yaml')),
191
+ alone: async (names, above) => !(await above()).some((dir) => dir.includes('pnpm-workspace.yaml')),
192
+ projects: (view, pnpm) => findPnpmProjects({ project: view, host: { pnpm } }),
193
+ installs: 'node_modules',
194
+ build: (view, pnpm, _file, given) => buildPnpmTree({ project: view, host: { pnpm, ...target(given) } }),
195
+ },
196
+ yarn1: {
197
+ kind: 'js',
198
+ lockfile: 'yarn.lock',
199
+ pin: 'yarn',
200
+ version: '1.22.22',
201
+ root: yarn1Root,
202
+ // A package.json above may declare it a workspace.
203
+ alone: async (names, above) => !(await above()).some((dir) => dir.includes('package.json')),
204
+ projects: (view) => findYarn1Workspaces({ project: view }),
205
+ installs: 'node_modules',
206
+ build: (view, yarn, file, given) => {
207
+ const { libc: _, ...host } = target({ ...given, libc: 'unknown' }) // yarn 1 matches no libc
208
+ return naming(file, buildYarn1Tree({ project: view, host: { yarn, ...host } }))
209
+ },
210
+ },
211
+ npm: {
212
+ kind: 'js',
213
+ lockfile: 'package-lock.json',
214
+ // npm reads no packageManager, so nothing pins it.
215
+ version: '11.21.0',
216
+ root: npmRoot,
217
+ // A package.json above may declare it a workspace.
218
+ alone: async (names, above) => !(await above()).some((dir) => dir.includes('package.json')),
219
+ projects: (view, _npm, given) => findNpmWorkspaces({ project: view, os: target(given).os }),
220
+ installs: 'node_modules',
221
+ build: (view, npm, file, given) => naming(file, buildNpmTree({ project: view, host: npmHost(npm, given) })),
222
+ },
223
+ soldeer: {
224
+ kind: 'sol',
225
+ lockfile: 'soldeer.lock',
226
+ version: '0.12.0',
227
+ // Soldeer installs from the nearest directory holding foundry.toml or soldeer.toml, never above
228
+ // the git repository's root; without one, from that root, else from cwd.
229
+ root: (host, cwd) => nearest(cwd, (dir) => holding(host, 'foundry.toml', 'soldeer.toml')(dir) || isDir(join(dir, '.git'), host)) ?? cwd,
230
+ alone: async (names, above) => names.includes('foundry.toml') || names.includes('soldeer.toml') || !(await above()).some((dir) => dir.includes('foundry.toml') || dir.includes('soldeer.toml')),
231
+ projects: () => ['.'],
232
+ installs: 'dependencies',
233
+ hides: 'node_modules',
234
+ async build(view, soldeer, file, given) {
235
+ const tree = await naming(file, buildSoldeerTree({ project: view, host: { soldeer, os: given?.os ?? process.platform } }))
236
+ settleSoldeer(view, tree.vfs, dirname(file))
237
+ return tree
238
+ },
239
+ },
240
+ }
241
+
242
+ // Their names.
243
+ const PACKAGE_MANAGER_NAMES = Object.keys(PACKAGE_MANAGERS)
244
+
245
+ // layOutTree's tree, with the host reading `project` through it: the tree serves what the package
246
+ // manager installs, and any file it writes beside it at the root.
247
+ export async function loadTree(options) {
248
+ const tree = await layOutTree(options)
249
+ const pm = PACKAGE_MANAGERS[options.packageManager]
250
+ const files = tree.vfs.readdir('/').filter((name) => tree.vfs.lstat(`/${name}`).type === 'file')
251
+ const installs = [...[...tree.projects].map((dir) => join(dir, pm.installs)), ...files]
252
+ return { ...tree, host: vfsHost(tree.vfs, { root: tree.root, outside: options.project, installs, hides: pm.hides }) }
253
+ }
254
+
255
+ // The lockfile `packageManager` installs from, e.g. 'pnpm-lock.yaml'.
256
+ export const lockfileOf = (packageManager) => PACKAGE_MANAGERS[packageManager].lockfile
257
+
258
+ // Whether `packageManager` installs a directory holding its lockfile from that directory itself, by
259
+ // the names in it and, from `above()`, the names in each directory above it.
260
+ export const installedAlone = (packageManager, names, above) => PACKAGE_MANAGERS[packageManager].alone(names, above)
261
+
262
+ // The real path of the directory `host` holds `cwd` (a real path) installed from by `pm`, for `os`,
263
+ // where it holds the lockfile; else null. That directory is cwd or above it, so none is without a
264
+ // lockfile there.
265
+ function rootOf(host, pm, cwd, os) {
266
+ if (nearest(cwd, holding(host, pm.lockfile)) === null) return null
267
+ const found = pm.root(host, cwd, os)
268
+ return found !== null && isFile(join(found, pm.lockfile), host) ? host.realpath(found) : null
269
+ }
270
+
271
+ // The real path of the directory the project `host` reads holds at `cwd` is installed from, for
272
+ // `os`, which holds the lockfile and which a bundle's paths are relative to; null without one.
273
+ export const lockfileRoot = (host, packageManager, cwd, os) => rootOf(host, PACKAGE_MANAGERS[packageManager], host.realpath(cwd), os)
274
+
275
+ // The package managers that install node_modules.
276
+ export const NODE_MODULES_MANAGERS = PACKAGE_MANAGER_NAMES.filter((name) => PACKAGE_MANAGERS[name].installs === 'node_modules')
277
+
278
+ // The one of the package managers `names` that installs `cwd` in the project `host` reads from a
279
+ // directory holding its lockfile, as lockfileRoot finds it for `os`: refused where none or more than
280
+ // one does.
281
+ export function detectPackageManager(name, host, cwd, { names = PACKAGE_MANAGER_NAMES, os } = {}) {
282
+ const real = host.realpath(cwd)
283
+ const found = names.flatMap((pm) => {
284
+ const root = rootOf(host, PACKAGE_MANAGERS[pm], real, os)
285
+ return root === null ? [] : [[pm, join(root, PACKAGE_MANAGERS[pm].lockfile)]]
286
+ })
287
+ if (found.length === 1) return found[0][0]
288
+ if (found.length === 0) throw noLockfile(name, real, names)
289
+ throw new Error(`${name}: no packageManager given, and more than one lockfile installs ${real}: ${found.map(([pm, file]) => `${file} (${pm})`).join(', ')}`)
290
+ }
291
+
292
+ // That none of the lockfiles of `names` installs `place`.
293
+ export const noLockfile = (name, place, names = PACKAGE_MANAGER_NAMES) => new Error(`${name}: no packageManager given, and none of ${names.map((pm) => PACKAGE_MANAGERS[pm].lockfile).join(', ')} installs ${place}`)
294
+
295
+ // Whether a directory's listing (listRepoDir's) holds `packageManager`'s lockfile.
296
+ const listsLockfile = (listing, packageManager) => listing.some((entry) => entry.path === PACKAGE_MANAGERS[packageManager].lockfile && entry.type === 'blob')
297
+
298
+ // The package managers whose lockfile `listings` (a directory's and those above it) hold, which are
299
+ // the only ones that may install that directory.
300
+ export const lockfilesListed = (listings) => PACKAGE_MANAGER_NAMES.filter((pm) => listings.some((listing) => listsLockfile(listing, pm)))
301
+
302
+ // A `packageManagerVersion` is one of the package manager given, never of one detected.
303
+ export function checkVersion(name, { packageManager, packageManagerVersion }) {
304
+ if (packageManagerVersion !== undefined && packageManager === undefined) throw new TypeError(`${name}: packageManagerVersion is only valid with packageManager`)
305
+ }
306
+
307
+ // The package manager of `names` the project `host` reads is built with at `cwd`: `packageManager`
308
+ // if given, else the one detected for `os`.
309
+ export function packageManagerFor(name, host, cwd, { packageManager, packageManagerVersion, os }, names = PACKAGE_MANAGER_NAMES) {
310
+ checkVersion(name, { packageManager, packageManagerVersion })
311
+ if (packageManager === undefined) return detectPackageManager(name, host, cwd, { names, os })
312
+ packageManagerOf(name, packageManager, names)
313
+ return packageManager
314
+ }
315
+
316
+ const KIND_LABELS = { js: 'JS', sol: 'Solidity' }
317
+
318
+ // Refused where no package manager of `names` builds bundles of `kind`.
319
+ export function checkKind(name, kind, names = PACKAGE_MANAGER_NAMES) {
320
+ if (names.some((pm) => PACKAGE_MANAGERS[pm].kind === kind)) return
321
+ const kinds = [...new Set(names.map((pm) => PACKAGE_MANAGERS[pm].kind))]
322
+ const builders = (of) => names.filter((pm) => PACKAGE_MANAGERS[pm].kind === of).join(', ')
323
+ throw new Error(names.length === 1
324
+ ? `${name}: only ${KIND_LABELS[kinds[0]]} bundles are built with ${names[0]}`
325
+ : `${name}: only ${kinds.map((of) => `${KIND_LABELS[of]} bundles (${builders(of)})`).join(' and ')} are built`)
326
+ }
327
+
328
+ // The PACKAGE_MANAGERS entry of `packageManager`, which has to be one of `names`.
329
+ export function packageManagerOf(name, packageManager, names = PACKAGE_MANAGER_NAMES) {
330
+ if (!names.includes(packageManager)) throw new TypeError(`${name}: packageManager must be one of ${names.map((n) => `'${n}'`).join(', ')}`)
331
+ return PACKAGE_MANAGERS[packageManager]
332
+ }
333
+
334
+ // Anything read as a @preventive/vfs Vfs: a Vfs of another copy of the package is one too.
335
+ export function checkVfs(name, vfs) {
336
+ for (const method of ['lstat', 'readdir', 'readFile', 'readlink']) {
337
+ if (typeof vfs?.[method] !== 'function') throw new TypeError(`${name}: vfs must be a @preventive/vfs Vfs holding the project`)
338
+ }
339
+ }
340
+
341
+ // -> { root, vfs, projects, stats, packageManager, packageManagerVersion }, of the project `project`
342
+ // holds that `cwd` is in, as `packageManager` installs it: the directory it installs from, as a real
343
+ // path; a new Vfs holding the tree, rooted there; the directories of the projects it finds, from
344
+ // there; deptree's counts; and the version reproduced, `packageManagerVersion` if given, else the one
345
+ // the root package.json's packageManager pins, else the default. deptree reads the project through a
346
+ // view of `project`, which nothing is written through.
347
+ async function layOutTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc }) {
348
+ const pm = PACKAGE_MANAGERS[packageManager]
349
+ const found = pm.root(project, cwd, os)
350
+ if (found === null) throw new Error(`no ${pm.lockfile} found in ${cwd} or any parent directory`)
351
+ const file = join(found, pm.lockfile)
352
+ if (!isFile(file, project)) throw new Error(`no ${pm.lockfile} found in ${found}, where ${cwd} is installed from`)
353
+ const pinned = pm.pin === undefined ? undefined : readJson(join(found, 'package.json'), project)?.packageManager
354
+ // Left out, deptree takes the one packageManager pins, and refuses another package manager.
355
+ const version = packageManagerVersion ?? (pinned === undefined ? pm.version : undefined)
356
+ const view = projectView(project, found)
357
+ const projects = new Set(pm.projects(view, version, { os, cpu, libc }))
358
+ const other = pm.kind === 'js' ? outsider(project, found, cwd, projects) : null
359
+ if (other !== null) throw new Error(`${file} does not install ${other}: it is none of the lockfile's projects`)
360
+ const { vfs, stats } = await pm.build(view, version, file, { os, cpu, libc })
361
+ return {
362
+ root: project.realpath(found),
363
+ vfs,
364
+ projects,
365
+ stats,
366
+ packageManager,
367
+ packageManagerVersion: version ?? (String(pinned).startsWith(`${pm.pin}@`) ? /^[^@]+@([^+]+)/u.exec(pinned)[1] : undefined),
368
+ }
369
+ }
370
+
371
+ // fs.Stats#mode carries the file type above the permission bits a Vfs stat holds.
372
+ const TYPE_BITS = { file: constants.S_IFREG, directory: constants.S_IFDIR, symlink: constants.S_IFLNK }
373
+
374
+ const statsFor = (st) => ({
375
+ isFile: () => st.type === 'file',
376
+ isDirectory: () => st.type === 'directory',
377
+ isSymbolicLink: () => st.type === 'symlink',
378
+ mode: TYPE_BITS[st.type] | st.mode,
379
+ })
380
+
381
+ const dirent = (name, type) => ({
382
+ name,
383
+ isFile: () => type === 'file',
384
+ isDirectory: () => type === 'directory',
385
+ isSymbolicLink: () => type === 'symlink',
386
+ })
387
+
388
+ // A view over the Vfs's bytes, never a copy.
389
+ const asBuffer = (bytes) => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
390
+
391
+ // What `vfs` holds at `p` itself (the last link not followed), or null.
392
+ function lstatOrNull(vfs, p) {
393
+ try {
394
+ return vfs.lstat(p)
395
+ } catch (err) {
396
+ if (NO_ENTRY.has(err?.code)) return null
397
+ throw err
398
+ }
399
+ }
400
+
401
+ const TREE = 'tree'
402
+ const NONE = 'none'
403
+ const OUTSIDE = 'outside'
404
+
405
+ const invalidPackageConfig = (path, cause) => Object.assign(new Error(`Invalid package config ${path}.`, { cause }), { code: 'ERR_INVALID_PACKAGE_CONFIG' })
406
+
407
+ // A `host` (@exodus/stasis-core/host) over `vfs`. Alone, every path is the Vfs's. With `root`, a
408
+ // real path, and `outside`, a host of the rest, the Vfs's `/` stands for `root`, holding the tree
409
+ // the package manager lays out there: each of the paths it `installs` (from `root`) is served from
410
+ // the Vfs only, whatever `outside` holds there; a directory named `hides` anywhere, and any
411
+ // node_modules out of `root`, is none; and everything else comes from `outside`. Symlinks cross
412
+ // between the two both ways, so realpaths are walked here one link at a time, and a path is read as
413
+ // the OS reads one: a `..` after a link leads up from where the link leads, never textually (Node's
414
+ // own resolution, `findPackageJSON` and `resolve`, normalizes its paths first, as Node does). A
415
+ // relative path is from `/`. What it reads is cached, as for a tree that holds still, unless `cache`
416
+ // is false.
417
+ export function vfsHost(vfs, { root, outside, installs = [], hides, cache = true } = {}) {
418
+ if (sep !== '/') throw new Error('The Vfs host is POSIX-only')
419
+ const disk = outside ?? null
420
+ const installed = new Set(installs)
421
+ // Paths here are absolute and normal.
422
+ const prefix = disk === null || root === '/' ? '/' : `${root}/`
423
+ // The root and the directories above it, whatever their names.
424
+ const towardRoot = (p) => p === root || prefix.startsWith(`${p}/`)
425
+ const zoneOf = (p) => {
426
+ if (disk === null) return TREE
427
+ if (!p.startsWith(prefix)) return hasNodeModulesSegment(p) && !towardRoot(p) ? NONE : OUTSIDE
428
+ const rel = p.slice(prefix.length)
429
+ for (let at = rel.indexOf('/'); at !== -1; at = rel.indexOf('/', at + 1)) {
430
+ if (installed.has(rel.slice(0, at))) return TREE
431
+ }
432
+ if (installed.has(rel)) return TREE
433
+ return hides !== undefined && rel.split('/').includes(hides) ? NONE : OUTSIDE
434
+ }
435
+ // The names of the installed paths in each directory holding one.
436
+ const installedIn = new Map()
437
+ for (const dir of disk === null ? [] : installed) {
438
+ const at = join(root, dirname(dir))
439
+ installedIn.set(at, [...(installedIn.get(at) ?? []), basename(dir)])
440
+ }
441
+ // A path in the tree, as the Vfs spells it.
442
+ const inVfs = disk === null ? (p) => p : (p) => `/${p.slice(prefix.length)}`
443
+ // `p` from `/`, as spelled: realpath resolves its `..` after the link before it.
444
+ const abs = (p) => (p.startsWith('/') ? p : `/${p}`)
445
+ const normal = (p) => resolve('/', p)
446
+ const memo = () => (cache ? new Map() : { get() {}, set() {} })
447
+
448
+ // `p` is a real path but for its last name; null when it exists and isn't a link.
449
+ const readlink = (p) => {
450
+ const zone = zoneOf(p)
451
+ if (zone === OUTSIDE) return disk.readlink(p)
452
+ if (zone === NONE) throw new VfsError('ENOENT', p)
453
+ const node = lstatOrNull(vfs, inVfs(p))
454
+ if (node === null) throw new VfsError('ENOENT', p)
455
+ return node.type === 'symlink' ? vfs.readlink(inVfs(p)) : null
456
+ }
457
+
458
+ const realCache = memo()
459
+ const realpath = (p, hops = 0) => {
460
+ if (p === '/') return '/'
461
+ const hit = realCache.get(p)
462
+ if (hit !== undefined) {
463
+ if (hit instanceof Error) throw hit
464
+ return hit
465
+ }
466
+ let result
467
+ try {
468
+ const parentReal = realpath(dirname(p), hops)
469
+ const name = basename(p)
470
+ // A `.` or `..` is taken in the real dir before it, which must be one.
471
+ if (name === '.' || name === '..') {
472
+ if (statOf(parentReal)?.isDirectory() !== true) throw new VfsError('ENOTDIR', p)
473
+ result = name === '.' ? parentReal : dirname(parentReal)
474
+ realCache.set(p, result)
475
+ return result
476
+ }
477
+ const candidate = join(parentReal, name)
478
+ const link = readlink(candidate)
479
+ if (link === null) {
480
+ result = candidate
481
+ } else {
482
+ if (hops >= 40) throw new VfsError('ELOOP', p)
483
+ result = realpath(link.startsWith('/') ? link : `${dirname(candidate)}/${link}`, hops + 1)
484
+ }
485
+ } catch (err) {
486
+ if (hops === 0 && (err.code === 'ENOENT' || err.code === 'ELOOP')) realCache.set(p, err)
487
+ throw err
488
+ }
489
+ realCache.set(p, result)
490
+ return result
491
+ }
492
+
493
+ const nodeOf = (real) => {
494
+ const node = lstatOrNull(vfs, inVfs(real))
495
+ if (node === null) throw new VfsError('ENOENT', real)
496
+ return node
497
+ }
498
+
499
+ // Node's findPackageJSON refuses a manifest that is no JSON object.
500
+ const manifestErrors = memo()
501
+ const checkManifest = (path) => {
502
+ let error = manifestErrors.get(path)
503
+ if (error === undefined) {
504
+ error = null
505
+ try {
506
+ const data = JSON.parse(packageJSONText(host.readFile(path)))
507
+ if (!isPlainObject(data)) error = invalidPackageConfig(path)
508
+ } catch (cause) {
509
+ error = invalidPackageConfig(path, cause)
510
+ }
511
+ manifestErrors.set(path, error)
512
+ }
513
+ if (error !== null) throw error
514
+ }
515
+
516
+ const statCache = memo()
517
+ const statOf = (real) => {
518
+ if (zoneOf(real) === OUTSIDE) return disk.stat(real)
519
+ let stats = statCache.get(real)
520
+ if (stats === undefined) {
521
+ const node = lstatOrNull(vfs, inVfs(real))
522
+ stats = node !== null && node.type !== 'symlink' ? statsFor(node) : null
523
+ statCache.set(real, stats)
524
+ }
525
+ return stats
526
+ }
527
+ // A path ending in `/` or `/.` names a directory, which path.resolve drops.
528
+ const namesDir = (p) => p.endsWith('/') || p.endsWith('/.')
529
+ const checkDir = (p, real) => {
530
+ if (namesDir(p) && statOf(real)?.isDirectory() === false) throw new VfsError('ENOTDIR', p)
531
+ }
532
+
533
+ const host = {
534
+ stat(p) {
535
+ let real
536
+ try {
537
+ real = realpath(abs(p))
538
+ } catch {
539
+ return null
540
+ }
541
+ const stats = statOf(real)
542
+ return stats !== null && namesDir(p) && !stats.isDirectory() ? null : stats
543
+ },
544
+ readFile(p) {
545
+ const real = realpath(abs(p))
546
+ checkDir(p, real)
547
+ if (zoneOf(real) === OUTSIDE) return disk.readFile(real)
548
+ if (nodeOf(real).type !== 'file') throw new VfsError('EISDIR', p)
549
+ return asBuffer(vfs.readFile(inVfs(real)))
550
+ },
551
+ readdir(p) {
552
+ const real = realpath(abs(p))
553
+ if (zoneOf(real) === TREE) {
554
+ if (nodeOf(real).type !== 'directory') throw new VfsError('ENOTDIR', p)
555
+ const dir = inVfs(real)
556
+ return vfs.readdir(dir).map((name) => dirent(name, vfs.lstat(join(dir, name)).type)).toSorted(byName)
557
+ }
558
+ // An outside directory shows the tree's installed paths in place of its own, and nothing that
559
+ // is none.
560
+ const out = disk.readdir(real).filter((d) => zoneOf(join(real, d.name)) === OUTSIDE)
561
+ for (const name of installedIn.get(real) ?? []) {
562
+ const node = lstatOrNull(vfs, inVfs(join(real, name)))
563
+ if (node !== null) out.push(dirent(name, node.type))
564
+ }
565
+ return out.toSorted(byName)
566
+ },
567
+ readlink(p) {
568
+ if (namesDir(p)) {
569
+ checkDir(p, realpath(abs(p)))
570
+ return null
571
+ }
572
+ p = abs(p)
573
+ // A `..` names a directory, as lstat takes it.
574
+ if (basename(p) === '..') {
575
+ realpath(p)
576
+ return null
577
+ }
578
+ return readlink(join(realpath(dirname(p)), basename(p)))
579
+ },
580
+ realpath(p) {
581
+ const real = realpath(abs(p))
582
+ checkDir(p, real)
583
+ return real
584
+ },
585
+ // As Node's: the nearest package.json above a file's real path, never out of a node_modules dir;
586
+ // one there that can't be read is refused (packageJSONStat).
587
+ findPackageJSON(p) {
588
+ let from = normal(p)
589
+ if (host.stat(from)?.isFile()) from = realpath(from)
590
+ for (let dir = dirname(from); basename(dir) !== 'node_modules'; dir = dirname(dir)) {
591
+ const candidate = join(dir, 'package.json')
592
+ if (packageJSONStat(host, candidate)?.isFile()) {
593
+ checkManifest(candidate)
594
+ return candidate
595
+ }
596
+ if (dirname(dir) === dir) break
597
+ }
598
+ return undefined
599
+ },
600
+ resolve(parentFile, specifier, conditions) {
601
+ return (resolver ?? createNodeResolver(host)).resolve(normal(parentFile), specifier, conditions)
602
+ },
603
+ }
604
+ const resolver = cache ? createNodeResolver(host) : undefined
605
+ return host
606
+ }