@exodus/stasis 1.0.0-beta.0 → 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 +389 -214
- 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,370 @@
|
|
|
1
|
+
import { isBuiltin } from 'node:module'
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { isTypeDeclaration, stripTypeDeclaration, toPosix } from '@exodus/stasis-core/util'
|
|
6
|
+
import {
|
|
7
|
+
inNodeModules,
|
|
8
|
+
isDir,
|
|
9
|
+
isFile,
|
|
10
|
+
locatePackage,
|
|
11
|
+
nearestPackage,
|
|
12
|
+
readJson,
|
|
13
|
+
resolveTypescriptFallback,
|
|
14
|
+
typescriptSiblings,
|
|
15
|
+
} from './resolve-typescript.js'
|
|
16
|
+
import { diskHost } from '@exodus/stasis-core/host'
|
|
17
|
+
import { createNodeResolver } from './resolve-node.js'
|
|
18
|
+
|
|
19
|
+
// Static module resolver for legacy package fields (`react-native`/`browser`/`main` + browser-spec
|
|
20
|
+
// redirect maps) and platform suffixes (`.ios`/`.android`/`.native`), reproducing Metro/React-Native
|
|
21
|
+
// resolution that Node's own resolver (used by scan.js) can't. `exports`-bearing packages are
|
|
22
|
+
// delegated to Node's algorithm, under a bundler's conditions rather than Node's (see
|
|
23
|
+
// resolveConditions). No user code is executed (only package.json is stat/read).
|
|
24
|
+
//
|
|
25
|
+
// Returns, for each specifier:
|
|
26
|
+
// { url } resolved to a file, by its real path (file: URL string; see fileResolution)
|
|
27
|
+
// { empty } a browser/react-native field mapped it to `false` (empty module)
|
|
28
|
+
// { builtin } a Node builtin: a `node:` specifier, or a builtin name no installed package resolves
|
|
29
|
+
// null unresolved
|
|
30
|
+
//
|
|
31
|
+
// Browser and React Native targets have no Node builtins, so a bare builtin name (`buffer`,
|
|
32
|
+
// `events`, `util`, `process`, ...) resolves to the installed npm package of that name, as esbuild
|
|
33
|
+
// (platform browser), webpack 5 (target web) and Metro bundle it. Where nothing is installed it
|
|
34
|
+
// stays a builtin, as the --metro-resolver adapter reports Metro's miss on one (metro-resolver.js),
|
|
35
|
+
// rather than an unresolved edge: Metro and webpack fail the build there.
|
|
36
|
+
|
|
37
|
+
// Metro divergence toggle. When a package's browser map maps its OWN entry to `false` (under
|
|
38
|
+
// Metro's matching rules, which include bare keys like {"buf": false} for main "./buf.js"),
|
|
39
|
+
// Metro's `getPackageEntryPoint` ignores the non-string replacement and keeps `main`. The
|
|
40
|
+
// `--metro` resolver matches that by default; flip this to `false` to fail closed instead:
|
|
41
|
+
// a `false` match on the entry then yields an EMPTY module. Note the off state is deliberately
|
|
42
|
+
// stricter than both tools -- for a bare-key `false`, Metro keeps `main` and esbuild would not
|
|
43
|
+
// match the entry at all -- it exists as an escape hatch, not an esbuild-parity mode. This only
|
|
44
|
+
// affects the `--metro` path; the `--mainFields` path always empties (matching esbuild).
|
|
45
|
+
// Tests can override per-resolver via createFieldResolver's `metroKeepEntryOnBrowserFalse`.
|
|
46
|
+
const METRO_KEEP_ENTRY_ON_BROWSER_FALSE = true
|
|
47
|
+
|
|
48
|
+
// First mainFields entry that's a non-empty string, else `index` (object-valued fields are redirect maps).
|
|
49
|
+
function packageEntry(pkg, mainFields) {
|
|
50
|
+
for (const name of mainFields) {
|
|
51
|
+
if (typeof pkg?.[name] === 'string' && pkg[name].length > 0) return pkg[name]
|
|
52
|
+
}
|
|
53
|
+
return 'index'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Merge object-valued mainFields (browser-spec maps) into one redirect map. Iterate in REVERSE
|
|
57
|
+
// so earlier mainFields win on conflict. Keys are verbatim: `./x` relative vs bare `x` (kept bare).
|
|
58
|
+
function mergeRedirectMap(pkg, mainFields) {
|
|
59
|
+
const map = new Map()
|
|
60
|
+
for (let i = mainFields.length - 1; i >= 0; i--) {
|
|
61
|
+
const field = pkg?.[mainFields[i]]
|
|
62
|
+
if (!field || typeof field !== 'object') continue
|
|
63
|
+
for (const [key, value] of Object.entries(field)) {
|
|
64
|
+
if (typeof value !== 'string' && value !== false) continue
|
|
65
|
+
map.set(key, value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return map
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Metro's entry-variant list for `main` (getPackageEntryPoint, metro-resolver
|
|
72
|
+
// PackageResolve.js): the main spelling and its ./-toggled twin, each expanded to
|
|
73
|
+
// [self, +'.js', +'.json', ext-stripped]. ORDER MATTERS -- the first variant with ANY mapping
|
|
74
|
+
// (string or false) decides, so this must reproduce Metro's list exactly, duplicates included.
|
|
75
|
+
function metroEntryVariants(main) {
|
|
76
|
+
const toggled = main.startsWith('./') ? main.slice(2) : `./${main}`
|
|
77
|
+
return [main, toggled].flatMap((v) => [v, `${v}.js`, `${v}.json`, v.replace(/\.(?:js|json)$/u, '')])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Resolve the package ENTRY (from mainFields) through the package's own redirect map.
|
|
81
|
+
// Returns { entry } (the redirected or original entry to resolve) or { empty: true }.
|
|
82
|
+
// Both halves of the entry semantics -- WHICH keys match, and what `false` means -- live here
|
|
83
|
+
// so no caller can get one without the other:
|
|
84
|
+
// - metro: Metro's getPackageEntryPoint. First matching variant (Metro's order) wins; a string
|
|
85
|
+
// redirects; `false` (any non-string) keeps `main`, unless the keep-on-false toggle is off,
|
|
86
|
+
// in which case it fails closed to an empty module (see METRO_KEEP_ENTRY_ON_BROWSER_FALSE).
|
|
87
|
+
// - default (--mainFields): esbuild-shaped candidates; `false` empties the entry, matching
|
|
88
|
+
// esbuild. The extension-STRIPPED bare spelling is not probed: for an extensioned main,
|
|
89
|
+
// esbuild treats it as a bare module name, not the entry. (For an extensionless main the
|
|
90
|
+
// `bare` candidate covers it -- esbuild does match that spelling; verified empirically.)
|
|
91
|
+
function resolveEntryThroughMap(map, main, opts) {
|
|
92
|
+
if (map.size === 0) return { entry: main }
|
|
93
|
+
let cands
|
|
94
|
+
if (opts.metro) {
|
|
95
|
+
cands = metroEntryVariants(main)
|
|
96
|
+
} else {
|
|
97
|
+
const bare = main.replace(/^\.\//u, '')
|
|
98
|
+
const stripped = bare.replace(/\.(?:js|json)$/u, '')
|
|
99
|
+
cands = [
|
|
100
|
+
`./${bare}`, bare,
|
|
101
|
+
`./${stripped}`,
|
|
102
|
+
`./${stripped}.js`, `${stripped}.js`,
|
|
103
|
+
`./${stripped}.json`, `${stripped}.json`,
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
for (const cand of cands) {
|
|
107
|
+
const v = map.get(cand)
|
|
108
|
+
if (typeof v === 'string') return { entry: v }
|
|
109
|
+
if (v === false) {
|
|
110
|
+
if (opts.metro && opts.metroKeepEntryOnBrowserFalse) return { entry: main }
|
|
111
|
+
return { empty: true }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { entry: main }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A resolved file by its REAL path (through `host`, so a Vfs host's links are followed too), as Node,
|
|
118
|
+
// Metro and esbuild record one: under pnpm's isolated layout `node_modules/debug` links into
|
|
119
|
+
// `node_modules/.pnpm/debug@x/node_modules/debug`, whose own imports (a sibling `ms`) resolve only
|
|
120
|
+
// from where it really lies. Probing stays on the path as reached, so a candidate and the package
|
|
121
|
+
// scope read from it (resolveSourceFile's metro redirect) are one spelling; only the hit is
|
|
122
|
+
// realpathed. null where the file has no real path (gone since it was probed).
|
|
123
|
+
function fileResolution(path, host) {
|
|
124
|
+
let real
|
|
125
|
+
try {
|
|
126
|
+
real = host.realpath(path)
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
return { url: pathToFileURL(real).toString() }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Match a package-relative path (`./x`) against the redirect map, trying `.js`/`.json` appended.
|
|
134
|
+
function matchRedirect(map, relPath) {
|
|
135
|
+
if (map.size === 0) return undefined
|
|
136
|
+
for (const cand of [relPath, `${relPath}.js`, `${relPath}.json`]) {
|
|
137
|
+
if (map.has(cand)) return map.get(cand)
|
|
138
|
+
}
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Probe `base` for a source file in platform order: bare name first, then per ext
|
|
143
|
+
// `base.<platform>.<ext>`, `base.native.<ext>` (native only), `base.<ext>`. `platform: null`
|
|
144
|
+
// disables suffixes. Under metro, every SUFFIXED candidate first passes through the candidate's
|
|
145
|
+
// own package redirect map -- mirroring Metro's resolveSourceFileForExt, which runs
|
|
146
|
+
// redirectModulePath on each non-bare candidate: `false` short-circuits the whole probe to an
|
|
147
|
+
// empty module (e.g. browser { "./index.ios.js": false }), and a string redirect re-lands the
|
|
148
|
+
// candidate package-root-relative. Returns a path, { empty: true } (metro only), or null.
|
|
149
|
+
function resolveSourceFile(base, opts) {
|
|
150
|
+
// A path that literally names a `.d.ts` names no runtime module (types are erased). Probe its
|
|
151
|
+
// declaration-free stem instead, so an entry field pointing at `dist/index.d.ts` lands on the real
|
|
152
|
+
// `dist/index.js` beside it rather than bundling the declaration -- or failing the package
|
|
153
|
+
// outright by falling back to a different index. The probe below still refuses a literal hit.
|
|
154
|
+
base = stripTypeDeclaration(base)
|
|
155
|
+
// Candidates share base's directory, hence its package scope; resolve it lazily, once.
|
|
156
|
+
let scope
|
|
157
|
+
const redirectCandidate = (p) => {
|
|
158
|
+
if (scope === undefined) {
|
|
159
|
+
const pkg = nearestPackage(base, opts.host)
|
|
160
|
+
scope = pkg ? { pkgDir: pkg.pkgDir, map: mergeRedirectMap(pkg.pkg, opts.mainFields) } : null
|
|
161
|
+
}
|
|
162
|
+
if (!scope || scope.map.size === 0) return p
|
|
163
|
+
const rel = `./${toPosix(relative(scope.pkgDir, p))}`
|
|
164
|
+
const r = matchRedirect(scope.map, rel)
|
|
165
|
+
if (r === false) return false
|
|
166
|
+
if (typeof r === 'string') return isAbsolute(r) ? r : resolvePath(scope.pkgDir, r)
|
|
167
|
+
return p
|
|
168
|
+
}
|
|
169
|
+
// `derived` marks a candidate that is not the literal base; Metro redirect-checks those only.
|
|
170
|
+
const probePath = (p, derived) => {
|
|
171
|
+
if (opts.metro && derived) {
|
|
172
|
+
const r = redirectCandidate(p)
|
|
173
|
+
if (r === false) return { empty: true }
|
|
174
|
+
p = r
|
|
175
|
+
}
|
|
176
|
+
// A `.d.ts` is types-only, erased at runtime: never a resolution target, so a package whose
|
|
177
|
+
// entry field points at one falls through to its real `.js` instead of bundling the declaration.
|
|
178
|
+
if (isTypeDeclaration(p)) return null
|
|
179
|
+
return isFile(p, opts.host) ? p : null
|
|
180
|
+
}
|
|
181
|
+
// `suffix` is everything appended to `base`.
|
|
182
|
+
const probe = (suffix) => probePath(`${base}${suffix}`, suffix !== '')
|
|
183
|
+
const tryExt = (sourceExt) => {
|
|
184
|
+
if (opts.platform) {
|
|
185
|
+
const hit = probe(`.${opts.platform}${sourceExt}`)
|
|
186
|
+
if (hit) return hit
|
|
187
|
+
}
|
|
188
|
+
if (opts.preferNative && sourceExt !== '') {
|
|
189
|
+
const hit = probe(`.native${sourceExt}`)
|
|
190
|
+
if (hit) return hit
|
|
191
|
+
}
|
|
192
|
+
return probe(sourceExt)
|
|
193
|
+
}
|
|
194
|
+
const bare = tryExt('')
|
|
195
|
+
if (bare) return bare
|
|
196
|
+
// --typescript: tsc's extension substitution. A base naming a JS output extension probes its TS
|
|
197
|
+
// source siblings once the literal file (the bare probe above) is absent, so the on-disk `.js`
|
|
198
|
+
// always wins over its `.ts` twin. Literal siblings only -- no platform suffixes (tsc has none);
|
|
199
|
+
// extensionless bases keep going through the appended-extension loop below (sourceExts carries
|
|
200
|
+
// ts and tsx). Placed before that loop so `x.js` -> `x.ts` beats a pathological
|
|
201
|
+
// `x.js.<ext>`, matching tsc's candidate order. Never into node_modules: where tsc's pick, the
|
|
202
|
+
// first sibling on disk, lies there by its real path, nothing maps (inNodeModules), as the shared
|
|
203
|
+
// fallback refuses its own -- never a later sibling tsc wouldn't pick. The resolver drops
|
|
204
|
+
// `typescript` for an importer there.
|
|
205
|
+
if (opts.typescript) {
|
|
206
|
+
for (const cand of typescriptSiblings(base)) {
|
|
207
|
+
const hit = probePath(cand, true)
|
|
208
|
+
if (hit === null) continue
|
|
209
|
+
return typeof hit === 'string' && inNodeModules(hit, opts.host) ? null : hit
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const ext of opts.sourceExts) {
|
|
213
|
+
const hit = tryExt(`.${ext}`)
|
|
214
|
+
if (hit) return hit
|
|
215
|
+
}
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve absolute `base` as a file (ext probing) else as a directory (package.json `main` via
|
|
220
|
+
// mainFields + entry redirect, else `index`). `exports` is NOT consulted — Node applies it only to
|
|
221
|
+
// bare package-name imports, not to a path landing on a directory.
|
|
222
|
+
function resolveFileOrDir(base, opts) {
|
|
223
|
+
// resolveSourceFile yields a path, { empty: true } (a metro candidate-redirect hit), or null.
|
|
224
|
+
const asResolution = (hit) => (hit == null ? null : typeof hit === 'string' ? fileResolution(hit, opts.host) : hit)
|
|
225
|
+
const file = asResolution(resolveSourceFile(base, opts))
|
|
226
|
+
if (file) return file
|
|
227
|
+
if (isDir(base, opts.host)) {
|
|
228
|
+
const dpkg = readJson(join(base, 'package.json'), opts.host)
|
|
229
|
+
if (dpkg) {
|
|
230
|
+
const r = resolveEntryThroughMap(mergeRedirectMap(dpkg, opts.mainFields), packageEntry(dpkg, opts.mainFields), opts)
|
|
231
|
+
if (r.empty) return { empty: true }
|
|
232
|
+
// Node's LOAD_AS_DIRECTORY resolves `main` as a file, else as a directory index
|
|
233
|
+
// (`main: "./lib/"` -> lib/index.js); a broken `main` falls back to the package index below.
|
|
234
|
+
const entryBase = join(base, r.entry)
|
|
235
|
+
const inner = asResolution(resolveSourceFile(entryBase, opts) ?? resolveSourceFile(join(entryBase, 'index'), opts))
|
|
236
|
+
if (inner) return inner
|
|
237
|
+
}
|
|
238
|
+
const index = asResolution(resolveSourceFile(join(base, 'index'), opts))
|
|
239
|
+
if (index) return index
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Build a resolver bound to options: `conditions` (a resolveConditions set) gates `exports`
|
|
245
|
+
// packages (delegated to Node's algorithm), `mainFields`/`platform`/`preferNative`/`sourceExts`
|
|
246
|
+
// drive the legacy-field + suffix probing.
|
|
247
|
+
// `metro` opts into Metro's package-entry + candidate-redirect semantics (see
|
|
248
|
+
// resolveEntryThroughMap and resolveSourceFile); leave it off for the esbuild-parity
|
|
249
|
+
// `--mainFields` path. `typescript` adds tsc's extension substitution (a missing `x.js` probes
|
|
250
|
+
// its `x.ts` sibling; see resolveSourceFile) plus the shared miss fallback (see below), and
|
|
251
|
+
// `typescriptPaths` (a tsconfig paths matcher; see resolveTypescriptFallback) its tsconfig alias mapping.
|
|
252
|
+
// `metroKeepEntryOnBrowserFalse` overrides the module-level toggle
|
|
253
|
+
// (METRO_KEEP_ENTRY_ON_BROWSER_FALSE) per resolver -- primarily so tests can cover both branches.
|
|
254
|
+
// `host` is the filesystem view (@exodus/stasis-core/host), the real disk by default.
|
|
255
|
+
export function createFieldResolver({
|
|
256
|
+
conditions = [],
|
|
257
|
+
mainFields = ['main'],
|
|
258
|
+
platform = null,
|
|
259
|
+
preferNative = false,
|
|
260
|
+
sourceExts = ['js', 'jsx', 'json', 'ts', 'tsx'],
|
|
261
|
+
metro = false,
|
|
262
|
+
typescript = false,
|
|
263
|
+
typescriptPaths = null,
|
|
264
|
+
metroKeepEntryOnBrowserFalse = METRO_KEEP_ENTRY_ON_BROWSER_FALSE,
|
|
265
|
+
host = diskHost,
|
|
266
|
+
} = {}) {
|
|
267
|
+
const opts = { platform, preferNative, sourceExts, mainFields, metro, typescript, metroKeepEntryOnBrowserFalse, host }
|
|
268
|
+
// An importer in node_modules gets no --typescript mapping (inNodeModules), wherever its import lands.
|
|
269
|
+
const fromNodeModules = { ...opts, typescript: false }
|
|
270
|
+
// Node answers a builtin name with the builtin itself, so an `exports`-bearing package named like
|
|
271
|
+
// one (see the header) resolves through this instead, with builtins off; built on first use.
|
|
272
|
+
let packageResolver
|
|
273
|
+
// `callConditions` (from scan) is the parent's format-driven condition set, so `exports`
|
|
274
|
+
// delegation matches Node resolving from THAT file; falls back to configured `conditions`.
|
|
275
|
+
const resolve = function resolve(parentFile, specifier, callConditions) {
|
|
276
|
+
const fileOpts = typescript && inNodeModules(parentFile, host) ? fromNodeModules : opts
|
|
277
|
+
const conds = new Set(callConditions ?? conditions)
|
|
278
|
+
const viaNode = (spec) => {
|
|
279
|
+
try {
|
|
280
|
+
if (!isBuiltin(spec)) return fileResolution(host.resolve(parentFile, spec, conds), host)
|
|
281
|
+
packageResolver ??= createNodeResolver(host)
|
|
282
|
+
return fileResolution(packageResolver.resolve(parentFile, spec, conds, { builtins: false }), host)
|
|
283
|
+
} catch {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// A bare specifier (`mod`, `mod/sub`) through node_modules.
|
|
288
|
+
const viaPackage = (spec) => {
|
|
289
|
+
const loc = locatePackage(dirname(parentFile), spec, host)
|
|
290
|
+
if (!loc) return null
|
|
291
|
+
const pkg = readJson(join(loc.pkgDir, 'package.json'), host) ?? {}
|
|
292
|
+
// `exports` wins over mainFields; Node's algorithm resolves it (with conditions) correctly.
|
|
293
|
+
if (pkg.exports != null) return viaNode(spec)
|
|
294
|
+
// A bare package import resolves its directory via the same dir algorithm as any other.
|
|
295
|
+
if (loc.subpath === '') return resolveFileOrDir(loc.pkgDir, fileOpts)
|
|
296
|
+
const sub = `./${loc.subpath}`
|
|
297
|
+
const r = matchRedirect(mergeRedirectMap(pkg, mainFields), sub)
|
|
298
|
+
if (r === false) return { empty: true }
|
|
299
|
+
const target = typeof r === 'string' ? r : sub
|
|
300
|
+
return resolveFileOrDir(join(loc.pkgDir, target), fileOpts)
|
|
301
|
+
}
|
|
302
|
+
// `#name` subpath imports use the `imports` field + conditions; Node's algorithm handles them.
|
|
303
|
+
if (specifier.startsWith('#')) return viaNode(specifier)
|
|
304
|
+
|
|
305
|
+
let spec = specifier
|
|
306
|
+
// Redirect via the IMPORTER's browser/react-native map, BEFORE the builtin check on purpose:
|
|
307
|
+
// a map entry can disable or shim a builtin (`{"crypto": false}` / `"crypto-browserify"`).
|
|
308
|
+
const imp = nearestPackage(parentFile, host)
|
|
309
|
+
if (imp) {
|
|
310
|
+
const map = mergeRedirectMap(imp.pkg, mainFields)
|
|
311
|
+
let r
|
|
312
|
+
if (spec.startsWith('.') || isAbsolute(spec)) {
|
|
313
|
+
const abs = isAbsolute(spec) ? spec : resolvePath(dirname(parentFile), spec)
|
|
314
|
+
r = matchRedirect(map, `./${toPosix(relative(imp.pkgDir, abs))}`)
|
|
315
|
+
} else {
|
|
316
|
+
r = map.get(spec) // a bare specifier (`mod`, `mod/sub`) matches the map's bare keys
|
|
317
|
+
}
|
|
318
|
+
if (r === false) return { empty: true }
|
|
319
|
+
if (typeof r === 'string') {
|
|
320
|
+
// Browser-map targets are relative to the PACKAGE ROOT, not the importing file; a
|
|
321
|
+
// bare-module target re-enters resolution below.
|
|
322
|
+
if (r.startsWith('.') || isAbsolute(r)) {
|
|
323
|
+
return resolveFileOrDir(isAbsolute(r) ? r : resolvePath(imp.pkgDir, r), fileOpts)
|
|
324
|
+
}
|
|
325
|
+
spec = r
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (spec.startsWith('.') || isAbsolute(spec)) {
|
|
330
|
+
const base = isAbsolute(spec) ? spec : resolvePath(dirname(parentFile), spec)
|
|
331
|
+
return resolveFileOrDir(base, fileOpts)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A builtin the browser map did not remap: its `node:` form names the builtin itself; a bare
|
|
335
|
+
// name is the installed package of that name (see the header), the builtin where none resolves.
|
|
336
|
+
if (isBuiltin(spec)) return spec.startsWith('node:') ? { builtin: true } : (viaPackage(spec) ?? { builtin: true })
|
|
337
|
+
return viaPackage(spec)
|
|
338
|
+
}
|
|
339
|
+
if (!typescript) return resolve
|
|
340
|
+
// --typescript: when the whole field flow leaves the specifier unresolved, give tsc's mapping
|
|
341
|
+
// the same shot the scanner's fallback gets, via the shared dispatcher. On this path it covers
|
|
342
|
+
// the layers the field resolver delegates to Node -- `exports`-bearing packages and `#` subpath
|
|
343
|
+
// imports, whose targets may name compiled files existing only as TS source -- plus tsconfig
|
|
344
|
+
// `paths` aliases; misses only, so no field/redirect/suffix resolution is ever overridden.
|
|
345
|
+
// (Relative paths re-probe tsc-style too -- redundant after resolveSourceFile, but harmless.)
|
|
346
|
+
return function resolveWithTypescriptFallback(parentFile, specifier, callConditions) {
|
|
347
|
+
const resolved = resolve(parentFile, specifier, callConditions)
|
|
348
|
+
if (resolved) return resolved
|
|
349
|
+
const hit = resolveTypescriptFallback(parentFile, specifier, {
|
|
350
|
+
conditions: new Set(callConditions ?? conditions),
|
|
351
|
+
paths: typescriptPaths,
|
|
352
|
+
host,
|
|
353
|
+
})
|
|
354
|
+
return hit == null ? null : fileResolution(hit, host)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// True when `format` is an ESM family (drives the base condition set, mirroring scan).
|
|
359
|
+
function isModuleFormat(format) {
|
|
360
|
+
return format === 'module' || format === 'module-typescript'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Condition set for a parent of the given format, as scan's #conditionSet gives a custom resolver:
|
|
364
|
+
// `import` or `require` by format, `default` (which Node's resolver matches anyway), plus extras.
|
|
365
|
+
// A bundler's base, not Node's: esbuild, webpack and Metro assert `import`/`require` and
|
|
366
|
+
// `default`, never `node`, so an `exports` map listing `node` first can't win over the extras.
|
|
367
|
+
// Order is irrelevant (Node membership-tests against exports key order).
|
|
368
|
+
export function resolveConditions(format, extras = []) {
|
|
369
|
+
return [...new Set([isModuleFormat(format) ? 'import' : 'require', 'default', ...extras])]
|
|
370
|
+
}
|