@exodus/stasis 1.0.0-beta.5 → 1.0.0-beta.7
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 +7 -6
- package/bin/stasis.js +30 -17
- package/package.json +14 -9
- package/src/audit-corrections.js +6 -2
- package/src/audit.js +63 -22
- package/src/bundle-from-lockfile.js +3 -5
- package/src/cmd/bundle.js +149 -69
- package/src/diff.js +16 -8
- package/src/loaders/cargo.js +26 -9
- package/src/loaders/php.js +4 -3
- package/src/metro-resolver.js +1 -1
- package/src/resolve-fields.js +18 -11
- package/src/resolve-typescript.js +175 -61
- package/src/sbom.js +3 -1
- package/src/scan.js +158 -73
- package/src/vfs-bundle/entries.js +5 -5
- package/src/vfs-bundle/github.js +15 -9
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/tree.js +18 -11
- package/src/vfs-bundle.js +4 -3
package/src/cmd/bundle.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { isUtf8 } from 'node:buffer'
|
|
2
2
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
-
import { extname, join, posix, relative, resolve } from 'node:path'
|
|
3
|
+
import { 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
7
|
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
8
8
|
import { Lockfile } from '@exodus/stasis-core/lockfile'
|
|
9
|
-
import { scan } from '../scan.js'
|
|
9
|
+
import { importsTypescriptByOutputName, scan } from '../scan.js'
|
|
10
10
|
import { createFieldResolver, resolveConditions } from '../resolve-fields.js'
|
|
11
|
-
import { discoverTsconfig, isDir, loadTsconfigPaths } from '../resolve-typescript.js'
|
|
11
|
+
import { discoverTsconfig, isDir, loadTsconfigPaths, packageTsconfigPaths } from '../resolve-typescript.js'
|
|
12
12
|
import { createMetroResolver } from '../metro-resolver.js'
|
|
13
13
|
import { State } from '@exodus/stasis-core/state'
|
|
14
14
|
import { sha512integrity } from '@exodus/stasis-core/state-util'
|
|
15
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, isPodspec, isSkippedNativeWalkDir, moduleFileKey, parseResourcesOption, posixPathEscapes, refineNativeCapture, relativeEscapes, splitNodeModulesPath, toPosix } from '@exodus/stasis-core/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'
|
|
17
17
|
import { diskHost } from '@exodus/stasis-core/host'
|
|
18
18
|
import {
|
|
19
19
|
SOLIDITY_PACKAGE_MANIFESTS,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from '../loaders/php.js'
|
|
38
38
|
import { DEFAULT_BUNDLE_FILE, bundledSummary, packagesLabel, writeBundle, writeFile } from './output.js'
|
|
39
39
|
|
|
40
|
-
const JS_EXTS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts'])
|
|
40
|
+
const JS_EXTS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'])
|
|
41
41
|
const BASH_EXTS = new Set(['.sh', '.bash'])
|
|
42
42
|
const RUST_EXTS = new Set(['.rs'])
|
|
43
43
|
|
|
@@ -124,16 +124,26 @@ function makeSolidityClassifier(baseDir, ownership, host) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// A vendored crate's ecosystem, by where `cargo vendor` copied it from (vendoredFrom): a registry's
|
|
128
|
+
// copy is `cargo`, which `stasis audit` asks OSV about as a crates.io crate's; a git checkout's is
|
|
129
|
+
// `cargo-git`, and a copy with no `.cargo-checksum.json`, which could be either, `cargo-unknown`.
|
|
130
|
+
// Neither is ever sent to a public advisory database: a private git dependency's name has no place
|
|
131
|
+
// there, and a crates.io crate of the same name isn't it.
|
|
132
|
+
const vendoredEcosystem = (from) => {
|
|
133
|
+
if (from === 'registry') return 'cargo'
|
|
134
|
+
return from === 'git' ? 'cargo-git' : 'cargo-unknown'
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
// Classify a Rust file by the nearest Cargo.toml `[package]`: a `cargo vendor`ed crate under the
|
|
128
138
|
// vendor dir (`vendor/<dir>/`, or where .cargo/config.toml points) is a dependency (tagged
|
|
129
|
-
// `cargo`); any other package (the crate
|
|
130
|
-
// dependency) is first-party, so no
|
|
131
|
-
// package.json/placeholder logic.
|
|
139
|
+
// `cargo`, `cargo-git` or `cargo-unknown`: see vendoredEcosystem); any other package (the crate
|
|
140
|
+
// itself, a workspace member reached through a `path` dependency) is first-party, so no
|
|
141
|
+
// ecosystem. Null (no manifest above the file) defers to the package.json/placeholder logic.
|
|
132
142
|
function makeRustClassifier(cargo) {
|
|
133
143
|
return (path) => {
|
|
134
144
|
const pkg = cargo.packageInfo(path)
|
|
135
145
|
if (!pkg) return null
|
|
136
|
-
return { bucketDir: pkg.dir, name: pkg.name, version: pkg.version, ecosystem: cargo.isVendored(path) ?
|
|
146
|
+
return { bucketDir: pkg.dir, name: pkg.name, version: pkg.version, ecosystem: cargo.isVendored(path) ? vendoredEcosystem(cargo.vendoredFrom(path)) : undefined }
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -195,19 +205,16 @@ function assembleCodeBundle({
|
|
|
195
205
|
packageOf = packageLookup(baseDir, { host }),
|
|
196
206
|
}) {
|
|
197
207
|
const modules = new Map()
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
? { name, version, files: Object.create(null) }
|
|
202
|
-
: { name, version, ecosystem: bucketEcosystem, files: Object.create(null) })
|
|
203
|
-
}
|
|
208
|
+
// `identity`'s name, version, ecosystem and repo (moduleInfo takes those alone).
|
|
209
|
+
const ensureBucket = (dir, identity) => {
|
|
210
|
+
if (!modules.has(dir)) modules.set(dir, moduleInfo({ ...identity, files: Object.create(null) }))
|
|
204
211
|
return modules.get(dir)
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
for (const [path, content] of sources) {
|
|
208
215
|
const dep = classifyDep?.(path)
|
|
209
216
|
if (dep) {
|
|
210
|
-
ensureBucket(dep.bucketDir, dep
|
|
217
|
+
ensureBucket(dep.bucketDir, dep).files[fileInBucket(dep.bucketDir, path)] = content
|
|
211
218
|
continue
|
|
212
219
|
}
|
|
213
220
|
const meta = packageOf(path)
|
|
@@ -216,11 +223,10 @@ function assembleCodeBundle({
|
|
|
216
223
|
if (inNodeModules && !hasNodeModulesSegment(meta.pkgDir)) {
|
|
217
224
|
throw new Error(`No package.json with name+version found for ${path}`)
|
|
218
225
|
}
|
|
219
|
-
|
|
220
|
-
ensureBucket(meta.pkgDir, meta.name, meta.version, bucketEcosystem).files[fileInBucket(meta.pkgDir, path)] = content
|
|
226
|
+
ensureBucket(meta.pkgDir, meta).files[fileInBucket(meta.pkgDir, path)] = content
|
|
221
227
|
} else {
|
|
222
228
|
if (inNodeModules) throw new Error(`No package.json with name+version found for ${path}`)
|
|
223
|
-
ensureBucket('.', workspaceName, workspaceVersion).files[path] = content
|
|
229
|
+
ensureBucket('.', { name: workspaceName, version: workspaceVersion }).files[path] = content
|
|
224
230
|
}
|
|
225
231
|
}
|
|
226
232
|
|
|
@@ -586,6 +592,13 @@ function displayPath(url, baseDir) {
|
|
|
586
592
|
return rel && !relativeEscapes(rel) ? rel : abs
|
|
587
593
|
}
|
|
588
594
|
|
|
595
|
+
// One unresolved scan edge for diagnostics, naming the file --typescript would resolve it to when
|
|
596
|
+
// the scan found one (Scan#typescriptCandidate).
|
|
597
|
+
function describeUnresolved(u, show) {
|
|
598
|
+
const typescript = u.typescriptURL ? `; resolves to ${show(u.typescriptURL)} under --typescript` : ''
|
|
599
|
+
return `${u.kind} ${u.spec ?? '<dynamic>'} from ${show(u.parentURL)} (${u.reason}${typescript})`
|
|
600
|
+
}
|
|
601
|
+
|
|
589
602
|
// Classify scanner unresolved edges + parse errors into fatal (broken/divergent at load)
|
|
590
603
|
// vs tolerated (a catchable runtime miss). Shared so the plain and --mainFields JS paths
|
|
591
604
|
// gate identically. `baseDir` relativizes the paths in the emitted messages (see displayPath).
|
|
@@ -607,7 +620,7 @@ function analyzeScanner(scanner, { baseDir }) {
|
|
|
607
620
|
const fatalUnresolved = (u) => staticKinds.has(u.kind) && staticReachable.has(u.parentURL)
|
|
608
621
|
const fatal = scanner.unresolved
|
|
609
622
|
.filter((u) => fatalUnresolved(u))
|
|
610
|
-
.map((u) => `unresolved ${u
|
|
623
|
+
.map((u) => `unresolved ${describeUnresolved(u, show)}`)
|
|
611
624
|
// Fatal parse: an eagerly-linked module-family file, or any file the parser couldn't process.
|
|
612
625
|
const fatalParse = (p) => staticReachable.has(p.url) && (p.format?.startsWith('module') === true || !p.recovered)
|
|
613
626
|
for (const p of scanner.parseErrors) {
|
|
@@ -643,7 +656,7 @@ function reportScanIssues({ fatal, tolerated, toleratedParse }, { label = '', ba
|
|
|
643
656
|
throw new Error(`JS bundle would be broken at load time${where}:\n${listed(fatal)}`)
|
|
644
657
|
}
|
|
645
658
|
if (tolerated.length > 0) {
|
|
646
|
-
const summary = listed(tolerated, (u) =>
|
|
659
|
+
const summary = listed(tolerated, (u) => describeUnresolved(u, show))
|
|
647
660
|
console.warn(`[stasis] Bundle has ${tolerated.length} unresolved import(s)${where}; they will fall through at load time:\n${summary}`)
|
|
648
661
|
}
|
|
649
662
|
if (toleratedParse.length > 0) {
|
|
@@ -656,9 +669,15 @@ function reportScanIssues({ fatal, tolerated, toleratedParse }, { label = '', ba
|
|
|
656
669
|
// token into the resolver.
|
|
657
670
|
const cleanConditions = (conditions) => conditions.map((c) => (typeof c === 'string' ? c.trim() : c)).filter(Boolean)
|
|
658
671
|
|
|
659
|
-
// --typescript honours tsconfig `paths` aliases: an explicit --tsconfig
|
|
660
|
-
//
|
|
661
|
-
|
|
672
|
+
// --typescript honours tsconfig `paths` aliases: an explicit --tsconfig's for every file (it must
|
|
673
|
+
// exist), else those of the tsconfig.json beside each importing file's package.json
|
|
674
|
+
// (packageTsconfigPaths, the project read as the State reads it, to `env`'s PROJECT_CWD; null
|
|
675
|
+
// matcher = no aliases). Without --typescript they only hint a miss (Scan#typescriptCandidate), so a
|
|
676
|
+
// config that doesn't load is none.
|
|
677
|
+
function typescriptPathsFor(typescript, baseDir, tsconfig, host, env) {
|
|
678
|
+
if (typescript && tsconfig !== undefined) return loadTsconfigPaths(discoverTsconfig(baseDir, tsconfig, host), host)
|
|
679
|
+
return packageTsconfigPaths(baseDir, { host, lenient: !typescript, projectCwd: env.PROJECT_CWD })
|
|
680
|
+
}
|
|
662
681
|
|
|
663
682
|
// Build a JS/TS Bundle (in-memory) by statically scanning the require/import graph; no
|
|
664
683
|
// user code is executed, and TS is stored verbatim (Node strips types at load). Scope comes
|
|
@@ -666,12 +685,15 @@ const typescriptPathsFor = (typescript, baseDir, tsconfig, host) => (typescript
|
|
|
666
685
|
// extra `exports`/`imports` conditions; on their own they don't honour legacy mainFields or
|
|
667
686
|
// platform suffixes (see `--mainFields` / buildResolvedJsBundle). `typescript` maps a failed
|
|
668
687
|
// resolution to its on-disk TS source (tsc's rules; see resolve-typescript.js), honouring the
|
|
669
|
-
// `paths` aliases of `tsconfig` (an explicit config path, default
|
|
688
|
+
// `paths` aliases of `tsconfig` (an explicit config path, default each package's tsconfig.json); it
|
|
689
|
+
// is taken as given where the entries import TS sources by output name (importsTypescriptByOutputName).
|
|
670
690
|
// Files are read through `host` (@exodus/stasis-core/host), the disk by default; EXODUS_STASIS_*
|
|
671
|
-
// settings from `env`.
|
|
672
|
-
|
|
691
|
+
// settings from `env`. With `innermostRoot`, the State is rooted at the innermost package at or
|
|
692
|
+
// above cwd that holds every file the scan reaches (innermostRootOf), rather than at the project's
|
|
693
|
+
// root above it, where that holds no stasis file of its own.
|
|
694
|
+
export async function buildJsBundle({ cwd = process.cwd(), env = process.env, entries, scope, conditions = [], jsx = false, flow = false, typescript = false, tsconfig, resources = [], packageJSON = false, host = diskHost, innermostRoot = false } = {}) {
|
|
673
695
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
674
|
-
throw new Error('buildJsBundle: at least one entry .js/.cjs/.mjs/.ts/.cts/.mts file is required')
|
|
696
|
+
throw new Error('buildJsBundle: at least one entry .js/.cjs/.mjs/.ts/.cts/.mts/.jsx/.tsx file is required')
|
|
675
697
|
}
|
|
676
698
|
for (const e of entries) {
|
|
677
699
|
if (!JS_EXTS.has(extname(e))) throw new Error(`buildJsBundle: not a JS/TS file: ${e}`)
|
|
@@ -684,7 +706,8 @@ export async function buildJsBundle({ cwd = process.cwd(), env = process.env, en
|
|
|
684
706
|
// --resources: extensions/filenames carried as opaque assets instead of failing "can't carry".
|
|
685
707
|
const resourceSet = parseResourcesOption('buildJsBundle', resources)
|
|
686
708
|
|
|
687
|
-
|
|
709
|
+
typescript ||= importsTypescriptByOutputName(absEntries, host)
|
|
710
|
+
const typescriptPaths = typescriptPathsFor(typescript, baseDir, tsconfig, host, env)
|
|
688
711
|
|
|
689
712
|
const scanner = scan(absEntries, { conditions: scanConditions, jsx, flow, typescript, typescriptPaths, resources: resourceSet, host })
|
|
690
713
|
|
|
@@ -696,7 +719,23 @@ export async function buildJsBundle({ cwd = process.cwd(), env = process.env, en
|
|
|
696
719
|
// bundle:'replace' skips reading any on-disk stasis.code.br (bundle:'add' would leak stale
|
|
697
720
|
// entries); lock:'ignore' tolerates a pre-existing lockfile without consuming it; and it is never
|
|
698
721
|
// written, so it claims no write target.
|
|
699
|
-
const
|
|
722
|
+
const stateOptions = { bundle: 'replace', lock: 'ignore', ...(scope ? { scope } : {}), host, claim: false }
|
|
723
|
+
let state = new State(baseDir, { ...stateOptions, env })
|
|
724
|
+
// A root holding a stasis file is the project's own choice, whose config the build takes.
|
|
725
|
+
if (innermostRoot && !STASIS_ROOT_FILES.some((file) => host.stat(join(state.root, file)) !== null)) {
|
|
726
|
+
const root = innermostRootOf(baseDir, [...scanner.files.keys()].map((url) => host.realpath(fileURLToPath(url))), state.root, host)
|
|
727
|
+
// The State's walk up from cwd stops at PROJECT_CWD, as it does where yarn sets it.
|
|
728
|
+
if (root !== state.root) state = new State(baseDir, { ...stateOptions, env: { ...env, PROJECT_CWD: root } })
|
|
729
|
+
}
|
|
730
|
+
// Named here rather than as the State's bare assertion: a stasis file in a directory below the
|
|
731
|
+
// project's root (a stray stasis.code.br there, say) roots the State at it, out of the files above.
|
|
732
|
+
for (const url of scanner.files.keys()) {
|
|
733
|
+
const file = fileURLToPath(url)
|
|
734
|
+
if (!relativeEscapes(relative(state.root, file))) continue
|
|
735
|
+
const markers = STASIS_ROOT_FILES.filter((name) => host.stat(join(state.root, name)) !== null)
|
|
736
|
+
const rootedBy = markers.length === 0 ? '' : ` (rooted there by its ${markers.join(' and ')}, which can be removed if stale)`
|
|
737
|
+
throw new Error(`buildJsBundle: ${relative(baseDir, file)} is outside the project root at ${relative(baseDir, state.root) || '.'}${rootedBy}`)
|
|
738
|
+
}
|
|
700
739
|
for (const [url, info] of scanner.files) {
|
|
701
740
|
// A resource carries bytes only: addFile derives resource vs resource:base64 from the content
|
|
702
741
|
// and stores it under `resources` (a resource can't be an entry, so no isEntry).
|
|
@@ -740,14 +779,26 @@ export async function buildJsBundle({ cwd = process.cwd(), env = process.env, en
|
|
|
740
779
|
return state
|
|
741
780
|
}
|
|
742
781
|
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
782
|
+
// The files that root a State in the directory holding one (state.js's discovery).
|
|
783
|
+
const STASIS_ROOT_FILES = ['stasis.config.json', 'stasis.lock.json', DEFAULT_BUNDLE_FILE]
|
|
784
|
+
|
|
785
|
+
// The innermost directory at or above `baseDir`, up to `root`, holding every one of `files` (real
|
|
786
|
+
// paths) and a package.json with a name, which a State can be rooted at; `root` where none below it
|
|
787
|
+
// does. One without a name, such as a `{"type":"module"}` marker, is no root: the State takes such a
|
|
788
|
+
// file's package to be the named one above it, which it then must hold.
|
|
789
|
+
function innermostRootOf(baseDir, files, root, host) {
|
|
790
|
+
for (let dir = baseDir; dir !== root && dirname(dir) !== dir; dir = dirname(dir)) {
|
|
791
|
+
if (files.every((file) => isPathWithin(dir, file)) && readPackageJson(dir, 'package.json', { host })?.name !== undefined) return dir
|
|
792
|
+
}
|
|
793
|
+
return root
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Extensions probed when a resolved target names none, in Metro's default sourceExts order.
|
|
797
|
+
// Limited to what a source bundle can carry (scan's RESOLVABLE_EXTS) so the resolver never
|
|
798
|
+
// resolves a file the bundle would reject; an extensionless `import './Foo'` can land on
|
|
799
|
+
// Foo.jsx/Foo.tsx, as it does under Metro. Like Metro's default, this doesn't track a project's
|
|
800
|
+
// own sourceExts.
|
|
801
|
+
const SOURCE_EXTS = ['js', 'jsx', 'json', 'ts', 'tsx']
|
|
751
802
|
// React Native preset mainFields for `--metro` (which also sets the RN conditions + platform suffixes).
|
|
752
803
|
const METRO_MAIN_FIELDS = ['react-native', 'browser', 'main']
|
|
753
804
|
// Synthetic path for the empty module a browser/react-native `false` redirect resolves to,
|
|
@@ -799,15 +850,14 @@ function nativeModuleFiles(pkgAbs, host) {
|
|
|
799
850
|
// `platform` (null for --mainFields), its `mainFields` (Metro's under --metro), and the conditions
|
|
800
851
|
// it adds to Node's: --metro asserts the RN conditions (+ browser on web); --mainFields carries the
|
|
801
852
|
// user's --conditions.
|
|
802
|
-
export function fieldResolverFor(platform, { mainFields, metro = false, conditions = [],
|
|
853
|
+
export function fieldResolverFor(platform, { mainFields, metro = false, conditions = [], typescript = false, typescriptPaths = null, host = diskHost }) {
|
|
803
854
|
const extras = metro ? ['react-native', ...(platform === 'web' ? ['browser'] : [])] : conditions
|
|
804
855
|
const fields = metro ? METRO_MAIN_FIELDS : mainFields
|
|
805
856
|
const resolver = createFieldResolver({
|
|
806
857
|
mainFields: fields,
|
|
807
858
|
platform,
|
|
808
859
|
preferNative: platform !== null && platform !== 'web',
|
|
809
|
-
|
|
810
|
-
sourceExts: jsx ? SOURCE_EXTS_JSX : SOURCE_EXTS,
|
|
860
|
+
sourceExts: SOURCE_EXTS,
|
|
811
861
|
conditions: resolveConditions('commonjs', extras),
|
|
812
862
|
// Opt into Metro's package-entry browser-field quirks only on the --metro path.
|
|
813
863
|
metro,
|
|
@@ -824,7 +874,7 @@ export function fieldResolverFor(platform, { mainFields, metro = false, conditio
|
|
|
824
874
|
// Build a JS/TS Bundle + companion Lockfile via the legacy-field resolver (`--mainFields`/
|
|
825
875
|
// `--metro`). Scanned once per platform; each edge is recorded flat when the platforms that
|
|
826
876
|
// have it agree, or as a `{ platform: target }` map where they diverge. Returns { bundle, lockfile }.
|
|
827
|
-
async function buildResolvedJsBundle({ cwd = process.cwd(), entries, mainFields, platforms, conditions = [], metro = false, metroResolver = false, jsx = false, flow = false, typescript = false, tsconfig, resources = [], packageJSON = false, host = diskHost }) {
|
|
877
|
+
async function buildResolvedJsBundle({ cwd = process.cwd(), env = process.env, entries, mainFields, platforms, conditions = [], metro = false, metroResolver = false, jsx = false, flow = false, typescript = false, tsconfig, resources = [], packageJSON = false, host = diskHost }) {
|
|
828
878
|
const baseDir = resolve(cwd)
|
|
829
879
|
const absEntries = entries.map((e) => resolve(baseDir, e))
|
|
830
880
|
const normalized = normalizeEntries(entries, cwd)
|
|
@@ -839,10 +889,11 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), entries, mainFields,
|
|
|
839
889
|
|
|
840
890
|
const scanConditions = cleanConditions(conditions)
|
|
841
891
|
|
|
842
|
-
//
|
|
843
|
-
|
|
892
|
+
// Taken as given where the entries import TS sources by output name, but under --metro-resolver,
|
|
893
|
+
// which has no tsc mapping (classifyEntries rejects --typescript there).
|
|
894
|
+
if (!metroResolver) typescript ||= importsTypescriptByOutputName(absEntries, host)
|
|
844
895
|
// --typescript's tsconfig `paths` matcher, shared by every per-platform resolver below.
|
|
845
|
-
const typescriptPaths = typescriptPathsFor(typescript, baseDir, tsconfig, host)
|
|
896
|
+
const typescriptPaths = typescriptPathsFor(typescript, baseDir, tsconfig, host, env)
|
|
846
897
|
// --resources: extensions/filenames carried as opaque assets instead of failing "can't carry".
|
|
847
898
|
const resourceSet = parseResourcesOption('buildResolvedJsBundle', resources)
|
|
848
899
|
|
|
@@ -855,16 +906,19 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), entries, mainFields,
|
|
|
855
906
|
let usesEmpty = false
|
|
856
907
|
|
|
857
908
|
for (const platform of platforms) {
|
|
858
|
-
const field = fieldResolverFor(platform, { mainFields, metro, conditions: scanConditions,
|
|
909
|
+
const field = fieldResolverFor(platform, { mainFields, metro, conditions: scanConditions, typescript, typescriptPaths, host })
|
|
859
910
|
const { extras } = field
|
|
860
911
|
// --metro --metro-resolver delegates to the project's own metro-resolver for byte-for-byte Metro
|
|
861
912
|
// fidelity; otherwise the built-in field/suffix resolver reproduces it. metro-resolver derives
|
|
862
913
|
// default/require|import/platform conditions itself, so it takes only the extra `react-native`
|
|
863
914
|
// condition (browser comes from its per-platform map, keyed on `web`).
|
|
864
915
|
const resolver = metroResolver
|
|
865
|
-
? createMetroResolver({ projectDir: baseDir, platform, sourceExts, mainFields, conditionNames: ['react-native'], host })
|
|
916
|
+
? createMetroResolver({ projectDir: baseDir, platform, sourceExts: SOURCE_EXTS, mainFields, conditionNames: ['react-native'], host })
|
|
866
917
|
: field.resolver
|
|
867
|
-
|
|
918
|
+
// Without --typescript, the field resolver --typescript would build names the file a miss would
|
|
919
|
+
// land on: a hint in the unresolved report, never an edge (see Scan#typescriptCandidate).
|
|
920
|
+
const typescriptResolve = typescript || metroResolver ? null : fieldResolverFor(platform, { mainFields, metro, conditions: scanConditions, typescript: true, typescriptPaths, host }).resolver
|
|
921
|
+
const scanner = scan(absEntries, { conditions: extras, resolve: resolver, typescriptResolve, jsx, flow, resources: resourceSet, host })
|
|
868
922
|
reportScanIssues(analyzeScanner(scanner, { baseDir }), { baseDir, label: platform ?? 'mainFields' })
|
|
869
923
|
|
|
870
924
|
const platformKey = platform ?? '*' // '*' is a private placeholder for the single mainFields pass; it never unflattens
|
|
@@ -1039,7 +1093,7 @@ async function buildResolvedJsBundle({ cwd = process.cwd(), entries, mainFields,
|
|
|
1039
1093
|
for (const [dir, m] of bundle.modules) {
|
|
1040
1094
|
const files = Object.create(null)
|
|
1041
1095
|
for (const rel of Object.keys(m.files)) files[rel] = integrities.get(moduleFileKey(dir, rel))
|
|
1042
|
-
lockModules.set(dir, {
|
|
1096
|
+
lockModules.set(dir, moduleInfo({ ...m, files }))
|
|
1043
1097
|
}
|
|
1044
1098
|
const lockfile = new Lockfile({
|
|
1045
1099
|
config: bundle.config,
|
|
@@ -1069,7 +1123,7 @@ function classifyEntries(name, { cwd = process.cwd(), entries, mappingFile, mani
|
|
|
1069
1123
|
else if (entries.every((e) => BASH_EXTS.has(extname(e)))) kind = 'bash'
|
|
1070
1124
|
else if (entries.every((e) => RUST_EXTS.has(extname(e)))) kind = 'rust'
|
|
1071
1125
|
else {
|
|
1072
|
-
throw new Error(`${name}: entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts, all be .sh/.bash, or all be .rs (no mixing)`)
|
|
1126
|
+
throw new Error(`${name}: entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts/.jsx/.tsx, all be .sh/.bash, or all be .rs (no mixing)`)
|
|
1073
1127
|
}
|
|
1074
1128
|
if (mappingFile && kind !== 'sol') {
|
|
1075
1129
|
throw new Error(`${name}: --mapping is only valid for .sol bundles`)
|
|
@@ -1117,7 +1171,8 @@ function classifyEntries(name, { cwd = process.cwd(), entries, mappingFile, mani
|
|
|
1117
1171
|
if (mainFields !== undefined && kind !== 'js') {
|
|
1118
1172
|
throw new Error(`${name}: --mainFields is only valid for JS bundles`)
|
|
1119
1173
|
}
|
|
1120
|
-
// --jsx toggles the scanner's JSX parsing for the .js family; only JS
|
|
1174
|
+
// --jsx toggles the scanner's JSX parsing for the .js family (.jsx/.tsx need no flag); only JS
|
|
1175
|
+
// entries are scanned.
|
|
1121
1176
|
if (jsx && kind !== 'js') {
|
|
1122
1177
|
throw new Error(`${name}: --jsx is only valid for JS bundles`)
|
|
1123
1178
|
}
|
|
@@ -1168,15 +1223,17 @@ function classifyEntries(name, { cwd = process.cwd(), entries, mappingFile, mani
|
|
|
1168
1223
|
|
|
1169
1224
|
// A JS bundle as `stasis bundle` builds it: by the legacy-field resolver with `mainFields` or
|
|
1170
1225
|
// `metro`, whose per-platform edges and synthetic empty module a State can't hold, else through a
|
|
1171
|
-
// State, which detects the bundle's repo itself.
|
|
1172
|
-
|
|
1226
|
+
// State, which detects the bundle's repo itself. `root` is the directory the bundle's paths are
|
|
1227
|
+
// relative to: cwd for the resolver's, the State's root for the State's.
|
|
1228
|
+
// -> { bundle, lockfile: () => Lockfile, stateBuilt, root }
|
|
1229
|
+
async function buildJs({ mainFields, platforms, metro, metroResolver, innermostRoot, ...options }) {
|
|
1173
1230
|
if (metro || mainFields !== undefined) {
|
|
1174
1231
|
const built = await buildResolvedJsBundle({ ...options, mainFields: metro ? METRO_MAIN_FIELDS : mainFields, platforms: metro ? platforms : [null], metro: Boolean(metro), metroResolver: Boolean(metroResolver) })
|
|
1175
|
-
return { bundle: built.bundle, lockfile: () => built.lockfile, stateBuilt: false }
|
|
1232
|
+
return { bundle: built.bundle, lockfile: () => built.lockfile, stateBuilt: false, root: resolve(options.cwd) }
|
|
1176
1233
|
}
|
|
1177
|
-
const state = await buildJsBundle(options)
|
|
1234
|
+
const state = await buildJsBundle({ ...options, innermostRoot })
|
|
1178
1235
|
// Stamp the `bundle` consumer (the static build carries none).
|
|
1179
|
-
return { bundle: state.sourceBundle.withReason('bundle'), lockfile: () => state.lockfile, stateBuilt: true }
|
|
1236
|
+
return { bundle: state.sourceBundle.withReason('bundle'), lockfile: () => state.lockfile, stateBuilt: true, root: state.root }
|
|
1180
1237
|
}
|
|
1181
1238
|
|
|
1182
1239
|
// The bundle of `kind` (classifyEntries') built from buildBundle's options.
|
|
@@ -1190,11 +1247,15 @@ async function buildOfKind(kind, { cwd, env, entries, mappingFile, manifests, sc
|
|
|
1190
1247
|
}
|
|
1191
1248
|
|
|
1192
1249
|
// buildVfsBundle's checks of `options`, which hold before anything is fetched (`host` the
|
|
1193
|
-
// project's, or an empty tree's with `fetched: false`): no metro-resolver, which reads the disk
|
|
1250
|
+
// project's, or an empty tree's with `fetched: false`): no metro-resolver, which reads the disk;
|
|
1251
|
+
// `generate` only for JS.
|
|
1194
1252
|
// -> the kind of bundle its entries make
|
|
1195
1253
|
export function checkVfsOptions(name, options) {
|
|
1196
1254
|
const kind = classifyEntries(name, options)
|
|
1197
1255
|
if (options.metroResolver) throw new Error(`${name}: metroResolver is not supported`)
|
|
1256
|
+
const { generate = [] } = options
|
|
1257
|
+
if (!Array.isArray(generate) || generate.some((step) => step !== 'prisma')) throw new TypeError(`${name}: generate must be an array of 'prisma'`)
|
|
1258
|
+
if (generate.length > 0 && kind !== 'js') throw new Error(`${name}: --generate is only valid for JS bundles`)
|
|
1198
1259
|
return kind
|
|
1199
1260
|
}
|
|
1200
1261
|
|
|
@@ -1203,13 +1264,19 @@ export function checkVfsOptions(name, options) {
|
|
|
1203
1264
|
// would install, or its Solidity options, through the dependencies folder 'soldeer' would install; with
|
|
1204
1265
|
// nothing read from disk but tarballs and zips, nor from the environment: a Solidity bundle is
|
|
1205
1266
|
// built with foundry.toml's default profile, whatever FOUNDRY_PROFILE or remappings one sets, and no
|
|
1206
|
-
// EXODUS_STASIS_* setting is read. `repo`, the informational `{ github, directory
|
|
1267
|
+
// EXODUS_STASIS_* setting is read. `repo`, the informational `{ github, directory, commit }`,
|
|
1207
1268
|
// is the Bundle's, over what is detected in the Vfs as `stasis bundle` detects it on disk. `os`,
|
|
1208
1269
|
// `cpu` and `libc` are loadNodeModules'. Without a `packageManager`, it is the one whose lockfile
|
|
1209
|
-
// installs cwd, where only one's does.
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1270
|
+
// installs cwd, where only one's does. `innermostRoot` is buildJsBundle's, for a JS bundle built
|
|
1271
|
+
// through a State. `generate: ['prisma']` writes each project's Prisma Client into the laid-out
|
|
1272
|
+
// tree before the scan, leaving `vfs` as it is (vfs-bundle/prisma.js). `client`, a
|
|
1273
|
+
// @preventive/upstream/github.js client, is what a Soldeer git dependency is fetched through; without
|
|
1274
|
+
// one, a soldeer.lock holding one is refused, naming deptree's `github`, before anything is fetched.
|
|
1275
|
+
// `root` is the directory in the Vfs the bundle's paths are relative to: cwd, or for a JS bundle
|
|
1276
|
+
// built through a State, the State's root, which is at or above it.
|
|
1277
|
+
// -> { 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')
|
|
1213
1280
|
checkVfs('buildVfsBundle', vfs)
|
|
1214
1281
|
checkTarget('buildVfsBundle', { os, cpu, libc })
|
|
1215
1282
|
// Checked as the Bundle checks it, before anything is fetched.
|
|
@@ -1219,7 +1286,7 @@ export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageMa
|
|
|
1219
1286
|
cwd = project.realpath(posix.resolve('/', cwd))
|
|
1220
1287
|
packageManager = packageManagerFor('buildVfsBundle', project, cwd, { packageManager, packageManagerVersion, os })
|
|
1221
1288
|
const pm = packageManagerOf('buildVfsBundle', packageManager)
|
|
1222
|
-
checkKind('buildVfsBundle', checkVfsOptions('buildVfsBundle', { ...options, cwd, host: project }), [packageManager])
|
|
1289
|
+
checkKind('buildVfsBundle', checkVfsOptions('buildVfsBundle', { ...options, generate, cwd, host: project }), [packageManager])
|
|
1223
1290
|
// Checked before anything is fetched: an entry out of what the tree installs is in the project
|
|
1224
1291
|
// already. (A Solidity entry that is no .sol file is a directory, skipped where it is missing.)
|
|
1225
1292
|
for (const entry of options.entries) {
|
|
@@ -1227,13 +1294,19 @@ export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageMa
|
|
|
1227
1294
|
if (pm.kind === 'sol' && !abs.endsWith('.sol')) continue
|
|
1228
1295
|
if (!posix.relative(cwd, abs).split('/').includes(pm.installs) && project.stat(abs) === null) throw new Error(`entry not found: ${abs}`)
|
|
1229
1296
|
}
|
|
1230
|
-
const
|
|
1297
|
+
const tree = await loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client })
|
|
1298
|
+
let { host } = tree
|
|
1299
|
+
if (generate.includes('prisma')) {
|
|
1300
|
+
const { generatePrismaClients, withPrismaClients } = await import('../vfs-bundle/prisma.js')
|
|
1301
|
+
const outputs = await generatePrismaClients({ host, root: tree.root, projects: tree.projects })
|
|
1302
|
+
if (outputs.length > 0) host = treeHost(tree, vfsHost(withPrismaClients(vfs, outputs)))
|
|
1303
|
+
}
|
|
1231
1304
|
const args = { ...options, cwd, host, env: {} }
|
|
1232
|
-
const { bundle, lockfile, stateBuilt } = pm.kind === 'sol' ? { bundle: await buildSolidityBundle(args) } : await buildJs(args)
|
|
1305
|
+
const { bundle, lockfile, stateBuilt, root } = pm.kind === 'sol' ? { bundle: await buildSolidityBundle(args), root: cwd } : await buildJs({ ...args, innermostRoot })
|
|
1233
1306
|
if (repo !== undefined) bundle.repo = repo
|
|
1234
1307
|
// Rooted at cwd, where `stasis bundle` detects its repo.
|
|
1235
1308
|
else if (!stateBuilt) bundle.repo ??= detectRepo(cwd, host)
|
|
1236
|
-
return { bundle, lockfile: lockfile?.(), stats, packageManager }
|
|
1309
|
+
return { bundle, lockfile: lockfile?.(), stats: tree.stats, packageManager, root }
|
|
1237
1310
|
}
|
|
1238
1311
|
|
|
1239
1312
|
// Programmatic equivalent of `stasis bundle`: build and return an in-memory Bundle without
|
|
@@ -1255,14 +1328,20 @@ export async function bundleCommand({ cwd = process.cwd(), env = process.env, en
|
|
|
1255
1328
|
const options = { cwd, env, entries, mappingFile, manifests, scope, conditions, mainFields, platforms, metro, metroResolver, jsx, flow, typescript, tsconfig, resources, packageJSON, cargo, cargoFeatures, cargoNoDefaultFeatures, cargoAllFeatures, cargoTarget, cargoManifests }
|
|
1256
1329
|
const kind = classifyEntries('bundleCommand', { ...options, lockfile })
|
|
1257
1330
|
|
|
1258
|
-
const target = output ?? DEFAULT_BUNDLE_FILE
|
|
1259
1331
|
// --add has nothing to merge into on stdout (write-only).
|
|
1260
|
-
if (add &&
|
|
1332
|
+
if (add && output === '-') {
|
|
1261
1333
|
throw new Error('bundleCommand: --add cannot be combined with --output=- (nothing to merge into on stdout)')
|
|
1262
1334
|
}
|
|
1263
1335
|
|
|
1264
1336
|
const built = await buildOfKind(kind, options)
|
|
1265
1337
|
let { bundle } = built
|
|
1338
|
+
// The directory the bundle's paths are relative to: a State-built JS bundle's root, which is the
|
|
1339
|
+
// project's at or above cwd (a workspace package's is the workspace root's), else cwd.
|
|
1340
|
+
const root = built.root ?? resolve(cwd)
|
|
1341
|
+
// By default the bundle goes there, where `stasis run --bundle=load` discovers it. In cwd below
|
|
1342
|
+
// the root it would load as rooted at cwd, and root the next `stasis bundle` there too, out of the
|
|
1343
|
+
// files above it.
|
|
1344
|
+
const target = output ?? relative(resolve(cwd), join(root, DEFAULT_BUNDLE_FILE))
|
|
1266
1345
|
// Only a JS bundle has a lockfile (classifyEntries refuses one for the others).
|
|
1267
1346
|
let lockData = lockfile ? built.lockfile().serialize() : undefined
|
|
1268
1347
|
|
|
@@ -1306,7 +1385,8 @@ export async function bundleCommand({ cwd = process.cwd(), env = process.env, en
|
|
|
1306
1385
|
|
|
1307
1386
|
const dest = writeBundle(cwd, target, serialized, brotliQuality)
|
|
1308
1387
|
if (lockData) writeFile(lockAbs, lockData)
|
|
1309
|
-
|
|
1388
|
+
// The bundle's paths are relative to its root; shown relative to cwd, as `dest` is.
|
|
1389
|
+
const fromDir = posix.join(toPosix(relative(resolve(cwd), root)), outermostDir(files, root))
|
|
1310
1390
|
// On a merge, report newly-added files alongside the totals; a fresh write keeps the plain line.
|
|
1311
1391
|
if (mergedFrom !== undefined) {
|
|
1312
1392
|
const added = files.length - mergedFrom
|
package/src/diff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sortPaths } from '@exodus/stasis-core/util'
|
|
1
|
+
import { reposAgree, sortPaths } from '@exodus/stasis-core/util'
|
|
2
2
|
|
|
3
3
|
// `@exodus/stasis/diff` — compare two parsed stasis artifacts (Bundle/Lockfile) at the module and
|
|
4
4
|
// file level. Never reads disk. Comparing across kinds works by reducing every file to the same
|
|
@@ -21,7 +21,7 @@ function digestOf(kind, format, value, hash) {
|
|
|
21
21
|
|
|
22
22
|
const projectPath = (dir, rel) => (dir === '.' ? rel : `${dir}/${rel}`)
|
|
23
23
|
|
|
24
|
-
// Project an artifact onto a uniform shape: scope + Map<dir, { name, version, ecosystem,
|
|
24
|
+
// Project an artifact onto a uniform shape: scope + Map<dir, { name, version, ecosystem, repo,
|
|
25
25
|
// files: Map<rel, digest> }>. `input` is { artifact, kind }; `hash` re-hashes bundle bytes only.
|
|
26
26
|
export function normalizeArtifact(input, { hash } = {}) {
|
|
27
27
|
const { artifact, kind } = input ?? {}
|
|
@@ -30,13 +30,13 @@ export function normalizeArtifact(input, { hash } = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
const formats = artifact.formats ?? new Map()
|
|
32
32
|
const modules = new Map()
|
|
33
|
-
for (const [dir, { name, version, ecosystem, files }] of artifact.modules) {
|
|
33
|
+
for (const [dir, { name, version, ecosystem, repo, files }] of artifact.modules) {
|
|
34
34
|
const digests = new Map()
|
|
35
35
|
for (const [rel, value] of Object.entries(files)) {
|
|
36
36
|
digests.set(rel, digestOf(kind, formats.get(projectPath(dir, rel)), value, hash))
|
|
37
37
|
}
|
|
38
38
|
// v0 bundles record no name/version; normalize undefined -> null for a single "unknown" sentinel.
|
|
39
|
-
modules.set(dir, { name: name ?? null, version: version ?? null, ecosystem: ecosystem ?? null, files: digests })
|
|
39
|
+
modules.set(dir, { name: name ?? null, version: version ?? null, ecosystem: ecosystem ?? null, repo, files: digests })
|
|
40
40
|
}
|
|
41
41
|
// `executable` decides which files `stasis extract` chmods +x, so a change to it changes the tree
|
|
42
42
|
// an artifact produces even when every digest matches -- it has to be part of "did these differ?".
|
|
@@ -45,8 +45,8 @@ export function normalizeArtifact(input, { hash } = {}) {
|
|
|
45
45
|
|
|
46
46
|
// Diff two `{ artifact, kind }` operands (`left` = baseline/"from", `right` = "to"). Modules are
|
|
47
47
|
// compared whole-package; files only within packages present on BOTH sides (a one-sided package is
|
|
48
|
-
// reported once as an added/removed module, not per-file). A version/name change is reported
|
|
49
|
-
// when both sides record a non-null value. `{ imports: true }` also diffs the resolution graphs
|
|
48
|
+
// reported once as an added/removed module, not per-file). A version/name/repo change is reported
|
|
49
|
+
// only when both sides record a non-null value (an artifact from before `repo` records none). `{ imports: true }` also diffs the resolution graphs
|
|
50
50
|
// (opt-in; verbose). `hash` is required only when a bundle is an operand.
|
|
51
51
|
export function diffArtifacts(left, right, { imports = false, hash } = {}) {
|
|
52
52
|
const L = normalizeArtifact(left, { hash })
|
|
@@ -80,7 +80,11 @@ export function diffArtifacts(left, right, { imports = false, hash } = {}) {
|
|
|
80
80
|
if (l.name !== null && r.name !== null && l.name !== r.name) {
|
|
81
81
|
change.nameChange = { from: l.name, to: r.name }
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// A dependency's repo picks the GitHub advisories audit asks, so a change to it is one to review.
|
|
84
|
+
if (!reposAgree(l.repo, r.repo)) {
|
|
85
|
+
change.repoChange = { from: l.repo, to: r.repo }
|
|
86
|
+
}
|
|
87
|
+
if (change.versionChange || change.nameChange || change.repoChange) modulesChanged.push(change)
|
|
84
88
|
|
|
85
89
|
const rels = new Set([...l.files.keys(), ...r.files.keys()])
|
|
86
90
|
for (const rel of rels) {
|
|
@@ -203,10 +207,14 @@ const REMOVED = '-'
|
|
|
203
207
|
const ADDED = '+'
|
|
204
208
|
const CHANGED = '*'
|
|
205
209
|
|
|
206
|
-
|
|
210
|
+
// `owner/name`, its commit and directory where recorded: `owner/name@0123abc (packages/x)`.
|
|
211
|
+
const repoLabel = ({ github, commit, directory }) => `${github ?? '?'}${commit ? `@${commit.slice(0, 7)}` : ''}${directory ? ` (${directory})` : ''}`
|
|
212
|
+
|
|
213
|
+
function describeModuleChange({ name, versionChange, nameChange, repoChange }) {
|
|
207
214
|
const segs = []
|
|
208
215
|
if (versionChange) segs.push(`${versionChange.from} -> ${versionChange.to}`)
|
|
209
216
|
if (nameChange) segs.push(`name ${nameChange.from} -> ${nameChange.to}`)
|
|
217
|
+
if (repoChange) segs.push(`repo ${repoLabel(repoChange.from)} -> ${repoLabel(repoChange.to)}`)
|
|
210
218
|
return `${name ? `${name} ` : ''}${segs.join(', ')}`
|
|
211
219
|
}
|
|
212
220
|
|
package/src/loaders/cargo.js
CHANGED
|
@@ -782,6 +782,12 @@ export function createCargoContext(baseDir, { entries = [], features = [], noDef
|
|
|
782
782
|
// sha256, as `cargo vendor` listed them, path relative to the package.
|
|
783
783
|
return value === null ? null : { text, git: (value?.package ?? null) === null, files: isPlainObject(value?.files) ? value.files : {} }
|
|
784
784
|
})
|
|
785
|
+
// Where `cargo vendor` copied the vendored package at `dir` from, as its `.cargo-checksum.json`
|
|
786
|
+
// tells: `registry` or `git` (a git checkout); null without that file, when it could be either.
|
|
787
|
+
const copySource = (dir) => {
|
|
788
|
+
const git = checksumOf(dir)?.git
|
|
789
|
+
return git === undefined ? null : (git ? 'git' : 'registry')
|
|
790
|
+
}
|
|
785
791
|
// Throws when `fileRel`, a file of the vendored package at `dir`, isn't byte for byte (`buf`) the
|
|
786
792
|
// file its `.cargo-checksum.json` lists -- edited after `cargo vendor`: cargo refuses to build
|
|
787
793
|
// the package, and so the bundle does. A file the list doesn't name, or a package without one,
|
|
@@ -900,8 +906,7 @@ export function createCargoContext(baseDir, { entries = [], features = [], noDef
|
|
|
900
906
|
const lockKeysOf = (lk, m) => {
|
|
901
907
|
const keys = lk.byId.get(`${normName(m.package.name)} ${version(m)}`) ?? []
|
|
902
908
|
if (!isVendoredDir(m.dir)) return keys.filter((k) => lk.lock.packages[k].source === undefined)
|
|
903
|
-
const
|
|
904
|
-
const kind = git === undefined ? null : (git ? 'git' : 'registry')
|
|
909
|
+
const kind = copySource(m.dir)
|
|
905
910
|
return keys.filter((k) => lk.lock.packages[k].source !== undefined && (kind === null || sourceKind(lk.lock.packages[k].source) === kind))
|
|
906
911
|
}
|
|
907
912
|
// Dependency → package, memoized per (package, request): the fixed-point loop asks many times.
|
|
@@ -997,16 +1002,20 @@ export function createCargoContext(baseDir, { entries = [], features = [], noDef
|
|
|
997
1002
|
|
|
998
1003
|
// --- feature resolution
|
|
999
1004
|
|
|
1000
|
-
// Cargo's feature resolver: v2 (edition 2021
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
+
// Cargo's feature resolver: v2 (edition 2021, or `resolver = "2"`) leaves dev-dependencies out of
|
|
1006
|
+
// a normal build's unification, resolves what is built for the host apart from what is built for
|
|
1007
|
+
// the target, and ignores the tables of platforms not being built; v1 unifies them all. v3
|
|
1008
|
+
// (edition 2024, or `resolver = "3"`) resolves features as v2 does, and picks versions by
|
|
1009
|
+
// `rust-version` too, which the lockfile and the vendored copies have decided here. It is the
|
|
1010
|
+
// workspace's setting: the build's workspace root's (buildWorkspaceRoot) `resolver`, else its
|
|
1011
|
+
// edition's, as linkCargo gives it -- never that of a vendored crate an entry is in, where cargo
|
|
1012
|
+
// isn't run.
|
|
1005
1013
|
let resolverMemo = null
|
|
1006
1014
|
const resolverVersion = () => {
|
|
1007
1015
|
if (resolverMemo === null) {
|
|
1008
1016
|
const root = buildWorkspaceRoot()
|
|
1009
|
-
|
|
1017
|
+
const edition = Number(root?.package?.edition ?? 0)
|
|
1018
|
+
resolverMemo = root?.resolver ?? (edition >= 2024 ? 3 : (edition >= 2021 ? 2 : 1))
|
|
1010
1019
|
}
|
|
1011
1020
|
return resolverMemo
|
|
1012
1021
|
}
|
|
@@ -1613,11 +1622,19 @@ export function createCargoContext(baseDir, { entries = [], features = [], noDef
|
|
|
1613
1622
|
const m = packageFor(fileRel)
|
|
1614
1623
|
if (m !== null && isVendoredDir(m.dir)) checkVendored(m.dir, fileRel, buf)
|
|
1615
1624
|
},
|
|
1616
|
-
// Whether `fileRel` belongs to a vendored package (a
|
|
1625
|
+
// Whether `fileRel` belongs to a vendored package (a crate `cargo vendor` copied in, a registry's
|
|
1626
|
+
// or a git checkout's: see vendoredFrom).
|
|
1617
1627
|
isVendored(fileRel) {
|
|
1618
1628
|
const m = packageFor(fileRel)
|
|
1619
1629
|
return m !== null && isVendoredDir(m.dir)
|
|
1620
1630
|
},
|
|
1631
|
+
// Where `cargo vendor` copied the vendored package owning `fileRel` from, as its
|
|
1632
|
+
// `.cargo-checksum.json` tells: `registry` (a package checksum) or `git` (a git checkout's: none);
|
|
1633
|
+
// null when it has no such file, and so could be either, or isn't vendored.
|
|
1634
|
+
vendoredFrom(fileRel) {
|
|
1635
|
+
const m = packageFor(fileRel)
|
|
1636
|
+
return m !== null && isVendoredDir(m.dir) ? copySource(m.dir) : null
|
|
1637
|
+
},
|
|
1621
1638
|
// The build script of `fileRel`'s package, project-relative, when there is one on disk:
|
|
1622
1639
|
// `[package] build = "…"`, else `build.rs` beside the manifest; `build = false` means none. A
|
|
1623
1640
|
// vendored package's may not name a file outside the package (a published crate never does).
|