@exodus/stasis 1.0.0-beta.1 → 1.0.0-beta.10

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