@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/loaders/php.js
CHANGED
|
@@ -861,9 +861,10 @@ export function phpClassDependencies(content) {
|
|
|
861
861
|
if (impl) for (const nm of impl[1].split(',')) add(nm)
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
// catch (A | B $e)
|
|
865
|
-
//
|
|
866
|
-
|
|
864
|
+
// catch (A | B $e), or PHP 8's catch (A | B) with no variable: capture the type
|
|
865
|
+
// list up to `$var` or `)`. One quantifier with nothing after it keeps matching
|
|
866
|
+
// linear on adversarial input.
|
|
867
|
+
for (const m of stripped.matchAll(/\bcatch\s*\(([^)$]*)/giu)) {
|
|
867
868
|
for (const nm of m[1].split('|')) add(nm)
|
|
868
869
|
}
|
|
869
870
|
|
package/src/metro-resolver.js
CHANGED
|
@@ -64,7 +64,7 @@ function getPackageForModule(absModulePath) {
|
|
|
64
64
|
export function createMetroResolver({
|
|
65
65
|
projectDir,
|
|
66
66
|
platform,
|
|
67
|
-
sourceExts = ['js', 'json', 'ts'],
|
|
67
|
+
sourceExts = ['js', 'jsx', 'json', 'ts', 'tsx'],
|
|
68
68
|
mainFields = ['react-native', 'browser', 'main'],
|
|
69
69
|
conditionNames = ['react-native'],
|
|
70
70
|
conditionsByPlatform = { web: ['browser'] },
|
package/src/resolve-fields.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url'
|
|
|
4
4
|
|
|
5
5
|
import { isTypeDeclaration, stripTypeDeclaration, toPosix } from '@exodus/stasis-core/util'
|
|
6
6
|
import {
|
|
7
|
+
inNodeModules,
|
|
7
8
|
isDir,
|
|
8
9
|
isFile,
|
|
9
10
|
locatePackage,
|
|
@@ -174,12 +175,16 @@ function resolveSourceFile(base, opts) {
|
|
|
174
175
|
// source siblings once the literal file (the bare probe above) is absent, so the on-disk `.js`
|
|
175
176
|
// always wins over its `.ts` twin. Literal siblings only -- no platform suffixes (tsc has none);
|
|
176
177
|
// extensionless bases keep going through the appended-extension loop below (sourceExts carries
|
|
177
|
-
// ts
|
|
178
|
-
// `x.js.<ext>`, matching tsc's candidate order.
|
|
178
|
+
// ts and tsx). Placed before that loop so `x.js` -> `x.ts` beats a pathological
|
|
179
|
+
// `x.js.<ext>`, matching tsc's candidate order. Never into node_modules: where tsc's pick, the
|
|
180
|
+
// first sibling on disk, lies there by its real path, nothing maps (inNodeModules), as the shared
|
|
181
|
+
// fallback refuses its own -- never a later sibling tsc wouldn't pick. The resolver drops
|
|
182
|
+
// `typescript` for an importer there.
|
|
179
183
|
if (opts.typescript) {
|
|
180
|
-
for (const cand of typescriptSiblings(base
|
|
184
|
+
for (const cand of typescriptSiblings(base)) {
|
|
181
185
|
const hit = probePath(cand, true)
|
|
182
|
-
if (hit)
|
|
186
|
+
if (hit === null) continue
|
|
187
|
+
return typeof hit === 'string' && inNodeModules(hit, opts.host) ? null : hit
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
for (const ext of opts.sourceExts) {
|
|
@@ -220,7 +225,7 @@ function resolveFileOrDir(base, opts) {
|
|
|
220
225
|
// resolveEntryThroughMap and resolveSourceFile); leave it off for the esbuild-parity
|
|
221
226
|
// `--mainFields` path. `typescript` adds tsc's extension substitution (a missing `x.js` probes
|
|
222
227
|
// its `x.ts` sibling; see resolveSourceFile) plus the shared miss fallback (see below), and
|
|
223
|
-
// `typescriptPaths` (a
|
|
228
|
+
// `typescriptPaths` (a tsconfig paths matcher; see resolveTypescriptFallback) its tsconfig alias mapping.
|
|
224
229
|
// `metroKeepEntryOnBrowserFalse` overrides the module-level toggle
|
|
225
230
|
// (METRO_KEEP_ENTRY_ON_BROWSER_FALSE) per resolver -- primarily so tests can cover both branches.
|
|
226
231
|
// `host` is the filesystem view (@exodus/stasis-core/host), the real disk by default.
|
|
@@ -229,7 +234,7 @@ export function createFieldResolver({
|
|
|
229
234
|
mainFields = ['main'],
|
|
230
235
|
platform = null,
|
|
231
236
|
preferNative = false,
|
|
232
|
-
sourceExts = ['js', 'json', 'ts'],
|
|
237
|
+
sourceExts = ['js', 'jsx', 'json', 'ts', 'tsx'],
|
|
233
238
|
metro = false,
|
|
234
239
|
typescript = false,
|
|
235
240
|
typescriptPaths = null,
|
|
@@ -237,9 +242,12 @@ export function createFieldResolver({
|
|
|
237
242
|
host = diskHost,
|
|
238
243
|
} = {}) {
|
|
239
244
|
const opts = { platform, preferNative, sourceExts, mainFields, metro, typescript, metroKeepEntryOnBrowserFalse, host }
|
|
245
|
+
// An importer in node_modules gets no --typescript mapping (inNodeModules), wherever its import lands.
|
|
246
|
+
const fromNodeModules = { ...opts, typescript: false }
|
|
240
247
|
// `callConditions` (from scan) is the parent's format-driven condition set, so `exports`
|
|
241
248
|
// delegation matches Node resolving from THAT file; falls back to configured `conditions`.
|
|
242
249
|
const resolve = function resolve(parentFile, specifier, callConditions) {
|
|
250
|
+
const fileOpts = typescript && inNodeModules(parentFile, host) ? fromNodeModules : opts
|
|
243
251
|
const conds = new Set(callConditions ?? conditions)
|
|
244
252
|
const viaNode = (spec) => {
|
|
245
253
|
try {
|
|
@@ -269,7 +277,7 @@ export function createFieldResolver({
|
|
|
269
277
|
// Browser-map targets are relative to the PACKAGE ROOT, not the importing file; a
|
|
270
278
|
// bare-module target re-enters resolution below.
|
|
271
279
|
if (r.startsWith('.') || isAbsolute(r)) {
|
|
272
|
-
return resolveFileOrDir(isAbsolute(r) ? r : resolvePath(imp.pkgDir, r),
|
|
280
|
+
return resolveFileOrDir(isAbsolute(r) ? r : resolvePath(imp.pkgDir, r), fileOpts)
|
|
273
281
|
}
|
|
274
282
|
spec = r
|
|
275
283
|
}
|
|
@@ -280,7 +288,7 @@ export function createFieldResolver({
|
|
|
280
288
|
|
|
281
289
|
if (spec.startsWith('.') || isAbsolute(spec)) {
|
|
282
290
|
const base = isAbsolute(spec) ? spec : resolvePath(dirname(parentFile), spec)
|
|
283
|
-
return resolveFileOrDir(base,
|
|
291
|
+
return resolveFileOrDir(base, fileOpts)
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
const loc = locatePackage(dirname(parentFile), spec, host)
|
|
@@ -289,12 +297,12 @@ export function createFieldResolver({
|
|
|
289
297
|
// `exports` wins over mainFields; Node's algorithm resolves it (with conditions) correctly.
|
|
290
298
|
if (pkg.exports != null) return viaNode(spec)
|
|
291
299
|
// A bare package import resolves its directory via the same dir algorithm as any other.
|
|
292
|
-
if (loc.subpath === '') return resolveFileOrDir(loc.pkgDir,
|
|
300
|
+
if (loc.subpath === '') return resolveFileOrDir(loc.pkgDir, fileOpts)
|
|
293
301
|
const sub = `./${loc.subpath}`
|
|
294
302
|
const r = matchRedirect(mergeRedirectMap(pkg, mainFields), sub)
|
|
295
303
|
if (r === false) return { empty: true }
|
|
296
304
|
const target = typeof r === 'string' ? r : sub
|
|
297
|
-
return resolveFileOrDir(join(loc.pkgDir, target),
|
|
305
|
+
return resolveFileOrDir(join(loc.pkgDir, target), fileOpts)
|
|
298
306
|
}
|
|
299
307
|
if (!typescript) return resolve
|
|
300
308
|
// --typescript: when the whole field flow leaves the specifier unresolved, give tsc's mapping
|
|
@@ -308,7 +316,6 @@ export function createFieldResolver({
|
|
|
308
316
|
if (resolved) return resolved
|
|
309
317
|
const hit = resolveTypescriptFallback(parentFile, specifier, {
|
|
310
318
|
conditions: new Set(callConditions ?? conditions),
|
|
311
|
-
tsx: sourceExts.includes('tsx'),
|
|
312
319
|
paths: typescriptPaths,
|
|
313
320
|
host,
|
|
314
321
|
})
|
|
@@ -8,7 +8,7 @@ import { diskHost } from '@exodus/stasis-core/host'
|
|
|
8
8
|
// everywhere: when normal resolution misses and tsc would land on an on-disk TypeScript source,
|
|
9
9
|
// complete the miss with that file. It never rewrites a resolution that succeeded, so an on-disk
|
|
10
10
|
// `.js` always beats its `.ts` twin. The rules, in the shapes they apply to:
|
|
11
|
-
// - extension substitution: `./x.js` -> `./x.ts` (`.jsx` -> `.tsx
|
|
11
|
+
// - extension substitution: `./x.js` -> `./x.ts` (`.jsx` -> `.tsx`, `.mjs` -> `.mts`,
|
|
12
12
|
// `.cjs` -> `.cts`) -- tsc never rewrites specifiers, so TS sources import each other by their
|
|
13
13
|
// OUTPUT names, and the specifier names a file that only exists as its TS source;
|
|
14
14
|
// - extension/index completion for an extensionless path (`./util` -> `./util.ts`,
|
|
@@ -16,13 +16,13 @@ import { diskHost } from '@exodus/stasis-core/host'
|
|
|
16
16
|
// - the same substitution applied to manifest-declared targets: a package `main`, an `exports`
|
|
17
17
|
// target, a `#name` `imports` target naming a compiled file whose only on-disk form is TS;
|
|
18
18
|
// - tsconfig `compilerOptions.paths` aliases (see loadTsconfigPaths), consulted for bare
|
|
19
|
-
// specifiers nothing else resolved.
|
|
19
|
+
// specifiers nothing else resolved. Node never probed an alias target, so its completion also
|
|
20
|
+
// tries the JS extensions after the TS ones (`@/x` -> x.ts, then x.js), as tsc does.
|
|
20
21
|
// This module also homes the generic manifest helpers (readJson/locatePackage/nearestPackage)
|
|
21
22
|
// both resolvers share -- it is the dependency-free lower layer, importing from neither.
|
|
22
23
|
|
|
23
24
|
// tsc's extension substitution table: the JS output extensions mapped back to the TS source
|
|
24
|
-
// siblings that may sit on disk in their place.
|
|
25
|
-
// carry/parse .tsx (i.e. --jsx). JS_OUTPUT_EXTS is derived, so the two cannot drift.
|
|
25
|
+
// siblings that may sit on disk in their place. JS_OUTPUT_EXTS is derived, so the two cannot drift.
|
|
26
26
|
const TS_SIBLING_EXTS = new Map([
|
|
27
27
|
['.js', ['.ts', '.tsx']],
|
|
28
28
|
['.jsx', ['.tsx']],
|
|
@@ -36,12 +36,12 @@ const JS_OUTPUT_EXTS = new Set(TS_SIBLING_EXTS.keys())
|
|
|
36
36
|
// extname sees no extension in './.js', matching the extensionless gates downstream). Candidates
|
|
37
37
|
// spelling a type declaration (./x.d.js -> ./x.d.ts) are returned too: every probe site refuses
|
|
38
38
|
// declarations (types-only, erased at runtime), so the screen lives at probe time, once.
|
|
39
|
-
export function typescriptSiblings(name
|
|
39
|
+
export function typescriptSiblings(name) {
|
|
40
40
|
const ext = extname(name)
|
|
41
41
|
const exts = TS_SIBLING_EXTS.get(ext)
|
|
42
42
|
if (!exts) return []
|
|
43
43
|
const stem = name.slice(0, -ext.length)
|
|
44
|
-
return exts.
|
|
44
|
+
return exts.map((e) => `${stem}${e}`)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Extensions that never take the appended-`.ts` completion: JS outputs (substitution territory)
|
|
@@ -49,6 +49,15 @@ export function typescriptSiblings(name, { tsx = false } = {}) {
|
|
|
49
49
|
// completed like an extensionless name, matching tsc's candidate list.
|
|
50
50
|
const NO_COMPLETION_EXTS = new Set([...JS_OUTPUT_EXTS, '.ts', '.tsx', '.mts', '.cts', '.json'])
|
|
51
51
|
|
|
52
|
+
// `path`'s real path, or `path` itself where it has none (not on disk).
|
|
53
|
+
function realpathOr(path, host) {
|
|
54
|
+
try {
|
|
55
|
+
return host.realpath(path)
|
|
56
|
+
} catch {
|
|
57
|
+
return path
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
export function isFile(p, host = diskHost) {
|
|
53
62
|
return host.stat(p)?.isFile() ?? false
|
|
54
63
|
}
|
|
@@ -113,8 +122,13 @@ export function nearestPackage(file, host = diskHost) {
|
|
|
113
122
|
// (a `.d.ts` is types-only, erased at runtime -- tsc records it for types, never for emit).
|
|
114
123
|
const probe = (p, host) => (!isTypeDeclaration(p) && isFile(p, host) ? p : null)
|
|
115
124
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
125
|
+
// The extensions tsc's completion appends, in its order (tryAddingExtensions): .ts, .tsx, then --
|
|
126
|
+
// with `js`, for a path Node never probed (a tsconfig paths target) -- .js, .jsx. A path Node
|
|
127
|
+
// already completed needs only the TS ones. .d.ts (tsc's third) is never a target (see probe).
|
|
128
|
+
const completionExts = (js) => (js ? ['.ts', '.tsx', '.js', '.jsx'] : ['.ts', '.tsx'])
|
|
129
|
+
|
|
130
|
+
// LOAD_INDEX with tsc's completions: dir/index.ts, dir/index.tsx (+ index.js/index.jsx with `js`).
|
|
131
|
+
// Without `js`, Node already probed the .js/.json indexes before the fallback ever runs.
|
|
118
132
|
function probeIndex(dir, exts, host) {
|
|
119
133
|
for (const ext of exts) {
|
|
120
134
|
const hit = probe(join(dir, `index${ext}`), host)
|
|
@@ -130,13 +144,14 @@ function probeIndex(dir, exts, host) {
|
|
|
130
144
|
// LOAD_AS_DIRECTORY) then its index. `dirOnly` skips the file rules for specs Node treats as
|
|
131
145
|
// directory-only ('.', '..', a trailing '/'), so './' never probes the pathological '.ts' dotfile.
|
|
132
146
|
// `completion`/`dir` are off for exports/imports targets: Node requires those to name exact files,
|
|
133
|
-
// so only substitution applies (matching tsc's node16 rules).
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
// so only substitution applies (matching tsc's node16 rules). `js` completes JS extensions too, for
|
|
148
|
+
// a base Node never probed (see completionExts).
|
|
149
|
+
function probeTypescriptTarget(base, { js = false, dirOnly = false, completion = true, dir = true, host = diskHost } = {}) {
|
|
150
|
+
const exts = completionExts(js)
|
|
136
151
|
if (!dirOnly) {
|
|
137
152
|
const literal = probe(base, host)
|
|
138
153
|
if (literal) return literal
|
|
139
|
-
for (const cand of typescriptSiblings(base
|
|
154
|
+
for (const cand of typescriptSiblings(base)) {
|
|
140
155
|
const hit = probe(cand, host)
|
|
141
156
|
if (hit) return hit
|
|
142
157
|
}
|
|
@@ -154,7 +169,7 @@ function probeTypescriptTarget(base, { tsx = false, dirOnly = false, completion
|
|
|
154
169
|
// LOAD_AS_FILE(main) with substitution/completion, then LOAD_INDEX(main); a broken main
|
|
155
170
|
// falls through to the package index, like Node.
|
|
156
171
|
const entry = resolvePath(base, main)
|
|
157
|
-
const hit = probeTypescriptTarget(entry, {
|
|
172
|
+
const hit = probeTypescriptTarget(entry, { js, dir: false, host }) ?? probeIndex(entry, exts, host)
|
|
158
173
|
if (hit) return hit
|
|
159
174
|
}
|
|
160
175
|
return probeIndex(base, exts, host)
|
|
@@ -228,9 +243,9 @@ function manifestTargets(map, subpathKey, conditions) {
|
|
|
228
243
|
// tsc's mapping of `key` ('.' or './sub') through the `exports` of the package in `pkgDir` (or of
|
|
229
244
|
// a '#name' key through its `imports`): the targets `conditions` select, substitution only (Node
|
|
230
245
|
// requires exports/imports targets to name exact files), the first on disk; or null.
|
|
231
|
-
export function typescriptExportsTarget(pkgDir, exports, key, { conditions = new Set(),
|
|
246
|
+
export function typescriptExportsTarget(pkgDir, exports, key, { conditions = new Set(), host = diskHost } = {}) {
|
|
232
247
|
for (const target of manifestTargets(exports, key, conditions)) {
|
|
233
|
-
const hit = probeTypescriptTarget(resolvePath(pkgDir, target), {
|
|
248
|
+
const hit = probeTypescriptTarget(resolvePath(pkgDir, target), { completion: false, dir: false, host })
|
|
234
249
|
if (hit) return hit
|
|
235
250
|
}
|
|
236
251
|
return null
|
|
@@ -299,24 +314,48 @@ function resolveExtendsTarget(fromFile, target, host) {
|
|
|
299
314
|
throw new Error(`tsconfig extends target not found: '${target}' (from ${fromFile})`)
|
|
300
315
|
}
|
|
301
316
|
|
|
302
|
-
//
|
|
303
|
-
// extending
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
317
|
+
// The configs a tsconfig's `extends` chain reads, as { file, raw }: each base (in the order
|
|
318
|
+
// `extends` names them) before the config extending it, and `file` last. A base two configs share
|
|
319
|
+
// is listed under each, so folding the list in order, later over earlier, is tsc's nested merge,
|
|
320
|
+
// where the second extender's resolved options carry the shared base's again. Like tsc's, the
|
|
321
|
+
// cycle check is per branch: `extending` holds the configs extending `file`, and only a config that
|
|
322
|
+
// extends itself, directly or not, throws.
|
|
323
|
+
function* tsconfigChain(file, host, extending = new Set()) {
|
|
324
|
+
if (extending.has(file)) throw new Error(`tsconfig extends cycle at ${file}`)
|
|
308
325
|
const raw = parseJsonc(host.readFile(file).toString('utf8'), file)
|
|
309
|
-
|
|
310
|
-
for (const base of [].concat(raw?.extends ?? []))
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
extending.add(file)
|
|
327
|
+
for (const base of [].concat(raw?.extends ?? [])) yield* tsconfigChain(resolveExtendsTarget(file, base, host), host, extending)
|
|
328
|
+
extending.delete(file)
|
|
329
|
+
yield { file, raw }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// tsc's path-valued compilerOptions (a list's element by element), each made absolute against the
|
|
333
|
+
// dir of the config declaring it, or, when it starts with `${configDir}`, against the loaded one's.
|
|
334
|
+
const TSCONFIG_PATH_OPTIONS = new Set(['baseUrl', 'declarationDir', 'generateCpuProfile', 'generateTrace', 'outDir', 'outFile', 'rootDir', 'tsBuildInfoFile'])
|
|
335
|
+
const TSCONFIG_PATH_LIST_OPTIONS = new Set(['rootDirs', 'typeRoots'])
|
|
336
|
+
const CONFIG_DIR = '${configDir}'
|
|
337
|
+
const tsconfigPath = (value, dir, configDir) => {
|
|
338
|
+
if (typeof value !== 'string') return value
|
|
339
|
+
return value.startsWith(CONFIG_DIR) ? resolvePath(configDir, value.replace(CONFIG_DIR, './')) : resolvePath(dir, value)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// The `compilerOptions` a tsconfig resolves to across its `extends` chain, merged one level deep
|
|
343
|
+
// as tsc merges them: each base in order, the extending file's own over them (`null` unsetting an
|
|
344
|
+
// option), each path-valued option absolute, and `paths` as declared, with tsc's `pathsBasePath`,
|
|
345
|
+
// the dir of the config declaring it.
|
|
346
|
+
export function loadTsconfigCompilerOptions(file, host = diskHost) {
|
|
347
|
+
let options = {}
|
|
348
|
+
for (const { file: at, raw } of tsconfigChain(file, host)) {
|
|
349
|
+
const own = { ...raw?.compilerOptions }
|
|
350
|
+
for (const [name, value] of Object.entries(own)) {
|
|
351
|
+
if (TSCONFIG_PATH_OPTIONS.has(name)) own[name] = tsconfigPath(value, dirname(at), dirname(file))
|
|
352
|
+
else if (TSCONFIG_PATH_LIST_OPTIONS.has(name) && Array.isArray(value)) own[name] = value.map((p) => tsconfigPath(p, dirname(at), dirname(file)))
|
|
353
|
+
}
|
|
354
|
+
delete own.pathsBasePath // tsc's own, never a config's
|
|
355
|
+
if (own.paths != null) own.pathsBasePath = dirname(at)
|
|
356
|
+
options = { ...options, ...own }
|
|
318
357
|
}
|
|
319
|
-
return
|
|
358
|
+
return options
|
|
320
359
|
}
|
|
321
360
|
|
|
322
361
|
// Load a tsconfig's `compilerOptions.paths` into a matcher, following `extends`. Returns null when
|
|
@@ -324,11 +363,12 @@ function loadConfigChain(file, seen, host) {
|
|
|
324
363
|
// substituted targets of the BEST-matching key -- exact match first, else the '*' pattern with the
|
|
325
364
|
// longest matched prefix, exactly one key, its targets in order (tsc tries no other key when they
|
|
326
365
|
// all miss) -- resolved against `baseUrl`, or against the declaring config's dir without one
|
|
327
|
-
// (TS 4.1 paths-without-baseUrl)
|
|
328
|
-
// (a non-array value, more than one '*' in a key or
|
|
366
|
+
// (TS 4.1 paths-without-baseUrl), or, starting with `${configDir}`, against `file`'s. Only
|
|
367
|
+
// `baseUrl`/`paths` are used; malformed shapes (a non-array value, more than one '*' in a key or
|
|
368
|
+
// target) fail closed like tsc's config errors.
|
|
329
369
|
export function loadTsconfigPaths(file, host = diskHost) {
|
|
330
370
|
if (file == null) return null
|
|
331
|
-
const { paths,
|
|
371
|
+
const { paths, pathsBasePath, baseUrl } = loadTsconfigCompilerOptions(file, host)
|
|
332
372
|
if (paths == null || typeof paths !== 'object' || Object.keys(paths).length === 0) return null
|
|
333
373
|
const starCount = (s) => s.split('*').length - 1
|
|
334
374
|
for (const [key, targets] of Object.entries(paths)) {
|
|
@@ -339,7 +379,7 @@ export function loadTsconfigPaths(file, host = diskHost) {
|
|
|
339
379
|
if (starCount(t) > 1) throw new Error(`tsconfig paths pattern '${t}' has more than one '*' (${file})`)
|
|
340
380
|
}
|
|
341
381
|
}
|
|
342
|
-
const base = baseUrl
|
|
382
|
+
const base = typeof baseUrl === 'string' ? baseUrl : pathsBasePath
|
|
343
383
|
return {
|
|
344
384
|
matchPaths(spec) {
|
|
345
385
|
let targets = null
|
|
@@ -360,22 +400,84 @@ export function loadTsconfigPaths(file, host = diskHost) {
|
|
|
360
400
|
targets = paths[best.key].map((t) => ({ target: t, wildcard }))
|
|
361
401
|
}
|
|
362
402
|
return targets.map(({ target, wildcard }) =>
|
|
363
|
-
|
|
403
|
+
tsconfigPath(wildcard == null ? target : target.replace('*', wildcard), base, dirname(file)))
|
|
364
404
|
},
|
|
365
405
|
}
|
|
366
406
|
}
|
|
367
407
|
|
|
368
|
-
// The tsconfig
|
|
369
|
-
// typo)
|
|
370
|
-
// discovery from a directory.
|
|
408
|
+
// The tsconfig an explicit `--tsconfig` names, resolved against `baseDir`; it must exist (fail
|
|
409
|
+
// closed on a typo). Without one, each package's own applies (packageTsconfigPaths).
|
|
371
410
|
export function discoverTsconfig(baseDir, explicit, host = diskHost) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
411
|
+
const p = resolvePath(baseDir, explicit)
|
|
412
|
+
if (!isFile(p, host)) throw new Error(`tsconfig not found: ${explicit}`)
|
|
413
|
+
return p
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Whether `dir` holds a package: a package.json naming one. One with no name is a `type` marker,
|
|
417
|
+
// its files the package's above (as vfs-bundle's holdsPackage reads it).
|
|
418
|
+
const holdsNamedPackage = (dir, host) => isFile(join(dir, 'package.json'), host) && readJson(join(dir, 'package.json'), host)?.name !== undefined
|
|
419
|
+
|
|
420
|
+
// Whether `dir` declares a monorepo's workspaces: a pnpm-workspace.yaml, or a package.json
|
|
421
|
+
// `workspaces` (npm's and yarn's).
|
|
422
|
+
const declaresWorkspaces = (dir, host) => isFile(join(dir, 'pnpm-workspace.yaml'), host) || Boolean(readJson(join(dir, 'package.json'), host)?.workspaces)
|
|
423
|
+
|
|
424
|
+
// The `paths` --typescript honours without --tsconfig: those of the tsconfig.json beside the
|
|
425
|
+
// package.json of the package the importing file is in, as a matcher whose matchPaths(spec,
|
|
426
|
+
// parentFile) is loadTsconfigPaths's for that config. The project is read up from `cwd` as the
|
|
427
|
+
// State reads it: the directories holding a package.json, up to one holding .git or
|
|
428
|
+
// pnpm-workspace.yaml, or up to `projectCwd` (the State's PROJECT_CWD, as yarn sets it). In a monorepo -- where one of those declares workspaces -- a file's package is
|
|
429
|
+
// the nearest directory at or above it, up to that workspace root, holding a named package.json
|
|
430
|
+
// (the root itself where none below it does); outside one, every file takes the one config beside
|
|
431
|
+
// the nearest named package.json at or above `cwd` (or the top-most package.json, where none is
|
|
432
|
+
// named). Never in node_modules: no walk enters a node_modules directory, and resolveTypescriptFallback
|
|
433
|
+
// matches no alias for a file under one. Each config loads once, when first matched; with `lenient`,
|
|
434
|
+
// one that doesn't load matches nothing instead of throwing.
|
|
435
|
+
export function packageTsconfigPaths(cwd, { host = diskHost, lenient = false, projectCwd } = {}) {
|
|
436
|
+
const stop = projectCwd ? realpathOr(resolvePath(projectCwd), host) : null
|
|
437
|
+
const dirs = [] // cwd and above, while the State's walk would go on; real paths, as importers are read
|
|
438
|
+
for (let dir = realpathOr(resolvePath(cwd), host); basename(dir) !== 'node_modules'; dir = dirname(dir)) {
|
|
439
|
+
dirs.push(dir)
|
|
440
|
+
if (dir === stop) break
|
|
441
|
+
if (host.stat(join(dir, '.git')) !== null || isFile(join(dir, 'pnpm-workspace.yaml'), host)) break
|
|
442
|
+
if (dirname(dir) === dir) break
|
|
443
|
+
}
|
|
444
|
+
const workspaceRoot = dirs.find((dir) => declaresWorkspaces(dir, host)) ?? null
|
|
445
|
+
const projectPackage = workspaceRoot === null
|
|
446
|
+
? dirs.find((dir) => holdsNamedPackage(dir, host)) ?? dirs.findLast((dir) => isFile(join(dir, 'package.json'), host)) ?? null
|
|
447
|
+
: null
|
|
448
|
+
// The package directory `parentFile` is in, or null outside the workspace root. By its real path,
|
|
449
|
+
// as inNodeModules reads it: a linked workspace package reached through node_modules takes its
|
|
450
|
+
// own config, read where it lies, so a relative `extends` resolves as tsc resolves it.
|
|
451
|
+
const packageOf = (parentFile) => {
|
|
452
|
+
if (workspaceRoot === null) return projectPackage
|
|
453
|
+
for (let dir = dirname(realpathOr(parentFile, host)); basename(dir) !== 'node_modules'; dir = dirname(dir)) {
|
|
454
|
+
if (dir === workspaceRoot || holdsNamedPackage(dir, host)) return dir
|
|
455
|
+
if (dirname(dir) === dir) return null
|
|
456
|
+
}
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
const matchers = new Map() // package dir -> loadTsconfigPaths matcher, or null
|
|
460
|
+
const matcherOf = (dir) => {
|
|
461
|
+
if (!matchers.has(dir)) {
|
|
462
|
+
const file = join(dir, 'tsconfig.json')
|
|
463
|
+
let matcher = null
|
|
464
|
+
if (isFile(file, host)) {
|
|
465
|
+
try {
|
|
466
|
+
matcher = loadTsconfigPaths(file, host)
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (!lenient) throw error
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
matchers.set(dir, matcher)
|
|
472
|
+
}
|
|
473
|
+
return matchers.get(dir)
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
matchPaths(spec, parentFile) {
|
|
477
|
+
const dir = packageOf(parentFile)
|
|
478
|
+
return dir === null ? [] : (matcherOf(dir)?.matchPaths(spec) ?? [])
|
|
479
|
+
},
|
|
376
480
|
}
|
|
377
|
-
const p = join(baseDir, 'tsconfig.json')
|
|
378
|
-
return isFile(p, host) ? p : null
|
|
379
481
|
}
|
|
380
482
|
|
|
381
483
|
// --- the fallback dispatcher ---
|
|
@@ -384,31 +486,43 @@ export function discoverTsconfig(baseDir, explicit, host = diskHost) {
|
|
|
384
486
|
// the file rules are skipped for them, exactly as Node skips LOAD_AS_FILE.
|
|
385
487
|
const DIR_ONLY_SPEC = /(?:^|\/)\.{1,2}$|\/$/u
|
|
386
488
|
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
489
|
+
// A node_modules path segment, the last one included (the directory itself).
|
|
490
|
+
const IN_NODE_MODULES = /(?:^|[\\/])node_modules(?:[\\/]|$)/u
|
|
491
|
+
|
|
492
|
+
// Whether `file` lies in node_modules, by its real path where it has one (by its own, where it
|
|
493
|
+
// doesn't): --typescript maps nothing from or into an installed package. A monorepo's workspace
|
|
494
|
+
// packages, linked in through node_modules, really lie outside it, so they keep the mapping.
|
|
495
|
+
export const inNodeModules = (file, host = diskHost) => IN_NODE_MODULES.test(realpathOr(file, host))
|
|
390
496
|
|
|
391
497
|
// Resolve `spec` from `parentFile` the way tsc would complete a resolution BOTH Node and the
|
|
392
|
-
// legacy-field resolver missed. Returns the absolute path of the on-disk source, or null
|
|
393
|
-
// `conditions` gates exports/imports maps (same set
|
|
394
|
-
//
|
|
498
|
+
// legacy-field resolver missed. Returns the absolute path of the on-disk source, or null -- always
|
|
499
|
+
// null from or into node_modules (inNodeModules). `conditions` gates exports/imports maps (same set
|
|
500
|
+
// the failed resolution used); `paths` is a tsconfig paths matcher (loadTsconfigPaths, or
|
|
501
|
+
// packageTsconfigPaths), or null. Dispatch by shape:
|
|
395
502
|
// '#name' -> the parent package's `imports` targets, substitution only;
|
|
396
503
|
// relative/abs -> path substitution/completion (+ directory main/index);
|
|
397
|
-
// bare -> tsconfig paths aliases first (tsc consults them before node_modules
|
|
504
|
+
// bare -> tsconfig paths aliases first (tsc consults them before node_modules; JS
|
|
505
|
+
// completion too, as Node never probed the target), then
|
|
398
506
|
// the named package: its `exports` targets (substitution only) when it has
|
|
399
507
|
// them, else its `main`/index (bare root) or subpath (substitution/completion).
|
|
400
|
-
export function resolveTypescriptFallback(parentFile, spec, { conditions = new Set(),
|
|
508
|
+
export function resolveTypescriptFallback(parentFile, spec, { conditions = new Set(), paths = null, host = diskHost } = {}) {
|
|
509
|
+
if (inNodeModules(parentFile, host)) return null
|
|
510
|
+
const hit = typescriptTarget(parentFile, spec, { conditions, paths, host })
|
|
511
|
+
return hit == null || inNodeModules(hit, host) ? null : hit
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function typescriptTarget(parentFile, spec, { conditions, paths, host }) {
|
|
401
515
|
if (spec.startsWith('#')) {
|
|
402
516
|
const scope = nearestPackage(parentFile, host)
|
|
403
|
-
return scope?.pkg.imports ? typescriptExportsTarget(scope.pkgDir, scope.pkg.imports, spec, { conditions,
|
|
517
|
+
return scope?.pkg.imports ? typescriptExportsTarget(scope.pkgDir, scope.pkg.imports, spec, { conditions, host }) : null
|
|
404
518
|
}
|
|
405
519
|
if (spec.startsWith('./') || spec.startsWith('../') || spec === '.' || spec === '..' || isAbsolute(spec)) {
|
|
406
520
|
const base = isAbsolute(spec) ? spec : resolvePath(dirname(parentFile), spec)
|
|
407
|
-
return probeTypescriptTarget(base, {
|
|
521
|
+
return probeTypescriptTarget(base, { dirOnly: DIR_ONLY_SPEC.test(spec), host })
|
|
408
522
|
}
|
|
409
|
-
if (paths
|
|
410
|
-
for (const target of paths.matchPaths(spec)) {
|
|
411
|
-
const hit = probeTypescriptTarget(target, {
|
|
523
|
+
if (paths) {
|
|
524
|
+
for (const target of paths.matchPaths(spec, parentFile)) {
|
|
525
|
+
const hit = probeTypescriptTarget(target, { js: true, dirOnly: target.endsWith('/'), host })
|
|
412
526
|
if (hit) return hit
|
|
413
527
|
}
|
|
414
528
|
}
|
|
@@ -417,11 +531,11 @@ export function resolveTypescriptFallback(parentFile, spec, { conditions = new S
|
|
|
417
531
|
const pkg = readJson(join(loc.pkgDir, 'package.json'), host) ?? {}
|
|
418
532
|
if (pkg.exports != null) {
|
|
419
533
|
// `exports` fully governs a bare import (main is not a fallback); targets name exact files.
|
|
420
|
-
return typescriptExportsTarget(loc.pkgDir, pkg.exports, loc.subpath === '' ? '.' : `./${loc.subpath}`, { conditions,
|
|
534
|
+
return typescriptExportsTarget(loc.pkgDir, pkg.exports, loc.subpath === '' ? '.' : `./${loc.subpath}`, { conditions, host })
|
|
421
535
|
}
|
|
422
536
|
if (loc.subpath === '') {
|
|
423
537
|
// Bare package root: LOAD_AS_DIRECTORY only (never `node_modules/dep.ts`).
|
|
424
|
-
return probeTypescriptTarget(loc.pkgDir, {
|
|
538
|
+
return probeTypescriptTarget(loc.pkgDir, { dirOnly: true, host })
|
|
425
539
|
}
|
|
426
|
-
return probeTypescriptTarget(join(loc.pkgDir, loc.subpath), {
|
|
540
|
+
return probeTypescriptTarget(join(loc.pkgDir, loc.subpath), { dirOnly: DIR_ONLY_SPEC.test(spec), host })
|
|
427
541
|
}
|
package/src/sbom.js
CHANGED
|
@@ -10,7 +10,9 @@ const DEFAULT_TOOL = { name: '@exodus/stasis', version: pkg.version, vendor: 'Ex
|
|
|
10
10
|
// Web Crypto (global) instead of node:crypto, keeping this module dependency-light.
|
|
11
11
|
const newUuid = () => globalThis.crypto.randomUUID()
|
|
12
12
|
|
|
13
|
-
// purl types we can mint; an ecosystem with no registered type (`soldeer`) gets no purl rather than a fabricated one
|
|
13
|
+
// purl types we can mint; an ecosystem with no registered type (`soldeer`) gets no purl rather than a fabricated one,
|
|
14
|
+
// and so does a crate vendored from git or from a source not known (`cargo-git`, `cargo-unknown`): `pkg:cargo` names
|
|
15
|
+
// crates.io's crate of that name, which it may not be.
|
|
14
16
|
const PURL_TYPES = new Set(['npm', 'composer', 'cargo', 'github'])
|
|
15
17
|
|
|
16
18
|
// Infer an artifact's ecosystem from its loader formats: PHP bundles map to Composer, everything
|