@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/scan.js
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
import { extname, resolve as resolvePath, relative } from 'node:path'
|
|
1
|
+
import { dirname, extname, resolve as resolvePath, relative } from 'node:path'
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
3
|
import { createRequire, isBuiltin } from 'node:module'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
5
5
|
import { packageType } from '@exodus/stasis-core/bundle-util'
|
|
6
|
-
import { classifyExtension, classifyFormat, relativeEscapes } from '@exodus/stasis-core/util'
|
|
7
|
-
import { resolveTypescriptFallback } from './resolve-typescript.js'
|
|
6
|
+
import { classifyExtension, classifyFormat, isTypeDeclaration, relativeEscapes } from '@exodus/stasis-core/util'
|
|
7
|
+
import { inNodeModules, resolveTypescriptFallback, typescriptSiblings } from './resolve-typescript.js'
|
|
8
8
|
import { diskHost } from '@exodus/stasis-core/host'
|
|
9
9
|
|
|
10
10
|
// Static require/import graph walker: parses source, never loads or executes user code.
|
|
11
11
|
// Dynamic specifiers (`require(name)`, `import('./'+x)`) are recorded as unresolved.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// .jsx/.tsx need no flag: oxc parses them as JSX/TSX purely from the filename, so a reached one
|
|
14
|
+
// -- e.g. a package whose React Native entry is src/index.tsx -- is parsed and carried like any .ts.
|
|
15
|
+
const SCRIPT_EXTS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'])
|
|
14
16
|
const RESOLVABLE_EXTS = new Set([...SCRIPT_EXTS, '.json'])
|
|
15
|
-
//
|
|
17
|
+
// The extensions whose name can't tell whether they hold JSX, parsed with JSX enabled only under
|
|
18
|
+
// `jsx` (React Native's JSX-in-.js convention). Excludes the .ts family: TypeScript's `<T>`
|
|
16
19
|
// generics collide with JSX, so tsc/oxc reserve JSX for .tsx — JSX-in-TS belongs in a .tsx file.
|
|
17
20
|
const JSX_EXTS = new Set(['.js', '.cjs', '.mjs'])
|
|
18
|
-
// The inherently-JSX extensions (oxc parses them as JSX/TSX purely from the filename, no flag).
|
|
19
|
-
// Scanned + carried only under `--jsx`: off by default the scanner treats them as opaque leaves,
|
|
20
|
-
// so a bundle whose toolchain can't build JSX never silently ships .jsx/.tsx source. Under `--jsx`
|
|
21
|
-
// they join SCRIPT_EXTS/RESOLVABLE_EXTS (see the constructor) so a reached .jsx/.tsx dependency
|
|
22
|
-
// — e.g. a package whose React Native entry is src/index.tsx — is parsed and carried like any .ts.
|
|
23
|
-
const JSX_FILE_EXTS = new Set(['.jsx', '.tsx'])
|
|
24
21
|
|
|
25
22
|
// Flow type syntax lives in the JS (non-TS) family; oxc parses TS natively, and running
|
|
26
23
|
// flow-remove-types over a .ts/.tsx file would corrupt TS-only constructs, so --flow only strips
|
|
27
24
|
// these. .jsx is included (it's JS + JSX, and can carry Flow types like .js); .tsx is not (it's
|
|
28
25
|
// TypeScript). flow-remove-types preserves JSX, so a stripped .jsx re-parses under lang:jsx.
|
|
29
26
|
const FLOW_EXTS = new Set(['.js', '.cjs', '.mjs', '.jsx'])
|
|
27
|
+
const TS_EXTS = new Set(['.ts', '.cts', '.mts', '.tsx'])
|
|
30
28
|
|
|
31
29
|
// Required lazily so non-JS bundlers don't load the native oxc parser.
|
|
32
30
|
let _parser
|
|
33
|
-
function getParser() {
|
|
31
|
+
export function getParser() {
|
|
34
32
|
_parser ??= createRequire(import.meta.url)('oxc-parser')
|
|
35
33
|
return _parser
|
|
36
34
|
}
|
|
@@ -63,7 +61,10 @@ function formatForFile(file, host) {
|
|
|
63
61
|
return `${type}${extname(file) === '.ts' ? '-typescript' : ''}`
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
// The errors oxc reports for a parse, but its warnings and advice.
|
|
65
|
+
export const syntaxErrors = (parsed) => (parsed.errors ?? []).filter((e) => e.severity !== 'Warning' && e.severity !== 'Advice')
|
|
66
|
+
|
|
67
|
+
export function literalSpec(node) {
|
|
67
68
|
if (!node) return null
|
|
68
69
|
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
69
70
|
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
@@ -111,6 +112,67 @@ function findCallSpecifiers(ast, push) {
|
|
|
111
112
|
visit(ast)
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// The import specifiers `program` holds, as { kind, spec } ({ kind, dynamic: true } for a computed
|
|
116
|
+
// one). STATEMENT-level type-ness decides an edge, matching what survives type erasure at runtime
|
|
117
|
+
// (verbatimModuleSyntax / Node's own type stripping): an `import type` / `export type` statement is
|
|
118
|
+
// erased whole, so no edge; a statement that merely lists inline `type` specifiers
|
|
119
|
+
// (`import { type A }`, `export { type B } from`) -- or none at all (`import {}`, `export {} from`)
|
|
120
|
+
// -- still loads its module, so its edge is real. oxc's module records can't draw that line
|
|
121
|
+
// (`import type { A }` and `import { type A }` yield identical all-isType entries, and
|
|
122
|
+
// `export {} from` yields no entry at all), so the statements are read off the AST, where
|
|
123
|
+
// importKind/exportKind carry it.
|
|
124
|
+
function collectSpecifiers(program) {
|
|
125
|
+
const specs = []
|
|
126
|
+
for (const node of program.body) {
|
|
127
|
+
if (node.type === 'ImportDeclaration') {
|
|
128
|
+
if (node.importKind !== 'type') specs.push({ kind: 'import', spec: node.source.value })
|
|
129
|
+
} else if (node.type === 'ExportNamedDeclaration') {
|
|
130
|
+
if (node.source != null && node.exportKind !== 'type') specs.push({ kind: 'export-from', spec: node.source.value })
|
|
131
|
+
} else if (node.type === 'ExportAllDeclaration') {
|
|
132
|
+
if (node.exportKind !== 'type') specs.push({ kind: 'export-from', spec: node.source.value })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
findCallSpecifiers(program, (s) => specs.push(s))
|
|
136
|
+
return specs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Whether `entries` (absolute paths) import TS sources by their OUTPUT names -- tsc's convention,
|
|
140
|
+
// which only --typescript resolves -- so the build can take --typescript as given: every entry is
|
|
141
|
+
// TypeScript, they hold at least one relative import naming a JS output (`./a.js`, or .jsx/.mjs/
|
|
142
|
+
// .cjs), and not one of those is on disk while the TS source it maps to is (`./a.ts`; see
|
|
143
|
+
// typescriptSiblings), outside node_modules as the resolver requires. Any part missing is no tell, and neither is an entry that doesn't parse as
|
|
144
|
+
// the scan parses it (Scan#parseFile; the scan reports it), nor one in node_modules, where
|
|
145
|
+
// --typescript maps nothing. Read off the entries alone, before the scan.
|
|
146
|
+
export function importsTypescriptByOutputName(entries, host = diskHost) {
|
|
147
|
+
if (!entries.every((entry) => TS_EXTS.has(extname(entry)) && !inNodeModules(entry, host))) return false
|
|
148
|
+
// Outside the try, as in the scan: a missing oxc-parser is an env error, not an entry's.
|
|
149
|
+
getParser()
|
|
150
|
+
const scanner = new Scan({ host })
|
|
151
|
+
const targets = []
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
let result
|
|
154
|
+
try {
|
|
155
|
+
result = scanner.parseFile(entry)
|
|
156
|
+
} catch {
|
|
157
|
+
return false // unreadable: the scan reports it
|
|
158
|
+
}
|
|
159
|
+
if (result.crash !== undefined || result.errors.length > 0) return false
|
|
160
|
+
for (const { spec } of collectSpecifiers(result.parsed.program)) {
|
|
161
|
+
if (spec?.startsWith('./') || spec?.startsWith('../')) {
|
|
162
|
+
const target = resolvePath(dirname(entry), spec)
|
|
163
|
+
if (typescriptSiblings(target).length > 0) targets.push(target)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// tsc's pick, the first TS source on disk, as the resolver maps it: none where it lies in node_modules.
|
|
168
|
+
const isSource = (file) => !isTypeDeclaration(file) && (host.stat(file)?.isFile() ?? false)
|
|
169
|
+
const mapsTo = (target) => {
|
|
170
|
+
const pick = typescriptSiblings(target).find(isSource)
|
|
171
|
+
return pick !== undefined && !inNodeModules(pick, host)
|
|
172
|
+
}
|
|
173
|
+
return targets.length > 0 && targets.every((target) => host.stat(target) === null && mapsTo(target))
|
|
174
|
+
}
|
|
175
|
+
|
|
114
176
|
function condKey(set) {
|
|
115
177
|
return [...set].join(', ')
|
|
116
178
|
}
|
|
@@ -129,25 +191,25 @@ export class Scan {
|
|
|
129
191
|
// off by default because oxc, like tsc, only auto-enables JSX for .jsx/.tsx by extension.
|
|
130
192
|
// `flow`: strip Flow type syntax from JS-family sources before parsing (see #scanFile).
|
|
131
193
|
// `typescript`: retry a failed resolution with tsc's mapping (see #typescriptResolve), with
|
|
132
|
-
// `typescriptPaths` (a
|
|
133
|
-
// on the built-in (Node) resolver -- a custom `resolve` owns its own TS handling.
|
|
194
|
+
// `typescriptPaths` (a tsconfig paths matcher; see resolveTypescriptFallback) adding tsconfig alias support; only consulted
|
|
195
|
+
// on the built-in (Node) resolver -- a custom `resolve` owns its own TS handling. Without it, the
|
|
196
|
+
// mapping only names the file a miss would resolve to under --typescript (see #typescriptCandidate).
|
|
197
|
+
// `typescriptResolve`: the custom `resolve`'s --typescript twin (same shape), consulted only for
|
|
198
|
+
// that hint; null when there is no custom resolver or `typescript` is on.
|
|
134
199
|
// `resources` (a `parseResourcesOption` Set of extensions/filenames): reached files matching it
|
|
135
200
|
// are carried as opaque resources (bytes only) rather than rejected as un-carryable -- for graphs
|
|
136
201
|
// that aren't fully loadable in JS (e.g. Metro consuming .png/.svg assets).
|
|
137
202
|
// `host`: the filesystem the walk reads and resolves through (@exodus/stasis-core/host).
|
|
138
|
-
constructor({ conditions = [], resolve = null, jsx = false, flow = false, typescript = false, typescriptPaths = null, resources = new Set(), host = diskHost } = {}) {
|
|
203
|
+
constructor({ conditions = [], resolve = null, jsx = false, flow = false, typescript = false, typescriptPaths = null, typescriptResolve = null, resources = new Set(), host = diskHost } = {}) {
|
|
139
204
|
this.extraConditions = [...conditions]
|
|
140
205
|
this.customResolve = resolve
|
|
206
|
+
this.typescriptResolve = typescriptResolve
|
|
141
207
|
this.jsx = jsx
|
|
142
208
|
this.flow = flow
|
|
143
209
|
this.typescript = typescript
|
|
144
210
|
this.typescriptPaths = typescriptPaths
|
|
145
211
|
this.resources = resources
|
|
146
212
|
this.host = host
|
|
147
|
-
// --jsx widens the script/resolvable extension sets to include .jsx/.tsx, so those files are
|
|
148
|
-
// parsed (not left as opaque leaves) and queued when reached. Off by default the base sets apply.
|
|
149
|
-
this.scriptExts = jsx ? new Set([...SCRIPT_EXTS, ...JSX_FILE_EXTS]) : SCRIPT_EXTS
|
|
150
|
-
this.resolvableExts = jsx ? new Set([...RESOLVABLE_EXTS, ...JSX_FILE_EXTS]) : RESOLVABLE_EXTS
|
|
151
213
|
}
|
|
152
214
|
|
|
153
215
|
// A reached file whose extension/filename is in the --resources allowlist: carried as an opaque
|
|
@@ -201,7 +263,6 @@ export class Scan {
|
|
|
201
263
|
#typescriptResolve(parentFile, spec, conditions) {
|
|
202
264
|
const hit = resolveTypescriptFallback(parentFile, spec, {
|
|
203
265
|
conditions,
|
|
204
|
-
tsx: this.jsx,
|
|
205
266
|
paths: this.typescriptPaths,
|
|
206
267
|
host: this.host,
|
|
207
268
|
})
|
|
@@ -213,6 +274,16 @@ export class Scan {
|
|
|
213
274
|
}
|
|
214
275
|
}
|
|
215
276
|
|
|
277
|
+
// The file: URL a miss would resolve to under --typescript, or null: a hint for the unresolved
|
|
278
|
+
// report only, never an edge. A custom resolver asks its --typescript twin; the built-in one
|
|
279
|
+
// tsc's mapping, as #typescriptResolve gives it under --typescript.
|
|
280
|
+
#typescriptCandidate(parentFile, spec, conditions) {
|
|
281
|
+
if (this.typescript) return null
|
|
282
|
+
if (this.customResolve) return this.typescriptResolve?.(parentFile, spec, conditions)?.url ?? null
|
|
283
|
+
const hit = this.#typescriptResolve(parentFile, spec, conditions)
|
|
284
|
+
return hit == null ? null : pathToFileURL(hit).toString()
|
|
285
|
+
}
|
|
286
|
+
|
|
216
287
|
// Strip Flow type syntax to plain JS via the optional flow-remove-types dep (resolved lazily; a
|
|
217
288
|
// missing dep is an env error and propagates). Only invoked as a parse fallback (see #scanFile),
|
|
218
289
|
// so plain-JS files never reach it. Returns the rewritten source, or null when the transform
|
|
@@ -229,25 +300,12 @@ export class Scan {
|
|
|
229
300
|
}
|
|
230
301
|
}
|
|
231
302
|
|
|
232
|
-
|
|
233
|
-
|
|
303
|
+
// Parse `file` (a script extension) as Node loads it: under the format its name and package
|
|
304
|
+
// declare, or for a typeless .js/.ts the one Node detects from its syntax. -> { format, parsed,
|
|
305
|
+
// errors } (`errors`: what the parse still reports), or { declared, crash } where the parser
|
|
306
|
+
// itself threw on the file, salvaging nothing.
|
|
307
|
+
parseFile(file) {
|
|
234
308
|
const ext = extname(file)
|
|
235
|
-
if (ext === '.json') {
|
|
236
|
-
this.files.set(url, { format: 'json', edges: [] })
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
if (this.#isResource(file)) {
|
|
240
|
-
// Allowlisted asset: carry it as a resource with no parse and no edges. `resource: true` is
|
|
241
|
-
// the authoritative signal (both bundle builders key off it); the concrete format
|
|
242
|
-
// (resource vs resource:base64) is byte-derived by the builder, so leave it unset here.
|
|
243
|
-
this.files.set(url, { format: null, edges: [], resource: true })
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
if (!this.scriptExts.has(ext)) {
|
|
247
|
-
this.files.set(url, { format: null, edges: [] })
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
309
|
// declared === null: typeless package; Node decides .js/.ts by syntax, resolved after parse.
|
|
252
310
|
const declared = formatForFile(file, this.host)
|
|
253
311
|
const src = this.host.readFile(file).toString('utf8')
|
|
@@ -259,14 +317,15 @@ export class Scan {
|
|
|
259
317
|
// force the JSX variant when opted in (`--jsx`) to parse React Native's JSX-in-.js source.
|
|
260
318
|
const lang = this.jsx && JSX_EXTS.has(ext) ? 'jsx' : undefined
|
|
261
319
|
const parseOptions = (sourceType) => (lang ? { sourceType, lang } : { sourceType })
|
|
320
|
+
// `commonjs`, not `script`: Node runs CJS inside its module wrapper function, where a top-level
|
|
321
|
+
// `return` or `new.target` is legal; oxc's `commonjs` accepts both and is otherwise `script`.
|
|
262
322
|
const baseSourceType = declared === null ? 'unambiguous'
|
|
263
|
-
: declared.startsWith('module') ? 'module' : '
|
|
323
|
+
: declared.startsWith('module') ? 'module' : 'commonjs'
|
|
264
324
|
let parsed
|
|
265
325
|
try {
|
|
266
326
|
parsed = parser.parseSync(file, src, parseOptions(baseSourceType))
|
|
267
327
|
} catch (cause) {
|
|
268
|
-
|
|
269
|
-
return
|
|
328
|
+
return { declared, crash: cause.message }
|
|
270
329
|
}
|
|
271
330
|
|
|
272
331
|
// Match Node's syntax detection for typeless packages: ESM syntax picks the module variant
|
|
@@ -280,15 +339,15 @@ export class Scan {
|
|
|
280
339
|
|
|
281
340
|
// oxc recovers from syntax errors (reports them in parsed.errors) rather than throwing;
|
|
282
341
|
// Warning/Advice severities aren't real parse errors.
|
|
283
|
-
|
|
284
|
-
let errors = nonWarning(parsed)
|
|
342
|
+
let errors = syntaxErrors(parsed)
|
|
285
343
|
|
|
286
|
-
// oxc's
|
|
287
|
-
//
|
|
344
|
+
// oxc's `unambiguous` mode counts top-level await as module syntax (since 0.109; before, a
|
|
345
|
+
// TLA-only ESM file parsed here as a broken script). Keep mirroring Node for any script parse
|
|
346
|
+
// that still fails: retry as module and prefer a clean module parse.
|
|
288
347
|
if (errors.length > 0 && declared === null && !format.startsWith('module')) {
|
|
289
348
|
try {
|
|
290
349
|
const asModule = parser.parseSync(file, src, parseOptions('module'))
|
|
291
|
-
if (
|
|
350
|
+
if (syntaxErrors(asModule).length === 0) {
|
|
292
351
|
parsed = asModule
|
|
293
352
|
format = `module${tsSuffix}`
|
|
294
353
|
errors = []
|
|
@@ -296,6 +355,19 @@ export class Scan {
|
|
|
296
355
|
} catch { /* keep the script parse and its recorded errors */ }
|
|
297
356
|
}
|
|
298
357
|
|
|
358
|
+
// A typeless file Node detects as CJS runs inside the module wrapper too, but `unambiguous`
|
|
359
|
+
// parses it as a plain script, so a top-level `return` still fails there: retry as `commonjs`
|
|
360
|
+
// and prefer a clean parse (the detected commonjs format stands).
|
|
361
|
+
if (errors.length > 0 && declared === null && !format.startsWith('module')) {
|
|
362
|
+
try {
|
|
363
|
+
const asCommonjs = parser.parseSync(file, src, parseOptions('commonjs'))
|
|
364
|
+
if (syntaxErrors(asCommonjs).length === 0) {
|
|
365
|
+
parsed = asCommonjs
|
|
366
|
+
errors = []
|
|
367
|
+
}
|
|
368
|
+
} catch { /* keep the script parse and its recorded errors */ }
|
|
369
|
+
}
|
|
370
|
+
|
|
299
371
|
// --flow: oxc parses JS/TS but not Flow, so a Flow-typed .js/.cjs/.mjs file lands here with
|
|
300
372
|
// parse errors. Only now -- after a raw oxc parse actually failed -- strip Flow types and
|
|
301
373
|
// re-parse, so plain-JS files never pay the transform cost. Only the parse input is rewritten;
|
|
@@ -307,7 +379,7 @@ export class Scan {
|
|
|
307
379
|
if (flowSrc !== null) {
|
|
308
380
|
try {
|
|
309
381
|
const asFlow = parser.parseSync(file, flowSrc, parseOptions(baseSourceType))
|
|
310
|
-
if (
|
|
382
|
+
if (syntaxErrors(asFlow).length === 0) {
|
|
311
383
|
parsed = asFlow
|
|
312
384
|
format = declared ?? detectedFormat(parsed)
|
|
313
385
|
errors = []
|
|
@@ -316,6 +388,35 @@ export class Scan {
|
|
|
316
388
|
}
|
|
317
389
|
}
|
|
318
390
|
|
|
391
|
+
return { format, parsed, errors }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#scanFile(url, queue) {
|
|
395
|
+
const file = fileURLToPath(url)
|
|
396
|
+
const ext = extname(file)
|
|
397
|
+
if (ext === '.json') {
|
|
398
|
+
this.files.set(url, { format: 'json', edges: [] })
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (this.#isResource(file)) {
|
|
402
|
+
// Allowlisted asset: carry it as a resource with no parse and no edges. `resource: true` is
|
|
403
|
+
// the authoritative signal (both bundle builders key off it); the concrete format
|
|
404
|
+
// (resource vs resource:base64) is byte-derived by the builder, so leave it unset here.
|
|
405
|
+
this.files.set(url, { format: null, edges: [], resource: true })
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
if (!SCRIPT_EXTS.has(ext)) {
|
|
409
|
+
this.files.set(url, { format: null, edges: [] })
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const result = this.parseFile(file)
|
|
414
|
+
if (result.crash !== undefined) {
|
|
415
|
+
this.#recordParseError(url, result.declared, result.crash, false)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
const { format, parsed, errors } = result
|
|
419
|
+
|
|
319
420
|
let parseError
|
|
320
421
|
if (errors.length > 0) {
|
|
321
422
|
parseError = errors.map((e) => e.message).join('; ')
|
|
@@ -328,25 +429,7 @@ export class Scan {
|
|
|
328
429
|
this.parseErrors.push({ url, format, message: parseError, recovered: true })
|
|
329
430
|
}
|
|
330
431
|
|
|
331
|
-
const specs =
|
|
332
|
-
// STATEMENT-level type-ness decides an edge, matching what survives type erasure at runtime
|
|
333
|
-
// (verbatimModuleSyntax / Node's own type stripping): an `import type` / `export type`
|
|
334
|
-
// statement is erased whole, so no edge; a statement that merely lists inline `type`
|
|
335
|
-
// specifiers (`import { type A }`, `export { type B } from`) -- or none at all (`import {}`,
|
|
336
|
-
// `export {} from`) -- still loads its module, so its edge is real. oxc's module records
|
|
337
|
-
// can't draw that line (`import type { A }` and `import { type A }` yield identical
|
|
338
|
-
// all-isType entries, and `export {} from` yields no entry at all), so the statements are
|
|
339
|
-
// read off the AST, where importKind/exportKind carry it.
|
|
340
|
-
for (const node of parsed.program.body) {
|
|
341
|
-
if (node.type === 'ImportDeclaration') {
|
|
342
|
-
if (node.importKind !== 'type') specs.push({ kind: 'import', spec: node.source.value })
|
|
343
|
-
} else if (node.type === 'ExportNamedDeclaration') {
|
|
344
|
-
if (node.source != null && node.exportKind !== 'type') specs.push({ kind: 'export-from', spec: node.source.value })
|
|
345
|
-
} else if (node.type === 'ExportAllDeclaration') {
|
|
346
|
-
if (node.exportKind !== 'type') specs.push({ kind: 'export-from', spec: node.source.value })
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
findCallSpecifiers(parsed.program, (s) => specs.push(s))
|
|
432
|
+
const specs = collectSpecifiers(parsed.program)
|
|
350
433
|
|
|
351
434
|
// The edge KIND picks the resolution context, not the parent's format: createRequire()d
|
|
352
435
|
// require() inside ESM resolves under REQUIRE conditions, literal import() inside CJS under
|
|
@@ -359,11 +442,13 @@ export class Scan {
|
|
|
359
442
|
edges.push({ ...s, child: childURL })
|
|
360
443
|
if (!specMaps.has(key)) specMaps.set(key, new Map())
|
|
361
444
|
specMaps.get(key).set(s.spec, childURL)
|
|
362
|
-
if (
|
|
445
|
+
if (RESOLVABLE_EXTS.has(extname(childPath)) || this.#isResource(childPath)) queue.push(childURL)
|
|
363
446
|
}
|
|
364
|
-
|
|
447
|
+
// Records the file --typescript would land the miss on too (typescriptURL), resolved as the miss was.
|
|
448
|
+
const addUnresolved = (s, reason, conditions) => {
|
|
365
449
|
edges.push({ ...s, error: reason })
|
|
366
|
-
|
|
450
|
+
const typescriptURL = this.#typescriptCandidate(file, s.spec, conditions)
|
|
451
|
+
this.unresolved.push({ parentURL: url, kind: s.kind, spec: s.spec, reason, ...(typescriptURL ? { typescriptURL } : {}) })
|
|
367
452
|
}
|
|
368
453
|
|
|
369
454
|
for (const s of specs) {
|
|
@@ -386,7 +471,7 @@ export class Scan {
|
|
|
386
471
|
} else if (r?.url) {
|
|
387
472
|
addChild(s, key, r.url, fileURLToPath(r.url))
|
|
388
473
|
} else {
|
|
389
|
-
addUnresolved(s, 'MODULE_NOT_FOUND')
|
|
474
|
+
addUnresolved(s, 'MODULE_NOT_FOUND', conditions)
|
|
390
475
|
}
|
|
391
476
|
continue
|
|
392
477
|
}
|
|
@@ -402,7 +487,7 @@ export class Scan {
|
|
|
402
487
|
// stays keyed by the ORIGINAL specifier -- only the target is the mapped file.
|
|
403
488
|
if (this.typescript) childPath = this.#typescriptResolve(file, s.spec, conditions)
|
|
404
489
|
if (childPath == null) {
|
|
405
|
-
addUnresolved(s, cause.code ?? cause.message)
|
|
490
|
+
addUnresolved(s, cause.code ?? cause.message, conditions)
|
|
406
491
|
continue
|
|
407
492
|
}
|
|
408
493
|
}
|
|
@@ -440,7 +525,7 @@ export class Scan {
|
|
|
440
525
|
),
|
|
441
526
|
])
|
|
442
527
|
),
|
|
443
|
-
unresolved: this.unresolved.map(({ parentURL, ...rest }) => ({ parent: rel(parentURL), ...rest })),
|
|
528
|
+
unresolved: this.unresolved.map(({ parentURL, typescriptURL, ...rest }) => ({ parent: rel(parentURL), ...rest, ...(typescriptURL ? { typescript: rel(typescriptURL) } : {}) })),
|
|
444
529
|
parseErrors: this.parseErrors.map(({ url, ...rest }) => ({ file: rel(url), ...rest })),
|
|
445
530
|
}
|
|
446
531
|
}
|
|
@@ -9,7 +9,7 @@ import { foundrySourceDir } from '../loaders/foundry.js'
|
|
|
9
9
|
import { resolveTypescriptFallback, typescriptExportsTarget } from '../resolve-typescript.js'
|
|
10
10
|
import { checkKind, checkVersion, packageManagerOf, vfsHost } from './tree.js'
|
|
11
11
|
|
|
12
|
-
const JS = /\.[cm]?[jt]s$/u
|
|
12
|
+
const JS = /\.(?:[cm]?[jt]s|[jt]sx)$/u
|
|
13
13
|
// Node's own conditions for require() and for import, which the build's are added to.
|
|
14
14
|
const NODE_CONDITIONS = [['require', 'node', 'node-addons', 'module-sync'], ['import', 'node', 'node-addons', 'module-sync']]
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ const NODE_CONDITIONS = [['require', 'node', 'node-addons', 'module-sync'], ['im
|
|
|
21
21
|
// fields, else index); each subpath `exports` holds (but a pattern), as the package's name resolves
|
|
22
22
|
// for require() and for import, with the conditions the build adds (the RN ones under `metro`); and
|
|
23
23
|
// each `bin`. Only JS files in `dir` that are there, named from within it; none without a package.json.
|
|
24
|
-
function packageEntries(host, dir, { conditions = [], mainFields, metro = false, platforms = [],
|
|
24
|
+
function packageEntries(host, dir, { conditions = [], mainFields, metro = false, platforms = [], typescript = false } = {}) {
|
|
25
25
|
const real = host.realpath(dir)
|
|
26
26
|
const manifest = join(real, 'package.json')
|
|
27
27
|
const pkg = readJson(manifest, host)
|
|
@@ -32,7 +32,7 @@ function packageEntries(host, dir, { conditions = [], mainFields, metro = false,
|
|
|
32
32
|
if (rel !== '' && !relativeEscapes(rel) && JS.test(rel)) found.add(rel)
|
|
33
33
|
}
|
|
34
34
|
const passes = metro || mainFields !== undefined
|
|
35
|
-
? (metro ? platforms : [null]).map((platform) => fieldResolverFor(platform, { mainFields, metro, conditions,
|
|
35
|
+
? (metro ? platforms : [null]).map((platform) => fieldResolverFor(platform, { mainFields, metro, conditions, typescript, host }))
|
|
36
36
|
: [{ extras: conditions, mainFields: ['main'] }]
|
|
37
37
|
// Node's resolution of `specifier`, else under `typescript` the miss `mapped` as tsc maps it; a
|
|
38
38
|
// real path either way, or undefined.
|
|
@@ -49,7 +49,7 @@ function packageEntries(host, dir, { conditions = [], mainFields, metro = false,
|
|
|
49
49
|
const entry = fields.map((name) => pkg[name]).find((value) => typeof value === 'string' && value !== '')
|
|
50
50
|
if (entry !== undefined && posixPathEscapes(entry)) continue
|
|
51
51
|
if (resolver === undefined) {
|
|
52
|
-
add(viaNode('./', NODE_CONDITIONS[0], extras, (set) => resolveTypescriptFallback(manifest, './', { conditions: set,
|
|
52
|
+
add(viaNode('./', NODE_CONDITIONS[0], extras, (set) => resolveTypescriptFallback(manifest, './', { conditions: set, host })))
|
|
53
53
|
} else {
|
|
54
54
|
// A real path, as Node's resolution gives and the scan takes.
|
|
55
55
|
const hit = resolver(manifest, './')
|
|
@@ -63,7 +63,7 @@ function packageEntries(host, dir, { conditions = [], mainFields, metro = false,
|
|
|
63
63
|
if (!subpath.startsWith('.') || subpath.includes('*') || subpath.endsWith('/')) continue
|
|
64
64
|
const specifier = subpath === '.' ? name : `${name}${subpath.slice(1)}`
|
|
65
65
|
for (const { extras } of passes) {
|
|
66
|
-
for (const names of NODE_CONDITIONS) add(viaNode(specifier, names, extras, (set) => typescriptExportsTarget(real, exports, subpath, { conditions: set,
|
|
66
|
+
for (const names of NODE_CONDITIONS) add(viaNode(specifier, names, extras, (set) => typescriptExportsTarget(real, exports, subpath, { conditions: set, host })))
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
const bins = typeof pkg.bin === 'string' ? [pkg.bin] : pkg.bin !== null && typeof pkg.bin === 'object' ? Object.values(pkg.bin) : []
|
package/src/vfs-bundle/github.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createClient } from '@preventive/upstream/github.js'
|
|
|
8
8
|
import { vfsFromEntries } from '@preventive/vfs'
|
|
9
9
|
import { buildVfsBundle } from '../cmd/bundle.js'
|
|
10
10
|
import { KINDS, checkAhead } from './entries.js'
|
|
11
|
-
import { checkTarget, detectPackageManager, installedAlone, lockfileOf,
|
|
11
|
+
import { checkTarget, detectPackageManager, installedAlone, lockfileOf, lockfilesListed, noLockfile, packageManagerOf, vfsHost } from './tree.js'
|
|
12
12
|
|
|
13
13
|
// As upstream's tree verification bounds a tarball's unpacked size.
|
|
14
14
|
const MAX_TAR_BYTES = 2 ** 30
|
|
@@ -166,23 +166,29 @@ async function repoTree(name, { github, sha, tag, directory, client, packageMana
|
|
|
166
166
|
// A GitHub repo at a full commit, or the one `tag` names, the default branch's head without either,
|
|
167
167
|
// as buildVfsBundle builds it: `directory` downloaded alone where it stands alone, else the whole
|
|
168
168
|
// repo, and without a `packageManager`, the one whose lockfile installs it, where only one's does
|
|
169
|
-
// (repoTree). Without `entries`, they are the ones suggestedEntries suggests.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
169
|
+
// (repoTree). Without `entries`, they are the ones suggestedEntries suggests. The bundle's paths are
|
|
170
|
+
// relative to `directory`, or for a JS bundle, to the innermost package directory at or above it
|
|
171
|
+
// holding every file it bundles (buildJsBundle's innermostRoot), which is the project's root where
|
|
172
|
+
// one is outside the others; `repo` names that directory and the commit. Nothing is read from disk:
|
|
173
|
+
// the tree's bytes come from GitHub, or from the cache setCacheDir names, held to the git tree id
|
|
174
|
+
// either way (@preventive/upstream), and are unpacked into a Vfs that buildVfsBundle reads alone.
|
|
173
175
|
export async function buildGitHubBundle({ github, sha, tag, directory, client, packageManager, ...options } = {}) {
|
|
174
176
|
checkTarget('buildGitHubBundle', options)
|
|
177
|
+
// Its own, for a Soldeer git dependency too.
|
|
178
|
+
client ??= createClient({ token: null })
|
|
175
179
|
const tree = await repoTree('buildGitHubBundle', { github, sha, tag, directory, client, packageManager }, options)
|
|
176
|
-
const root = lockfileRoot(tree.host, tree.packageManager, tree.cwd, options.os)
|
|
177
|
-
const at = tree.subtree ? posix.join(directory, (root ?? '/').slice(1)) : (root ?? '/').slice(1)
|
|
178
|
-
const location = at === '' ? { root: true } : isValidRepoField('directory', at) ? { directory: at } : {}
|
|
179
180
|
let { entries } = options
|
|
180
181
|
if (entries === undefined) {
|
|
181
182
|
// As suggestedEntries suggests them, from the tree at hand.
|
|
182
183
|
entries = tree.kind.entries(tree.host, tree.cwd, options)
|
|
183
184
|
if (entries.length === 0) throw new Error(`${tree.where}: no entries given, and ${directory || 'the repo root'} ${tree.kind.none}`)
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
+
const repo = { github, commit: tree.sha }
|
|
187
|
+
const built = await buildVfsBundle({ ...options, entries, packageManager: tree.packageManager, vfs: tree.vfs, cwd: tree.cwd, innermostRoot: true, repo, client })
|
|
188
|
+
// The Vfs is the repo's tree, or the subtree at `directory`: a layout known, so its root is `''`.
|
|
189
|
+
const at = posix.join(tree.subtree ? directory : '.', posix.relative('/', built.root)).replace(/^\.$/u, '')
|
|
190
|
+
built.bundle.repo = { ...repo, ...(isValidRepoField('directory', at) ? { directory: at } : {}) }
|
|
191
|
+
return built
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
// suggestedEntries of a GitHub repo, `{ github, sha, tag, directory, client, packageManager }`, as
|