@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.
- package/README.md +32 -8
- package/bin/stasis.js +386 -213
- package/package.json +52 -12
- package/src/audit-corrections.js +96 -0
- package/src/audit.js +289 -64
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +14 -22
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +96 -128
- package/src/cmd/bundle.js +1121 -654
- package/src/cmd/diff.js +2 -19
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +7 -19
- package/src/diff.js +80 -129
- package/src/installed-commits.js +182 -0
- package/src/json-stream.js +283 -0
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +49 -116
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +375 -402
- package/src/loaders/rust.js +3254 -242
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -162
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +150 -0
- package/src/metro-transformer.js +3 -2
- package/src/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +370 -0
- package/src/resolve-node.js +419 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +120 -97
- package/src/scan.js +348 -180
- package/src/vfs-bundle/entries.js +144 -0
- package/src/vfs-bundle/github.js +202 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +647 -0
- package/src/vfs-bundle.js +80 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -44
- package/src/cmd/extract.js +0 -193
package/src/loaders/bash.js
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
// Bash loader:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
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
|
|
19
|
-
//
|
|
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`,
|
|
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
|
-
//
|
|
38
|
-
//
|
|
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
|
|
46
|
-
//
|
|
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
|
-
//
|
|
65
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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
|
-
//
|
|
156
|
-
//
|
|
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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
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
|
|
186
|
+
return [relPath, await readScript(baseDir, relPath)]
|
|
254
187
|
}),
|
|
255
188
|
)
|
|
256
189
|
return buildBashTree(new Map(reads))
|