@exodus/stasis 1.0.0-beta.3 → 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.
- package/README.md +7 -1
- package/bin/stasis.js +236 -17
- package/package.json +36 -9
- package/src/audit-corrections.js +68 -0
- package/src/audit.js +153 -108
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +3 -5
- package/src/cmd/bundle.js +571 -376
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +3 -11
- package/src/diff.js +6 -6
- package/src/json-stream.js +283 -0
- package/src/loaders/bash.js +17 -36
- package/src/loaders/cargo.js +1699 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +208 -124
- package/src/loaders/rust.js +3234 -166
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -137
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +8 -23
- package/src/mock.js +3 -5
- package/src/resolve-fields.js +331 -0
- package/src/resolve-node.js +416 -0
- package/src/resolve-typescript.js +427 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +32 -18
- package/src/scan.js +79 -43
- package/src/vfs-bundle/entries.js +140 -0
- package/src/vfs-bundle/github.js +193 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +606 -0
- package/src/vfs-bundle.js +74 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -42
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://esbuild.github.io/)
|
|
7
7
|
[](https://metrobundler.dev/)
|
|
8
|
+
[](https://rollupjs.org/)
|
|
8
9
|
[](https://webpack.js.org/)
|
|
9
10
|
|
|
10
11
|
Both lockfiles and bundles only include what is actually _used_.\
|
|
@@ -57,6 +58,7 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
57
58
|
| `stasis run --lock=add --bundle=add --mock app.js` | build without the app's side effects (network, fs writes) |
|
|
58
59
|
| `stasis run --lock=add --child-process app.js` | also attest modules loaded in forked child processes (e.g. Metro transform workers) |
|
|
59
60
|
| `stasis run --bundle=add --fs=sync app.js` | build a bundle that also captures sync `fs.readFileSync`/`readdirSync` reads |
|
|
61
|
+
| `stasis run --lock=add --import=./instrument.mjs app.js` | forward extra preload modules (repeatable) to the node process running the entry: a preload's own module graph is runner infrastructure like stasis's loader itself — evaluated, but not captured. Modules the app graph *also* reaches (or that execute through a preload-transplanted `require()` pipeline) are still attested like any other app code, so frozen replays and `--bundle=load` stay fail-closed |
|
|
60
62
|
| `stasis bundle src/index.js` | build a bundle statically, without executing it |
|
|
61
63
|
| `stasis bundle --add src/worker.js` | merge more entries (and their import graph) into an existing `stasis.code.br` instead of replacing it |
|
|
62
64
|
| `stasis add a.js icon.svg` | add the listed files to the project's bundles (config-driven), with no dependency resolution |
|
|
@@ -66,17 +68,21 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
66
68
|
| `stasis bundle --metro --platforms=ios,android app.js` | statically bundle the way Metro resolves: RN conditions + mainFields + `.ios`/`.android`/`.native` suffixes, all platforms at once, plus each bundled native dependency's `ios/`/`android/` sources + podspec |
|
|
67
69
|
| `stasis bundle --metro --metro-resolver --platforms=ios,android app.js` | same, but resolve through the project's own `metro-resolver` (byte-for-byte Metro fidelity) instead of the built-in approximation; requires `metro-resolver` installed (it ships with `react-native`/`metro`) |
|
|
68
70
|
| `stasis bundle --flow app.js` | statically bundle Flow-typed sources: when oxc can't parse a `.js`/`.cjs`/`.mjs` file, strip its Flow syntax and retry so the import graph resolves (needs the optional `flow-remove-types` dependency; the stored source stays the original bytes). Combines with `--jsx`/`--conditions`/`--mainFields`/`--metro` |
|
|
71
|
+
| `stasis bundle --typescript src/index.ts` | statically bundle TS sources that import by output name, the way tsc resolves: a `./x.js` (or `.mjs`/`.cjs`) specifier with no such file on disk lands on its on-disk `./x.ts` (`.mts`/`.cts`) source, and an extensionless `./x` probes `./x.ts` and `./x/index.ts`; the same substitution applies to package `main`/`exports`/`imports` targets and bare subpaths, and an existing `.js` always wins over its `.ts` twin. Combines with `--jsx` (also maps to `.tsx`)/`--conditions`/`--mainFields`/`--metro` (not `--metro-resolver`, which can't substitute) |
|
|
72
|
+
| `stasis bundle --typescript --tsconfig=tsconfig.build.json src/index.ts` | same, honoring tsconfig `compilerOptions.paths` aliases (e.g. `"@/*": ["./src/*"]`, resolved against `baseUrl`, `extends` followed) for bare specifiers nothing else resolves; without `--tsconfig`, the project root's tsconfig.json applies when present |
|
|
73
|
+
| `stasis github-bundle --github=owner/name [--sha=<commit>] [--tag=<name>] [--directory=path] [--package-manager=pnpm] [src/index.js]` | statically bundle a GitHub repo at a commit, or the one `--tag` names (the default branch's head without either), as `stasis bundle` bundles a clone of it once installed: the tree is fetched (with `GITHUB_TOKEN` where set) and held to its git tree id, and the dependencies are laid out in memory from the lockfile alone (`pnpm`, `yarn1`, `npm`, or `soldeer` for `.sol` entries; without `--package-manager`, the one whose lockfile installs the directory, where only one's does), with nothing installed and no package script run; where no entries are given, they are the JS files the directory's `package.json` names (`main`, `exports`, `bin`), resolved as the build resolves (`--conditions`, `--mainFields`, `--metro --platforms`, `--typescript`), or for Soldeer its `.sol` files directly in it, under `contracts/`, and under its source directory (`foundry.toml`'s `src`, else `src/`), but tests, scripts, mocks and dependency or build directories; written to `owner-name.<commit's first 7>.stasis.code.br` by default, `owner-name.<directory, its / made ->.<commit's first 7>.stasis.code.br` with `--directory` (each character outside `[A-Za-z0-9._-]` made `_`, and a directory too deep to fit in 255 characters cut, with a hash of it); takes `stasis bundle`'s options for the entries and `--lockfile` (as a library: `buildGitHubBundle` in `@exodus/stasis/vfs-bundle`, and `suggestedEntries` for the entries it would take, of a GitHub repo or a Vfs) |
|
|
69
74
|
| `stasis build --output=out.js app.stasis.code.br [entry]` | rebuild a runnable JS bundle with esbuild, following the bundle's recorded import graph exactly (entry optional when the bundle has one) |
|
|
70
75
|
| `stasis build --output=out.js stasis.lock.json [entry]` | same, reading + verifying sources from disk against the lockfile |
|
|
71
76
|
| `stasis extract app.stasis.code.br` | unpack a bundle back to sources + a `stasis.lock.json` |
|
|
72
77
|
| `stasis diff --stat a.lock.json b.stasis.code.br` | summarize module/file differences between two lockfiles/bundles |
|
|
73
78
|
| `stasis prune` | trim `node_modules` to the lockfile, verifying the rest |
|
|
74
79
|
| `stasis audit stasis.lock.json` | report npm advisories for a lockfile's dependencies |
|
|
75
|
-
| `stasis audit app.stasis.code.br` | report
|
|
80
|
+
| `stasis audit app.stasis.code.br` | report advisories for a bundle's dependencies: npm packages by npm's advisories, vendored crates and Composer packages by OSV's, and Soldeer packages and GitHub repos (Foundry's `lib/`) by those their GitHub repository publishes, asked with `GITHUB_TOKEN` where it is set; a Composer dev version is listed as not audited |
|
|
76
81
|
| `stasis audit --why app.stasis.code.br` | same, with the cross-module import paths that pull each flagged package in (`run: a -> b -> c`), prefixed by the consumer that imports each chain at the top level; a chain is skipped when its full tail is already listed as its own chain |
|
|
77
82
|
| `stasis audit --why-deep app.stasis.code.br` | like `--why`, but keep those longer chains too (every path, shared tails collapsed to `a -> b -> ... -> d`) |
|
|
78
83
|
| `stasis audit --why-full app.stasis.code.br` | like `--why`, but spell every chain out with no `...` collapse (combine with `--why-deep` for the complete raw listing) |
|
|
79
84
|
| `stasis audit --reason=run app.stasis.code.br` | show only advisories related to one consumer (`run`); with `--why`, keep only that consumer's chains |
|
|
85
|
+
| `GITHUB_TOKEN=... stasis audit --repo-advisories stasis.lock.json` | also ask each dependency's GitHub repository for the advisories its maintainers published there, which npm's database has only once GitHub reviews them; needs a GitHub token in `GITHUB_TOKEN`. Each package's repository is cached for a month in the user cache directory (`$XDG_CACHE_HOME/stasis`, `~/.cache/stasis`, `~/Library/Caches/stasis` on macOS, `%LOCALAPPDATA%\stasis\Cache` on Windows) |
|
|
80
86
|
| `stasis sbom --format=spdx stasis.lock.json` | export an SPDX SBOM for a lockfile or bundle |
|
|
81
87
|
| `stasis sbom --format=cyclonedx app.stasis.code.br` | export a CycloneDX SBOM for a lockfile or bundle |
|
|
82
88
|
|
package/bin/stasis.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { once } from 'node:events'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { basename, dirname, resolve } from 'node:path'
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
7
7
|
import { existsSync, realpathSync } from 'node:fs'
|
|
8
|
-
import { constants as osConstants } from 'node:os'
|
|
8
|
+
import { homedir, constants as osConstants } from 'node:os'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
10
|
import { parseBrotliQuality, parseLeadingOptions, parseResourcesOption } from '@exodus/stasis-core/util'
|
|
11
11
|
import pkg from '../package.json' with { type: 'json' }
|
|
@@ -20,19 +20,69 @@ assert(basename(jsname) === 'stasis' || pathsEqual(jsname, fileURLToPath(import.
|
|
|
20
20
|
|
|
21
21
|
function usage(prefix = '') {
|
|
22
22
|
console.error(`${prefix}\nUsage:
|
|
23
|
-
stasis run --lock=(add|replace|frozen|ignore) [--bundle=(add|replace|load|frozen|ignore)] [--bundle-file=path/to/bundle.br] [--resources-bundle-file=path/to/resources.br] [--dependencies] [--child-process] [--package-json] [--mock] [--fs=(sync|async)] [--resources=ext,ext] [--brotli-quality=0..11] path/to/file.js ...
|
|
24
|
-
|
|
23
|
+
stasis run --lock=(add|replace|frozen|ignore) [--bundle=(add|replace|load|frozen|ignore)] [--bundle-file=path/to/bundle.br] [--resources-bundle-file=path/to/resources.br] [--dependencies] [--child-process] [--package-json] [--mock] [--import=module ...] [--fs=(sync|async)] [--resources=ext,ext] [--brotli-quality=0..11] path/to/file.js ...
|
|
24
|
+
(--import forwards extra preload modules (repeatable) to the node process running the entry,
|
|
25
|
+
e.g. --import=./instrument.mjs for a setup/instrumentation preload; a preload's own module
|
|
26
|
+
graph is runner infrastructure like stasis's loader itself, so it stays out of the
|
|
27
|
+
lockfile/bundle -- except modules the app graph also reaches, which are attested like any
|
|
28
|
+
other app code; with --mock, preloads run under the mock's side-effect denials, so a preload
|
|
29
|
+
that spawns helper processes cannot work there)
|
|
30
|
+
stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--manifests] [--add] [--output=(path|-)] path/to/(file.sol|dir) ...
|
|
31
|
+
(Solidity: a directory stands for every .sol file under it (a missing or empty one is skipped) --
|
|
32
|
+
"stasis bundle src test script" is what "forge build" compiles, "stasis bundle contracts" what
|
|
33
|
+
Hardhat does; imports resolve through the remappings "forge build" uses (remappings.txt,
|
|
34
|
+
foundry.toml's profile, FOUNDRY_PROFILE, lib/ auto-detection incl. dependencies' own configs),
|
|
35
|
+
else a root remappings.txt, then node_modules by file path; --mapping instead takes exactly the
|
|
36
|
+
remappings that one file lists; an import must reach a .sol file inside the project, and a
|
|
37
|
+
dependency's only other dependencies' files; --manifests also carries foundry.toml,
|
|
38
|
+
remappings.txt, foundry.lock, soldeer.lock, .gitmodules and package.json of the project and its
|
|
39
|
+
bundled dependencies, as written: RPC/Etherscan keys and URL credentials in them included)
|
|
25
40
|
stasis bundle [--add] [--output=(path|-)] path/to/file.php ...
|
|
26
|
-
stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--jsx] [--flow] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
27
|
-
stasis bundle --metro [--metro-resolver] --platforms=ios,android [--platforms=web] [--jsx] [--flow] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
41
|
+
stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
42
|
+
stasis bundle --metro [--metro-resolver] --platforms=ios,android [--platforms=web] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
28
43
|
stasis bundle [--add] [--output=(path|-)] path/to/file.(sh|bash) ...
|
|
29
|
-
stasis bundle [--add] [--output=(path|-)] path/to/file.rs ...
|
|
44
|
+
stasis bundle [--cargo] [--cargo-features=a,b,pkg/c] [--cargo-no-default-features] [--cargo-all-features] [--cargo-target=(triple|host)] [--cargo-manifests] [--add] [--output=(path|-)] path/to/file.rs ...
|
|
45
|
+
(Rust: each crate's Cargo features are resolved from Cargo.toml/Cargo.lock like "cargo build" of the
|
|
46
|
+
entries' packages, so #[cfg(feature = ...)] code that is off stays out -- by cargo's own resolver
|
|
47
|
+
when there is a Cargo.lock (version 3 or 4; an older one stops the build), --cargo-target and every
|
|
48
|
+
locked package in-tree, by replaying the manifests otherwise (said, with why); --cargo takes the resolution
|
|
49
|
+
from "cargo metadata" instead -- it runs cargo, so only on a project you trust; the --cargo-*
|
|
50
|
+
feature flags are cargo's --features / --no-default-features / --all-features for those packages;
|
|
51
|
+
--cargo-target asks rustc for the target's cfgs so #[cfg(unix)]-style code for other targets stays
|
|
52
|
+
out too, otherwise it is all kept; --cargo-manifests also bundles each bundled package's Cargo.toml
|
|
53
|
+
and build script (a vendored crate's .cargo-checksum.json too), the workspace Cargo.toml, Cargo.lock
|
|
54
|
+
and cargo configs, as written: tokens and URL credentials in them included)
|
|
30
55
|
(writes to stasis.code.br by default; --output=- streams to stdout; --add merges into an
|
|
31
56
|
existing bundle instead of replacing it (not with --output=-); --brotli-quality=0..11, default 9;
|
|
32
57
|
--jsx parses JSX in .js/.cjs/.mjs files, e.g. React Native source (put JSX-in-TS in a .tsx file);
|
|
33
58
|
--flow strips Flow types from .js/.cjs/.mjs sources oxc can't parse (needs the optional flow-remove-types dep);
|
|
59
|
+
--typescript resolves TS the way tsc does: an import of ./x.js lands on ./x.ts when no .js is on disk
|
|
60
|
+
(likewise .mjs/.cjs -> .mts/.cts, extensionless ./x -> ./x.ts, incl. exports/imports/main targets),
|
|
61
|
+
honouring tsconfig compilerOptions.paths aliases (from ./tsconfig.json, or --tsconfig=path);
|
|
62
|
+
not with --metro-resolver;
|
|
34
63
|
--resources carries reached assets (e.g. --resources=png,svg) as resources instead of failing to bundle them;
|
|
35
|
-
--package-json auto-includes each bundled module's package.json, even ones the scan never reached)
|
|
64
|
+
--package-json auto-includes each bundled module's package.json, even ones the scan never reached)
|
|
65
|
+
stasis github-bundle --github=owner/name [--sha=commit|--tag=name] [--directory=path] [--package-manager=(pnpm|yarn1|npm|soldeer)] [--package-manager-version=version] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] [stasis bundle's options for the entries] [path/in/repo/to/(file.(js|ts)|file.sol|dir) ...]
|
|
66
|
+
(bundles a GitHub repo at a commit, or the one --tag names, its default branch's head without
|
|
67
|
+
either, as "stasis bundle" bundles a clone of it once installed: the tree is fetched (with
|
|
68
|
+
GITHUB_TOKEN where set) and held to its git tree id, and the dependencies are laid out in memory
|
|
69
|
+
from the lockfile alone, as "pnpm install --frozen-lockfile --ignore-scripts" (pnpm 9, 10, 11 or
|
|
70
|
+
12), "yarn install --frozen-lockfile --ignore-scripts" (yarn 1.22), "npm ci --ignore-scripts"
|
|
71
|
+
(npm 10.9.3 to 10.9.9, or 11.11.1 to 11.21.0) or "soldeer install" (0.12, for .sol entries)
|
|
72
|
+
would: nothing is installed, no package script runs, and every tarball and zip is held to the
|
|
73
|
+
lockfile, all cached where "stasis audit" caches. Without --package-manager, the one whose
|
|
74
|
+
lockfile installs --directory (or the repo's root), if only one's does; at
|
|
75
|
+
--package-manager-version, else at the root package.json's packageManager pin, else pnpm
|
|
76
|
+
10.33.4, yarn 1.22.22, npm 11.21.0 (which no packageManager pins) or Soldeer 0.12.0. The entries
|
|
77
|
+
are paths in --directory, or in the repo; without them, the JS files its package.json names as
|
|
78
|
+
main, exports and bin, resolved as the build resolves them (--conditions, --mainFields, --metro
|
|
79
|
+
--platforms, --typescript), or for Soldeer its .sol files directly in it, under contracts/, and
|
|
80
|
+
under its source directory (foundry.toml's src, else src/), but tests, scripts, mocks and
|
|
81
|
+
dependency or build directories; not with --metro-resolver, --cargo* or --add; writes to
|
|
82
|
+
owner-name.<commit's first 7>.stasis.code.br by default,
|
|
83
|
+
owner-name.<--directory, its / made ->.<commit's first 7>.stasis.code.br with --directory, each
|
|
84
|
+
character outside [A-Za-z0-9._-] made _, and a directory too deep to fit in 255 characters cut,
|
|
85
|
+
with a hash of it; --output=- streams to stdout)
|
|
36
86
|
stasis add path/to/(file|dir) ...
|
|
37
87
|
(adds the listed files to the project's bundle(s) with no dependency resolution;
|
|
38
88
|
a directory expands to its files. Requires a stasis.config.json (all fields optional).)
|
|
@@ -43,12 +93,20 @@ function usage(prefix = '') {
|
|
|
43
93
|
stasis extract [--output=path/to/dir] path/to/bundle.stasis.code.br
|
|
44
94
|
stasis diff --stat [--imports] path/to/(lockfile|bundle) path/to/(lockfile|bundle)
|
|
45
95
|
stasis prune [path/to/project]
|
|
46
|
-
stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] path/to/file ...
|
|
47
|
-
(
|
|
96
|
+
stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] [--repo-advisories] path/to/file ...
|
|
97
|
+
(audits the dependencies the files hold: npm packages by npm's advisories, vendored crates and
|
|
98
|
+
Composer packages by OSV's, and Soldeer packages and GitHub repos (Foundry's lib/) by those their
|
|
99
|
+
GitHub repository publishes, asked with GITHUB_TOKEN where it is set; a Composer dev version is
|
|
100
|
+
listed as not audited, no database having one;
|
|
101
|
+
--why lists, per advisory, the cross-module import paths that pull an npm package in,
|
|
48
102
|
prefixed by the bundle consumer that imports each chain at the top level, e.g. "run: a -> b -> c";
|
|
49
103
|
it skips chains whose full tail is already listed as its own chain -- --why-deep keeps them all;
|
|
50
104
|
--why-full spells every chain out instead of collapsing repeated tails to "a -> b -> ... -> d";
|
|
51
|
-
--reason=consumer shows only advisories related to that consumer, and with --why only its chains
|
|
105
|
+
--reason=consumer shows only advisories related to that consumer, and with --why only its chains;
|
|
106
|
+
--repo-advisories also asks each package's GitHub repository for the advisories its maintainers
|
|
107
|
+
published there, before GitHub reviews them into the databases above; it needs a token in
|
|
108
|
+
GITHUB_TOKEN, and caches each package's repository for a month in the user cache dir, e.g.
|
|
109
|
+
~/.cache/stasis)
|
|
52
110
|
stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
|
|
53
111
|
(streams to stdout by default; --output=- is explicit stdout)
|
|
54
112
|
`.trim())
|
|
@@ -61,6 +119,14 @@ function setEnv(name, value) {
|
|
|
61
119
|
process.env[name] = value === undefined ? '' : value
|
|
62
120
|
}
|
|
63
121
|
|
|
122
|
+
// The per-user cache directory, as each platform names it (XDG ignores a relative path).
|
|
123
|
+
function userCacheDir() {
|
|
124
|
+
if (isAbsolute(process.env.XDG_CACHE_HOME ?? '')) return join(process.env.XDG_CACHE_HOME, 'stasis')
|
|
125
|
+
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Caches', 'stasis')
|
|
126
|
+
if (process.platform === 'win32') return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'stasis', 'Cache')
|
|
127
|
+
return join(homedir(), '.cache', 'stasis')
|
|
128
|
+
}
|
|
129
|
+
|
|
64
130
|
const command = argv.shift()
|
|
65
131
|
|
|
66
132
|
if (command === '-v' || command === '--version') {
|
|
@@ -77,12 +143,13 @@ if (command === '-v' || command === '--version') {
|
|
|
77
143
|
'child-process': { type: 'boolean' },
|
|
78
144
|
'package-json': { type: 'boolean' },
|
|
79
145
|
mock: { type: 'boolean' },
|
|
146
|
+
import: { type: 'string', multiple: true },
|
|
80
147
|
fs: { type: 'string' },
|
|
81
148
|
resources: { type: 'string' },
|
|
82
149
|
'brotli-quality': { type: 'string' },
|
|
83
150
|
}
|
|
84
151
|
const values = parseLeadingOptions(argv, options, {
|
|
85
|
-
valueFlags: ['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--resources', '--brotli-quality'],
|
|
152
|
+
valueFlags: ['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--import', '--resources', '--brotli-quality'],
|
|
86
153
|
onError: usage,
|
|
87
154
|
})
|
|
88
155
|
if (argv.length === 0) usage('Nothing to run: no path to file given')
|
|
@@ -115,6 +182,15 @@ if (command === '-v' || command === '--version') {
|
|
|
115
182
|
usage(`Error: ${cause.message}`)
|
|
116
183
|
}
|
|
117
184
|
}
|
|
185
|
+
// --import: extra preload module(s) passed through to the spawned node. Node resolves each
|
|
186
|
+
// against the project cwd; they ride AFTER stasis's own loader import.
|
|
187
|
+
const imports = values.import ?? []
|
|
188
|
+
if (imports.some((s) => s === '')) usage('Error: --import requires a module specifier (e.g. --import=./instrument.mjs)')
|
|
189
|
+
// Not a usage error: plain instrumentation/setup preloads still work under the mock; only ones
|
|
190
|
+
// that need the denied surfaces (spawning helper processes, network) die, and they do so loudly.
|
|
191
|
+
if (values.mock && imports.length > 0) {
|
|
192
|
+
console.warn("[stasis] Warning: --import preloads run under --mock's side-effect denials; a preload that spawns helper processes will fail there")
|
|
193
|
+
}
|
|
118
194
|
// --child-process: forward forked-child (e.g. Metro worker) capture to the root via per-pid shards.
|
|
119
195
|
const childProcess = values['child-process'] ? '1' : ''
|
|
120
196
|
// --package-json: auto-include every bundled module's package.json. Only meaningful while WRITING
|
|
@@ -123,7 +199,7 @@ if (command === '-v' || command === '--version') {
|
|
|
123
199
|
usage('Error: --package-json requires --bundle=(add|replace)')
|
|
124
200
|
}
|
|
125
201
|
const packageJSON = values['package-json'] ? '1' : ''
|
|
126
|
-
console.warn('[stasis] Running stasis with config:', { lock, scope, bundle, ...(bundleFile && { bundleFile }), ...(resourcesBundleFile && { resourcesBundleFile }), ...(childProcess && { childProcess: true }), ...(packageJSON && { packageJSON: true }), ...(values.mock && { mock: true }), ...(values.fs && { fs: values.fs }), ...(resources && { resources }), ...(brotliQuality !== undefined && { brotliQuality }) })
|
|
202
|
+
console.warn('[stasis] Running stasis with config:', { lock, scope, bundle, ...(bundleFile && { bundleFile }), ...(resourcesBundleFile && { resourcesBundleFile }), ...(childProcess && { childProcess: true }), ...(packageJSON && { packageJSON: true }), ...(values.mock && { mock: true }), ...(imports.length > 0 && { import: imports }), ...(values.fs && { fs: values.fs }), ...(resources && { resources }), ...(brotliQuality !== undefined && { brotliQuality }) })
|
|
127
203
|
if (debug) console.warn(`[stasis] Warning: stasis debug mode active`)
|
|
128
204
|
setEnv('EXODUS_STASIS_LOCK', lock)
|
|
129
205
|
setEnv('EXODUS_STASIS_SCOPE', scope)
|
|
@@ -166,6 +242,10 @@ if (command === '-v' || command === '--version') {
|
|
|
166
242
|
// --mock uses stasis's loader-mock entry (composes the mock before installing the hooks).
|
|
167
243
|
const loaderEntry = values.mock ? '../src/loader-mock.js' : '@exodus/stasis-core/loader'
|
|
168
244
|
nodeArgs.push('--import', import.meta.resolve(loaderEntry))
|
|
245
|
+
// Passthrough preloads ride after the loader (and after --mock's denials), so they evaluate
|
|
246
|
+
// under its hooks; their module graphs pass through uncaptured unless the app graph reaches
|
|
247
|
+
// them -- then they're promoted into the capture (see core hooks.js).
|
|
248
|
+
for (const specifier of imports) nodeArgs.push('--import', specifier)
|
|
169
249
|
const child = spawn(process.execPath, [...nodeArgs, ...argv], { stdio: 'inherit' })
|
|
170
250
|
const [code, signal] = await once(child, 'close')
|
|
171
251
|
// code is null when the child died from a signal; report 128+signo (shell convention) instead of the implicit 0
|
|
@@ -173,6 +253,7 @@ if (command === '-v' || command === '--version') {
|
|
|
173
253
|
} else if (command === 'bundle') {
|
|
174
254
|
const options = {
|
|
175
255
|
mapping: { type: 'string' },
|
|
256
|
+
manifests: { type: 'boolean' },
|
|
176
257
|
output: { type: 'string', short: 'o' },
|
|
177
258
|
scope: { type: 'string' },
|
|
178
259
|
lockfile: { type: 'string' },
|
|
@@ -183,17 +264,30 @@ if (command === '-v' || command === '--version') {
|
|
|
183
264
|
platforms: { type: 'string', multiple: true },
|
|
184
265
|
jsx: { type: 'boolean' },
|
|
185
266
|
flow: { type: 'boolean' },
|
|
267
|
+
typescript: { type: 'boolean' },
|
|
268
|
+
tsconfig: { type: 'string' },
|
|
186
269
|
resources: { type: 'string' },
|
|
187
270
|
'package-json': { type: 'boolean' },
|
|
271
|
+
cargo: { type: 'boolean' },
|
|
272
|
+
'cargo-features': { type: 'string', multiple: true },
|
|
273
|
+
'cargo-no-default-features': { type: 'boolean' },
|
|
274
|
+
'cargo-all-features': { type: 'boolean' },
|
|
275
|
+
'cargo-target': { type: 'string' },
|
|
276
|
+
'cargo-manifests': { type: 'boolean' },
|
|
188
277
|
'brotli-quality': { type: 'string' },
|
|
189
278
|
add: { type: 'boolean' },
|
|
190
279
|
}
|
|
191
280
|
const values = parseLeadingOptions(argv, options, {
|
|
192
|
-
valueFlags: ['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--resources', '--brotli-quality', '-o'],
|
|
281
|
+
valueFlags: ['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--resources', '--tsconfig', '--cargo-features', '--cargo-target', '--brotli-quality', '-o'],
|
|
193
282
|
onError: usage,
|
|
194
283
|
})
|
|
195
284
|
if (argv.length === 0) usage('Nothing to bundle: no entry file given')
|
|
196
|
-
|
|
285
|
+
// A directory entry stands for the .sol files under it (Solidity only); an extensionless path
|
|
286
|
+
// that doesn't exist is a missing one (skipped with a warning).
|
|
287
|
+
const { directoryEntryError, isSolidityEntry } = await import('../src/cmd/bundle.js')
|
|
288
|
+
const dirError = directoryEntryError(argv)
|
|
289
|
+
if (dirError !== null) usage(`Error: ${dirError}`)
|
|
290
|
+
const allSol = argv.every((f) => isSolidityEntry(f))
|
|
197
291
|
const allPhp = argv.every((f) => f.endsWith('.php'))
|
|
198
292
|
const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts)$/u.test(f))
|
|
199
293
|
const allBash = argv.every((f) => /\.(?:sh|bash)$/u.test(f))
|
|
@@ -202,6 +296,35 @@ if (command === '-v' || command === '--version') {
|
|
|
202
296
|
usage('Error: bundle entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts, all be .sh/.bash, or all be .rs')
|
|
203
297
|
}
|
|
204
298
|
if (values.mapping && !allSol) usage('Error: --mapping is only valid for .sol bundles')
|
|
299
|
+
// --manifests: carry the Solidity build's description files (foundry.toml, remappings.txt, ...).
|
|
300
|
+
const manifests = Boolean(values.manifests)
|
|
301
|
+
if (manifests && !allSol) usage('Error: --manifests is only valid for .sol bundles')
|
|
302
|
+
// --cargo: take the Rust feature/dependency resolution from `cargo metadata` (runs cargo; opt-in).
|
|
303
|
+
const cargo = Boolean(values.cargo)
|
|
304
|
+
if (cargo && !allRust) usage('Error: --cargo is only valid for Rust bundles')
|
|
305
|
+
// --cargo-features / --cargo-no-default-features / --cargo-all-features: cargo's own feature flags
|
|
306
|
+
// for the entries' packages (`pkg/feat` targets one, or a dependency). --cargo-features is
|
|
307
|
+
// repeatable and/or comma-separated; parseFeatureList is the one splitter (the resolver reuses it).
|
|
308
|
+
const { parseFeatureList } = await import('../src/loaders/cargo.js')
|
|
309
|
+
const cargoFeatures = parseFeatureList(values['cargo-features'] ?? [])
|
|
310
|
+
if (values['cargo-features'] !== undefined && cargoFeatures.length === 0) {
|
|
311
|
+
usage('Error: --cargo-features must list at least one feature (e.g. --cargo-features=serde,app/tls)')
|
|
312
|
+
}
|
|
313
|
+
const cargoNoDefaultFeatures = Boolean(values['cargo-no-default-features'])
|
|
314
|
+
const cargoAllFeatures = Boolean(values['cargo-all-features'])
|
|
315
|
+
// --cargo-target=<triple|host>: the build's target, whose cfgs (from `rustc --print cfg`) decide
|
|
316
|
+
// `#[cfg(unix)]`-style code. A triple is letters, digits, `-`, `_` and `.` (`x86_64-unknown-linux-gnu`).
|
|
317
|
+
const cargoTarget = values['cargo-target'] ?? null
|
|
318
|
+
if (cargoTarget !== null && !/^[\w.-]+$/u.test(cargoTarget)) {
|
|
319
|
+
usage('Error: --cargo-target must be a target triple or "host" (e.g. --cargo-target=aarch64-apple-darwin)')
|
|
320
|
+
}
|
|
321
|
+
// --cargo-manifests: carry each bundled package's Cargo.toml and build script, the workspace
|
|
322
|
+
// Cargo.toml, Cargo.lock and .cargo/config.toml too (the Rust counterpart of --package-json).
|
|
323
|
+
const cargoManifests = Boolean(values['cargo-manifests'])
|
|
324
|
+
if (!allRust) {
|
|
325
|
+
const given = { 'cargo-features': cargoFeatures.length > 0, 'cargo-no-default-features': cargoNoDefaultFeatures, 'cargo-all-features': cargoAllFeatures, 'cargo-target': cargoTarget !== null, 'cargo-manifests': cargoManifests }
|
|
326
|
+
for (const [flag, on] of Object.entries(given)) if (on) usage(`Error: --${flag} is only valid for Rust bundles`)
|
|
327
|
+
}
|
|
205
328
|
if (values.scope && !allJs) usage('Error: --scope is only valid for JS bundles')
|
|
206
329
|
if (values.scope && !['node_modules', 'full'].includes(values.scope)) {
|
|
207
330
|
usage('Error: --scope must be node_modules or full')
|
|
@@ -223,6 +346,13 @@ if (command === '-v' || command === '--version') {
|
|
|
223
346
|
// --flow: strip Flow type syntax (via the optional flow-remove-types dep) before parsing; JS-only.
|
|
224
347
|
if (values.flow && !allJs) usage('Error: --flow is only valid for JS bundles')
|
|
225
348
|
const flow = Boolean(values.flow)
|
|
349
|
+
// --typescript: resolve like tsc -- a specifier naming a missing JS output (./x.js, .mjs, .cjs,
|
|
350
|
+
// extensionless) lands on its on-disk TS source (./x.ts, .mts, .cts); an existing .js always wins.
|
|
351
|
+
if (values.typescript && !allJs) usage('Error: --typescript is only valid for JS bundles')
|
|
352
|
+
const typescript = Boolean(values.typescript)
|
|
353
|
+
// --tsconfig: the config whose compilerOptions.paths aliases --typescript honours (default:
|
|
354
|
+
// the project root's tsconfig.json when present). Meaningless without --typescript.
|
|
355
|
+
if (values.tsconfig !== undefined && !typescript) usage('Error: --tsconfig is only valid with --typescript')
|
|
226
356
|
// --mainFields: legacy package entry fields (e.g. react-native,browser,main) for the non-exports resolver.
|
|
227
357
|
if (values.mainFields !== undefined && !allJs) usage('Error: --mainFields is only valid for JS bundles')
|
|
228
358
|
const mainFields = values.mainFields === undefined
|
|
@@ -275,6 +405,8 @@ if (command === '-v' || command === '--version') {
|
|
|
275
405
|
usage('Error: --platforms is only valid with --metro')
|
|
276
406
|
}
|
|
277
407
|
if (metroResolver && !metro) usage('Error: --metro-resolver is only valid with --metro')
|
|
408
|
+
// metro-resolver can't substitute .js -> .ts, so --typescript would silently not apply -- reject it.
|
|
409
|
+
if (typescript && metroResolver) usage("Error: --typescript is not supported with --metro-resolver (the project's metro-resolver doesn't substitute .js -> .ts)")
|
|
278
410
|
// --brotli-quality: valid for every entry language (no allJs gate).
|
|
279
411
|
let brotliQuality
|
|
280
412
|
if (values['brotli-quality'] !== undefined) {
|
|
@@ -292,6 +424,7 @@ if (command === '-v' || command === '--version') {
|
|
|
292
424
|
cwd: process.cwd(),
|
|
293
425
|
entries: argv,
|
|
294
426
|
mappingFile: values.mapping,
|
|
427
|
+
manifests,
|
|
295
428
|
output: values.output,
|
|
296
429
|
scope: values.scope,
|
|
297
430
|
lockfile: values.lockfile,
|
|
@@ -302,11 +435,88 @@ if (command === '-v' || command === '--version') {
|
|
|
302
435
|
platforms,
|
|
303
436
|
jsx,
|
|
304
437
|
flow,
|
|
438
|
+
typescript,
|
|
439
|
+
tsconfig: values.tsconfig,
|
|
305
440
|
resources,
|
|
306
441
|
packageJSON,
|
|
442
|
+
cargo,
|
|
443
|
+
cargoFeatures,
|
|
444
|
+
cargoNoDefaultFeatures,
|
|
445
|
+
cargoAllFeatures,
|
|
446
|
+
cargoTarget,
|
|
447
|
+
cargoManifests,
|
|
307
448
|
brotliQuality,
|
|
308
449
|
add,
|
|
309
450
|
})
|
|
451
|
+
} else if (command === 'github-bundle') {
|
|
452
|
+
const values = parseLeadingOptions(argv, {
|
|
453
|
+
github: { type: 'string' },
|
|
454
|
+
sha: { type: 'string' },
|
|
455
|
+
tag: { type: 'string' },
|
|
456
|
+
directory: { type: 'string' },
|
|
457
|
+
'package-manager': { type: 'string' },
|
|
458
|
+
'package-manager-version': { type: 'string' },
|
|
459
|
+
mapping: { type: 'string' },
|
|
460
|
+
manifests: { type: 'boolean' },
|
|
461
|
+
output: { type: 'string', short: 'o' },
|
|
462
|
+
scope: { type: 'string' },
|
|
463
|
+
lockfile: { type: 'string' },
|
|
464
|
+
conditions: { type: 'string' },
|
|
465
|
+
mainFields: { type: 'string' },
|
|
466
|
+
metro: { type: 'boolean' },
|
|
467
|
+
platforms: { type: 'string', multiple: true },
|
|
468
|
+
jsx: { type: 'boolean' },
|
|
469
|
+
flow: { type: 'boolean' },
|
|
470
|
+
typescript: { type: 'boolean' },
|
|
471
|
+
tsconfig: { type: 'string' },
|
|
472
|
+
resources: { type: 'string' },
|
|
473
|
+
'package-json': { type: 'boolean' },
|
|
474
|
+
'brotli-quality': { type: 'string' },
|
|
475
|
+
}, {
|
|
476
|
+
valueFlags: ['--github', '--sha', '--tag', '--directory', '--package-manager', '--package-manager-version', '--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--tsconfig', '--resources', '--brotli-quality', '-o'],
|
|
477
|
+
onError: usage,
|
|
478
|
+
})
|
|
479
|
+
if (values.github === undefined) usage('Error: github-bundle requires --github=owner/name, the repo to bundle')
|
|
480
|
+
if (values.sha !== undefined && values.tag !== undefined) usage('Error: github-bundle takes --sha or --tag, not both')
|
|
481
|
+
// Whether each option applies to the entries is buildGitHubBundle's to say.
|
|
482
|
+
const list = (value) => [value ?? []].flat().flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean)
|
|
483
|
+
let brotliQuality
|
|
484
|
+
if (values['brotli-quality'] !== undefined) {
|
|
485
|
+
try {
|
|
486
|
+
brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
|
|
487
|
+
} catch (cause) {
|
|
488
|
+
usage(`Error: ${cause.message}`)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Tarballs, zips and GitHub trees are cached where `stasis audit` caches.
|
|
492
|
+
const { setCacheDir } = await import('../src/vfs-bundle.js')
|
|
493
|
+
setCacheDir(userCacheDir())
|
|
494
|
+
const { githubBundleCommand } = await import('../src/cmd/github-bundle.js')
|
|
495
|
+
await githubBundleCommand({
|
|
496
|
+
github: values.github,
|
|
497
|
+
sha: values.sha,
|
|
498
|
+
tag: values.tag,
|
|
499
|
+
directory: values.directory,
|
|
500
|
+
packageManager: values['package-manager'],
|
|
501
|
+
packageManagerVersion: values['package-manager-version'],
|
|
502
|
+
entries: argv.length === 0 ? undefined : argv,
|
|
503
|
+
mappingFile: values.mapping,
|
|
504
|
+
manifests: Boolean(values.manifests),
|
|
505
|
+
output: values.output,
|
|
506
|
+
lockfile: values.lockfile,
|
|
507
|
+
scope: values.scope,
|
|
508
|
+
conditions: list(values.conditions),
|
|
509
|
+
mainFields: values.mainFields === undefined ? undefined : list(values.mainFields),
|
|
510
|
+
metro: Boolean(values.metro),
|
|
511
|
+
platforms: list(values.platforms),
|
|
512
|
+
jsx: Boolean(values.jsx),
|
|
513
|
+
flow: Boolean(values.flow),
|
|
514
|
+
typescript: Boolean(values.typescript),
|
|
515
|
+
tsconfig: values.tsconfig,
|
|
516
|
+
resources: list(values.resources),
|
|
517
|
+
packageJSON: Boolean(values['package-json']),
|
|
518
|
+
brotliQuality,
|
|
519
|
+
})
|
|
310
520
|
} else if (command === 'add') {
|
|
311
521
|
// add packs the listed files (no resolver), taking split targets + resource allowlist from
|
|
312
522
|
// stasis.config.json; it takes no flags.
|
|
@@ -409,11 +619,20 @@ if (command === '-v' || command === '--version') {
|
|
|
409
619
|
const { removed, validated } = prune({ root })
|
|
410
620
|
console.warn(`[stasis] prune: validated ${validated.length} file(s), removed ${removed.length} file(s)`)
|
|
411
621
|
} else if (command === 'audit') {
|
|
412
|
-
const values = parseLeadingOptions(argv, { why: { type: 'boolean' }, 'why-deep': { type: 'boolean' }, 'why-full': { type: 'boolean' }, reason: { type: 'string' } }, { valueFlags: ['--reason'], onError: usage })
|
|
622
|
+
const values = parseLeadingOptions(argv, { why: { type: 'boolean' }, 'why-deep': { type: 'boolean' }, 'why-full': { type: 'boolean' }, reason: { type: 'string' }, 'repo-advisories': { type: 'boolean' } }, { valueFlags: ['--reason'], onError: usage })
|
|
413
623
|
if (argv.length === 0) usage('Nothing to audit: no path to file given')
|
|
624
|
+
const repoAdvisories = Boolean(values['repo-advisories'])
|
|
625
|
+
if (repoAdvisories && !process.env.GITHUB_TOKEN) usage('Error: --repo-advisories requires a GitHub token in GITHUB_TOKEN')
|
|
626
|
+
// A Soldeer package's and a GitHub repo's advisories are their repository's, asked of GitHub with
|
|
627
|
+
// GITHUB_TOKEN where it is set; the client asks nothing for any other package by itself.
|
|
628
|
+
const github = (await import('@preventive/upstream/github.js')).createClient({ token: process.env.GITHUB_TOKEN || null })
|
|
629
|
+
// Upstream caches what it looks up (a package's GitHub repo, a month each) only where told to, and
|
|
630
|
+
// answers from it without asking: a per-user directory, never one shared with less trusted jobs.
|
|
631
|
+
const { setCacheDir } = await import('@preventive/upstream/npm.js')
|
|
632
|
+
setCacheDir(userCacheDir())
|
|
414
633
|
const { audit, printAuditReport } = await import('../src/audit.js')
|
|
415
634
|
const files = argv.map((f) => resolve(f))
|
|
416
|
-
const report = await audit(files, { why: Boolean(values.why), whyDeep: Boolean(values['why-deep']), whyFull: Boolean(values['why-full']), reason: values.reason })
|
|
635
|
+
const report = await audit(files, { why: Boolean(values.why), whyDeep: Boolean(values['why-deep']), whyFull: Boolean(values['why-full']), reason: values.reason, repoAdvisories, github })
|
|
417
636
|
printAuditReport(report)
|
|
418
637
|
process.exitCode = report.rows.length === 0 ? 0 : 1
|
|
419
638
|
} else if (command === 'sbom') {
|
package/package.json
CHANGED
|
@@ -1,46 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/stasis",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"description": "File-level source bundles and lockfiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./esbuild": "./src/esbuild.js",
|
|
8
8
|
"./webpack": "./src/webpack.js",
|
|
9
|
+
"./rollup": "./src/rollup.js",
|
|
9
10
|
"./metro": "./src/metro.js",
|
|
10
11
|
"./metro-transformer": "./src/metro-transformer.js",
|
|
11
12
|
"./loader": "./src/loader.js",
|
|
12
13
|
"./mock": "./src/mock.js",
|
|
13
14
|
"./bundle": "./src/bundle.js",
|
|
15
|
+
"./bundle-reader": "./src/bundle-reader.js",
|
|
14
16
|
"./cmd/bundle": "./src/cmd/bundle.js",
|
|
15
17
|
"./cmd/build": "./src/cmd/build.js",
|
|
16
18
|
"./diff": "./src/diff.js",
|
|
17
19
|
"./lockfile": "./src/lockfile.js",
|
|
18
20
|
"./prune": "./src/prune.js",
|
|
19
21
|
"./sbom": "./src/sbom.js",
|
|
22
|
+
"./vfs-bundle": "./src/vfs-bundle.js",
|
|
20
23
|
"./logo.svg": "./logo.svg"
|
|
21
24
|
},
|
|
22
25
|
"files": [
|
|
23
26
|
"bin/stasis.js",
|
|
24
27
|
"logo.svg",
|
|
25
|
-
"src/
|
|
26
|
-
"src/apis/npm/semver.cjs",
|
|
28
|
+
"src/audit-corrections.js",
|
|
27
29
|
"src/audit.js",
|
|
28
30
|
"src/babel.js",
|
|
29
31
|
"src/babel-worker.cjs",
|
|
30
32
|
"src/bundle.js",
|
|
31
33
|
"src/bundle-from-lockfile.js",
|
|
34
|
+
"src/bundle-reader.js",
|
|
32
35
|
"src/cmd/bundle.js",
|
|
33
36
|
"src/cmd/build.js",
|
|
34
37
|
"src/cmd/diff.js",
|
|
38
|
+
"src/cmd/github-bundle.js",
|
|
39
|
+
"src/cmd/output.js",
|
|
35
40
|
"src/cmd/sbom.js",
|
|
36
41
|
"src/diff.js",
|
|
37
42
|
"src/esbuild.js",
|
|
43
|
+
"src/json-stream.js",
|
|
38
44
|
"src/loader.js",
|
|
39
45
|
"src/loader-mock.js",
|
|
40
46
|
"src/loaders/bash.js",
|
|
47
|
+
"src/loaders/cargo.js",
|
|
48
|
+
"src/loaders/foundry.js",
|
|
41
49
|
"src/loaders/rust.js",
|
|
42
50
|
"src/loaders/solidity.js",
|
|
51
|
+
"src/loaders/solidity-ownership.js",
|
|
52
|
+
"src/loaders/toml.js",
|
|
43
53
|
"src/loaders/php.js",
|
|
54
|
+
"src/loaders/paths.js",
|
|
44
55
|
"src/lockfile.js",
|
|
45
56
|
"src/metro.js",
|
|
46
57
|
"src/metro-resolver.js",
|
|
@@ -48,9 +59,19 @@
|
|
|
48
59
|
"src/mock.js",
|
|
49
60
|
"src/parse.js",
|
|
50
61
|
"src/prune.js",
|
|
62
|
+
"src/resolve-fields.js",
|
|
63
|
+
"src/resolve-node.js",
|
|
64
|
+
"src/resolve-typescript.js",
|
|
65
|
+
"src/rollup.js",
|
|
51
66
|
"src/sbom.js",
|
|
52
67
|
"src/scan.js",
|
|
53
|
-
"src/
|
|
68
|
+
"src/vfs-bundle.js",
|
|
69
|
+
"src/vfs-bundle/entries.js",
|
|
70
|
+
"src/vfs-bundle/github.js",
|
|
71
|
+
"src/vfs-bundle/soldeer.js",
|
|
72
|
+
"src/vfs-bundle/tree.js",
|
|
73
|
+
"src/webpack.js",
|
|
74
|
+
"src/why.js"
|
|
54
75
|
],
|
|
55
76
|
"engines": {
|
|
56
77
|
"node": ">=24.14.0"
|
|
@@ -60,18 +81,24 @@
|
|
|
60
81
|
},
|
|
61
82
|
"repository": {
|
|
62
83
|
"type": "git",
|
|
63
|
-
"url": "git+https://github.com/
|
|
84
|
+
"url": "git+https://github.com/PreventiveMeasures/stasis.git",
|
|
64
85
|
"directory": "stasis"
|
|
65
86
|
},
|
|
66
87
|
"author": "Exodus Movement, Inc.",
|
|
67
88
|
"license": "MIT",
|
|
68
89
|
"bugs": {
|
|
69
|
-
"url": "https://github.com/
|
|
90
|
+
"url": "https://github.com/PreventiveMeasures/stasis/issues"
|
|
70
91
|
},
|
|
71
|
-
"homepage": "https://github.com/
|
|
92
|
+
"homepage": "https://github.com/PreventiveMeasures/stasis#readme",
|
|
72
93
|
"dependencies": {
|
|
73
|
-
"@exodus/
|
|
74
|
-
"@exodus/stasis-
|
|
94
|
+
"@exodus/bytes": "^1.16.0",
|
|
95
|
+
"@exodus/stasis-core": "1.0.0-beta.5",
|
|
96
|
+
"@exodus/stasis-plugins": "1.0.0-beta.5",
|
|
97
|
+
"@preventive/archive": "1.0.0-beta.3",
|
|
98
|
+
"@preventive/deptree": "1.0.0-alpha.6",
|
|
99
|
+
"@preventive/lockfile": "1.0.0-alpha.5",
|
|
100
|
+
"@preventive/upstream": "1.0.0-alpha.4",
|
|
101
|
+
"@preventive/vfs": "1.0.0-beta.3",
|
|
75
102
|
"oxc-parser": "^0.94.0"
|
|
76
103
|
},
|
|
77
104
|
"peerDependencies": {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { satisfies, valid } from '@preventive/upstream/semver.js'
|
|
2
|
+
|
|
3
|
+
// Manual corrections to audit findings: files that must NOT count as evidence
|
|
4
|
+
// that a (potentially vulnerable) package's code is present. An import edge whose
|
|
5
|
+
// target is one of these files is not evidence the package is really used, so a
|
|
6
|
+
// package pulled in only through corrected files is skipped by the audit (see
|
|
7
|
+
// collectPackagesFromFile) -- none of its real code ships, so no advisory applies.
|
|
8
|
+
//
|
|
9
|
+
// Each entry names a package, the audit-irrelevant files (relative to the module
|
|
10
|
+
// dir), and `range` -- the semver range VERIFIED to match the rationale. The
|
|
11
|
+
// correction never applies outside it: a release could change the file, so the
|
|
12
|
+
// range is widened only after re-checking the file in the new versions.
|
|
13
|
+
const CORRECTIONS = [
|
|
14
|
+
{
|
|
15
|
+
// ws's browser build is a noop stub -- `module.exports = function () { throw
|
|
16
|
+
// new Error('ws does not work in the browser...') }` -- with none of the
|
|
17
|
+
// WebSocket implementation in it. Verified against the ws 8.21.1 tarball
|
|
18
|
+
// (latest at the time of writing); re-check browser.js before widening.
|
|
19
|
+
name: 'ws',
|
|
20
|
+
files: new Set(['browser.js']),
|
|
21
|
+
range: '<=8.21.1',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
// node-fetch's browser build re-exports the environment's native fetch
|
|
25
|
+
// (`module.exports = globalObject.fetch`, plus Headers/Request/Response) --
|
|
26
|
+
// none of the node-fetch implementation, where its advisories live, is in it.
|
|
27
|
+
// browser.js ships only through 2.7.0 (3.x dropped it); verified 2.6.13/2.7.0.
|
|
28
|
+
name: 'node-fetch',
|
|
29
|
+
files: new Set(['browser.js']),
|
|
30
|
+
range: '<=2.7.0',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Is `rel` (a file path relative to the module dir) audit-irrelevant for
|
|
35
|
+
// `name@version`? Unknown or unparsable versions are never corrected (fail
|
|
36
|
+
// closed: the package stays audited).
|
|
37
|
+
function isCorrectedFile(name, version, rel) {
|
|
38
|
+
for (const { name: pkg, files, range } of CORRECTIONS) {
|
|
39
|
+
if (pkg !== name || !files.has(rel)) continue
|
|
40
|
+
if (valid(version) && satisfies(version, range)) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A manifest (top-level or nested) is never code: the resolution graph records
|
|
46
|
+
// resolver/metadata reads of `package.json` as edges (react-native scans sibling
|
|
47
|
+
// packages' manifests for Haste/asset resolution) and consumers record them
|
|
48
|
+
// alongside bundled files, but no package code ships through one. So too for the
|
|
49
|
+
// manifests the other ecosystems' bundles carry beside a dependency's code
|
|
50
|
+
// (--cargo-manifests, --manifests): its Cargo.toml and the checksums cargo vendored
|
|
51
|
+
// it with, its composer.json, a Solidity dependency's foundry.toml and remappings.
|
|
52
|
+
const MANIFESTS = {
|
|
53
|
+
npm: new Set(['package.json']),
|
|
54
|
+
cargo: new Set(['Cargo.toml', 'Cargo.lock', '.cargo-checksum.json']),
|
|
55
|
+
composer: new Set(['composer.json', 'composer.lock']),
|
|
56
|
+
soldeer: new Set(['package.json', 'foundry.toml', 'remappings.txt', 'soldeer.toml']),
|
|
57
|
+
github: new Set(['package.json', 'foundry.toml', 'remappings.txt', 'soldeer.toml']),
|
|
58
|
+
}
|
|
59
|
+
const isManifest = (rel, ecosystem) => MANIFESTS[ecosystem]?.has(rel.slice(rel.lastIndexOf('/') + 1)) ?? false
|
|
60
|
+
|
|
61
|
+
// Is `rel` evidence that `name@version`'s REAL code is present, of a dependency of
|
|
62
|
+
// `ecosystem` (npm by default)? This is the one rule every audit surface shares --
|
|
63
|
+
// package presence, the reason column, and the --why chain graph all count a file
|
|
64
|
+
// (or an edge targeting it) only when it passes. Manifests never do; corrected
|
|
65
|
+
// files (npm's) don't within their verified range.
|
|
66
|
+
export function isEvidenceFile(name, version, rel, ecosystem = 'npm') {
|
|
67
|
+
return !isManifest(rel, ecosystem) && (ecosystem !== 'npm' || !isCorrectedFile(name, version, rel))
|
|
68
|
+
}
|