@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.
- package/README.md +32 -8
- package/bin/stasis.js +386 -213
- package/package.json +52 -12
- package/src/audit-corrections.js +96 -0
- package/src/audit.js +289 -64
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +14 -22
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +96 -128
- package/src/cmd/bundle.js +1121 -654
- package/src/cmd/diff.js +2 -19
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +7 -19
- package/src/diff.js +80 -129
- package/src/installed-commits.js +182 -0
- package/src/json-stream.js +283 -0
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +49 -116
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +375 -402
- package/src/loaders/rust.js +3254 -242
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -162
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +150 -0
- package/src/metro-transformer.js +3 -2
- package/src/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +370 -0
- package/src/resolve-node.js +419 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +120 -97
- package/src/scan.js +348 -180
- package/src/vfs-bundle/entries.js +144 -0
- package/src/vfs-bundle/github.js +202 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +647 -0
- package/src/vfs-bundle.js +80 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -44
- 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
|
+
}
|