@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.5

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.
Files changed (45) hide show
  1. package/README.md +23 -3
  2. package/bin/stasis.js +375 -170
  3. package/package.json +44 -11
  4. package/src/audit-corrections.js +68 -0
  5. package/src/audit.js +243 -59
  6. package/src/babel-worker.cjs +65 -0
  7. package/src/babel.js +131 -0
  8. package/src/bundle-from-lockfile.js +12 -18
  9. package/src/bundle-reader.js +77 -0
  10. package/src/cmd/build.js +95 -128
  11. package/src/cmd/bundle.js +877 -641
  12. package/src/cmd/diff.js +2 -19
  13. package/src/cmd/github-bundle.js +39 -0
  14. package/src/cmd/output.js +36 -0
  15. package/src/cmd/sbom.js +4 -18
  16. package/src/diff.js +67 -124
  17. package/src/json-stream.js +283 -0
  18. package/src/loader-mock.js +8 -15
  19. package/src/loaders/bash.js +49 -116
  20. package/src/loaders/cargo.js +1699 -0
  21. package/src/loaders/foundry.js +923 -0
  22. package/src/loaders/paths.js +33 -0
  23. package/src/loaders/php.js +339 -394
  24. package/src/loaders/rust.js +3254 -242
  25. package/src/loaders/solidity-ownership.js +531 -0
  26. package/src/loaders/solidity.js +412 -162
  27. package/src/loaders/toml.js +68 -0
  28. package/src/metro-resolver.js +150 -0
  29. package/src/mock.js +57 -165
  30. package/src/parse.js +4 -16
  31. package/src/resolve-fields.js +331 -0
  32. package/src/resolve-node.js +416 -0
  33. package/src/resolve-typescript.js +427 -0
  34. package/src/rollup.js +5 -0
  35. package/src/sbom.js +64 -92
  36. package/src/scan.js +234 -156
  37. package/src/vfs-bundle/entries.js +140 -0
  38. package/src/vfs-bundle/github.js +193 -0
  39. package/src/vfs-bundle/soldeer.js +125 -0
  40. package/src/vfs-bundle/tree.js +606 -0
  41. package/src/vfs-bundle.js +74 -0
  42. package/src/why.js +434 -0
  43. package/src/apis/npm/index.js +0 -36
  44. package/src/apis/npm/semver.cjs +0 -44
  45. package/src/cmd/extract.js +0 -193
@@ -0,0 +1,427 @@
1
+ import { basename, dirname, extname, isAbsolute, join, resolve as resolvePath } from 'node:path'
2
+
3
+ import { isTypeDeclaration } from '@exodus/stasis-core/util'
4
+ import { diskHost } from '@exodus/stasis-core/host'
5
+
6
+ // tsc-style module resolution (`--typescript`), shared by BOTH JS resolvers -- scan.js's built-in
7
+ // Node resolver and resolve-fields.js's legacy-field resolver -- so the flag means one thing
8
+ // everywhere: when normal resolution misses and tsc would land on an on-disk TypeScript source,
9
+ // complete the miss with that file. It never rewrites a resolution that succeeded, so an on-disk
10
+ // `.js` always beats its `.ts` twin. The rules, in the shapes they apply to:
11
+ // - extension substitution: `./x.js` -> `./x.ts` (`.jsx` -> `.tsx` under --jsx, `.mjs` -> `.mts`,
12
+ // `.cjs` -> `.cts`) -- tsc never rewrites specifiers, so TS sources import each other by their
13
+ // OUTPUT names, and the specifier names a file that only exists as its TS source;
14
+ // - extension/index completion for an extensionless path (`./util` -> `./util.ts`,
15
+ // `./dir` -> `./dir/index.ts`), incl. through a directory's package.json `main`;
16
+ // - the same substitution applied to manifest-declared targets: a package `main`, an `exports`
17
+ // target, a `#name` `imports` target naming a compiled file whose only on-disk form is TS;
18
+ // - tsconfig `compilerOptions.paths` aliases (see loadTsconfigPaths), consulted for bare
19
+ // specifiers nothing else resolved.
20
+ // This module also homes the generic manifest helpers (readJson/locatePackage/nearestPackage)
21
+ // both resolvers share -- it is the dependency-free lower layer, importing from neither.
22
+
23
+ // 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. `.tsx` entries apply only when the caller can
25
+ // carry/parse .tsx (i.e. --jsx). JS_OUTPUT_EXTS is derived, so the two cannot drift.
26
+ const TS_SIBLING_EXTS = new Map([
27
+ ['.js', ['.ts', '.tsx']],
28
+ ['.jsx', ['.tsx']],
29
+ ['.mjs', ['.mts']],
30
+ ['.cjs', ['.cts']],
31
+ ])
32
+ const JS_OUTPUT_EXTS = new Set(TS_SIBLING_EXTS.keys())
33
+
34
+ // The TS source siblings of a path/specifier naming a JS output extension (./x.js -> ./x.ts).
35
+ // Returns [] for a non-substitutable name (extensionless, already-TS, .json, unknown, dotfile --
36
+ // extname sees no extension in './.js', matching the extensionless gates downstream). Candidates
37
+ // spelling a type declaration (./x.d.js -> ./x.d.ts) are returned too: every probe site refuses
38
+ // declarations (types-only, erased at runtime), so the screen lives at probe time, once.
39
+ export function typescriptSiblings(name, { tsx = false } = {}) {
40
+ const ext = extname(name)
41
+ const exts = TS_SIBLING_EXTS.get(ext)
42
+ if (!exts) return []
43
+ const stem = name.slice(0, -ext.length)
44
+ return exts.filter((e) => tsx || e !== '.tsx').map((e) => `${stem}${e}`)
45
+ }
46
+
47
+ // Extensions that never take the appended-`.ts` completion: JS outputs (substitution territory)
48
+ // and the extensions a resolver could already land on directly. Anything else (`./x.service`) is
49
+ // completed like an extensionless name, matching tsc's candidate list.
50
+ const NO_COMPLETION_EXTS = new Set([...JS_OUTPUT_EXTS, '.ts', '.tsx', '.mts', '.cts', '.json'])
51
+
52
+ export function isFile(p, host = diskHost) {
53
+ return host.stat(p)?.isFile() ?? false
54
+ }
55
+
56
+ export function isDir(p, host = diskHost) {
57
+ return host.stat(p)?.isDirectory() ?? false
58
+ }
59
+
60
+ export function readJson(file, host = diskHost) {
61
+ let text
62
+ try {
63
+ text = host.readFile(file).toString('utf8')
64
+ } catch {
65
+ return null // absent / unreadable -- no manifest here
66
+ }
67
+ // A manifest that EXISTS but is malformed must fail closed (Node throws rather than resolving past it).
68
+ try {
69
+ return JSON.parse(text)
70
+ } catch (cause) {
71
+ throw new Error(`Invalid package.json: ${file}`, { cause })
72
+ }
73
+ }
74
+
75
+ // Nearest node_modules/<pkg> up from `fromDir` (handles @scope/name). Returns { pkgDir, subpath }
76
+ // (subpath '' = bare package import), or null if not installed up the tree.
77
+ export function locatePackage(fromDir, spec, host = diskHost) {
78
+ const parts = spec.split('/')
79
+ const pkgLen = spec.startsWith('@') ? 2 : 1
80
+ if (parts.length < pkgLen || parts.slice(0, pkgLen).some((p) => !p)) return null
81
+ const pkgName = parts.slice(0, pkgLen).join('/')
82
+ const subpath = parts.slice(pkgLen).join('/')
83
+ let dir = fromDir
84
+ while (true) {
85
+ // Skip a dir literally named node_modules (basename, not endsWith — `my-node_modules` must not match).
86
+ if (basename(dir) !== 'node_modules') {
87
+ const pkgDir = join(dir, 'node_modules', pkgName)
88
+ if (isDir(pkgDir, host)) return { pkgDir, subpath }
89
+ }
90
+ const parent = dirname(dir)
91
+ if (parent === dir) return null
92
+ dir = parent
93
+ }
94
+ }
95
+
96
+ // Nearest package.json at/above `file`'s dir — the package whose fields (browser/RN map, imports)
97
+ // govern the file's own imports.
98
+ export function nearestPackage(file, host = diskHost) {
99
+ let dir = dirname(file)
100
+ while (true) {
101
+ const pkgPath = join(dir, 'package.json')
102
+ if (isFile(pkgPath, host)) {
103
+ const pkg = readJson(pkgPath, host)
104
+ if (pkg) return { pkgDir: dir, pkg }
105
+ }
106
+ const parent = dirname(dir)
107
+ if (parent === dir) return null
108
+ dir = parent
109
+ }
110
+ }
111
+
112
+ // A probe candidate is a target only if it names a real file that is not a type declaration
113
+ // (a `.d.ts` is types-only, erased at runtime -- tsc records it for types, never for emit).
114
+ const probe = (p, host) => (!isTypeDeclaration(p) && isFile(p, host) ? p : null)
115
+
116
+ // LOAD_INDEX with TS completions: dir/index.ts (+ index.tsx under tsx). Node/tsc already probed
117
+ // the .js/.json indexes before the fallback ever runs.
118
+ function probeIndex(dir, exts, host) {
119
+ for (const ext of exts) {
120
+ const hit = probe(join(dir, `index${ext}`), host)
121
+ if (hit) return hit
122
+ }
123
+ return null
124
+ }
125
+
126
+ // Probe an absolute `base` the way tsc completes a missed path: the literal file (manifest-declared
127
+ // targets are never probed by Node, so a paths/main target can hit literally), its TS siblings,
128
+ // the appended-extension completion, and -- when `dir` allows and `base` is a directory -- the
129
+ // directory's package.json `main` (substituted + completed like any path, mirroring Node's
130
+ // LOAD_AS_DIRECTORY) then its index. `dirOnly` skips the file rules for specs Node treats as
131
+ // directory-only ('.', '..', a trailing '/'), so './' never probes the pathological '.ts' dotfile.
132
+ // `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
+ function probeTypescriptTarget(base, { tsx = false, dirOnly = false, completion = true, dir = true, host = diskHost } = {}) {
135
+ const exts = tsx ? ['.ts', '.tsx'] : ['.ts']
136
+ if (!dirOnly) {
137
+ const literal = probe(base, host)
138
+ if (literal) return literal
139
+ for (const cand of typescriptSiblings(base, { tsx })) {
140
+ const hit = probe(cand, host)
141
+ if (hit) return hit
142
+ }
143
+ if (completion && !NO_COMPLETION_EXTS.has(extname(base))) {
144
+ for (const ext of exts) {
145
+ const hit = probe(`${base}${ext}`, host)
146
+ if (hit) return hit
147
+ }
148
+ }
149
+ }
150
+ if (dir && isDir(base, host)) {
151
+ const pkg = readJson(join(base, 'package.json'), host)
152
+ const main = typeof pkg?.main === 'string' && pkg.main.length > 0 ? pkg.main : null
153
+ if (main) {
154
+ // LOAD_AS_FILE(main) with substitution/completion, then LOAD_INDEX(main); a broken main
155
+ // falls through to the package index, like Node.
156
+ const entry = resolvePath(base, main)
157
+ const hit = probeTypescriptTarget(entry, { tsx, dir: false, host }) ?? probeIndex(entry, exts, host)
158
+ if (hit) return hit
159
+ }
160
+ return probeIndex(base, exts, host)
161
+ }
162
+ return null
163
+ }
164
+
165
+ // --- package `exports`/`imports` map resolution (candidate generation) ---
166
+
167
+ // Expand a target value (string | array | conditions object | null) to the target strings the
168
+ // active `conditions` select, wildcard-substituted. Mirrors Node's PACKAGE_TARGET_RESOLVE closely
169
+ // enough for fallback candidates: a conditions object commits to its FIRST matching key ('default'
170
+ // always matches); an array contributes its entries in order (the probe decides).
171
+ function expandTarget(value, wildcard, conditions) {
172
+ if (typeof value === 'string') {
173
+ return [wildcard == null ? value : value.replaceAll('*', wildcard)]
174
+ }
175
+ if (Array.isArray(value)) {
176
+ return value.flatMap((v) => expandTarget(v, wildcard, conditions))
177
+ }
178
+ if (value !== null && typeof value === 'object') {
179
+ for (const [key, v] of Object.entries(value)) {
180
+ if (key === 'default' || conditions.has(key)) return expandTarget(v, wildcard, conditions)
181
+ }
182
+ }
183
+ return [] // null (an explicit block) or no matching condition
184
+ }
185
+
186
+ // Resolve `key` through an exports/imports-shaped map: exact key first (a key containing '*' is
187
+ // never exact), else the best '*' pattern -- longest prefix, then longest key, exactly Node's
188
+ // PATTERN_KEY_COMPARE order -- with the matched wildcard substituted into the targets.
189
+ function resolveMapKey(map, key, conditions) {
190
+ if (Object.hasOwn(map, key) && !key.includes('*')) return expandTarget(map[key], null, conditions)
191
+ let best = null
192
+ for (const pattern of Object.keys(map)) {
193
+ const star = pattern.indexOf('*')
194
+ if (star === -1 || pattern.indexOf('*', star + 1) !== -1) continue
195
+ const prefix = pattern.slice(0, star)
196
+ const suffix = pattern.slice(star + 1)
197
+ if (key.length < prefix.length + suffix.length || !key.startsWith(prefix) || !key.endsWith(suffix)) continue
198
+ if (best === null || prefix.length > best.prefix.length
199
+ || (prefix.length === best.prefix.length && pattern.length > best.pattern.length)) {
200
+ best = { pattern, prefix, suffix }
201
+ }
202
+ }
203
+ if (!best) return []
204
+ const wildcard = key.slice(best.prefix.length, key.length - best.suffix.length)
205
+ return expandTarget(map[best.pattern], wildcard, conditions)
206
+ }
207
+
208
+ // A './' map target is only followed when it stays inside the package, like Node's
209
+ // PACKAGE_TARGET_RESOLVE invalid-target rules ('..' hops and node_modules re-entry are refused).
210
+ function validRelativeTarget(target) {
211
+ return target.startsWith('./')
212
+ && !target.split('/').some((seg) => seg === '..' || seg === 'node_modules')
213
+ }
214
+
215
+ // Candidate targets for `subpathKey` ('.', './sub', or '#name') through a package's exports or
216
+ // imports map. Exports sugar (a bare string/array/conditions object) is the '.' target.
217
+ function manifestTargets(map, subpathKey, conditions) {
218
+ if (map == null) return []
219
+ let byKey = map
220
+ if (typeof map !== 'object' || Array.isArray(map)
221
+ || Object.keys(map).every((k) => !k.startsWith('.') && !k.startsWith('#'))) {
222
+ if (subpathKey !== '.') return []
223
+ byKey = { '.': map }
224
+ }
225
+ return resolveMapKey(byKey, subpathKey, conditions).filter((t) => validRelativeTarget(t))
226
+ }
227
+
228
+ // tsc's mapping of `key` ('.' or './sub') through the `exports` of the package in `pkgDir` (or of
229
+ // a '#name' key through its `imports`): the targets `conditions` select, substitution only (Node
230
+ // requires exports/imports targets to name exact files), the first on disk; or null.
231
+ export function typescriptExportsTarget(pkgDir, exports, key, { conditions = new Set(), tsx = false, host = diskHost } = {}) {
232
+ for (const target of manifestTargets(exports, key, conditions)) {
233
+ const hit = probeTypescriptTarget(resolvePath(pkgDir, target), { tsx, completion: false, dir: false, host })
234
+ if (hit) return hit
235
+ }
236
+ return null
237
+ }
238
+
239
+ // --- tsconfig `compilerOptions.paths` ---
240
+
241
+ // tsconfig.json is JSONC: strip // and /* */ comments and trailing commas (never inside string
242
+ // literals), then JSON.parse. A config that exists but does not parse must fail closed.
243
+ function parseJsonc(text, file) {
244
+ if (text.charCodeAt(0) === 0xFE_FF) text = text.slice(1) // BOM
245
+ let out = ''
246
+ let pendingComma = -1 // index in `out` of a comma that may turn out to be trailing
247
+ for (let i = 0; i < text.length; i++) {
248
+ const c = text[i]
249
+ if (c === '"') {
250
+ let j = i + 1
251
+ while (j < text.length && text[j] !== '"') j += text[j] === '\\' ? 2 : 1
252
+ out += text.slice(i, j + 1)
253
+ i = j
254
+ pendingComma = -1
255
+ continue
256
+ }
257
+ if (c === '/' && text[i + 1] === '/') {
258
+ const nl = text.indexOf('\n', i)
259
+ i = (nl === -1 ? text.length : nl) - 1
260
+ continue
261
+ }
262
+ if (c === '/' && text[i + 1] === '*') {
263
+ const end = text.indexOf('*/', i + 2)
264
+ i = end === -1 ? text.length : end + 1
265
+ continue
266
+ }
267
+ if (c === ',') {
268
+ pendingComma = out.length
269
+ } else if (c === '}' || c === ']') {
270
+ if (pendingComma !== -1) out = `${out.slice(0, pendingComma)}${out.slice(pendingComma + 1)}`
271
+ pendingComma = -1
272
+ } else if (pendingComma !== -1 && !/\s/u.test(c)) {
273
+ pendingComma = -1
274
+ }
275
+ out += c
276
+ }
277
+ try {
278
+ return JSON.parse(out)
279
+ } catch (cause) {
280
+ throw new Error(`Invalid tsconfig: ${file}`, { cause })
281
+ }
282
+ }
283
+
284
+ // Resolve an `extends` target like tsc: relative/absolute against the extending file (with the
285
+ // implied .json), bare through node_modules (the spelled path, its .json twin, or the package's
286
+ // tsconfig.json). A named base that cannot be found fails closed -- its options are load-bearing.
287
+ function resolveExtendsTarget(fromFile, target, host) {
288
+ if (target.startsWith('./') || target.startsWith('../') || isAbsolute(target)) {
289
+ const p = resolvePath(dirname(fromFile), target)
290
+ if (isFile(p, host)) return p
291
+ if (isFile(`${p}.json`, host)) return `${p}.json`
292
+ } else {
293
+ for (const cand of [target, `${target}.json`, `${target}/tsconfig.json`]) {
294
+ try {
295
+ return host.resolve(fromFile, cand)
296
+ } catch { /* not this spelling -- try the next */ }
297
+ }
298
+ }
299
+ throw new Error(`tsconfig extends target not found: '${target}' (from ${fromFile})`)
300
+ }
301
+
302
+ // Effective { paths, pathsDir, baseUrl } across the `extends` chain: bases apply in order, the
303
+ // extending file overrides them; `paths` replaces wholesale (tsc never deep-merges it) and
304
+ // remembers its declaring dir; `baseUrl` is resolved against its declaring file.
305
+ function loadConfigChain(file, seen, host) {
306
+ if (seen.has(file)) throw new Error(`tsconfig extends cycle at ${file}`)
307
+ seen.add(file)
308
+ const raw = parseJsonc(host.readFile(file).toString('utf8'), file)
309
+ const acc = {}
310
+ for (const base of [].concat(raw?.extends ?? [])) {
311
+ Object.assign(acc, loadConfigChain(resolveExtendsTarget(file, base, host), seen, host))
312
+ }
313
+ const co = raw?.compilerOptions ?? {}
314
+ if (typeof co.baseUrl === 'string') acc.baseUrl = resolvePath(dirname(file), co.baseUrl)
315
+ if (co.paths != null) {
316
+ acc.paths = co.paths
317
+ acc.pathsDir = dirname(file)
318
+ }
319
+ return acc
320
+ }
321
+
322
+ // Load a tsconfig's `compilerOptions.paths` into a matcher, following `extends`. Returns null when
323
+ // `file` is null or the effective config declares no paths. matchPaths(spec) returns the absolute
324
+ // substituted targets of the BEST-matching key -- exact match first, else the '*' pattern with the
325
+ // longest matched prefix, exactly one key, its targets in order (tsc tries no other key when they
326
+ // all miss) -- resolved against `baseUrl`, or against the declaring config's dir without one
327
+ // (TS 4.1 paths-without-baseUrl). Only `extends`/`baseUrl`/`paths` are read; malformed shapes
328
+ // (a non-array value, more than one '*' in a key or target) fail closed like tsc's config errors.
329
+ export function loadTsconfigPaths(file, host = diskHost) {
330
+ if (file == null) return null
331
+ const { paths, pathsDir, baseUrl } = loadConfigChain(file, new Set(), host)
332
+ if (paths == null || typeof paths !== 'object' || Object.keys(paths).length === 0) return null
333
+ const starCount = (s) => s.split('*').length - 1
334
+ for (const [key, targets] of Object.entries(paths)) {
335
+ if (!Array.isArray(targets) || targets.some((t) => typeof t !== 'string')) {
336
+ throw new Error(`tsconfig paths['${key}'] must be an array of strings (${file})`)
337
+ }
338
+ for (const t of [key, ...targets]) {
339
+ if (starCount(t) > 1) throw new Error(`tsconfig paths pattern '${t}' has more than one '*' (${file})`)
340
+ }
341
+ }
342
+ const base = baseUrl ?? pathsDir
343
+ return {
344
+ matchPaths(spec) {
345
+ let targets = null
346
+ if (Object.hasOwn(paths, spec) && !spec.includes('*')) {
347
+ targets = paths[spec].map((t) => ({ target: t, wildcard: null }))
348
+ } else {
349
+ let best = null
350
+ for (const key of Object.keys(paths)) {
351
+ const star = key.indexOf('*')
352
+ if (star === -1) continue
353
+ const prefix = key.slice(0, star)
354
+ const suffix = key.slice(star + 1)
355
+ if (spec.length < prefix.length + suffix.length || !spec.startsWith(prefix) || !spec.endsWith(suffix)) continue
356
+ if (best === null || prefix.length > best.prefix.length) best = { key, prefix, suffix }
357
+ }
358
+ if (!best) return []
359
+ const wildcard = spec.slice(best.prefix.length, spec.length - best.suffix.length)
360
+ targets = paths[best.key].map((t) => ({ target: t, wildcard }))
361
+ }
362
+ return targets.map(({ target, wildcard }) =>
363
+ resolvePath(base, wildcard == null ? target : target.replace('*', wildcard)))
364
+ },
365
+ }
366
+ }
367
+
368
+ // The tsconfig `--typescript` reads: an explicit `--tsconfig` path must exist (fail closed on a
369
+ // typo); with none given, the project root's tsconfig.json applies when present, like tsc's own
370
+ // discovery from a directory.
371
+ export function discoverTsconfig(baseDir, explicit, host = diskHost) {
372
+ if (explicit != null) {
373
+ const p = resolvePath(baseDir, explicit)
374
+ if (!isFile(p, host)) throw new Error(`tsconfig not found: ${explicit}`)
375
+ return p
376
+ }
377
+ const p = join(baseDir, 'tsconfig.json')
378
+ return isFile(p, host) ? p : null
379
+ }
380
+
381
+ // --- the fallback dispatcher ---
382
+
383
+ // Specs Node resolves as a directory only ('.', '..', a './..'-style tail, a trailing '/'):
384
+ // the file rules are skipped for them, exactly as Node skips LOAD_AS_FILE.
385
+ const DIR_ONLY_SPEC = /(?:^|\/)\.{1,2}$|\/$/u
386
+
387
+ // tsconfig paths map first-party aliases; a dependency's own bare imports must never be hijacked
388
+ // by the app's aliases (tsc doesn't resolve node_modules files' imports through the app config).
389
+ const IN_NODE_MODULES = /(?:^|[\\/])node_modules[\\/]/u
390
+
391
+ // 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 the failed resolution used); `tsx` widens the
394
+ // substitutions to .tsx; `paths` is a loadTsconfigPaths matcher (or null). Dispatch by shape:
395
+ // '#name' -> the parent package's `imports` targets, substitution only;
396
+ // relative/abs -> path substitution/completion (+ directory main/index);
397
+ // bare -> tsconfig paths aliases first (tsc consults them before node_modules), then
398
+ // the named package: its `exports` targets (substitution only) when it has
399
+ // them, else its `main`/index (bare root) or subpath (substitution/completion).
400
+ export function resolveTypescriptFallback(parentFile, spec, { conditions = new Set(), tsx = false, paths = null, host = diskHost } = {}) {
401
+ if (spec.startsWith('#')) {
402
+ const scope = nearestPackage(parentFile, host)
403
+ return scope?.pkg.imports ? typescriptExportsTarget(scope.pkgDir, scope.pkg.imports, spec, { conditions, tsx, host }) : null
404
+ }
405
+ if (spec.startsWith('./') || spec.startsWith('../') || spec === '.' || spec === '..' || isAbsolute(spec)) {
406
+ const base = isAbsolute(spec) ? spec : resolvePath(dirname(parentFile), spec)
407
+ return probeTypescriptTarget(base, { tsx, dirOnly: DIR_ONLY_SPEC.test(spec), host })
408
+ }
409
+ if (paths && !IN_NODE_MODULES.test(parentFile)) {
410
+ for (const target of paths.matchPaths(spec)) {
411
+ const hit = probeTypescriptTarget(target, { tsx, dirOnly: target.endsWith('/'), host })
412
+ if (hit) return hit
413
+ }
414
+ }
415
+ const loc = locatePackage(dirname(parentFile), spec, host)
416
+ if (!loc) return null
417
+ const pkg = readJson(join(loc.pkgDir, 'package.json'), host) ?? {}
418
+ if (pkg.exports != null) {
419
+ // `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, tsx, host })
421
+ }
422
+ if (loc.subpath === '') {
423
+ // Bare package root: LOAD_AS_DIRECTORY only (never `node_modules/dep.ts`).
424
+ return probeTypescriptTarget(loc.pkgDir, { tsx, dirOnly: true, host })
425
+ }
426
+ return probeTypescriptTarget(join(loc.pkgDir, loc.subpath), { tsx, dirOnly: DIR_ONLY_SPEC.test(spec), host })
427
+ }
package/src/rollup.js ADDED
@@ -0,0 +1,5 @@
1
+ // Public-export adapter: backs `@exodus/stasis/rollup`. The plugin lives in
2
+ // @exodus/stasis-plugins (which depends on @exodus/stasis-core for State /
3
+ // resolvePluginState). Internal code uses `@exodus/stasis-plugins/rollup` directly;
4
+ // this file exists only to back the package's `exports` map.
5
+ export * from '@exodus/stasis-plugins/rollup'