@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.3
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 +16 -2
- package/bin/stasis.js +153 -167
- package/package.json +11 -5
- package/src/apis/npm/semver.cjs +0 -2
- package/src/audit.js +161 -22
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +12 -18
- package/src/cmd/build.js +92 -123
- package/src/cmd/bundle.js +469 -428
- package/src/cmd/diff.js +2 -19
- package/src/cmd/sbom.js +1 -7
- package/src/diff.js +61 -118
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +33 -81
- package/src/loaders/php.js +143 -282
- package/src/loaders/rust.js +37 -93
- package/src/loaders/solidity.js +27 -52
- package/src/metro-resolver.js +165 -0
- package/src/mock.js +54 -160
- package/src/parse.js +4 -16
- package/src/sbom.js +37 -79
- package/src/scan.js +166 -124
- package/src/cmd/extract.js +0 -193
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
*Analyzeable source code bundles with resolutions + fine-grained lockfiles.*
|
|
4
4
|
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://esbuild.github.io/)
|
|
7
|
+
[](https://metrobundler.dev/)
|
|
8
|
+
[](https://webpack.js.org/)
|
|
9
|
+
|
|
5
10
|
Both lockfiles and bundles only include what is actually _used_.\
|
|
6
11
|
Generally about 10x smaller than production-focused `node_modules` install.
|
|
7
12
|
|
|
@@ -53,9 +58,14 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
53
58
|
| `stasis run --lock=add --child-process app.js` | also attest modules loaded in forked child processes (e.g. Metro transform workers) |
|
|
54
59
|
| `stasis run --bundle=add --fs=sync app.js` | build a bundle that also captures sync `fs.readFileSync`/`readdirSync` reads |
|
|
55
60
|
| `stasis bundle src/index.js` | build a bundle statically, without executing it |
|
|
61
|
+
| `stasis bundle --add src/worker.js` | merge more entries (and their import graph) into an existing `stasis.code.br` instead of replacing it |
|
|
62
|
+
| `stasis add a.js icon.svg` | add the listed files to the project's bundles (config-driven), with no dependency resolution |
|
|
63
|
+
| `stasis add src assets` | same, expanding each directory to every file under it, minus an auto-excluded set unless explicitly named |
|
|
56
64
|
| `stasis bundle --conditions=react-native,browser app.js` | statically bundle, asserting extra `exports`/`imports` resolution conditions |
|
|
57
65
|
| `stasis bundle --mainFields=react-native,browser,main app.js` | statically bundle, honoring legacy package `mainFields` (incl. browser-field object redirection) |
|
|
58
|
-
| `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 |
|
|
66
|
+
| `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
|
+
| `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
|
+
| `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` |
|
|
59
69
|
| `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) |
|
|
60
70
|
| `stasis build --output=out.js stasis.lock.json [entry]` | same, reading + verifying sources from disk against the lockfile |
|
|
61
71
|
| `stasis extract app.stasis.code.br` | unpack a bundle back to sources + a `stasis.lock.json` |
|
|
@@ -63,12 +73,16 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
63
73
|
| `stasis prune` | trim `node_modules` to the lockfile, verifying the rest |
|
|
64
74
|
| `stasis audit stasis.lock.json` | report npm advisories for a lockfile's dependencies |
|
|
65
75
|
| `stasis audit app.stasis.code.br` | report npm advisories for a bundle's dependencies |
|
|
76
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `stasis audit --reason=run app.stasis.code.br` | show only advisories related to one consumer (`run`); with `--why`, keep only that consumer's chains |
|
|
66
80
|
| `stasis sbom --format=spdx stasis.lock.json` | export an SPDX SBOM for a lockfile or bundle |
|
|
67
81
|
| `stasis sbom --format=cyclonedx app.stasis.code.br` | export a CycloneDX SBOM for a lockfile or bundle |
|
|
68
82
|
|
|
69
83
|
## Runtime
|
|
70
84
|
|
|
71
|
-
The zero-dependency [`@exodus/stasis-core`](../stasis-core) CLI provides `run` and `
|
|
85
|
+
The zero-dependency [`@exodus/stasis-core`](../stasis-core) CLI provides `run`, `prune`, and `add` commands only; the bundler plugins live in [`@exodus/stasis-plugins`](../stasis-plugins).
|
|
72
86
|
|
|
73
87
|
## License
|
|
74
88
|
|
package/bin/stasis.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
|
-
import { parseArgs } from 'node:util'
|
|
5
4
|
import { once } from 'node:events'
|
|
6
5
|
import { fileURLToPath } from 'node:url'
|
|
7
6
|
import { basename, dirname, resolve } from 'node:path'
|
|
8
7
|
import { existsSync, realpathSync } from 'node:fs'
|
|
9
8
|
import { constants as osConstants } from 'node:os'
|
|
10
9
|
import assert from 'node:assert/strict'
|
|
11
|
-
import { parseBrotliQuality } from '@exodus/stasis-core/util'
|
|
10
|
+
import { parseBrotliQuality, parseLeadingOptions, parseResourcesOption } from '@exodus/stasis-core/util'
|
|
12
11
|
import pkg from '../package.json' with { type: 'json' }
|
|
13
12
|
|
|
14
13
|
const argv = [...process.argv]
|
|
@@ -16,27 +15,42 @@ assert(['node', 'node.exe'].includes(basename(argv.shift())))
|
|
|
16
15
|
assert(['node', 'node.exe'].includes(basename(process.argv0)))
|
|
17
16
|
|
|
18
17
|
const jsname = argv.shift()
|
|
19
|
-
const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b)
|
|
18
|
+
const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b)
|
|
20
19
|
assert(basename(jsname) === 'stasis' || pathsEqual(jsname, fileURLToPath(import.meta.url)))
|
|
21
20
|
|
|
22
21
|
function usage(prefix = '') {
|
|
23
22
|
console.error(`${prefix}\nUsage:
|
|
24
|
-
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] [--mock] [--fs=(sync|async)] [--resources=ext,ext] [--brotli-quality=0..11] path/to/file.js ...
|
|
25
|
-
stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--output=(path|-)] path/to/file.sol ...
|
|
26
|
-
stasis bundle [--output=(path|-)] path/to/file.php ...
|
|
27
|
-
stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
28
|
-
stasis bundle --metro --platforms=ios,android [--platforms=web] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] path/to/file.(js|ts) ...
|
|
29
|
-
stasis bundle [--output=(path|-)] path/to/file.(sh|bash) ...
|
|
30
|
-
stasis bundle [--output=(path|-)] path/to/file.rs ...
|
|
31
|
-
(
|
|
32
|
-
--brotli-quality=0..11
|
|
33
|
-
|
|
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
|
+
stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--add] [--output=(path|-)] path/to/file.sol ...
|
|
25
|
+
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) ...
|
|
28
|
+
stasis bundle [--add] [--output=(path|-)] path/to/file.(sh|bash) ...
|
|
29
|
+
stasis bundle [--add] [--output=(path|-)] path/to/file.rs ...
|
|
30
|
+
(writes to stasis.code.br by default; --output=- streams to stdout; --add merges into an
|
|
31
|
+
existing bundle instead of replacing it (not with --output=-); --brotli-quality=0..11, default 9;
|
|
32
|
+
--jsx parses JSX in .js/.cjs/.mjs files, e.g. React Native source (put JSX-in-TS in a .tsx file);
|
|
33
|
+
--flow strips Flow types from .js/.cjs/.mjs sources oxc can't parse (needs the optional flow-remove-types dep);
|
|
34
|
+
--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))
|
|
36
|
+
stasis add path/to/(file|dir) ...
|
|
37
|
+
(adds the listed files to the project's bundle(s) with no dependency resolution;
|
|
38
|
+
a directory expands to its files. Requires a stasis.config.json (all fields optional).)
|
|
39
|
+
stasis build --output=(dir|file.js) [--format=(esm|cjs|iife)] [--platform=(node|browser|neutral|hermes)] [--babel] [--minify] [--sourcemap] [--define=K=V ...] [--external=pkg ...] [--loader=.ext:name ...] path/to/(stasis.code.br|stasis.lock.json) [entry]
|
|
40
|
+
(--platform=hermes downlevels every file for Hermes (classes, let/const, arrows, ...; via esbuild + Babel)
|
|
41
|
+
and defaults --format to iife; --babel instead transforms every file with the project's own
|
|
42
|
+
babel.config.(js|cjs|mjs|json); both resolve @babel/core and plugins from the project's dev deps)
|
|
34
43
|
stasis extract [--output=path/to/dir] path/to/bundle.stasis.code.br
|
|
35
44
|
stasis diff --stat [--imports] path/to/(lockfile|bundle) path/to/(lockfile|bundle)
|
|
36
45
|
stasis prune [path/to/project]
|
|
37
|
-
stasis audit path/to/file ...
|
|
46
|
+
stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] path/to/file ...
|
|
47
|
+
(--why lists, per advisory, the cross-module import paths that pull the package in,
|
|
48
|
+
prefixed by the bundle consumer that imports each chain at the top level, e.g. "run: a -> b -> c";
|
|
49
|
+
it skips chains whose full tail is already listed as its own chain -- --why-deep keeps them all;
|
|
50
|
+
--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)
|
|
38
52
|
stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
|
|
39
|
-
(
|
|
53
|
+
(streams to stdout by default; --output=- is explicit stdout)
|
|
40
54
|
`.trim())
|
|
41
55
|
process.exit(1)
|
|
42
56
|
}
|
|
@@ -53,12 +67,6 @@ if (command === '-v' || command === '--version') {
|
|
|
53
67
|
console.log(`v${pkg.version}`)
|
|
54
68
|
process.exit(0)
|
|
55
69
|
} else if (command === 'run') {
|
|
56
|
-
const flags = []
|
|
57
|
-
const valueFlags = new Set(['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--resources', '--brotli-quality'])
|
|
58
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
59
|
-
flags.push(argv.shift())
|
|
60
|
-
}
|
|
61
|
-
|
|
62
70
|
const options = {
|
|
63
71
|
lock: { type: 'string', default: 'none' },
|
|
64
72
|
bundle: { type: 'string', default: 'none' },
|
|
@@ -67,18 +75,16 @@ if (command === '-v' || command === '--version') {
|
|
|
67
75
|
debug: { type: 'boolean' },
|
|
68
76
|
dependencies: { type: 'boolean' },
|
|
69
77
|
'child-process': { type: 'boolean' },
|
|
78
|
+
'package-json': { type: 'boolean' },
|
|
70
79
|
mock: { type: 'boolean' },
|
|
71
80
|
fs: { type: 'string' },
|
|
72
81
|
resources: { type: 'string' },
|
|
73
82
|
'brotli-quality': { type: 'string' },
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} catch (cause) {
|
|
80
|
-
usage(`Error: ${cause.message}`)
|
|
81
|
-
}
|
|
84
|
+
const values = parseLeadingOptions(argv, options, {
|
|
85
|
+
valueFlags: ['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--resources', '--brotli-quality'],
|
|
86
|
+
onError: usage,
|
|
87
|
+
})
|
|
82
88
|
if (argv.length === 0) usage('Nothing to run: no path to file given')
|
|
83
89
|
if (!['none', 'ignore', 'add', 'replace', 'frozen'].includes(values.lock)) usage('Error: invalid --lock value')
|
|
84
90
|
const lock = values.lock
|
|
@@ -89,25 +95,18 @@ if (command === '-v' || command === '--version') {
|
|
|
89
95
|
const debug = values.debug ? '1' : ''
|
|
90
96
|
if (!['none', 'ignore', 'add', 'replace', 'load', 'frozen'].includes(bundle)) usage('Error: invalid --bundle value')
|
|
91
97
|
if (bundleFile && bundle === 'none') usage('Error: --bundle-file requires --bundle=(add|replace|load|frozen|ignore)')
|
|
92
|
-
//
|
|
93
|
-
// it), --resources-bundle-file needs an active bundle: Config rejects bundle=none AND ignore
|
|
94
|
-
// (resourcesBundleFile requires an active bundle mode). Front-run that with a clean usage error.
|
|
98
|
+
// --resources-bundle-file needs an active bundle mode (unlike --bundle-file, inert under ignore).
|
|
95
99
|
if (resourcesBundleFile && (bundle === 'none' || bundle === 'ignore')) usage('Error: --resources-bundle-file requires --bundle=(add|replace|load|frozen)')
|
|
96
100
|
if (bundle === 'load' && lock !== 'frozen' && lock !== 'none' && lock !== 'ignore') usage('Error: --bundle=load is incompatible with --lock=(add|replace)')
|
|
97
101
|
if (lock === 'none' && bundle === 'none') usage('Error: stasis needs a lockfile or a bundle: set --lock or --bundle')
|
|
98
102
|
if (values.mock && bundle === 'load') usage('Error: --mock is for capturing imports while building a bundle; not compatible with --bundle=load')
|
|
99
|
-
// --fs
|
|
100
|
-
//
|
|
101
|
-
// nothing to record into or read from without one of those bundle modes. The mode
|
|
102
|
-
// argument is required: `sync` patches the sync readers, `async` adds their async
|
|
103
|
-
// (callback + fs.promises) counterparts on top.
|
|
103
|
+
// --fs requires a bundle mode (add|replace|load); `sync` patches the sync fs readers,
|
|
104
|
+
// `async` adds their callback + fs.promises counterparts on top.
|
|
104
105
|
if (values.fs !== undefined && !['sync', 'async'].includes(values.fs)) usage("Error: --fs must be 'sync' or 'async'")
|
|
105
106
|
if (values.fs !== undefined && !['add', 'replace', 'load'].includes(bundle)) usage('Error: --fs requires --bundle=(add|replace|load)')
|
|
106
107
|
const captureFs = values.fs ?? ''
|
|
107
|
-
// --resources: comma-separated extension/filename allowlist for `--fs`
|
|
108
|
-
// (e.g. png,svg,LICENSE). The child's Config validates each entry via parseResourcesOption.
|
|
108
|
+
// --resources: comma-separated extension/filename allowlist for `--fs` captures.
|
|
109
109
|
const resources = values.resources ?? ''
|
|
110
|
-
// --brotli-quality: bundle compression quality (0..11; unset = brotli's default 11).
|
|
111
110
|
let brotliQuality
|
|
112
111
|
if (values['brotli-quality'] !== undefined) {
|
|
113
112
|
try {
|
|
@@ -116,10 +115,15 @@ if (command === '-v' || command === '--version') {
|
|
|
116
115
|
usage(`Error: ${cause.message}`)
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
|
-
// --child-process: forward forked-child (e.g. Metro
|
|
120
|
-
// via per-pid shards. Opt-in -- it stands up a process-coordination channel, off by default.
|
|
118
|
+
// --child-process: forward forked-child (e.g. Metro worker) capture to the root via per-pid shards.
|
|
121
119
|
const childProcess = values['child-process'] ? '1' : ''
|
|
122
|
-
|
|
120
|
+
// --package-json: auto-include every bundled module's package.json. Only meaningful while WRITING
|
|
121
|
+
// a bundle (add|replace) -- there's nothing to fold the manifests into otherwise.
|
|
122
|
+
if (values['package-json'] && bundle !== 'add' && bundle !== 'replace') {
|
|
123
|
+
usage('Error: --package-json requires --bundle=(add|replace)')
|
|
124
|
+
}
|
|
125
|
+
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 }) })
|
|
123
127
|
if (debug) console.warn(`[stasis] Warning: stasis debug mode active`)
|
|
124
128
|
setEnv('EXODUS_STASIS_LOCK', lock)
|
|
125
129
|
setEnv('EXODUS_STASIS_SCOPE', scope)
|
|
@@ -128,58 +132,38 @@ if (command === '-v' || command === '--version') {
|
|
|
128
132
|
setEnv('EXODUS_STASIS_RESOURCES_BUNDLE_FILE', resourcesBundleFile)
|
|
129
133
|
setEnv('EXODUS_STASIS_DEBUG', debug)
|
|
130
134
|
setEnv('EXODUS_STASIS_CHILD_PROCESS', childProcess)
|
|
135
|
+
setEnv('EXODUS_STASIS_PACKAGE_JSON', packageJSON)
|
|
131
136
|
setEnv('EXODUS_STASIS_FS', captureFs)
|
|
132
137
|
setEnv('EXODUS_STASIS_RESOURCES', resources)
|
|
133
|
-
// Only set when given
|
|
134
|
-
// to the child (an unconditional setEnv('') would reject it as a conflict).
|
|
138
|
+
// Only set when given; an unconditional setEnv('') would conflict with an ambient value.
|
|
135
139
|
if (brotliQuality !== undefined) setEnv('EXODUS_STASIS_BROTLI_QUALITY', String(brotliQuality))
|
|
136
|
-
// --mock: capture imports by running user code with side-effects denied
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// --allow-* counterparts, so src/mock.js neutralizes them in JS. We --import
|
|
141
|
-
// src/loader-mock.js (instead of core's plain loader): it evaluates the core
|
|
142
|
-
// hooks lib first -- snapshotting stasis's real fs bindings -- then src/mock.js
|
|
143
|
-
// (whose syncBuiltinESMExports() refresh reaches user-code ESM imports), then
|
|
144
|
-
// installs the hooks. So the mock runs after stasis's snapshots and before
|
|
145
|
-
// registration, by static import order and with no env var. Reads stay open
|
|
146
|
-
// (--allow-fs-read=*) so node_modules resolution works; reads aren't a side
|
|
147
|
-
// effect. Writes are still scoped at the kernel level as defense in depth (the
|
|
148
|
-
// JS layer covers user code, --permission covers anything that bypasses JS,
|
|
149
|
-
// e.g. process.binding or a future leak).
|
|
140
|
+
// --mock: capture imports by running user code with side-effects denied (fail-closed).
|
|
141
|
+
// --permission blocks fs writes/child procs/workers/addons/inspector at the kernel level;
|
|
142
|
+
// src/mock.js neutralizes network + timers in JS (no --allow-* for those). Reads stay open
|
|
143
|
+
// so node_modules resolution works.
|
|
150
144
|
// Node 24 dropped comma-separated --allow-fs-write; repeat the flag instead.
|
|
151
145
|
const nodeArgs = []
|
|
152
146
|
if (values.mock) {
|
|
153
147
|
const writeAllow = [process.cwd()]
|
|
154
|
-
// --allow-fs-write paths must already exist on disk
|
|
155
|
-
//
|
|
156
|
-
// bundle target (code and/or split resources), walk up to the nearest existing ancestor and
|
|
157
|
-
// grant write there -- broader than ideal, but the user explicitly chose these locations.
|
|
158
|
-
// (Note: the cwd check uses '/' as a separator and is therefore unix-only; that matches the
|
|
159
|
-
// rest of the project's path handling.)
|
|
148
|
+
// --allow-fs-write paths must already exist on disk, so for each out-of-cwd bundle target
|
|
149
|
+
// walk up to the nearest existing ancestor and grant write there.
|
|
160
150
|
for (const [flag, file] of [['--bundle-file', bundleFile], ['--resources-bundle-file', resourcesBundleFile]]) {
|
|
161
151
|
if (!file || file.startsWith(`${process.cwd()}/`)) continue
|
|
162
152
|
let p = dirname(file)
|
|
163
153
|
while (!existsSync(p) && dirname(p) !== p) p = dirname(p)
|
|
164
|
-
// Refuse
|
|
165
|
-
// --permission layer everywhere, and reaching it means none of the path's ancestors exist --
|
|
166
|
-
// the user almost certainly didn't intend this. Ask them to create a parent.
|
|
154
|
+
// Refuse write access to the filesystem root -- it would disable the --permission layer everywhere.
|
|
167
155
|
if (p === dirname(p)) {
|
|
168
156
|
usage(`Error: no existing parent directory for ${flag}=${file}; create one first or choose a path under an existing directory`)
|
|
169
157
|
}
|
|
170
|
-
if (!writeAllow.includes(p)) writeAllow.push(p)
|
|
158
|
+
if (!writeAllow.includes(p)) writeAllow.push(p)
|
|
171
159
|
}
|
|
172
160
|
nodeArgs.push('--permission', '--allow-fs-read=*')
|
|
173
161
|
for (const p of writeAllow) nodeArgs.push(`--allow-fs-write=${p}`)
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// resolve and make the captured import map incompatible with a normal
|
|
177
|
-
// replay. Restore the condition so resolution matches a non-mock run; this
|
|
178
|
-
// doesn't allow native addons to actually load (--allow-addons still off).
|
|
162
|
+
// Restore the node-addons resolution condition (dropped by --permission) so the captured
|
|
163
|
+
// import map matches a non-mock run; native addons still can't load (--allow-addons off).
|
|
179
164
|
nodeArgs.push('--conditions=node-addons')
|
|
180
165
|
}
|
|
181
|
-
//
|
|
182
|
-
// entry, which composes the mock into the same hooks lib (see comment above).
|
|
166
|
+
// --mock uses stasis's loader-mock entry (composes the mock before installing the hooks).
|
|
183
167
|
const loaderEntry = values.mock ? '../src/loader-mock.js' : '@exodus/stasis-core/loader'
|
|
184
168
|
nodeArgs.push('--import', import.meta.resolve(loaderEntry))
|
|
185
169
|
const child = spawn(process.execPath, [...nodeArgs, ...argv], { stdio: 'inherit' })
|
|
@@ -187,11 +171,6 @@ if (command === '-v' || command === '--version') {
|
|
|
187
171
|
// code is null when the child died from a signal; report 128+signo (shell convention) instead of the implicit 0
|
|
188
172
|
process.exitCode = code ?? 128 + (osConstants.signals[signal] ?? 0)
|
|
189
173
|
} else if (command === 'bundle') {
|
|
190
|
-
const flags = []
|
|
191
|
-
const valueFlags = new Set(['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--brotli-quality', '-o'])
|
|
192
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
193
|
-
flags.push(argv.shift())
|
|
194
|
-
}
|
|
195
174
|
const options = {
|
|
196
175
|
mapping: { type: 'string' },
|
|
197
176
|
output: { type: 'string', short: 'o' },
|
|
@@ -200,15 +179,19 @@ if (command === '-v' || command === '--version') {
|
|
|
200
179
|
conditions: { type: 'string' },
|
|
201
180
|
mainFields: { type: 'string' },
|
|
202
181
|
metro: { type: 'boolean' },
|
|
182
|
+
'metro-resolver': { type: 'boolean' },
|
|
203
183
|
platforms: { type: 'string', multiple: true },
|
|
184
|
+
jsx: { type: 'boolean' },
|
|
185
|
+
flow: { type: 'boolean' },
|
|
186
|
+
resources: { type: 'string' },
|
|
187
|
+
'package-json': { type: 'boolean' },
|
|
204
188
|
'brotli-quality': { type: 'string' },
|
|
189
|
+
add: { type: 'boolean' },
|
|
205
190
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
usage(`Error: ${cause.message}`)
|
|
211
|
-
}
|
|
191
|
+
const values = parseLeadingOptions(argv, options, {
|
|
192
|
+
valueFlags: ['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--resources', '--brotli-quality', '-o'],
|
|
193
|
+
onError: usage,
|
|
194
|
+
})
|
|
212
195
|
if (argv.length === 0) usage('Nothing to bundle: no entry file given')
|
|
213
196
|
const allSol = argv.every((f) => f.endsWith('.sol'))
|
|
214
197
|
const allPhp = argv.every((f) => f.endsWith('.php'))
|
|
@@ -224,16 +207,12 @@ if (command === '-v' || command === '--version') {
|
|
|
224
207
|
usage('Error: --scope must be node_modules or full')
|
|
225
208
|
}
|
|
226
209
|
if (values.lockfile && !allJs) usage('Error: --lockfile is only valid for JS bundles')
|
|
227
|
-
// --
|
|
228
|
-
// emits a full-scope bundle, so the two together would silently ignore --scope.
|
|
210
|
+
// --mainFields/--metro always emit a full-scope bundle, so they'd silently ignore --scope.
|
|
229
211
|
if (values.scope !== undefined && (values.mainFields !== undefined || values.metro)) {
|
|
230
212
|
usage('Error: --scope is not supported with --mainFields or --metro')
|
|
231
213
|
}
|
|
232
|
-
// --conditions:
|
|
233
|
-
//
|
|
234
|
-
// They steer which branch of a package's conditional `exports` the static scan
|
|
235
|
-
// follows. NOTE: conditions alone do not honour legacy package `mainFields` or
|
|
236
|
-
// platform-specific file suffixes -- see --mainFields.
|
|
214
|
+
// --conditions: extra exports/imports resolution conditions merged onto Node's defaults;
|
|
215
|
+
// they don't honour legacy `mainFields` or platform suffixes (see --mainFields).
|
|
237
216
|
if (values.conditions !== undefined && !allJs) usage('Error: --conditions is only valid for JS bundles')
|
|
238
217
|
const conditions = values.conditions === undefined
|
|
239
218
|
? []
|
|
@@ -241,9 +220,10 @@ if (command === '-v' || command === '--version') {
|
|
|
241
220
|
if (values.conditions !== undefined && conditions.length === 0) {
|
|
242
221
|
usage('Error: --conditions must list at least one condition name (e.g. --conditions=react-native,browser)')
|
|
243
222
|
}
|
|
244
|
-
// --
|
|
245
|
-
|
|
246
|
-
|
|
223
|
+
// --flow: strip Flow type syntax (via the optional flow-remove-types dep) before parsing; JS-only.
|
|
224
|
+
if (values.flow && !allJs) usage('Error: --flow is only valid for JS bundles')
|
|
225
|
+
const flow = Boolean(values.flow)
|
|
226
|
+
// --mainFields: legacy package entry fields (e.g. react-native,browser,main) for the non-exports resolver.
|
|
247
227
|
if (values.mainFields !== undefined && !allJs) usage('Error: --mainFields is only valid for JS bundles')
|
|
248
228
|
const mainFields = values.mainFields === undefined
|
|
249
229
|
? undefined
|
|
@@ -251,19 +231,42 @@ if (command === '-v' || command === '--version') {
|
|
|
251
231
|
if (values.mainFields !== undefined && mainFields.length === 0) {
|
|
252
232
|
usage('Error: --mainFields must list at least one field (e.g. --mainFields=react-native,browser,main)')
|
|
253
233
|
}
|
|
254
|
-
// --metro: resolve like Metro/
|
|
255
|
-
//
|
|
256
|
-
// (.ios/.android/.native) for each --platforms target, bundling all of them at once.
|
|
257
|
-
// --platforms is repeatable and/or comma-separated; the values union.
|
|
234
|
+
// --metro: resolve like Metro/RN (its own conditions + mainFields, platform suffixes per
|
|
235
|
+
// --platforms target). --platforms is repeatable and/or comma-separated; the values union.
|
|
258
236
|
const metro = Boolean(values.metro)
|
|
237
|
+
// --metro-resolver: resolve through the project's own metro-resolver instead of the built-in
|
|
238
|
+
// approximation. Only meaningful alongside --metro (enforced below and in classifyEntries).
|
|
239
|
+
const metroResolver = Boolean(values['metro-resolver'])
|
|
259
240
|
const platforms = [...new Set((values.platforms ?? []).flatMap((p) => p.split(',')).map((s) => s.trim()).filter(Boolean))]
|
|
260
|
-
// A platform name becomes
|
|
261
|
-
// contains '/'; '*' is the reserved private placeholder. Reject both here so we never
|
|
262
|
-
// emit a bundle the readers can't parse (the divergent-edge case is data-dependent).
|
|
241
|
+
// A platform name becomes an edge key: reject '/' (parsers reject it) and '*' (reserved placeholder).
|
|
263
242
|
for (const p of platforms) {
|
|
264
243
|
if (p === '*' || p.includes('/')) usage(`Error: invalid --platforms value '${p}' (a platform name can't contain '/' or be '*')`)
|
|
265
244
|
}
|
|
266
245
|
if ((metro || platforms.length > 0) && !allJs) usage('Error: --metro is only valid for JS bundles')
|
|
246
|
+
// --jsx: parse React Native's JSX-in-.js source in the static scanner (off by default; oxc,
|
|
247
|
+
// like tsc, only auto-enables JSX for .jsx/.tsx). Orthogonal to the resolver, so it pairs with
|
|
248
|
+
// plain/--mainFields/--metro alike -- JS-only, since no other entry language is scanned.
|
|
249
|
+
const jsx = Boolean(values.jsx)
|
|
250
|
+
if (jsx && !allJs) usage('Error: --jsx is only valid for JS bundles')
|
|
251
|
+
// --package-json: fold each bundled module's package.json into the bundle. JS-only (only JS
|
|
252
|
+
// modules carry an npm package.json; the sol/php/bash/rust bucketizers have no such manifest).
|
|
253
|
+
const packageJSON = Boolean(values['package-json'])
|
|
254
|
+
if (packageJSON && !allJs) usage('Error: --package-json is only valid for JS bundles')
|
|
255
|
+
// --resources: comma-separated extension/filename allowlist for assets reached through the graph
|
|
256
|
+
// (e.g. --resources=png,svg). They're carried as resources instead of failing "can't carry" --
|
|
257
|
+
// for graphs that aren't fully loadable in JS (Metro consumes such assets). JS-only.
|
|
258
|
+
if (values.resources !== undefined && !allJs) usage('Error: --resources is only valid for JS bundles')
|
|
259
|
+
const resources = values.resources === undefined
|
|
260
|
+
? []
|
|
261
|
+
: values.resources.split(',').map((s) => s.trim()).filter(Boolean)
|
|
262
|
+
if (values.resources !== undefined && resources.length === 0) {
|
|
263
|
+
usage('Error: --resources must list at least one extension or filename (e.g. --resources=png,svg)')
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
parseResourcesOption('--resources', resources) // validate early for a clean usage error (code exts / bad chars rejected)
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
usage(`Error: ${cause.message}`)
|
|
269
|
+
}
|
|
267
270
|
if (metro) {
|
|
268
271
|
if (conditions.length > 0) usage("Error: --conditions can't be combined with --metro (it sets its own conditions)")
|
|
269
272
|
if (mainFields !== undefined) usage("Error: --mainFields can't be combined with --metro (it sets its own mainFields)")
|
|
@@ -271,7 +274,8 @@ if (command === '-v' || command === '--version') {
|
|
|
271
274
|
} else if (platforms.length > 0) {
|
|
272
275
|
usage('Error: --platforms is only valid with --metro')
|
|
273
276
|
}
|
|
274
|
-
|
|
277
|
+
if (metroResolver && !metro) usage('Error: --metro-resolver is only valid with --metro')
|
|
278
|
+
// --brotli-quality: valid for every entry language (no allJs gate).
|
|
275
279
|
let brotliQuality
|
|
276
280
|
if (values['brotli-quality'] !== undefined) {
|
|
277
281
|
try {
|
|
@@ -280,6 +284,9 @@ if (command === '-v' || command === '--version') {
|
|
|
280
284
|
usage(`Error: ${cause.message}`)
|
|
281
285
|
}
|
|
282
286
|
}
|
|
287
|
+
// --add merges into the existing bundle at --output, so it can't target write-only stdout.
|
|
288
|
+
const add = Boolean(values.add)
|
|
289
|
+
if (add && values.output === '-') usage('Error: --add cannot be combined with --output=-')
|
|
283
290
|
const { bundleCommand } = await import('../src/cmd/bundle.js')
|
|
284
291
|
await bundleCommand({
|
|
285
292
|
cwd: process.cwd(),
|
|
@@ -291,58 +298,65 @@ if (command === '-v' || command === '--version') {
|
|
|
291
298
|
conditions,
|
|
292
299
|
mainFields,
|
|
293
300
|
metro,
|
|
301
|
+
metroResolver,
|
|
294
302
|
platforms,
|
|
303
|
+
jsx,
|
|
304
|
+
flow,
|
|
305
|
+
resources,
|
|
306
|
+
packageJSON,
|
|
295
307
|
brotliQuality,
|
|
308
|
+
add,
|
|
296
309
|
})
|
|
310
|
+
} else if (command === 'add') {
|
|
311
|
+
// add packs the listed files (no resolver), taking split targets + resource allowlist from
|
|
312
|
+
// stasis.config.json; it takes no flags.
|
|
313
|
+
if (argv.length === 0) usage('Nothing to add: no file given')
|
|
314
|
+
if (argv.some((a) => a.startsWith('-'))) usage('Error: add takes no options; its targets and resource allowlist come from stasis.config.json')
|
|
315
|
+
const { addCommand } = await import('@exodus/stasis-core/add')
|
|
316
|
+
// Let addCommand's runtime errors propagate as-is; wrapping in usage() would bury the cause.
|
|
317
|
+
addCommand({ cwd: process.cwd(), entries: argv, logLabel: 'stasis' })
|
|
297
318
|
} else if (command === 'build') {
|
|
298
|
-
const flags = []
|
|
299
|
-
const valueFlags = new Set(['--output', '-o', '--format', '--platform', '--define', '--external', '--loader'])
|
|
300
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
301
|
-
flags.push(argv.shift())
|
|
302
|
-
}
|
|
303
319
|
const options = {
|
|
304
320
|
output: { type: 'string', short: 'o' },
|
|
305
321
|
format: { type: 'string' },
|
|
306
322
|
platform: { type: 'string' },
|
|
323
|
+
babel: { type: 'boolean' },
|
|
307
324
|
minify: { type: 'boolean' },
|
|
308
325
|
sourcemap: { type: 'boolean' },
|
|
309
|
-
// Repeatable esbuild passthroughs
|
|
310
|
-
// --external=PATTERN (one external each), and --loader=.ext:name (one extension
|
|
311
|
-
// loader override each). Each collects into an array.
|
|
326
|
+
// Repeatable esbuild passthroughs (--define=K=V, --external=PATTERN, --loader=.ext:name).
|
|
312
327
|
define: { type: 'string', multiple: true },
|
|
313
328
|
external: { type: 'string', multiple: true },
|
|
314
329
|
loader: { type: 'string', multiple: true },
|
|
315
330
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
usage(`Error: ${cause.message}`)
|
|
321
|
-
}
|
|
331
|
+
const values = parseLeadingOptions(argv, options, {
|
|
332
|
+
valueFlags: ['--output', '-o', '--format', '--platform', '--define', '--external', '--loader'],
|
|
333
|
+
onError: usage,
|
|
334
|
+
})
|
|
322
335
|
if (argv.length === 0) usage('Nothing to build: no bundle or lockfile given')
|
|
323
336
|
if (argv.length > 2) usage('Error: build takes a bundle or lockfile and an optional entry point')
|
|
324
337
|
// esbuild output knobs; the import graph itself is fixed by the artifact, not these.
|
|
325
338
|
if (values.format !== undefined && !['esm', 'cjs', 'iife'].includes(values.format)) {
|
|
326
339
|
usage('Error: --format must be esm, cjs, or iife')
|
|
327
340
|
}
|
|
328
|
-
if (values.platform !== undefined && !['node', 'browser', 'neutral'].includes(values.platform)) {
|
|
329
|
-
usage('Error: --platform must be node, browser, or
|
|
341
|
+
if (values.platform !== undefined && !['node', 'browser', 'neutral', 'hermes'].includes(values.platform)) {
|
|
342
|
+
usage('Error: --platform must be node, browser, neutral, or hermes')
|
|
343
|
+
}
|
|
344
|
+
// Hermes has no import/export; --format defaults to iife under --platform=hermes (see buildCommand).
|
|
345
|
+
if (values.platform === 'hermes' && values.format === 'esm') {
|
|
346
|
+
usage('Error: --platform=hermes cannot emit ESM; use --format=iife (the hermes default) or cjs')
|
|
330
347
|
}
|
|
331
348
|
if (!values.output) usage('Error: --output is required (a .js/.cjs/.mjs file, or a directory)')
|
|
332
|
-
//
|
|
333
|
-
// stream; without this it would silently create a directory literally named `-`.
|
|
349
|
+
// build can't stream to stdout; without this, --output=- would create a dir literally named `-`.
|
|
334
350
|
if (values.output === '-') usage('Error: stasis build cannot stream to stdout; --output must be a .js/.cjs/.mjs file or a directory')
|
|
335
|
-
// --define=KEY=VALUE -> esbuild
|
|
336
|
-
//
|
|
337
|
-
// identifier, ...): e.g. --define=process.env.NODE_ENV='"production"' or --define=DEBUG=false.
|
|
351
|
+
// --define=KEY=VALUE -> esbuild define map; VALUE is forwarded verbatim and must be valid JS
|
|
352
|
+
// (a JSON literal or an identifier), e.g. --define=DEBUG=false.
|
|
338
353
|
const define = {}
|
|
339
354
|
for (const entry of values.define ?? []) {
|
|
340
355
|
const eq = entry.indexOf('=')
|
|
341
356
|
if (eq <= 0) usage(`Error: --define must be KEY=VALUE (got '${entry}')`)
|
|
342
357
|
define[entry.slice(0, eq)] = entry.slice(eq + 1)
|
|
343
358
|
}
|
|
344
|
-
// --loader=.ext:NAME -> esbuild loader override for that extension
|
|
345
|
-
// --loader=.js:jsx so a .js file carrying JSX (the React Native convention) parses.
|
|
359
|
+
// --loader=.ext:NAME -> esbuild loader override for that extension (e.g. --loader=.js:jsx).
|
|
346
360
|
const loader = {}
|
|
347
361
|
for (const entry of values.loader ?? []) {
|
|
348
362
|
const colon = entry.indexOf(':')
|
|
@@ -357,6 +371,7 @@ if (command === '-v' || command === '--version') {
|
|
|
357
371
|
output: values.output,
|
|
358
372
|
format: values.format,
|
|
359
373
|
platform: values.platform,
|
|
374
|
+
babel: Boolean(values.babel),
|
|
360
375
|
minify: values.minify,
|
|
361
376
|
sourcemap: values.sourcemap,
|
|
362
377
|
define,
|
|
@@ -364,48 +379,28 @@ if (command === '-v' || command === '--version') {
|
|
|
364
379
|
loader,
|
|
365
380
|
})
|
|
366
381
|
} else if (command === 'extract') {
|
|
367
|
-
const flags = []
|
|
368
|
-
const valueFlags = new Set(['--output', '-o'])
|
|
369
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
370
|
-
flags.push(argv.shift())
|
|
371
|
-
}
|
|
372
382
|
const options = {
|
|
373
383
|
output: { type: 'string', short: 'o' },
|
|
374
384
|
}
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
({ values } = parseArgs({ args: flags, options }))
|
|
378
|
-
} catch (cause) {
|
|
379
|
-
usage(`Error: ${cause.message}`)
|
|
380
|
-
}
|
|
385
|
+
const values = parseLeadingOptions(argv, options, { valueFlags: ['--output', '-o'], onError: usage })
|
|
381
386
|
if (argv.length === 0) usage('Nothing to extract: no bundle file given')
|
|
382
387
|
if (argv.length > 1) usage('Error: extract takes exactly one bundle file')
|
|
383
|
-
const { extractCommand } = await import('
|
|
384
|
-
extractCommand({ cwd: process.cwd(), bundleFile: argv[0], output: values.output })
|
|
388
|
+
const { extractCommand } = await import('@exodus/stasis-core/extract')
|
|
389
|
+
extractCommand({ cwd: process.cwd(), bundleFile: argv[0], output: values.output, logLabel: 'stasis' })
|
|
385
390
|
} else if (command === 'diff') {
|
|
386
|
-
const flags = []
|
|
387
|
-
while (argv.length > 0 && argv[0].startsWith('-')) flags.push(argv.shift())
|
|
388
391
|
const options = {
|
|
389
392
|
stat: { type: 'boolean' },
|
|
390
393
|
imports: { type: 'boolean' },
|
|
391
394
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
} catch (cause) {
|
|
396
|
-
usage(`Error: ${cause.message}`)
|
|
397
|
-
}
|
|
398
|
-
// --stat is required until full text diffs land: it reports what changed
|
|
399
|
-
// (modules/files), not the differing content. Reserving bare `stasis diff`
|
|
400
|
-
// keeps the door open for a future content-level diff without a breaking flag.
|
|
395
|
+
const values = parseLeadingOptions(argv, options, { onError: usage })
|
|
396
|
+
// --stat is required for now (it reports what changed, not the content); bare `stasis diff`
|
|
397
|
+
// is reserved for a future content-level diff.
|
|
401
398
|
if (!values.stat) usage('Error: stasis diff currently requires --stat')
|
|
402
399
|
if (argv.length !== 2) usage('Error: stasis diff takes exactly two files (lockfile or bundle)')
|
|
403
|
-
// --imports
|
|
404
|
-
// added/removed/redirected); off by default since the edge list is verbose.
|
|
400
|
+
// --imports also diffs the resolution graphs (added/removed/redirected edges); off by default (verbose).
|
|
405
401
|
const { diffCommand } = await import('../src/cmd/diff.js')
|
|
406
402
|
const { differences } = diffCommand({ cwd: process.cwd(), left: argv[0], right: argv[1], stat: true, imports: values.imports })
|
|
407
|
-
// Exit non-zero when the artifacts differ, so
|
|
408
|
-
// (e.g. fail a build when a bundle drifts from its lockfile).
|
|
403
|
+
// Exit non-zero when the artifacts differ, so it composes in CI.
|
|
409
404
|
process.exitCode = differences ? 1 : 0
|
|
410
405
|
} else if (command === 'prune') {
|
|
411
406
|
if (argv.length > 1) usage('Error: prune takes at most one path argument')
|
|
@@ -414,28 +409,19 @@ if (command === '-v' || command === '--version') {
|
|
|
414
409
|
const { removed, validated } = prune({ root })
|
|
415
410
|
console.warn(`[stasis] prune: validated ${validated.length} file(s), removed ${removed.length} file(s)`)
|
|
416
411
|
} 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 })
|
|
417
413
|
if (argv.length === 0) usage('Nothing to audit: no path to file given')
|
|
418
414
|
const { audit, printAuditReport } = await import('../src/audit.js')
|
|
419
415
|
const files = argv.map((f) => resolve(f))
|
|
420
|
-
const report = await audit(files)
|
|
416
|
+
const report = await audit(files, { why: Boolean(values.why), whyDeep: Boolean(values['why-deep']), whyFull: Boolean(values['why-full']), reason: values.reason })
|
|
421
417
|
printAuditReport(report)
|
|
422
418
|
process.exitCode = report.rows.length === 0 ? 0 : 1
|
|
423
419
|
} else if (command === 'sbom') {
|
|
424
|
-
const flags = []
|
|
425
|
-
const valueFlags = new Set(['--format', '--output', '-o'])
|
|
426
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
427
|
-
flags.push(argv.shift())
|
|
428
|
-
}
|
|
429
420
|
const options = {
|
|
430
421
|
format: { type: 'string' },
|
|
431
422
|
output: { type: 'string', short: 'o' },
|
|
432
423
|
}
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
({ values } = parseArgs({ args: flags, options }))
|
|
436
|
-
} catch (cause) {
|
|
437
|
-
usage(`Error: ${cause.message}`)
|
|
438
|
-
}
|
|
424
|
+
const values = parseLeadingOptions(argv, options, { valueFlags: ['--format', '--output', '-o'], onError: usage })
|
|
439
425
|
if (!values.format) usage('Error: stasis sbom requires --format=(spdx|cyclonedx)')
|
|
440
426
|
if (!['spdx', 'cyclonedx'].includes(values.format)) usage('Error: --format must be spdx or cyclonedx')
|
|
441
427
|
if (argv.length === 0) usage('Nothing to export: no lockfile or bundle given')
|