@exodus/stasis 1.0.0-beta.1 → 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.
Files changed (48) hide show
  1. package/README.md +32 -8
  2. package/bin/stasis.js +386 -213
  3. package/package.json +52 -12
  4. package/src/audit-corrections.js +96 -0
  5. package/src/audit.js +289 -64
  6. package/src/babel-worker.cjs +65 -0
  7. package/src/babel.js +131 -0
  8. package/src/bundle-from-lockfile.js +14 -22
  9. package/src/bundle-reader.js +77 -0
  10. package/src/cmd/build.js +96 -128
  11. package/src/cmd/bundle.js +1121 -654
  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 +7 -19
  16. package/src/diff.js +80 -129
  17. package/src/installed-commits.js +182 -0
  18. package/src/json-stream.js +283 -0
  19. package/src/loader-mock.js +8 -15
  20. package/src/loaders/bash.js +49 -116
  21. package/src/loaders/cargo.js +1716 -0
  22. package/src/loaders/foundry.js +923 -0
  23. package/src/loaders/paths.js +33 -0
  24. package/src/loaders/php.js +375 -402
  25. package/src/loaders/rust.js +3254 -242
  26. package/src/loaders/solidity-ownership.js +531 -0
  27. package/src/loaders/solidity.js +412 -162
  28. package/src/loaders/toml.js +68 -0
  29. package/src/metro-resolver.js +150 -0
  30. package/src/metro-transformer.js +3 -2
  31. package/src/mock.js +57 -165
  32. package/src/parse.js +4 -16
  33. package/src/resolve-fields.js +370 -0
  34. package/src/resolve-node.js +419 -0
  35. package/src/resolve-typescript.js +541 -0
  36. package/src/rollup.js +5 -0
  37. package/src/sbom.js +120 -97
  38. package/src/scan.js +348 -180
  39. package/src/vfs-bundle/entries.js +144 -0
  40. package/src/vfs-bundle/github.js +202 -0
  41. package/src/vfs-bundle/prisma.js +439 -0
  42. package/src/vfs-bundle/soldeer.js +125 -0
  43. package/src/vfs-bundle/tree.js +647 -0
  44. package/src/vfs-bundle.js +80 -0
  45. package/src/why.js +434 -0
  46. package/src/apis/npm/index.js +0 -36
  47. package/src/apis/npm/semver.cjs +0 -44
  48. package/src/cmd/extract.js +0 -193
@@ -1,30 +1,25 @@
1
- // Bash loader: produces a `{ sources, resolutions, missing }` triple from a
2
- // set of shell scripts, mirroring the Solidity loader's interface (see
3
- // ./solidity.js). A reference to a .sh/.bash file by a path that lands INSIDE
4
- // the bundle root (`./lib.sh`, `dir/lib.sh`) must resolve to an in-set script;
5
- // when it doesn't it lands in `missing` so the bundle builder can reject the
6
- // dangling dependency. Everything else is best-effort and dropped: PATH
7
- // commands (`grep`), `$VAR` paths, extensionless `source ./env`, absolute
8
- // `source /etc/x.sh`, and `../` paths that escape the root — none is bundle-able.
9
- // A `source "${VAR}/x.sh"` whose path can't be evaluated statically is picked
10
- // up from its `# shellcheck source=...` directive when one is present.
11
-
12
- import { realpathSync, statSync } from 'node:fs'
1
+ // Bash loader: `{ sources, resolutions, missing }` from a set of scripts. A
2
+ // .sh/.bash reference landing INSIDE the bundle root must resolve to an in-set
3
+ // script, else it lands in `missing`; everything else (PATH commands, `$VAR`
4
+ // paths, absolute/escaping paths) is best-effort and dropped. A dynamically
5
+ // sourced path can be pinned by a `# shellcheck source=...` directive.
6
+
7
+ import { isUtf8 } from 'node:buffer'
8
+ import { realpathSync } from 'node:fs'
13
9
  import { readFile } from 'node:fs/promises'
14
- import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'
10
+ import { dirname, extname, join, resolve } from 'node:path'
15
11
 
16
12
  import { assertRealPathWithinBase } from '@exodus/stasis-core/util'
13
+ import { isFile } from '../resolve-typescript.js'
14
+ import { applyToDir, assertWithinBase } from './paths.js'
17
15
 
18
- // The capture class excludes `` ` ``, `(`, `)` (on top of whitespace, quotes,
19
- // `;`, `&`, `|`, `#`) so a reference inside a command substitution —
20
- // `` `source x.sh` `` or `$(bash x.sh)` — doesn't swallow the closing delimiter
21
- // into the path (which would silently drop the edge from the bundle).
16
+ // The capture class excludes backtick/`(`/`)` so a ref inside a command
17
+ // substitution (`` `source x.sh` ``, `$(bash x.sh)`) doesn't swallow the closing delimiter.
22
18
 
23
19
  // `source script.sh`, `. script.sh`, `. ./script.sh`
24
20
  const SOURCE_RE = /(?:^|\n|;|&&|\|\||`|\$\()\s*(?:source|\.)\s+["']?([^\s"';&|#`()]+)["']?/gu
25
21
 
26
- // `bash script.sh`, `sh script.sh`, `/usr/bin/env bash script.sh`,
27
- // `/bin/sh script.sh`, `/usr/bin/bash script.sh`, etc.
22
+ // `bash`/`sh script.sh`, incl. `/usr/bin/env bash`, `/bin/sh`, `/usr/bin/bash`.
28
23
  const EXEC_RE = /(?:^|\n|;|&&|\|\||`|\$\()\s*(?:\/usr\/bin\/env\s+|\/bin\/|\/usr\/bin\/)?(?:ba)?sh\s+["']?([^\s"';&|#`()]+)["']?/gu
29
24
 
30
25
  // Direct invocation: `./script.sh`, `../lib/helper.bash`, `/path/to/script.sh`
@@ -33,21 +28,13 @@ const DIRECT_RE = /(?:^|\n|;|&&|\|\||`|\$\()\s*["']?(\.\.?\/[^\s"';&|#`()]+\.(?:
33
28
  // Dependency hint comment: `# Depends on: example.sh`
34
29
  const COMMENT_RE = /^# Depends on: ([^ ]+\.(?:sh|bash))( |$)/gmu
35
30
 
36
- // shellcheck `source=` directive: `# shellcheck source=../lib/config.sh`
37
- // (optionally alongside other directives, e.g. `# shellcheck disable=SC1091 source=lib/x.sh`).
38
- // This is the standard, explicit way to tell tooling where a dynamically
39
- // sourced file lives — e.g. `source "${LIB_DIR}/config.sh"`, whose path is a
40
- // `$VAR` expansion that can't be evaluated statically. The path is resolved
41
- // relative to the script (matching shellcheck's default). `source=/dev/null`
42
- // is shellcheck's "don't follow this source" sentinel and is ignored below.
31
+ // shellcheck `source=` directive: `# shellcheck source=../lib/config.sh`, the
32
+ // explicit way to point at a dynamically sourced (`$VAR`) file. Resolved relative
33
+ // to the script. `source=/dev/null` is shellcheck's "don't follow" sentinel, ignored below.
43
34
  const SHELLCHECK_SOURCE_RE = /#\s*shellcheck\s[^\n]*?\bsource=["']?([^\s"'#]+)["']?/gu
44
35
 
45
- // Extract every file-like reference from a script's text. Variable
46
- // expansions (`$VAR`, `${VAR}`, `$HOME/x.sh`) and flags (`-c`) are dropped
47
- // here — they are never bundle-able file paths; a `source "${VAR}/x.sh"` whose
48
- // real location can't be computed statically should carry a `# shellcheck
49
- // source=` directive instead. The remaining strings are candidate references;
50
- // resolveBashCall decides which name a real file.
36
+ // Extract candidate file references. `$VAR` expansions and `-flags` are dropped
37
+ // (never bundle-able); resolveBashCall decides which of the rest name a real file.
51
38
  export function extractBashCalls(content) {
52
39
  const calls = new Set()
53
40
  for (const re of [SOURCE_RE, EXEC_RE, DIRECT_RE, COMMENT_RE, SHELLCHECK_SOURCE_RE]) {
@@ -60,63 +47,27 @@ export function extractBashCalls(content) {
60
47
  return [...calls]
61
48
  }
62
49
 
63
- // Resolve a bash reference to a baseDir-relative POSIX path. Existence is
64
- // checked against `knownFiles` (tree mode, used once every script is in
65
- // hand) or the filesystem under `baseDir` (walk mode, while discovering the
66
- // graph); with neither, only path computation happens and unvalidated names
67
- // return null.
68
- //
69
- // Reference kinds are treated differently:
70
- // - absolute (`/x`): mapped to project-relative by stripping the leading
71
- // slash, accepted only if that file actually exists in the set/on disk
72
- // (so system paths like /etc/profile.d/x.sh are dropped, not bundled).
73
- // - relative / project-relative WITH a slash (`./x`, `../x`, `lib/x.sh`):
74
- // resolved against the caller's directory and returned as a computed
75
- // path WITHOUT an existence check — the walk's readFile surfaces a
76
- // missing target and the tree flags or drops it, mirroring the Solidity
77
- // loader. Traversal above baseDir returns null rather than clamping, so
78
- // an escaping `../x.sh` is treated as external (never flagged missing).
79
- // - bare name (`helper.sh`, but also `grep`, `node`): could be a sibling
80
- // script or a PATH command, so it is accepted only when it names a real
81
- // file — caller's dir, then project root, then (knownFiles only) any
82
- // in-set file by basename.
50
+ // Resolve a bash reference to a baseDir-relative POSIX path. Existence is checked
51
+ // against `knownFiles` (tree mode) or the filesystem under `baseDir` (walk mode);
52
+ // with neither, only path computation happens. Handling differs by kind (below).
83
53
  export function resolveBashCall(ref, fromFile, { knownFiles, baseDir } = {}) {
84
54
  const present = (p) => {
85
55
  if (!p) return false
86
56
  if (knownFiles) return knownFiles.has(p)
87
- if (baseDir) {
88
- try {
89
- return statSync(join(baseDir, p)).isFile()
90
- } catch {
91
- return false
92
- }
93
- }
57
+ if (baseDir) return isFile(join(baseDir, p))
94
58
  return false
95
59
  }
96
60
 
97
- const fromDir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : ''
98
- const resolveRel = (r) => {
99
- const parts = [...(fromDir ? fromDir.split('/') : []), ...r.split('/')]
100
- const out = []
101
- for (const part of parts) {
102
- if (part === '.' || part === '') continue
103
- if (part === '..') {
104
- if (out.length === 0) return null // escapes baseDir
105
- out.pop()
106
- } else {
107
- out.push(part)
108
- }
109
- }
110
- return out.join('/') || null
111
- }
61
+ // Null when it escapes baseDir or names baseDir itself.
62
+ const resolveRel = (r) => applyToDir(fromFile, r.split('/')) || null
112
63
 
113
- // Absolute → project-relative, validated.
64
+ // Absolute → project-relative, accepted only if it exists (so `/etc/...` is dropped).
114
65
  if (ref.startsWith('/')) {
115
66
  const stripped = ref.replace(/^\/+/u, '')
116
67
  return present(stripped) ? stripped : null
117
68
  }
118
69
 
119
- // Path with a slash → computed, validation deferred to the caller.
70
+ // Path with a slash → computed against the caller's dir; existence deferred to the caller.
120
71
  if (ref.includes('/')) return resolveRel(ref)
121
72
 
122
73
  // Bare name → only accept a real file.
@@ -132,19 +83,13 @@ export function resolveBashCall(ref, fromFile, { knownFiles, baseDir } = {}) {
132
83
  return null
133
84
  }
134
85
 
135
- // A `.sh`/`.bash` extension marks a reference as a shell script (as opposed to
136
- // an extensionless `source ./env`, which we don't treat as a missing script
137
- // when absent).
86
+ // True for `.sh`/`.bash` refs; an extensionless `source ./env` isn't flagged missing when absent.
138
87
  const hasShellScriptExt = (ref) => ref.endsWith('.sh') || ref.endsWith('.bash')
139
88
 
140
- // Build the { sources, resolutions, missing } triple from a Map of
141
- // already-loaded scripts. A reference is recorded in `missing` (which the
142
- // bundle builder treats as fatal) only when it resolves to a path INSIDE the
143
- // bundle root that wasn't collected — i.e. a dangling `source ./lib.sh`.
144
- // References that resolve to null are external and tolerated: bare names
145
- // (PATH commands / runtime-cwd scripts), absolute `/etc/...` paths, `$VAR`
146
- // paths, and `../` paths that escape the root — none can live in the bundle.
147
- // Self-references and non-script extensions are dropped without flagging.
89
+ // Build the triple from already-loaded scripts. A ref is `missing` (fatal) only
90
+ // when it resolves INSIDE the bundle root but wasn't collected (a dangling
91
+ // `source ./lib.sh`); null-resolving refs are external and tolerated, and
92
+ // self-refs/non-script exts are dropped silently.
148
93
  export function buildBashTree(sources) {
149
94
  const knownFiles = new Set(sources.keys())
150
95
  const resolutions = new Map()
@@ -152,9 +97,8 @@ export function buildBashTree(sources) {
152
97
  for (const [path, content] of sources) {
153
98
  const specMap = new Map()
154
99
  for (const ref of extractBashCalls(content)) {
155
- // In tree mode the slash branch returns a computed in-bundle path
156
- // without checking existence, so a non-null target that isn't a known
157
- // file is a genuine dangling local dependency; a null target is external.
100
+ // Slash branch returns an unchecked in-bundle path, so a non-null target not
101
+ // in knownFiles is a genuine dangling dep; a null target is external.
158
102
  const target = resolveBashCall(ref, path, { knownFiles })
159
103
  if (target && knownFiles.has(target)) {
160
104
  if (target !== path) specMap.set(ref, target) // drop self-references
@@ -168,12 +112,15 @@ export function buildBashTree(sources) {
168
112
  return { sources, resolutions, missing }
169
113
  }
170
114
 
171
- // Walk the filesystem from `entries`, following resolved source/exec/direct
172
- // references and reading each reachable script exactly once. Files in the
173
- // same wave are read in parallel; the next wave depends on references found
174
- // in the previous one. A reference that resolves to a path missing on disk
175
- // warns and is skipped (the graph stays partial but the walk continues);
176
- // unresolved references (commands, dynamic paths) are silently ignored.
115
+ // A script's text. One that isn't UTF-8 is refused rather than carried with U+FFFD in it.
116
+ async function readScript(baseDir, relPath) {
117
+ const bytes = await readFile(join(baseDir, relPath))
118
+ if (!isUtf8(bytes)) throw new Error(`Shell script is not valid UTF-8: ${relPath}`)
119
+ return bytes.toString('utf8')
120
+ }
121
+
122
+ // Walk from `entries` following resolved refs, reading each script once; a ref
123
+ // missing on disk warns and is skipped without aborting the walk.
177
124
  export async function collectBashFilesFromDisk(baseDir, entries) {
178
125
  const sources = new Map()
179
126
  const realBase = realpathSync(baseDir)
@@ -185,14 +132,13 @@ export async function collectBashFilesFromDisk(baseDir, entries) {
185
132
  toLoad.map(async (relPath) => {
186
133
  try {
187
134
  assertRealPathWithinBase(realBase, baseDir, relPath)
188
- return [relPath, await readFile(join(baseDir, relPath), 'utf8')]
135
+ return [relPath, await readScript(baseDir, relPath)]
189
136
  } catch (err) {
190
137
  if (err.code === 'ENOENT') {
191
138
  console.warn(`[loader.bash] Missing file: ${relPath}`)
192
139
  return null
193
140
  }
194
- // A slash-bearing ref can resolve to a directory (e.g. `source ./lib`);
195
- // readFile then throws EISDIR. Skip it rather than aborting the walk.
141
+ // A slash ref can resolve to a directory (`source ./lib`) → EISDIR; skip, don't abort.
196
142
  if (err.code === 'EISDIR') {
197
143
  console.warn(`[loader.bash] Skipping directory reference: ${relPath}`)
198
144
  return null
@@ -218,22 +164,9 @@ export async function collectBashFilesFromDisk(baseDir, entries) {
218
164
  return sources
219
165
  }
220
166
 
221
- // Reject absolute paths and `..`-escaping paths in a `.sh.txt` listing so a
222
- // malicious or sloppy listing can't trick the loader into reading arbitrary
223
- // files outside the listing's own directory.
224
- function assertWithinBase(baseDir, candidate, label) {
225
- if (isAbsolute(candidate)) throw new Error(`${label} must not be absolute: ${candidate}`)
226
- const rel = relative(baseDir, resolve(baseDir, candidate)).split(/[\\/]/u).join('/')
227
- if (rel.startsWith('..') || isAbsolute(rel)) {
228
- throw new Error(`${label} escapes baseDir: ${candidate}`)
229
- }
230
- }
231
-
232
- // High-level: takes a `.sh.txt` listing file whose lines are the scripts to
233
- // load (`.sh`, `.bash`, or extensionless), resolved relative to the listing.
234
- // Unlike loadSolidity, the listing is the authoritative manifest — bash
235
- // sourcing is too dynamic to walk reliably — so exactly the listed files are
236
- // read and resolutions are computed among them. Returns { sources, resolutions, missing }.
167
+ // High-level entry: reads the scripts named in a `.sh.txt` listing (relative to
168
+ // it). Unlike loadRust, the listing is authoritative — bash sourcing is too
169
+ // dynamic to walk — so exactly the listed files are read, resolutions computed among them.
237
170
  export async function loadBash(shTxtFile) {
238
171
  const baseDir = dirname(resolve(shTxtFile))
239
172
  const listing = await readFile(shTxtFile, 'utf8')
@@ -250,7 +183,7 @@ export async function loadBash(shTxtFile) {
250
183
  const reads = await Promise.all(
251
184
  lines.map(async (relPath) => {
252
185
  assertRealPathWithinBase(realBase, baseDir, relPath)
253
- return [relPath, await readFile(join(baseDir, relPath), 'utf8')]
186
+ return [relPath, await readScript(baseDir, relPath)]
254
187
  }),
255
188
  )
256
189
  return buildBashTree(new Map(reads))