@exodus/stasis 1.0.0-beta.7 → 1.0.0-beta.9
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 +27 -24
- package/bin/stasis.js +106 -177
- package/package.json +7 -5
- package/src/audit-corrections.js +45 -21
- package/src/cmd/bundle.js +194 -47
- package/src/installed-commits.js +182 -0
- package/src/loaders/php.js +43 -16
- package/src/resolve-fields.js +62 -30
- package/src/resolve-node.js +9 -6
- package/src/scan.js +8 -3
- package/src/vfs-bundle/entries.js +9 -5
- package/src/vfs-bundle/github.js +4 -1
- package/src/vfs-bundle/tree.js +49 -15
- package/src/vfs-bundle.js +13 -8
package/src/audit-corrections.js
CHANGED
|
@@ -6,38 +6,62 @@ import { satisfies, valid } from '@preventive/upstream/semver.js'
|
|
|
6
6
|
// package pulled in only through corrected files is skipped by the audit (see
|
|
7
7
|
// collectPackagesFromFile) -- none of its real code ships, so no advisory applies.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// Exported as `@exodus/stasis/audit-corrections`, as plain data, so a tool that
|
|
10
|
+
// lays out or reads packages without stasis -- a tree @preventive/deptree builds,
|
|
11
|
+
// a server reading bundles -- can apply the same corrections without its own copy.
|
|
12
|
+
// Each entry names a package of `ecosystem`, one audit-irrelevant `file` (relative
|
|
13
|
+
// to the module dir), `range` -- the semver range VERIFIED to match the rationale --
|
|
14
|
+
// and `integrity`, the sha512 of every distinct copy of `file` the versions in
|
|
15
|
+
// `range` publish, as a stasis lockfile records a file: a holder of the file's bytes
|
|
16
|
+
// can tell they are one of the copies verified. The correction never applies outside
|
|
17
|
+
// the range: a release could change the file, so the range is widened only after
|
|
18
|
+
// re-checking the file in the new versions, adding any new copy's integrity. Frozen,
|
|
19
|
+
// as the audit reads these very objects.
|
|
20
|
+
export const CORRECTIONS = Object.freeze([
|
|
14
21
|
{
|
|
15
22
|
// ws's browser build is a noop stub -- `module.exports = function () { throw
|
|
16
23
|
// new Error('ws does not work in the browser...') }` -- with none of the
|
|
17
|
-
// WebSocket implementation in it.
|
|
18
|
-
// (latest at the time of writing)
|
|
24
|
+
// WebSocket implementation in it. browser.js ships from 6.0.0 on; every copy
|
|
25
|
+
// through 8.22.0 (latest at the time of writing) is one of these two, which
|
|
26
|
+
// differ only in a space; re-check browser.js before widening.
|
|
27
|
+
ecosystem: 'npm',
|
|
19
28
|
name: 'ws',
|
|
20
|
-
|
|
21
|
-
range: '<=8.
|
|
29
|
+
file: 'browser.js',
|
|
30
|
+
range: '<=8.22.0',
|
|
31
|
+
integrity: [
|
|
32
|
+
'sha512-98uEc4/THUpHCA7AQwBKLHHmxV6leyGmPkDAYZsoBiigxjKL8//TIb/oUKDUJLc+s/pwdGA9LR0F9e7nZHfXUQ==', // `function ()`: 6.0.0 - 6.1.1, 7.3.1 - 8.22.0
|
|
33
|
+
'sha512-F7vx6oiX9vsFoxNPTudk20Hjp7IzJePAJCYaSoX9FMJ2JaGVcFx4Bkw5W7+I3a1DsonitnosbpuG1qAMsUwH7w==', // `function()`: 6.1.2 - 7.3.0
|
|
34
|
+
],
|
|
22
35
|
},
|
|
23
36
|
{
|
|
24
37
|
// node-fetch's browser build re-exports the environment's native fetch
|
|
25
38
|
// (`module.exports = globalObject.fetch`, plus Headers/Request/Response) --
|
|
26
39
|
// none of the node-fetch implementation, where its advisories live, is in it.
|
|
27
|
-
// browser.js ships only through 2.7.0 (3.x dropped it);
|
|
40
|
+
// browser.js ships only from 2.0.0 through 2.7.0 (3.x dropped it); every copy
|
|
41
|
+
// in the range is one of these, which differ in how they find the global.
|
|
42
|
+
ecosystem: 'npm',
|
|
28
43
|
name: 'node-fetch',
|
|
29
|
-
|
|
44
|
+
file: 'browser.js',
|
|
30
45
|
range: '<=2.7.0',
|
|
46
|
+
integrity: [
|
|
47
|
+
'sha512-uCZB0X6rUVIf5lZU2NN9/Z1Fd6cPCLg4M9eduYxOgSP3QZNwi0/xSa/s6AUFtvLz4y4MK02SJihOIhDoFIzdiA==', // 2.0.0 - 2.1.1
|
|
48
|
+
'sha512-toQ4RDdyjsAxTaxnaLs2AE56rXws+jMZV896U8pP0MSuKRiq8ZGKkOTMsvyCJoAJTbrgoQpr89H2n8+Z1ALcYA==', // 2.1.2
|
|
49
|
+
'sha512-LOd+I1R3xYSCHFN9oj3xeN3wa7fYYx5XQKTiKkvWn4dSYVgHI2jsXZp0mJt5KlQg3ECXXh5UtpEW7kGu1dG/aQ==', // 2.2.0 - 2.2.1
|
|
50
|
+
'sha512-2yo4Ysl+g09ocycwEBcN7IkSvM/KPpO9CW+1FI+0QfzvWJfaQepo+WEqn1/o5Og1weM14flcdv23hqZtxFXFUw==', // 2.3.0 - 2.6.0
|
|
51
|
+
'sha512-WgHaZJVwFovVrKVySaKzxzXssn5kY8y3x8NJThodDRL9nwUxV7Q1z/RRzuLZ2lNkxabHLpG/l+vu7KQ2tGbP8g==', // 2.6.1 - 2.6.7
|
|
52
|
+
'sha512-84sQFcu3uzgEsJOjIhz6tE8vInV1STCA4sX5EI9NK3Tt8n/Kb1serbKMz+YkdOAuCspHRiSUW2qV01z0wOgIXg==', // 2.6.8
|
|
53
|
+
'sha512-SkBfLO20d2Ngr6nhxgh/Pt6Yqe6oW8EB99KSPq0lrW0se8jIiRF3YpOe7hHbfNWxuNCsKoZZT98uCJI9OJAa/Q==', // 2.6.9 - 2.7.0
|
|
54
|
+
],
|
|
31
55
|
},
|
|
32
|
-
]
|
|
56
|
+
].map((correction) => Object.freeze({ ...correction, integrity: Object.freeze(correction.integrity) })))
|
|
33
57
|
|
|
34
58
|
// Is `rel` (a file path relative to the module dir) audit-irrelevant for
|
|
35
|
-
// `name@version`? Unknown or unparsable versions are never
|
|
36
|
-
// closed: the package stays audited).
|
|
37
|
-
function isCorrectedFile(name, version, rel) {
|
|
38
|
-
for (const
|
|
39
|
-
if (
|
|
40
|
-
if (valid(version) && satisfies(version, range)) return true
|
|
59
|
+
// `name@version` of `ecosystem`? Unknown or unparsable versions are never
|
|
60
|
+
// corrected (fail closed: the package stays audited).
|
|
61
|
+
function isCorrectedFile(name, version, rel, ecosystem) {
|
|
62
|
+
for (const correction of CORRECTIONS) {
|
|
63
|
+
if (correction.ecosystem !== ecosystem || correction.name !== name || correction.file !== rel) continue
|
|
64
|
+
if (valid(version) && satisfies(version, correction.range)) return true
|
|
41
65
|
}
|
|
42
66
|
return false
|
|
43
67
|
}
|
|
@@ -65,8 +89,8 @@ const isManifest = (rel, ecosystem) => MANIFESTS[ecosystem]?.has(rel.slice(rel.l
|
|
|
65
89
|
// Is `rel` evidence that `name@version`'s REAL code is present, of a dependency of
|
|
66
90
|
// `ecosystem` (npm by default)? This is the one rule every audit surface shares --
|
|
67
91
|
// package presence, the reason column, and the --why chain graph all count a file
|
|
68
|
-
// (or an edge targeting it) only when it passes. Manifests never do;
|
|
69
|
-
// files
|
|
92
|
+
// (or an edge targeting it) only when it passes. Manifests never do; CORRECTIONS'
|
|
93
|
+
// files don't within their verified range.
|
|
70
94
|
export function isEvidenceFile(name, version, rel, ecosystem = 'npm') {
|
|
71
|
-
return !isManifest(rel, ecosystem) &&
|
|
95
|
+
return !isManifest(rel, ecosystem) && !isCorrectedFile(name, version, rel, ecosystem)
|
|
72
96
|
}
|
package/src/cmd/bundle.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { isUtf8 } from 'node:buffer'
|
|
2
2
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
-
import { dirname, extname, join, posix, relative, resolve } from 'node:path'
|
|
3
|
+
import { basename, dirname, extname, join, posix, relative, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { brotliDecompressSync } from 'node:zlib'
|
|
6
6
|
|
|
7
|
-
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
7
|
+
import { Bundle, isValidRepoField } from '@exodus/stasis-core/bundle'
|
|
8
8
|
import { Lockfile } from '@exodus/stasis-core/lockfile'
|
|
9
9
|
import { importsTypescriptByOutputName, scan } from '../scan.js'
|
|
10
10
|
import { createFieldResolver, resolveConditions } from '../resolve-fields.js'
|
|
11
11
|
import { discoverTsconfig, isDir, loadTsconfigPaths, packageTsconfigPaths } from '../resolve-typescript.js'
|
|
12
12
|
import { createMetroResolver } from '../metro-resolver.js'
|
|
13
|
+
import { pinInstalledCommits } from '../installed-commits.js'
|
|
13
14
|
import { State } from '@exodus/stasis-core/state'
|
|
14
15
|
import { sha512integrity } from '@exodus/stasis-core/state-util'
|
|
15
|
-
import { detectRepo, findPackageMetadata, normalizeEntries, packageType, readJson, readModuleManifest, readPackageJson, readRegularFileOrNull } from '@exodus/stasis-core/bundle-util'
|
|
16
|
-
import { RN_CORE_INCLUDE_FILES, assertRealPathWithinBase, classifyNativeCapture, hasNodeModulesSegment, isDotEnvFile, isExcludedNativeDir, isExecutableFile, isNativeArtifact, isNativeManifest, isPathWithin, isPodspec, isSkippedNativeWalkDir, moduleFileKey, moduleInfo, parseResourcesOption, posixPathEscapes, refineNativeCapture, relativeEscapes, splitNodeModulesPath, toPosix } from '@exodus/stasis-core/util'
|
|
16
|
+
import { checkoutCommit, detectRepo, findPackageMetadata, normalizeEntries, packageType, parseGithubRepository, readJson, readModuleManifest, readPackageJson, readRegularFileOrNull } from '@exodus/stasis-core/bundle-util'
|
|
17
|
+
import { EMPTY_MODULE_PATH, RN_CORE_INCLUDE_FILES, assertRealPathWithinBase, classifyNativeCapture, hasNodeModulesSegment, isDotEnvFile, isExcludedNativeDir, isExecutableFile, isNativeArtifact, isNativeManifest, isPathWithin, isPodspec, isSkippedNativeWalkDir, moduleFileKey, moduleInfo, parseResourcesOption, posixPathEscapes, refineNativeCapture, relativeEscapes, splitNodeModulesPath, toPosix } from '@exodus/stasis-core/util'
|
|
17
18
|
import { diskHost } from '@exodus/stasis-core/host'
|
|
19
|
+
import { parseSoldeerLockfile } from '@preventive/lockfile/soldeer.js'
|
|
18
20
|
import {
|
|
19
21
|
SOLIDITY_PACKAGE_MANIFESTS,
|
|
20
22
|
SOLIDITY_ROOT_MANIFESTS,
|
|
@@ -82,42 +84,105 @@ function parseSoldeerDir(seg) {
|
|
|
82
84
|
return m ? { name: m[1], version: m[2] } : { name: seg, version: '0.0.0' }
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
87
|
+
// A url as git fetches from a host: scp's `[user@]host:path`, with no scheme and a colon before its
|
|
88
|
+
// first slash, or a URL of a transport git has itself (`http`, `https`, `git`, `ssh` and its
|
|
89
|
+
// `git+ssh`/`ssh+git` aliases, lowercase, as git spells them), its authority, `[userinfo@]host[:port]`,
|
|
90
|
+
// ending at the first slash after it. Not, then, what a package.json spells GitHub's that git reads
|
|
91
|
+
// another way or not at all: `ssh://git@github.com:owner/name`, whose host is `github.com:owner` to
|
|
92
|
+
// git; `git+https://` or `HTTPS://`, transports git doesn't have; nor `owner/name`, a path, as is any
|
|
93
|
+
// url with no colon before its first slash.
|
|
94
|
+
const GIT_URL = /^(?:[^/]*:(?!\/\/)|(?:https?|git|ssh|git\+ssh|ssh\+git):\/\/(?:[^/@]*@)?[^/:@]*(?::\d*)?\/)/u
|
|
95
|
+
|
|
96
|
+
// The GitHub repository, `owner/name`, a .gitmodules `url` names (parseGithubRepository), else null:
|
|
97
|
+
// none for a host merely holding `github.com` (`https://notgithub.com/o/n`), nor for a url git
|
|
98
|
+
// doesn't fetch as one (GIT_URL), nor for npm's `github:owner/name`, the host `github`'s path to git,
|
|
99
|
+
// whatever an ssh alias or `insteadOf` makes of it, nor one with a `#`, which git keeps in the path
|
|
100
|
+
// it asks for (`owner/name.git#v1`) where npm takes a committish. git's `ssh+git://`, which a
|
|
101
|
+
// package.json doesn't spell, is its `ssh://`.
|
|
102
|
+
const submoduleGithub = (url) => (GIT_URL.test(url) && !/^github:/iu.test(url) && !url.includes('#') ? parseGithubRepository(url.replace(/^ssh\+git:/u, 'ssh:')) : null)
|
|
103
|
+
|
|
104
|
+
// The GitHub ones of `submodules` (readGitmodules), as Map<submodulePath, { name, branch }>.
|
|
92
105
|
function githubSubmodules(submodules) {
|
|
93
106
|
const byPath = new Map()
|
|
94
107
|
for (const { path, url, branch } of submodules) {
|
|
95
|
-
const name = url &&
|
|
108
|
+
const name = url && submoduleGithub(url)
|
|
96
109
|
if (name) byPath.set(path, { name, branch })
|
|
97
110
|
}
|
|
98
111
|
return byPath
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
// The root of the GitHub repository `github` at `commit`, where both are valid; else undefined.
|
|
115
|
+
const repoRootAt = (github, commit) => (isValidRepoField('github', github) && isValidRepoField('commit', commit) ? { github, directory: '', commit } : undefined)
|
|
116
|
+
|
|
117
|
+
// The text of `rel`, a file of the project root, read as `ownership` vouches for it; null where it
|
|
118
|
+
// refuses it, nothing is there, or it isn't UTF-8.
|
|
119
|
+
function ownedText(baseDir, rel, ownership, host) {
|
|
120
|
+
const { reason, real, outside } = ownership.of(rel)
|
|
121
|
+
if (reason || real === null || outside) return null
|
|
122
|
+
try {
|
|
123
|
+
const buf = readRegularFileOrNull(join(host.realpath(baseDir), real), rel, host)
|
|
124
|
+
return buf === null ? null : decodeUtf8(buf, rel)
|
|
125
|
+
} catch {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Each git dependency the root's soldeer.lock installs from GitHub, by its folder under
|
|
131
|
+
// `dependencies/` (`<name>-<version>`, as Soldeer names it): the repository's root at the lockfile's
|
|
132
|
+
// `rev`, which Soldeer checks out there. None where the lockfile can't be read or parsed: it is
|
|
133
|
+
// metadata, which never stops a build.
|
|
134
|
+
function soldeerGitRepos(baseDir, ownership, host) {
|
|
135
|
+
const text = ownedText(baseDir, 'soldeer.lock', ownership, host)
|
|
136
|
+
let lock = null
|
|
137
|
+
try {
|
|
138
|
+
lock = text === null ? null : parseSoldeerLockfile(text)
|
|
139
|
+
} catch {
|
|
140
|
+
// Not one Soldeer writes: nothing is recorded from it.
|
|
141
|
+
}
|
|
142
|
+
const repos = new Map()
|
|
143
|
+
for (const dep of Object.values(lock?.dependencies ?? {})) {
|
|
144
|
+
const repo = dep.type === 'git' ? repoRootAt(parseGithubRepository(dep.git), dep.rev) : undefined
|
|
145
|
+
if (repo !== undefined) repos.set(`${dep.name}-${dep.version}`, repo)
|
|
146
|
+
}
|
|
147
|
+
return repos
|
|
148
|
+
}
|
|
149
|
+
|
|
101
150
|
// Classify a Solidity file's dep bucket: Soldeer (`dependencies/<name>-<version>/`) or a
|
|
102
151
|
// github submodule (`lib/`, via the `.gitmodules` `ownership` read), else null to defer to the
|
|
103
152
|
// node_modules/workspace logic. `ownership.assert` vets a package.json path before it is read
|
|
104
|
-
// through `host`.
|
|
153
|
+
// through `host`. A bucket's `repo` is the GitHub repository it is checked out from, at its root, at
|
|
154
|
+
// the commit its checkout's HEAD is at, as the bundle's own `repo` reads one: a submodule's, from
|
|
155
|
+
// the GitHub repository its .gitmodules url names; a Soldeer git dependency's, where it is its
|
|
156
|
+
// soldeer.lock `rev` (soldeerGitRepos), as Soldeer holds a checkout to the lockfile before taking it
|
|
157
|
+
// for installed: a folder left at another commit since the lockfile changed, or no checkout, records
|
|
158
|
+
// none.
|
|
105
159
|
function makeSolidityClassifier(baseDir, ownership, host) {
|
|
106
160
|
const submodules = githubSubmodules(ownership.submodules)
|
|
107
161
|
const check = ownership.assert
|
|
108
162
|
const versions = new Map() // a submodule's package.json version, read once
|
|
163
|
+
const commits = new Map() // a bucket's checkout's commit, read once
|
|
164
|
+
const commitOf = (dir) => {
|
|
165
|
+
if (!commits.has(dir)) commits.set(dir, checkoutCommit(join(baseDir, dir), host))
|
|
166
|
+
return commits.get(dir)
|
|
167
|
+
}
|
|
168
|
+
let soldeer // soldeerGitRepos, read at the first Soldeer file
|
|
109
169
|
return (path) => {
|
|
110
170
|
if (path.startsWith('dependencies/')) {
|
|
111
171
|
const seg = path.slice('dependencies/'.length).split('/')[0]
|
|
112
172
|
if (seg) {
|
|
113
173
|
const { name, version } = parseSoldeerDir(seg)
|
|
114
|
-
|
|
174
|
+
const bucketDir = `dependencies/${seg}`
|
|
175
|
+
soldeer ??= soldeerGitRepos(baseDir, ownership, host)
|
|
176
|
+
const locked = soldeer.get(seg)
|
|
177
|
+
const repo = locked !== undefined && commitOf(bucketDir) === locked.commit ? locked : undefined
|
|
178
|
+
return { bucketDir, name, version, ecosystem: 'soldeer', ...(repo === undefined ? {} : { repo }) }
|
|
115
179
|
}
|
|
116
180
|
}
|
|
117
181
|
for (const [sub, { name, branch }] of submodules) {
|
|
118
182
|
if (path === sub || path.startsWith(`${sub}/`)) {
|
|
119
183
|
if (!versions.has(sub)) versions.set(sub, readPackageJson(baseDir, moduleFileKey(sub, 'package.json'), { strict: true, check, host })?.version)
|
|
120
|
-
|
|
184
|
+
const repo = repoRootAt(name, commitOf(sub))
|
|
185
|
+
return { bucketDir: sub, name, version: versions.get(sub) ?? branch ?? '0.0.0', ecosystem: 'github', ...(repo === undefined ? {} : { repo }) }
|
|
121
186
|
}
|
|
122
187
|
}
|
|
123
188
|
return null
|
|
@@ -801,9 +866,11 @@ function innermostRootOf(baseDir, files, root, host) {
|
|
|
801
866
|
const SOURCE_EXTS = ['js', 'jsx', 'json', 'ts', 'tsx']
|
|
802
867
|
// React Native preset mainFields for `--metro` (which also sets the RN conditions + platform suffixes).
|
|
803
868
|
const METRO_MAIN_FIELDS = ['react-native', 'browser', 'main']
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
869
|
+
// What esbuild and webpack 5 both assert on every platform besides `import`/`require` and
|
|
870
|
+
// `default`, and so `--mainFields` too: `module`, the bundler-only condition a package's ESM build
|
|
871
|
+
// sits under, from an import and a require alike. (esbuild drops it when given conditions of its
|
|
872
|
+
// own; webpack keeps it, as `--conditions` adds to it here.) Node and Metro never assert it.
|
|
873
|
+
const BUNDLER_CONDITIONS = ['module']
|
|
807
874
|
|
|
808
875
|
// Recursively collect the files under `dirAbs` whose name `keep` takes, skipping build output,
|
|
809
876
|
// symlinks (cycle/escape hazard) and, directly in `dirAbs`, the dirs `skipAtRoot` names. Absolute
|
|
@@ -848,10 +915,11 @@ function nativeModuleFiles(pkgAbs, host) {
|
|
|
848
915
|
|
|
849
916
|
// The built-in field/suffix resolver (resolve-fields.js) the legacy-field build resolves with on
|
|
850
917
|
// `platform` (null for --mainFields), its `mainFields` (Metro's under --metro), and the conditions
|
|
851
|
-
// it adds to
|
|
852
|
-
//
|
|
918
|
+
// it adds to the bundler base (`import`/`require` + `default`, never Node's `node`; see
|
|
919
|
+
// resolveConditions): --metro asserts the RN conditions (+ browser on web), Metro's own set;
|
|
920
|
+
// --mainFields asserts esbuild's and webpack's `module` (BUNDLER_CONDITIONS) and the user's --conditions.
|
|
853
921
|
export function fieldResolverFor(platform, { mainFields, metro = false, conditions = [], typescript = false, typescriptPaths = null, host = diskHost }) {
|
|
854
|
-
const extras = metro ? ['react-native', ...(platform === 'web' ? ['browser'] : [])] : conditions
|
|
922
|
+
const extras = metro ? ['react-native', ...(platform === 'web' ? ['browser'] : [])] : [...new Set([...BUNDLER_CONDITIONS, ...conditions])]
|
|
855
923
|
const fields = metro ? METRO_MAIN_FIELDS : mainFields
|
|
856
924
|
const resolver = createFieldResolver({
|
|
857
925
|
mainFields: fields,
|
|
@@ -879,8 +947,17 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), env = process.env, e
|
|
|
879
947
|
const absEntries = entries.map((e) => resolve(baseDir, e))
|
|
880
948
|
const normalized = normalizeEntries(entries, cwd)
|
|
881
949
|
|
|
950
|
+
// A resolved file arrives by its real path, an entry as spelled: under a cwd named through a link,
|
|
951
|
+
// the files the scan reaches lie below the root's real path, not below `baseDir` as spelled. Either
|
|
952
|
+
// is the root; a file below neither is outside it. (Realpathed only when needed, so a missing cwd
|
|
953
|
+
// is still the scan's missing entry.)
|
|
954
|
+
let realRoot
|
|
882
955
|
const toRel = (abs) => {
|
|
883
|
-
|
|
956
|
+
let rel = toPosix(relative(baseDir, abs))
|
|
957
|
+
if (relativeEscapes(rel)) {
|
|
958
|
+
realRoot ??= host.realpath(baseDir)
|
|
959
|
+
rel = toPosix(relative(realRoot, abs))
|
|
960
|
+
}
|
|
884
961
|
if (relativeEscapes(rel)) {
|
|
885
962
|
throw new Error(`Bundle would reach a file outside the project root: ${abs}`)
|
|
886
963
|
}
|
|
@@ -958,8 +1035,10 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), env = process.env, e
|
|
|
958
1035
|
const realBase = host.realpath(baseDir)
|
|
959
1036
|
for (const abs of reached) {
|
|
960
1037
|
const rel = toRel(abs)
|
|
961
|
-
// Security:
|
|
962
|
-
//
|
|
1038
|
+
// Security: a resolved file arrives by its real path, which toRel already holds to the root,
|
|
1039
|
+
// but an entry is taken as spelled (and --metro-resolver keeps a path it can't realpath), so an
|
|
1040
|
+
// in-tree-named symlink escaping the root would slip past toRel's textual check -- realpath and
|
|
1041
|
+
// fail closed.
|
|
963
1042
|
assertRealPathWithinBase(realBase, baseDir, rel, host)
|
|
964
1043
|
const buf = host.readFile(abs)
|
|
965
1044
|
if (resourceRels.has(rel)) {
|
|
@@ -978,25 +1057,89 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), env = process.env, e
|
|
|
978
1057
|
put(EMPTY_MODULE_PATH, '', 'commonjs', Buffer.alloc(0))
|
|
979
1058
|
}
|
|
980
1059
|
|
|
1060
|
+
// The package a bundled file is in (findPackageMetadata: its pkgDir and identity, null where no
|
|
1061
|
+
// manifest names one), memoized per directory so a package's many files don't each re-walk to it.
|
|
1062
|
+
const packageOf = packageLookup(baseDir, { host })
|
|
1063
|
+
|
|
981
1064
|
// --metro also carries each bundled dependency's native build-input surface (ios/android
|
|
982
|
-
// sources + podspecs), scoped to the
|
|
983
|
-
// source is stored as code under a language tag; other assets as
|
|
1065
|
+
// sources + podspecs), scoped to the packages in the bundle installed or linked into
|
|
1066
|
+
// node_modules. Native source is stored as code under a language tag; other assets as
|
|
1067
|
+
// 'resource'/'resource:base64'.
|
|
984
1068
|
if (metro) {
|
|
985
|
-
const pkgDirs = new
|
|
1069
|
+
const pkgDirs = new Map() // package dir -> its package's name
|
|
1070
|
+
// Follow the CODE/module graph only: a package reached solely for an asset (--resources) is
|
|
1071
|
+
// not a linked native dependency, so it must not drag in its ios/android surface.
|
|
1072
|
+
const isCode = (rel) => !resourceRels.has(rel)
|
|
986
1073
|
for (const abs of reached) {
|
|
987
|
-
// Follow the CODE/module graph only: a package reached solely for an asset (--resources) is
|
|
988
|
-
// not a linked native dependency, so it must not drag in its ios/android surface.
|
|
989
1074
|
const rel = toRel(abs)
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1075
|
+
const nm = isCode(rel) && splitNodeModulesPath(rel)
|
|
1076
|
+
if (nm) pkgDirs.set(nm.dir, nm.name)
|
|
1077
|
+
}
|
|
1078
|
+
// A package linked into node_modules from out of it (a workspace package) is reached by its real
|
|
1079
|
+
// path, out of node_modules, but is a dependency all the same, however an import reaches it (by
|
|
1080
|
+
// name or alias, through a browser/react-native map, by a relative path): one linked, under any
|
|
1081
|
+
// name, into a node_modules its importer finds packages in (the directories the resolver looks
|
|
1082
|
+
// in). One linked nowhere there is the project's own source.
|
|
1083
|
+
const linksIn = new Map() // directory -> Set of the real package dirs linked into a node_modules it finds packages in
|
|
1084
|
+
const linkedFrom = (dir) => {
|
|
1085
|
+
let links = linksIn.get(dir)
|
|
1086
|
+
if (links !== undefined) return links
|
|
1087
|
+
const parent = dirname(dir)
|
|
1088
|
+
links = new Set(parent === dir ? [] : linkedFrom(parent))
|
|
1089
|
+
const link = (abs) => {
|
|
1090
|
+
try {
|
|
1091
|
+
const rel = toRel(host.realpath(abs))
|
|
1092
|
+
if (!splitNodeModulesPath(rel)) links.add(rel)
|
|
1093
|
+
} catch { /* a dangling link, or one out of the root, whose files the scan pass refused already */ }
|
|
1094
|
+
}
|
|
1095
|
+
const listing = (abs) => {
|
|
1096
|
+
try {
|
|
1097
|
+
return host.readdir(abs)
|
|
1098
|
+
} catch {
|
|
1099
|
+
return []
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// As locatePackage: a directory named node_modules holds no node_modules of its own to look in.
|
|
1103
|
+
if (basename(dir) !== 'node_modules') {
|
|
1104
|
+
const nodeModules = join(dir, 'node_modules')
|
|
1105
|
+
for (const ent of listing(nodeModules)) {
|
|
1106
|
+
const abs = join(nodeModules, ent.name)
|
|
1107
|
+
if (ent.name.startsWith('@')) {
|
|
1108
|
+
// A scope holds packages, a directory or a link to one (a whole scope linked, where
|
|
1109
|
+
// every package in it is reached through the link).
|
|
1110
|
+
for (const scoped of listing(abs)) {
|
|
1111
|
+
if (ent.isSymbolicLink() || scoped.isSymbolicLink()) link(join(abs, scoped.name))
|
|
1112
|
+
}
|
|
1113
|
+
} else if (ent.isSymbolicLink()) {
|
|
1114
|
+
link(abs)
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
linksIn.set(dir, links)
|
|
1119
|
+
return links
|
|
993
1120
|
}
|
|
994
|
-
for (const
|
|
1121
|
+
for (const [parent, bySpec] of edges) {
|
|
1122
|
+
for (const byPlatform of bySpec.values()) {
|
|
1123
|
+
for (const target of byPlatform.values()) {
|
|
1124
|
+
if (!isCode(target) || splitNodeModulesPath(target)) continue
|
|
1125
|
+
const links = linkedFrom(dirname(join(baseDir, parent)))
|
|
1126
|
+
// The linked root holding the target, nearest first: its entry may lie below a manifest of
|
|
1127
|
+
// its own (a named dist/package.json), as a node_modules path names the package it's in.
|
|
1128
|
+
let root = posix.dirname(target)
|
|
1129
|
+
while (root !== '.' && !links.has(root)) root = posix.dirname(root)
|
|
1130
|
+
// The project's own source isn't a dependency, nor is an import within the package itself.
|
|
1131
|
+
if (root === '.' || parent.startsWith(`${root}/`) || pkgDirs.has(root)) continue
|
|
1132
|
+
// Named by its own manifest, whatever name it's linked under.
|
|
1133
|
+
pkgDirs.set(root, readPackageJson(baseDir, `${root}/package.json`, { host })?.name)
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
for (const [pkgDir, name] of [...pkgDirs].toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
995
1138
|
const pkgAbs = join(baseDir, pkgDir)
|
|
996
1139
|
// react-native core isn't a Pod (config reports it via reactNativePath, not `dependencies`),
|
|
997
1140
|
// so walk its whole tree for native source (React/, ReactCommon/, ReactAndroid/, ...) the same
|
|
998
1141
|
// way a native dep's ios/android surface is walked; every other dep gets its ios/android + podspecs.
|
|
999
|
-
const isRnCore =
|
|
1142
|
+
const isRnCore = name === 'react-native'
|
|
1000
1143
|
const files = isRnCore ? [] : nativeModuleFiles(pkgAbs, host)
|
|
1001
1144
|
if (isRnCore) walkNative(pkgAbs, files, host, isNativeSource)
|
|
1002
1145
|
for (const abs of files) {
|
|
@@ -1036,12 +1179,9 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), env = process.env, e
|
|
|
1036
1179
|
|
|
1037
1180
|
// --package-json: fold each bundled module's package.json into `sources` (and its integrity into
|
|
1038
1181
|
// the companion lockfile) even when the scan never reached it. Buckets are the ones
|
|
1039
|
-
// assembleCodeBundle derives, from the same `packageOf` (
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
// with the State path (containment, UTF-8-aborts); no identity check here -- these buckets are
|
|
1043
|
-
// all fresh from disk.
|
|
1044
|
-
const packageOf = packageLookup(baseDir, { host })
|
|
1182
|
+
// assembleCodeBundle derives, from the same `packageOf` (its pkgDir, else the '.' workspace
|
|
1183
|
+
// bucket). readModuleManifest applies the read/validate rules shared with the State path
|
|
1184
|
+
// (containment, UTF-8-aborts); no identity check here -- these buckets are all fresh from disk.
|
|
1045
1185
|
if (packageJSON) {
|
|
1046
1186
|
const pkgDirs = new Set()
|
|
1047
1187
|
for (const abs of reached) {
|
|
@@ -1236,14 +1376,17 @@ async function buildJs({ mainFields, platforms, metro, metroResolver, innermostR
|
|
|
1236
1376
|
return { bundle: state.sourceBundle.withReason('bundle'), lockfile: () => state.lockfile, stateBuilt: true, root: state.root }
|
|
1237
1377
|
}
|
|
1238
1378
|
|
|
1239
|
-
// The bundle of `kind` (classifyEntries') built from buildBundle's options
|
|
1379
|
+
// The bundle of `kind` (classifyEntries') built from buildBundle's options, from disk: a JS bundle's
|
|
1380
|
+
// git dependencies at the commits their package managers' records name (pinInstalledCommits).
|
|
1240
1381
|
// -> { bundle, lockfile: () => Lockfile, stateBuilt } (the last two of a JS bundle alone)
|
|
1241
1382
|
async function buildOfKind(kind, { cwd, env, entries, mappingFile, manifests, scope, conditions, mainFields, platforms, metro, metroResolver, jsx, flow, typescript, tsconfig, resources, packageJSON, cargo, cargoFeatures, cargoNoDefaultFeatures, cargoAllFeatures, cargoTarget, cargoManifests }) {
|
|
1242
1383
|
if (kind === 'sol') return { bundle: await buildSolidityBundle({ cwd, env, entries, mappingFile, manifests }) }
|
|
1243
1384
|
if (kind === 'php') return { bundle: await buildPhpBundle({ cwd, entries }) }
|
|
1244
1385
|
if (kind === 'bash') return { bundle: await buildBashBundle({ cwd, entries }) }
|
|
1245
1386
|
if (kind === 'rust') return { bundle: await buildRustBundle({ cwd, entries, cargo, cargoFeatures, cargoNoDefaultFeatures, cargoAllFeatures, cargoTarget, cargoManifests }) }
|
|
1246
|
-
|
|
1387
|
+
const built = await buildJs({ cwd, env, entries, scope, conditions, mainFields, platforms, metro, metroResolver, jsx, flow, typescript, tsconfig, resources, packageJSON })
|
|
1388
|
+
pinInstalledCommits(built.bundle, built.root)
|
|
1389
|
+
return built
|
|
1247
1390
|
}
|
|
1248
1391
|
|
|
1249
1392
|
// buildVfsBundle's checks of `options`, which hold before anything is fetched (`host` the
|
|
@@ -1272,13 +1415,16 @@ export function checkVfsOptions(name, options) {
|
|
|
1272
1415
|
// tree before the scan, leaving `vfs` as it is (vfs-bundle/prisma.js). `client`, a
|
|
1273
1416
|
// @preventive/upstream/github.js client, is what a Soldeer git dependency is fetched through; without
|
|
1274
1417
|
// one, a soldeer.lock holding one is refused, naming deptree's `github`, before anything is fetched.
|
|
1275
|
-
// `
|
|
1276
|
-
//
|
|
1418
|
+
// `cache` is where npm packages' tarballs and version documents are kept (checkCache). Each
|
|
1419
|
+
// dependency is recorded at the commit the tree installs it from, where deptree names one
|
|
1420
|
+
// (pinCommits). `root` is the directory in the Vfs the bundle's paths are relative to: cwd, or for a
|
|
1421
|
+
// JS bundle built through a State, the State's root, which is at or above it.
|
|
1277
1422
|
// -> { bundle: Bundle, lockfile: Lockfile (of a JS bundle), stats, packageManager, root }
|
|
1278
|
-
export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc, repo, innermostRoot = false, generate = [], client, ...options } = {}) {
|
|
1279
|
-
const { checkKind, checkTarget, checkVfs, loadTree, packageManagerFor, packageManagerOf, treeHost, vfsHost } = await import('../vfs-bundle/tree.js')
|
|
1423
|
+
export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc, repo, innermostRoot = false, generate = [], client, cache, ...options } = {}) {
|
|
1424
|
+
const { checkCache, checkKind, checkTarget, checkVfs, loadTree, packageManagerFor, packageManagerOf, pinCommits, treeHost, vfsHost } = await import('../vfs-bundle/tree.js')
|
|
1280
1425
|
checkVfs('buildVfsBundle', vfs)
|
|
1281
1426
|
checkTarget('buildVfsBundle', { os, cpu, libc })
|
|
1427
|
+
checkCache('buildVfsBundle', cache)
|
|
1282
1428
|
// Checked as the Bundle checks it, before anything is fetched.
|
|
1283
1429
|
if (repo !== undefined) repo = new Bundle({ repo }).repo
|
|
1284
1430
|
const project = vfsHost(vfs)
|
|
@@ -1294,7 +1440,7 @@ export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageMa
|
|
|
1294
1440
|
if (pm.kind === 'sol' && !abs.endsWith('.sol')) continue
|
|
1295
1441
|
if (!posix.relative(cwd, abs).split('/').includes(pm.installs) && project.stat(abs) === null) throw new Error(`entry not found: ${abs}`)
|
|
1296
1442
|
}
|
|
1297
|
-
const tree = await loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client })
|
|
1443
|
+
const tree = await loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client, cache })
|
|
1298
1444
|
let { host } = tree
|
|
1299
1445
|
if (generate.includes('prisma')) {
|
|
1300
1446
|
const { generatePrismaClients, withPrismaClients } = await import('../vfs-bundle/prisma.js')
|
|
@@ -1306,6 +1452,7 @@ export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageMa
|
|
|
1306
1452
|
if (repo !== undefined) bundle.repo = repo
|
|
1307
1453
|
// Rooted at cwd, where `stasis bundle` detects its repo.
|
|
1308
1454
|
else if (!stateBuilt) bundle.repo ??= detectRepo(cwd, host)
|
|
1455
|
+
pinCommits(bundle, root, tree)
|
|
1309
1456
|
return { bundle, lockfile: lockfile?.(), stats: tree.stats, packageManager, root }
|
|
1310
1457
|
}
|
|
1311
1458
|
|