@exodus/stasis 1.0.0-beta.0 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/bin/stasis.js +389 -214
- package/package.json +52 -12
- package/src/audit-corrections.js +96 -0
- package/src/audit.js +289 -64
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +14 -22
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +96 -128
- package/src/cmd/bundle.js +1121 -654
- package/src/cmd/diff.js +2 -19
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +7 -19
- package/src/diff.js +80 -129
- package/src/installed-commits.js +182 -0
- package/src/json-stream.js +283 -0
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +49 -116
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +375 -402
- package/src/loaders/rust.js +3254 -242
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -162
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +150 -0
- package/src/metro-transformer.js +3 -2
- package/src/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +370 -0
- package/src/resolve-node.js +419 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +120 -97
- package/src/scan.js +348 -180
- package/src/vfs-bundle/entries.js +144 -0
- package/src/vfs-bundle/github.js +202 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +647 -0
- package/src/vfs-bundle.js +80 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -44
- package/src/cmd/extract.js +0 -193
package/bin/stasis.js
CHANGED
|
@@ -1,12 +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
|
-
import { basename, dirname, resolve } from 'node:path'
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
8
7
|
import { existsSync, realpathSync } from 'node:fs'
|
|
8
|
+
import { homedir, constants as osConstants } from 'node:os'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
+
import { parseBrotliQuality, parseLeadingOptions, parseResourcesOption } from '@exodus/stasis-core/util'
|
|
10
11
|
import pkg from '../package.json' with { type: 'json' }
|
|
11
12
|
|
|
12
13
|
const argv = [...process.argv]
|
|
@@ -14,27 +15,37 @@ assert(['node', 'node.exe'].includes(basename(argv.shift())))
|
|
|
14
15
|
assert(['node', 'node.exe'].includes(basename(process.argv0)))
|
|
15
16
|
|
|
16
17
|
const jsname = argv.shift()
|
|
17
|
-
const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b)
|
|
18
|
+
const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b)
|
|
18
19
|
assert(basename(jsname) === 'stasis' || pathsEqual(jsname, fileURLToPath(import.meta.url)))
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stasis
|
|
23
|
-
stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--output=(path|-)] path/to/file.sol ...
|
|
24
|
-
stasis bundle [--output=(path|-)] path/to/file.
|
|
25
|
-
stasis bundle [--
|
|
26
|
-
stasis bundle --
|
|
27
|
-
stasis
|
|
28
|
-
stasis
|
|
29
|
-
(stasis bundle writes to stasis.code.br by default; --output=- streams to stdout)
|
|
30
|
-
stasis build --output=(dir|file.js) [--format=(esm|cjs|iife)] [--platform=(node|browser|neutral)] [--minify] [--sourcemap] [--define=K=V ...] [--external=pkg ...] [--loader=.ext:name ...] path/to/(stasis.code.br|stasis.lock.json) [entry]
|
|
21
|
+
const HELP = `Usage:
|
|
22
|
+
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 ...
|
|
23
|
+
stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--metro [--metro-resolver] --platforms=ios,android] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--brotli-quality=0..11] [--add] [--output=(path|-)] path/to/file.(js|ts|jsx|tsx) ...
|
|
24
|
+
stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--manifests] [--add] [--output=(path|-)] path/to/(file.sol|dir) ...
|
|
25
|
+
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 ...
|
|
26
|
+
stasis bundle [--add] [--output=(path|-)] path/to/file.(php|sh|bash) ...
|
|
27
|
+
stasis github-bundle --github=owner/name [--sha=commit|--tag=name] [--directory=path] [--package-manager=(pnpm|yarn1|npm|soldeer) [--package-manager-version=version]] [--generate=prisma] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] [stasis bundle's options for the entries] [path/in/repo/to/(file.(js|ts|jsx|tsx)|file.sol|dir) ...]
|
|
28
|
+
stasis add path/to/(file|dir) ...
|
|
29
|
+
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]
|
|
31
30
|
stasis extract [--output=path/to/dir] path/to/bundle.stasis.code.br
|
|
32
31
|
stasis diff --stat [--imports] path/to/(lockfile|bundle) path/to/(lockfile|bundle)
|
|
33
32
|
stasis prune [path/to/project]
|
|
34
|
-
stasis audit path/to/
|
|
33
|
+
stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] [--repo-advisories] path/to/(lockfile|bundle) ...
|
|
35
34
|
stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
stasis --version
|
|
36
|
+
|
|
37
|
+
Each command's options: https://github.com/PreventiveMeasures/stasis/blob/main/doc/<command>.md
|
|
38
|
+
(run, bundle, github-bundle, build, extract, diff, prune, audit, sbom; add is in bundle.md)`
|
|
39
|
+
|
|
40
|
+
// The help, on stderr. Exits 0 when asked for (--help), 1 when printed for want of a command.
|
|
41
|
+
function usage(asked = false) {
|
|
42
|
+
console.error(HELP)
|
|
43
|
+
process.exit(asked ? 0 : 1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A usage error: the message and where the help is, never the help itself.
|
|
47
|
+
function fail(message) {
|
|
48
|
+
console.error(`${message}\nRun 'stasis --help' for usage.`)
|
|
38
49
|
process.exit(1)
|
|
39
50
|
}
|
|
40
51
|
|
|
@@ -44,18 +55,22 @@ function setEnv(name, value) {
|
|
|
44
55
|
process.env[name] = value === undefined ? '' : value
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
// The per-user cache directory, as each platform names it (XDG ignores a relative path).
|
|
59
|
+
function userCacheDir() {
|
|
60
|
+
if (isAbsolute(process.env.XDG_CACHE_HOME ?? '')) return join(process.env.XDG_CACHE_HOME, 'stasis')
|
|
61
|
+
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Caches', 'stasis')
|
|
62
|
+
if (process.platform === 'win32') return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'stasis', 'Cache')
|
|
63
|
+
return join(homedir(), '.cache', 'stasis')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
const command = argv.shift()
|
|
48
67
|
|
|
49
68
|
if (command === '-v' || command === '--version') {
|
|
50
69
|
console.log(`v${pkg.version}`)
|
|
51
70
|
process.exit(0)
|
|
71
|
+
} else if (command === '-h' || command === '--help') {
|
|
72
|
+
usage(true)
|
|
52
73
|
} else if (command === 'run') {
|
|
53
|
-
const flags = []
|
|
54
|
-
const valueFlags = new Set(['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--resources'])
|
|
55
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
56
|
-
flags.push(argv.shift())
|
|
57
|
-
}
|
|
58
|
-
|
|
59
74
|
const options = {
|
|
60
75
|
lock: { type: 'string', default: 'none' },
|
|
61
76
|
bundle: { type: 'string', default: 'none' },
|
|
@@ -64,49 +79,65 @@ if (command === '-v' || command === '--version') {
|
|
|
64
79
|
debug: { type: 'boolean' },
|
|
65
80
|
dependencies: { type: 'boolean' },
|
|
66
81
|
'child-process': { type: 'boolean' },
|
|
82
|
+
'package-json': { type: 'boolean' },
|
|
67
83
|
mock: { type: 'boolean' },
|
|
84
|
+
import: { type: 'string', multiple: true },
|
|
68
85
|
fs: { type: 'string' },
|
|
69
86
|
resources: { type: 'string' },
|
|
87
|
+
'brotli-quality': { type: 'string' },
|
|
70
88
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (argv.length === 0) usage('Nothing to run: no path to file given')
|
|
79
|
-
if (!['none', 'ignore', 'add', 'replace', 'frozen'].includes(values.lock)) usage('Error: invalid --lock value')
|
|
89
|
+
const values = parseLeadingOptions(argv, options, {
|
|
90
|
+
valueFlags: ['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--import', '--resources', '--brotli-quality'],
|
|
91
|
+
onError: fail,
|
|
92
|
+
})
|
|
93
|
+
if (argv.length === 0) fail('Nothing to run: no path to file given')
|
|
94
|
+
if (!['none', 'ignore', 'add', 'replace', 'frozen'].includes(values.lock)) fail('Error: invalid --lock value')
|
|
80
95
|
const lock = values.lock
|
|
81
96
|
const scope = values.dependencies ? 'node_modules' : 'full'
|
|
82
97
|
const bundle = values.bundle
|
|
83
98
|
const bundleFile = values['bundle-file'] ? resolve(values['bundle-file']) : ''
|
|
84
99
|
const resourcesBundleFile = values['resources-bundle-file'] ? resolve(values['resources-bundle-file']) : ''
|
|
85
100
|
const debug = values.debug ? '1' : ''
|
|
86
|
-
if (!['none', 'ignore', 'add', 'replace', 'load', 'frozen'].includes(bundle))
|
|
87
|
-
if (bundleFile && bundle === 'none')
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
if (bundle === 'load'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// nothing to record into or read from without one of those bundle modes. The mode
|
|
98
|
-
// argument is required: `sync` patches the sync readers, `async` adds their async
|
|
99
|
-
// (callback + fs.promises) counterparts on top.
|
|
100
|
-
if (values.fs !== undefined && !['sync', 'async'].includes(values.fs)) usage("Error: --fs must be 'sync' or 'async'")
|
|
101
|
-
if (values.fs !== undefined && !['add', 'replace', 'load'].includes(bundle)) usage('Error: --fs requires --bundle=(add|replace|load)')
|
|
101
|
+
if (!['none', 'ignore', 'add', 'replace', 'load', 'frozen'].includes(bundle)) fail('Error: invalid --bundle value')
|
|
102
|
+
if (bundleFile && bundle === 'none') fail('Error: --bundle-file requires --bundle=(add|replace|load|frozen|ignore)')
|
|
103
|
+
// --resources-bundle-file needs an active bundle mode (unlike --bundle-file, inert under ignore).
|
|
104
|
+
if (resourcesBundleFile && (bundle === 'none' || bundle === 'ignore')) fail('Error: --resources-bundle-file requires --bundle=(add|replace|load|frozen)')
|
|
105
|
+
if (bundle === 'load' && lock !== 'frozen' && lock !== 'none' && lock !== 'ignore') fail('Error: --bundle=load is incompatible with --lock=(add|replace)')
|
|
106
|
+
if (lock === 'none' && bundle === 'none') fail('Error: stasis needs a lockfile or a bundle: set --lock or --bundle')
|
|
107
|
+
if (values.mock && bundle === 'load') fail('Error: --mock is for capturing imports while building a bundle; not compatible with --bundle=load')
|
|
108
|
+
// --fs requires a bundle mode (add|replace|load); `sync` patches the sync fs readers,
|
|
109
|
+
// `async` adds their callback + fs.promises counterparts on top.
|
|
110
|
+
if (values.fs !== undefined && !['sync', 'async'].includes(values.fs)) fail("Error: --fs must be 'sync' or 'async'")
|
|
111
|
+
if (values.fs !== undefined && !['add', 'replace', 'load'].includes(bundle)) fail('Error: --fs requires --bundle=(add|replace|load)')
|
|
102
112
|
const captureFs = values.fs ?? ''
|
|
103
|
-
// --resources: comma-separated extension/filename allowlist for `--fs`
|
|
104
|
-
// (e.g. png,svg,LICENSE). The child's Config validates each entry via parseResourcesOption.
|
|
113
|
+
// --resources: comma-separated extension/filename allowlist for `--fs` captures.
|
|
105
114
|
const resources = values.resources ?? ''
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
let brotliQuality
|
|
116
|
+
if (values['brotli-quality'] !== undefined) {
|
|
117
|
+
try {
|
|
118
|
+
brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
fail(`Error: ${cause.message}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// --import: extra preload module(s) passed through to the spawned node. Node resolves each
|
|
124
|
+
// against the project cwd; they ride AFTER stasis's own loader import.
|
|
125
|
+
const imports = values.import ?? []
|
|
126
|
+
if (imports.some((s) => s === '')) fail('Error: --import requires a module specifier (e.g. --import=./instrument.mjs)')
|
|
127
|
+
// Not a usage error: plain instrumentation/setup preloads still work under the mock; only ones
|
|
128
|
+
// that need the denied surfaces (spawning helper processes, network) die, and they do so loudly.
|
|
129
|
+
if (values.mock && imports.length > 0) {
|
|
130
|
+
console.warn("[stasis] Warning: --import preloads run under --mock's side-effect denials; a preload that spawns helper processes will fail there")
|
|
131
|
+
}
|
|
132
|
+
// --child-process: forward forked-child (e.g. Metro worker) capture to the root via per-pid shards.
|
|
108
133
|
const childProcess = values['child-process'] ? '1' : ''
|
|
109
|
-
|
|
134
|
+
// --package-json: auto-include every bundled module's package.json. Only meaningful while WRITING
|
|
135
|
+
// a bundle (add|replace) -- there's nothing to fold the manifests into otherwise.
|
|
136
|
+
if (values['package-json'] && bundle !== 'add' && bundle !== 'replace') {
|
|
137
|
+
fail('Error: --package-json requires --bundle=(add|replace)')
|
|
138
|
+
}
|
|
139
|
+
const packageJSON = values['package-json'] ? '1' : ''
|
|
140
|
+
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 }) })
|
|
110
141
|
if (debug) console.warn(`[stasis] Warning: stasis debug mode active`)
|
|
111
142
|
setEnv('EXODUS_STASIS_LOCK', lock)
|
|
112
143
|
setEnv('EXODUS_STASIS_SCOPE', scope)
|
|
@@ -115,210 +146,371 @@ if (command === '-v' || command === '--version') {
|
|
|
115
146
|
setEnv('EXODUS_STASIS_RESOURCES_BUNDLE_FILE', resourcesBundleFile)
|
|
116
147
|
setEnv('EXODUS_STASIS_DEBUG', debug)
|
|
117
148
|
setEnv('EXODUS_STASIS_CHILD_PROCESS', childProcess)
|
|
149
|
+
setEnv('EXODUS_STASIS_PACKAGE_JSON', packageJSON)
|
|
118
150
|
setEnv('EXODUS_STASIS_FS', captureFs)
|
|
119
151
|
setEnv('EXODUS_STASIS_RESOURCES', resources)
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// hooks lib first -- snapshotting stasis's real fs bindings -- then src/mock.js
|
|
127
|
-
// (whose syncBuiltinESMExports() refresh reaches user-code ESM imports), then
|
|
128
|
-
// installs the hooks. So the mock runs after stasis's snapshots and before
|
|
129
|
-
// registration, by static import order and with no env var. Reads stay open
|
|
130
|
-
// (--allow-fs-read=*) so node_modules resolution works; reads aren't a side
|
|
131
|
-
// effect. Writes are still scoped at the kernel level as defense in depth (the
|
|
132
|
-
// JS layer covers user code, --permission covers anything that bypasses JS,
|
|
133
|
-
// e.g. process.binding or a future leak).
|
|
152
|
+
// Only set when given; an unconditional setEnv('') would conflict with an ambient value.
|
|
153
|
+
if (brotliQuality !== undefined) setEnv('EXODUS_STASIS_BROTLI_QUALITY', String(brotliQuality))
|
|
154
|
+
// --mock: capture imports by running user code with side-effects denied (fail-closed).
|
|
155
|
+
// --permission blocks fs writes/child procs/workers/addons/inspector at the kernel level;
|
|
156
|
+
// src/mock.js neutralizes network + timers in JS (no --allow-* for those). Reads stay open
|
|
157
|
+
// so node_modules resolution works.
|
|
134
158
|
// Node 24 dropped comma-separated --allow-fs-write; repeat the flag instead.
|
|
135
159
|
const nodeArgs = []
|
|
136
160
|
if (values.mock) {
|
|
137
161
|
const writeAllow = [process.cwd()]
|
|
138
|
-
// --allow-fs-write paths must already exist on disk
|
|
139
|
-
//
|
|
140
|
-
// bundle target (code and/or split resources), walk up to the nearest existing ancestor and
|
|
141
|
-
// grant write there -- broader than ideal, but the user explicitly chose these locations.
|
|
142
|
-
// (Note: the cwd check uses '/' as a separator and is therefore unix-only; that matches the
|
|
143
|
-
// rest of the project's path handling.)
|
|
162
|
+
// --allow-fs-write paths must already exist on disk, so for each out-of-cwd bundle target
|
|
163
|
+
// walk up to the nearest existing ancestor and grant write there.
|
|
144
164
|
for (const [flag, file] of [['--bundle-file', bundleFile], ['--resources-bundle-file', resourcesBundleFile]]) {
|
|
145
165
|
if (!file || file.startsWith(`${process.cwd()}/`)) continue
|
|
146
166
|
let p = dirname(file)
|
|
147
167
|
while (!existsSync(p) && dirname(p) !== p) p = dirname(p)
|
|
148
|
-
// Refuse
|
|
149
|
-
// --permission layer everywhere, and reaching it means none of the path's ancestors exist --
|
|
150
|
-
// the user almost certainly didn't intend this. Ask them to create a parent.
|
|
168
|
+
// Refuse write access to the filesystem root -- it would disable the --permission layer everywhere.
|
|
151
169
|
if (p === dirname(p)) {
|
|
152
|
-
|
|
170
|
+
fail(`Error: no existing parent directory for ${flag}=${file}; create one first or choose a path under an existing directory`)
|
|
153
171
|
}
|
|
154
|
-
if (!writeAllow.includes(p)) writeAllow.push(p)
|
|
172
|
+
if (!writeAllow.includes(p)) writeAllow.push(p)
|
|
155
173
|
}
|
|
156
174
|
nodeArgs.push('--permission', '--allow-fs-read=*')
|
|
157
175
|
for (const p of writeAllow) nodeArgs.push(`--allow-fs-write=${p}`)
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// resolve and make the captured import map incompatible with a normal
|
|
161
|
-
// replay. Restore the condition so resolution matches a non-mock run; this
|
|
162
|
-
// doesn't allow native addons to actually load (--allow-addons still off).
|
|
176
|
+
// Restore the node-addons resolution condition (dropped by --permission) so the captured
|
|
177
|
+
// import map matches a non-mock run; native addons still can't load (--allow-addons off).
|
|
163
178
|
nodeArgs.push('--conditions=node-addons')
|
|
164
179
|
}
|
|
165
|
-
//
|
|
166
|
-
// entry, which composes the mock into the same hooks lib (see comment above).
|
|
180
|
+
// --mock uses stasis's loader-mock entry (composes the mock before installing the hooks).
|
|
167
181
|
const loaderEntry = values.mock ? '../src/loader-mock.js' : '@exodus/stasis-core/loader'
|
|
168
182
|
nodeArgs.push('--import', import.meta.resolve(loaderEntry))
|
|
183
|
+
// Passthrough preloads ride after the loader (and after --mock's denials), so they evaluate
|
|
184
|
+
// under its hooks; their module graphs pass through uncaptured unless the app graph reaches
|
|
185
|
+
// them -- then they're promoted into the capture (see core hooks.js).
|
|
186
|
+
for (const specifier of imports) nodeArgs.push('--import', specifier)
|
|
169
187
|
const child = spawn(process.execPath, [...nodeArgs, ...argv], { stdio: 'inherit' })
|
|
170
|
-
const [code] = await once(child, 'close')
|
|
171
|
-
|
|
188
|
+
const [code, signal] = await once(child, 'close')
|
|
189
|
+
// code is null when the child died from a signal; report 128+signo (shell convention) instead of the implicit 0
|
|
190
|
+
process.exitCode = code ?? 128 + (osConstants.signals[signal] ?? 0)
|
|
172
191
|
} else if (command === 'bundle') {
|
|
173
|
-
const flags = []
|
|
174
|
-
const valueFlags = new Set(['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '-o'])
|
|
175
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
176
|
-
flags.push(argv.shift())
|
|
177
|
-
}
|
|
178
192
|
const options = {
|
|
179
193
|
mapping: { type: 'string' },
|
|
194
|
+
manifests: { type: 'boolean' },
|
|
180
195
|
output: { type: 'string', short: 'o' },
|
|
181
196
|
scope: { type: 'string' },
|
|
182
197
|
lockfile: { type: 'string' },
|
|
183
198
|
conditions: { type: 'string' },
|
|
184
199
|
mainFields: { type: 'string' },
|
|
185
200
|
metro: { type: 'boolean' },
|
|
201
|
+
'metro-resolver': { type: 'boolean' },
|
|
186
202
|
platforms: { type: 'string', multiple: true },
|
|
203
|
+
jsx: { type: 'boolean' },
|
|
204
|
+
flow: { type: 'boolean' },
|
|
205
|
+
typescript: { type: 'boolean' },
|
|
206
|
+
tsconfig: { type: 'string' },
|
|
207
|
+
resources: { type: 'string' },
|
|
208
|
+
'package-json': { type: 'boolean' },
|
|
209
|
+
cargo: { type: 'boolean' },
|
|
210
|
+
'cargo-features': { type: 'string', multiple: true },
|
|
211
|
+
'cargo-no-default-features': { type: 'boolean' },
|
|
212
|
+
'cargo-all-features': { type: 'boolean' },
|
|
213
|
+
'cargo-target': { type: 'string' },
|
|
214
|
+
'cargo-manifests': { type: 'boolean' },
|
|
215
|
+
'brotli-quality': { type: 'string' },
|
|
216
|
+
add: { type: 'boolean' },
|
|
187
217
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
218
|
+
const values = parseLeadingOptions(argv, options, {
|
|
219
|
+
valueFlags: ['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--resources', '--tsconfig', '--cargo-features', '--cargo-target', '--brotli-quality', '-o'],
|
|
220
|
+
onError: fail,
|
|
221
|
+
})
|
|
222
|
+
if (argv.length === 0) fail('Nothing to bundle: no entry file given')
|
|
223
|
+
// A directory entry stands for the .sol files under it (Solidity only); an extensionless path
|
|
224
|
+
// that doesn't exist is a missing one (skipped with a warning).
|
|
225
|
+
const { directoryEntryError, isSolidityEntry } = await import('../src/cmd/bundle.js')
|
|
226
|
+
const dirError = directoryEntryError(argv)
|
|
227
|
+
if (dirError !== null) fail(`Error: ${dirError}`)
|
|
228
|
+
const allSol = argv.every((f) => isSolidityEntry(f))
|
|
196
229
|
const allPhp = argv.every((f) => f.endsWith('.php'))
|
|
197
|
-
const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts)$/u.test(f))
|
|
230
|
+
const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts|jsx|tsx)$/u.test(f))
|
|
198
231
|
const allBash = argv.every((f) => /\.(?:sh|bash)$/u.test(f))
|
|
199
232
|
const allRust = argv.every((f) => f.endsWith('.rs'))
|
|
200
233
|
if (!allSol && !allPhp && !allJs && !allBash && !allRust) {
|
|
201
|
-
|
|
234
|
+
fail('Error: bundle entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts/.jsx/.tsx, all be .sh/.bash, or all be .rs')
|
|
202
235
|
}
|
|
203
|
-
if (values.mapping && !allSol)
|
|
204
|
-
|
|
236
|
+
if (values.mapping && !allSol) fail('Error: --mapping is only valid for .sol bundles')
|
|
237
|
+
// --manifests: carry the Solidity build's description files (foundry.toml, remappings.txt, ...).
|
|
238
|
+
const manifests = Boolean(values.manifests)
|
|
239
|
+
if (manifests && !allSol) fail('Error: --manifests is only valid for .sol bundles')
|
|
240
|
+
// --cargo: take the Rust feature/dependency resolution from `cargo metadata` (runs cargo; opt-in).
|
|
241
|
+
const cargo = Boolean(values.cargo)
|
|
242
|
+
if (cargo && !allRust) fail('Error: --cargo is only valid for Rust bundles')
|
|
243
|
+
// --cargo-features / --cargo-no-default-features / --cargo-all-features: cargo's own feature flags
|
|
244
|
+
// for the entries' packages (`pkg/feat` targets one, or a dependency). --cargo-features is
|
|
245
|
+
// repeatable and/or comma-separated; parseFeatureList is the one splitter (the resolver reuses it).
|
|
246
|
+
const { parseFeatureList } = await import('../src/loaders/cargo.js')
|
|
247
|
+
const cargoFeatures = parseFeatureList(values['cargo-features'] ?? [])
|
|
248
|
+
if (values['cargo-features'] !== undefined && cargoFeatures.length === 0) {
|
|
249
|
+
fail('Error: --cargo-features must list at least one feature (e.g. --cargo-features=serde,app/tls)')
|
|
250
|
+
}
|
|
251
|
+
const cargoNoDefaultFeatures = Boolean(values['cargo-no-default-features'])
|
|
252
|
+
const cargoAllFeatures = Boolean(values['cargo-all-features'])
|
|
253
|
+
// --cargo-target=<triple|host>: the build's target, whose cfgs (from `rustc --print cfg`) decide
|
|
254
|
+
// `#[cfg(unix)]`-style code. A triple is letters, digits, `-`, `_` and `.` (`x86_64-unknown-linux-gnu`).
|
|
255
|
+
const cargoTarget = values['cargo-target'] ?? null
|
|
256
|
+
if (cargoTarget !== null && !/^[\w.-]+$/u.test(cargoTarget)) {
|
|
257
|
+
fail('Error: --cargo-target must be a target triple or "host" (e.g. --cargo-target=aarch64-apple-darwin)')
|
|
258
|
+
}
|
|
259
|
+
// --cargo-manifests: carry each bundled package's Cargo.toml and build script, the workspace
|
|
260
|
+
// Cargo.toml, Cargo.lock and .cargo/config.toml too (the Rust counterpart of --package-json).
|
|
261
|
+
const cargoManifests = Boolean(values['cargo-manifests'])
|
|
262
|
+
if (!allRust) {
|
|
263
|
+
const given = { 'cargo-features': cargoFeatures.length > 0, 'cargo-no-default-features': cargoNoDefaultFeatures, 'cargo-all-features': cargoAllFeatures, 'cargo-target': cargoTarget !== null, 'cargo-manifests': cargoManifests }
|
|
264
|
+
for (const [flag, on] of Object.entries(given)) if (on) fail(`Error: --${flag} is only valid for Rust bundles`)
|
|
265
|
+
}
|
|
266
|
+
if (values.scope && !allJs) fail('Error: --scope is only valid for JS bundles')
|
|
205
267
|
if (values.scope && !['node_modules', 'full'].includes(values.scope)) {
|
|
206
|
-
|
|
268
|
+
fail('Error: --scope must be node_modules or full')
|
|
207
269
|
}
|
|
208
|
-
if (values.lockfile && !allJs)
|
|
209
|
-
// --
|
|
210
|
-
// emits a full-scope bundle, so the two together would silently ignore --scope.
|
|
270
|
+
if (values.lockfile && !allJs) fail('Error: --lockfile is only valid for JS bundles')
|
|
271
|
+
// --mainFields/--metro always emit a full-scope bundle, so they'd silently ignore --scope.
|
|
211
272
|
if (values.scope !== undefined && (values.mainFields !== undefined || values.metro)) {
|
|
212
|
-
|
|
273
|
+
fail('Error: --scope is not supported with --mainFields or --metro')
|
|
213
274
|
}
|
|
214
|
-
// --conditions:
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
// follows. NOTE: conditions alone do not honour legacy package `mainFields` or
|
|
218
|
-
// platform-specific file suffixes -- see --mainFields.
|
|
219
|
-
if (values.conditions !== undefined && !allJs) usage('Error: --conditions is only valid for JS bundles')
|
|
275
|
+
// --conditions: extra exports/imports resolution conditions merged onto Node's defaults;
|
|
276
|
+
// they don't honour legacy `mainFields` or platform suffixes (see --mainFields).
|
|
277
|
+
if (values.conditions !== undefined && !allJs) fail('Error: --conditions is only valid for JS bundles')
|
|
220
278
|
const conditions = values.conditions === undefined
|
|
221
279
|
? []
|
|
222
280
|
: values.conditions.split(',').map((s) => s.trim()).filter(Boolean)
|
|
223
281
|
if (values.conditions !== undefined && conditions.length === 0) {
|
|
224
|
-
|
|
282
|
+
fail('Error: --conditions must list at least one condition name (e.g. --conditions=react-native,browser)')
|
|
225
283
|
}
|
|
226
|
-
// --
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
284
|
+
// --flow: strip Flow type syntax (via the optional flow-remove-types dep) before parsing; JS-only.
|
|
285
|
+
if (values.flow && !allJs) fail('Error: --flow is only valid for JS bundles')
|
|
286
|
+
const flow = Boolean(values.flow)
|
|
287
|
+
// --typescript: resolve like tsc -- a specifier naming a missing JS output (./x.js, .mjs, .cjs,
|
|
288
|
+
// extensionless) lands on its on-disk TS source (./x.ts, .mts, .cts); an existing .js always wins.
|
|
289
|
+
if (values.typescript && !allJs) fail('Error: --typescript is only valid for JS bundles')
|
|
290
|
+
const typescript = Boolean(values.typescript)
|
|
291
|
+
// --tsconfig: the config whose compilerOptions.paths aliases --typescript honours (default: the
|
|
292
|
+
// tsconfig.json beside each importing file's package.json). Meaningless without --typescript.
|
|
293
|
+
if (values.tsconfig !== undefined && !typescript) fail('Error: --tsconfig is only valid with --typescript')
|
|
294
|
+
// --mainFields: legacy package entry fields (e.g. react-native,browser,main) for the non-exports resolver.
|
|
295
|
+
if (values.mainFields !== undefined && !allJs) fail('Error: --mainFields is only valid for JS bundles')
|
|
230
296
|
const mainFields = values.mainFields === undefined
|
|
231
297
|
? undefined
|
|
232
298
|
: values.mainFields.split(',').map((s) => s.trim()).filter(Boolean)
|
|
233
299
|
if (values.mainFields !== undefined && mainFields.length === 0) {
|
|
234
|
-
|
|
300
|
+
fail('Error: --mainFields must list at least one field (e.g. --mainFields=react-native,browser,main)')
|
|
235
301
|
}
|
|
236
|
-
// --metro: resolve like Metro/
|
|
237
|
-
//
|
|
238
|
-
// (.ios/.android/.native) for each --platforms target, bundling all of them at once.
|
|
239
|
-
// --platforms is repeatable and/or comma-separated; the values union.
|
|
302
|
+
// --metro: resolve like Metro/RN (its own conditions + mainFields, platform suffixes per
|
|
303
|
+
// --platforms target). --platforms is repeatable and/or comma-separated; the values union.
|
|
240
304
|
const metro = Boolean(values.metro)
|
|
305
|
+
// --metro-resolver: resolve through the project's own metro-resolver instead of the built-in
|
|
306
|
+
// approximation. Only meaningful alongside --metro (enforced below and in classifyEntries).
|
|
307
|
+
const metroResolver = Boolean(values['metro-resolver'])
|
|
241
308
|
const platforms = [...new Set((values.platforms ?? []).flatMap((p) => p.split(',')).map((s) => s.trim()).filter(Boolean))]
|
|
242
|
-
// A platform name becomes
|
|
243
|
-
// contains '/'; '*' is the reserved private placeholder. Reject both here so we never
|
|
244
|
-
// emit a bundle the readers can't parse (the divergent-edge case is data-dependent).
|
|
309
|
+
// A platform name becomes an edge key: reject '/' (parsers reject it) and '*' (reserved placeholder).
|
|
245
310
|
for (const p of platforms) {
|
|
246
|
-
if (p === '*' || p.includes('/'))
|
|
311
|
+
if (p === '*' || p.includes('/')) fail(`Error: invalid --platforms value '${p}' (a platform name can't contain '/' or be '*')`)
|
|
312
|
+
}
|
|
313
|
+
if ((metro || platforms.length > 0) && !allJs) fail('Error: --metro is only valid for JS bundles')
|
|
314
|
+
// --jsx: parse React Native's JSX-in-.js source in the static scanner (off by default; oxc,
|
|
315
|
+
// like tsc, auto-enables JSX for .jsx/.tsx only). Orthogonal to the resolver, so it pairs with
|
|
316
|
+
// plain/--mainFields/--metro alike -- JS-only, since no other entry language is scanned.
|
|
317
|
+
const jsx = Boolean(values.jsx)
|
|
318
|
+
if (jsx && !allJs) fail('Error: --jsx is only valid for JS bundles')
|
|
319
|
+
// --package-json: fold each bundled module's package.json into the bundle. JS-only (only JS
|
|
320
|
+
// modules carry an npm package.json; the sol/php/bash/rust bucketizers have no such manifest).
|
|
321
|
+
const packageJSON = Boolean(values['package-json'])
|
|
322
|
+
if (packageJSON && !allJs) fail('Error: --package-json is only valid for JS bundles')
|
|
323
|
+
// --resources: comma-separated extension/filename allowlist for assets reached through the graph
|
|
324
|
+
// (e.g. --resources=png,svg). They're carried as resources instead of failing "can't carry" --
|
|
325
|
+
// for graphs that aren't fully loadable in JS (Metro consumes such assets). JS-only.
|
|
326
|
+
if (values.resources !== undefined && !allJs) fail('Error: --resources is only valid for JS bundles')
|
|
327
|
+
const resources = values.resources === undefined
|
|
328
|
+
? []
|
|
329
|
+
: values.resources.split(',').map((s) => s.trim()).filter(Boolean)
|
|
330
|
+
if (values.resources !== undefined && resources.length === 0) {
|
|
331
|
+
fail('Error: --resources must list at least one extension or filename (e.g. --resources=png,svg)')
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
parseResourcesOption('--resources', resources) // validate early for a clean usage error (code exts / bad chars rejected)
|
|
335
|
+
} catch (cause) {
|
|
336
|
+
fail(`Error: ${cause.message}`)
|
|
247
337
|
}
|
|
248
|
-
if ((metro || platforms.length > 0) && !allJs) usage('Error: --metro is only valid for JS bundles')
|
|
249
338
|
if (metro) {
|
|
250
|
-
if (conditions.length > 0)
|
|
251
|
-
if (mainFields !== undefined)
|
|
252
|
-
if (platforms.length === 0)
|
|
339
|
+
if (conditions.length > 0) fail("Error: --conditions can't be combined with --metro (it sets its own conditions)")
|
|
340
|
+
if (mainFields !== undefined) fail("Error: --mainFields can't be combined with --metro (it sets its own mainFields)")
|
|
341
|
+
if (platforms.length === 0) fail('Error: --metro requires --platforms (e.g. --platforms=ios,android)')
|
|
253
342
|
} else if (platforms.length > 0) {
|
|
254
|
-
|
|
343
|
+
fail('Error: --platforms is only valid with --metro')
|
|
255
344
|
}
|
|
345
|
+
if (metroResolver && !metro) fail('Error: --metro-resolver is only valid with --metro')
|
|
346
|
+
// metro-resolver can't substitute .js -> .ts, so --typescript would silently not apply -- reject it.
|
|
347
|
+
if (typescript && metroResolver) fail("Error: --typescript is not supported with --metro-resolver (the project's metro-resolver doesn't substitute .js -> .ts)")
|
|
348
|
+
// --brotli-quality: valid for every entry language (no allJs gate).
|
|
349
|
+
let brotliQuality
|
|
350
|
+
if (values['brotli-quality'] !== undefined) {
|
|
351
|
+
try {
|
|
352
|
+
brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
|
|
353
|
+
} catch (cause) {
|
|
354
|
+
fail(`Error: ${cause.message}`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// --add merges into the existing bundle at --output, so it can't target write-only stdout.
|
|
358
|
+
const add = Boolean(values.add)
|
|
359
|
+
if (add && values.output === '-') fail('Error: --add cannot be combined with --output=-')
|
|
256
360
|
const { bundleCommand } = await import('../src/cmd/bundle.js')
|
|
257
361
|
await bundleCommand({
|
|
258
362
|
cwd: process.cwd(),
|
|
259
363
|
entries: argv,
|
|
260
364
|
mappingFile: values.mapping,
|
|
365
|
+
manifests,
|
|
261
366
|
output: values.output,
|
|
262
367
|
scope: values.scope,
|
|
263
368
|
lockfile: values.lockfile,
|
|
264
369
|
conditions,
|
|
265
370
|
mainFields,
|
|
266
371
|
metro,
|
|
372
|
+
metroResolver,
|
|
267
373
|
platforms,
|
|
374
|
+
jsx,
|
|
375
|
+
flow,
|
|
376
|
+
typescript,
|
|
377
|
+
tsconfig: values.tsconfig,
|
|
378
|
+
resources,
|
|
379
|
+
packageJSON,
|
|
380
|
+
cargo,
|
|
381
|
+
cargoFeatures,
|
|
382
|
+
cargoNoDefaultFeatures,
|
|
383
|
+
cargoAllFeatures,
|
|
384
|
+
cargoTarget,
|
|
385
|
+
cargoManifests,
|
|
386
|
+
brotliQuality,
|
|
387
|
+
add,
|
|
268
388
|
})
|
|
269
|
-
} else if (command === '
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
389
|
+
} else if (command === 'github-bundle') {
|
|
390
|
+
const values = parseLeadingOptions(argv, {
|
|
391
|
+
github: { type: 'string' },
|
|
392
|
+
sha: { type: 'string' },
|
|
393
|
+
tag: { type: 'string' },
|
|
394
|
+
directory: { type: 'string' },
|
|
395
|
+
'package-manager': { type: 'string' },
|
|
396
|
+
'package-manager-version': { type: 'string' },
|
|
397
|
+
mapping: { type: 'string' },
|
|
398
|
+
manifests: { type: 'boolean' },
|
|
399
|
+
output: { type: 'string', short: 'o' },
|
|
400
|
+
scope: { type: 'string' },
|
|
401
|
+
lockfile: { type: 'string' },
|
|
402
|
+
conditions: { type: 'string' },
|
|
403
|
+
mainFields: { type: 'string' },
|
|
404
|
+
metro: { type: 'boolean' },
|
|
405
|
+
platforms: { type: 'string', multiple: true },
|
|
406
|
+
jsx: { type: 'boolean' },
|
|
407
|
+
flow: { type: 'boolean' },
|
|
408
|
+
typescript: { type: 'boolean' },
|
|
409
|
+
tsconfig: { type: 'string' },
|
|
410
|
+
resources: { type: 'string' },
|
|
411
|
+
'package-json': { type: 'boolean' },
|
|
412
|
+
'brotli-quality': { type: 'string' },
|
|
413
|
+
generate: { type: 'string', multiple: true },
|
|
414
|
+
}, {
|
|
415
|
+
valueFlags: ['--github', '--sha', '--tag', '--directory', '--package-manager', '--package-manager-version', '--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--tsconfig', '--resources', '--brotli-quality', '--generate', '-o'],
|
|
416
|
+
onError: fail,
|
|
417
|
+
})
|
|
418
|
+
if (values.github === undefined) fail('Error: github-bundle requires --github=owner/name, the repo to bundle')
|
|
419
|
+
if (values.sha !== undefined && values.tag !== undefined) fail('Error: github-bundle takes --sha or --tag, not both')
|
|
420
|
+
// Whether each option applies to the entries is buildGitHubBundle's to say.
|
|
421
|
+
const list = (value) => [value ?? []].flat().flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean)
|
|
422
|
+
let brotliQuality
|
|
423
|
+
if (values['brotli-quality'] !== undefined) {
|
|
424
|
+
try {
|
|
425
|
+
brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
|
|
426
|
+
} catch (cause) {
|
|
427
|
+
fail(`Error: ${cause.message}`)
|
|
428
|
+
}
|
|
274
429
|
}
|
|
430
|
+
// Tarballs, zips and GitHub trees are cached where `stasis audit` caches.
|
|
431
|
+
const { setCacheDir } = await import('../src/vfs-bundle.js')
|
|
432
|
+
setCacheDir(userCacheDir())
|
|
433
|
+
const { githubBundleCommand } = await import('../src/cmd/github-bundle.js')
|
|
434
|
+
await githubBundleCommand({
|
|
435
|
+
github: values.github,
|
|
436
|
+
sha: values.sha,
|
|
437
|
+
tag: values.tag,
|
|
438
|
+
directory: values.directory,
|
|
439
|
+
packageManager: values['package-manager'],
|
|
440
|
+
packageManagerVersion: values['package-manager-version'],
|
|
441
|
+
entries: argv.length === 0 ? undefined : argv,
|
|
442
|
+
mappingFile: values.mapping,
|
|
443
|
+
manifests: Boolean(values.manifests),
|
|
444
|
+
output: values.output,
|
|
445
|
+
lockfile: values.lockfile,
|
|
446
|
+
scope: values.scope,
|
|
447
|
+
conditions: list(values.conditions),
|
|
448
|
+
mainFields: values.mainFields === undefined ? undefined : list(values.mainFields),
|
|
449
|
+
metro: Boolean(values.metro),
|
|
450
|
+
platforms: list(values.platforms),
|
|
451
|
+
jsx: Boolean(values.jsx),
|
|
452
|
+
flow: Boolean(values.flow),
|
|
453
|
+
typescript: Boolean(values.typescript),
|
|
454
|
+
tsconfig: values.tsconfig,
|
|
455
|
+
resources: list(values.resources),
|
|
456
|
+
packageJSON: Boolean(values['package-json']),
|
|
457
|
+
generate: list(values.generate),
|
|
458
|
+
brotliQuality,
|
|
459
|
+
})
|
|
460
|
+
} else if (command === 'add') {
|
|
461
|
+
// add packs the listed files (no resolver), taking split targets + resource allowlist from
|
|
462
|
+
// stasis.config.json; it takes no flags.
|
|
463
|
+
if (argv.length === 0) fail('Nothing to add: no file given')
|
|
464
|
+
if (argv.some((a) => a.startsWith('-'))) fail('Error: add takes no options; its targets and resource allowlist come from stasis.config.json')
|
|
465
|
+
const { addCommand } = await import('@exodus/stasis-core/add')
|
|
466
|
+
// Let addCommand's runtime errors propagate as-is; wrapping in fail() would bury the cause.
|
|
467
|
+
addCommand({ cwd: process.cwd(), entries: argv, logLabel: 'stasis' })
|
|
468
|
+
} else if (command === 'build') {
|
|
275
469
|
const options = {
|
|
276
470
|
output: { type: 'string', short: 'o' },
|
|
277
471
|
format: { type: 'string' },
|
|
278
472
|
platform: { type: 'string' },
|
|
473
|
+
babel: { type: 'boolean' },
|
|
279
474
|
minify: { type: 'boolean' },
|
|
280
475
|
sourcemap: { type: 'boolean' },
|
|
281
|
-
// Repeatable esbuild passthroughs
|
|
282
|
-
// --external=PATTERN (one external each), and --loader=.ext:name (one extension
|
|
283
|
-
// loader override each). Each collects into an array.
|
|
476
|
+
// Repeatable esbuild passthroughs (--define=K=V, --external=PATTERN, --loader=.ext:name).
|
|
284
477
|
define: { type: 'string', multiple: true },
|
|
285
478
|
external: { type: 'string', multiple: true },
|
|
286
479
|
loader: { type: 'string', multiple: true },
|
|
287
480
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (argv.length === 0) usage('Nothing to build: no bundle or lockfile given')
|
|
295
|
-
if (argv.length > 2) usage('Error: build takes a bundle or lockfile and an optional entry point')
|
|
481
|
+
const values = parseLeadingOptions(argv, options, {
|
|
482
|
+
valueFlags: ['--output', '-o', '--format', '--platform', '--define', '--external', '--loader'],
|
|
483
|
+
onError: fail,
|
|
484
|
+
})
|
|
485
|
+
if (argv.length === 0) fail('Nothing to build: no bundle or lockfile given')
|
|
486
|
+
if (argv.length > 2) fail('Error: build takes a bundle or lockfile and an optional entry point')
|
|
296
487
|
// esbuild output knobs; the import graph itself is fixed by the artifact, not these.
|
|
297
488
|
if (values.format !== undefined && !['esm', 'cjs', 'iife'].includes(values.format)) {
|
|
298
|
-
|
|
489
|
+
fail('Error: --format must be esm, cjs, or iife')
|
|
299
490
|
}
|
|
300
|
-
if (values.platform !== undefined && !['node', 'browser', 'neutral'].includes(values.platform)) {
|
|
301
|
-
|
|
491
|
+
if (values.platform !== undefined && !['node', 'browser', 'neutral', 'hermes'].includes(values.platform)) {
|
|
492
|
+
fail('Error: --platform must be node, browser, neutral, or hermes')
|
|
302
493
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
494
|
+
// Hermes has no import/export; --format defaults to iife under --platform=hermes (see buildCommand).
|
|
495
|
+
if (values.platform === 'hermes' && values.format === 'esm') {
|
|
496
|
+
fail('Error: --platform=hermes cannot emit ESM; use --format=iife (the hermes default) or cjs')
|
|
497
|
+
}
|
|
498
|
+
if (!values.output) fail('Error: --output is required (a .js/.cjs/.mjs file, or a directory)')
|
|
499
|
+
// build can't stream to stdout; without this, --output=- would create a dir literally named `-`.
|
|
500
|
+
if (values.output === '-') fail('Error: stasis build cannot stream to stdout; --output must be a .js/.cjs/.mjs file or a directory')
|
|
501
|
+
// --define=KEY=VALUE -> esbuild define map; VALUE is forwarded verbatim and must be valid JS
|
|
502
|
+
// (a JSON literal or an identifier), e.g. --define=DEBUG=false.
|
|
310
503
|
const define = {}
|
|
311
504
|
for (const entry of values.define ?? []) {
|
|
312
505
|
const eq = entry.indexOf('=')
|
|
313
|
-
if (eq <= 0)
|
|
506
|
+
if (eq <= 0) fail(`Error: --define must be KEY=VALUE (got '${entry}')`)
|
|
314
507
|
define[entry.slice(0, eq)] = entry.slice(eq + 1)
|
|
315
508
|
}
|
|
316
|
-
// --loader=.ext:NAME -> esbuild loader override for that extension
|
|
317
|
-
// --loader=.js:jsx so a .js file carrying JSX (the React Native convention) parses.
|
|
509
|
+
// --loader=.ext:NAME -> esbuild loader override for that extension (e.g. --loader=.js:jsx).
|
|
318
510
|
const loader = {}
|
|
319
511
|
for (const entry of values.loader ?? []) {
|
|
320
512
|
const colon = entry.indexOf(':')
|
|
321
|
-
if (colon <= 0 || !entry.startsWith('.'))
|
|
513
|
+
if (colon <= 0 || !entry.startsWith('.')) fail(`Error: --loader must be .EXT:LOADER (got '${entry}')`)
|
|
322
514
|
loader[entry.slice(0, colon)] = entry.slice(colon + 1)
|
|
323
515
|
}
|
|
324
516
|
const { buildCommand } = await import('../src/cmd/build.js')
|
|
@@ -329,6 +521,7 @@ if (command === '-v' || command === '--version') {
|
|
|
329
521
|
output: values.output,
|
|
330
522
|
format: values.format,
|
|
331
523
|
platform: values.platform,
|
|
524
|
+
babel: Boolean(values.babel),
|
|
332
525
|
minify: values.minify,
|
|
333
526
|
sourcemap: values.sourcemap,
|
|
334
527
|
define,
|
|
@@ -336,81 +529,61 @@ if (command === '-v' || command === '--version') {
|
|
|
336
529
|
loader,
|
|
337
530
|
})
|
|
338
531
|
} else if (command === 'extract') {
|
|
339
|
-
const flags = []
|
|
340
|
-
const valueFlags = new Set(['--output', '-o'])
|
|
341
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
342
|
-
flags.push(argv.shift())
|
|
343
|
-
}
|
|
344
532
|
const options = {
|
|
345
533
|
output: { type: 'string', short: 'o' },
|
|
346
534
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
if (argv.length === 0) usage('Nothing to extract: no bundle file given')
|
|
354
|
-
if (argv.length > 1) usage('Error: extract takes exactly one bundle file')
|
|
355
|
-
const { extractCommand } = await import('../src/cmd/extract.js')
|
|
356
|
-
extractCommand({ cwd: process.cwd(), bundleFile: argv[0], output: values.output })
|
|
535
|
+
const values = parseLeadingOptions(argv, options, { valueFlags: ['--output', '-o'], onError: fail })
|
|
536
|
+
if (argv.length === 0) fail('Nothing to extract: no bundle file given')
|
|
537
|
+
if (argv.length > 1) fail('Error: extract takes exactly one bundle file')
|
|
538
|
+
const { extractCommand } = await import('@exodus/stasis-core/extract')
|
|
539
|
+
extractCommand({ cwd: process.cwd(), bundleFile: argv[0], output: values.output, logLabel: 'stasis' })
|
|
357
540
|
} else if (command === 'diff') {
|
|
358
|
-
const flags = []
|
|
359
|
-
while (argv.length > 0 && argv[0].startsWith('-')) flags.push(argv.shift())
|
|
360
541
|
const options = {
|
|
361
542
|
stat: { type: 'boolean' },
|
|
362
543
|
imports: { type: 'boolean' },
|
|
363
544
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// --stat is required until full text diffs land: it reports what changed
|
|
371
|
-
// (modules/files), not the differing content. Reserving bare `stasis diff`
|
|
372
|
-
// keeps the door open for a future content-level diff without a breaking flag.
|
|
373
|
-
if (!values.stat) usage('Error: stasis diff currently requires --stat')
|
|
374
|
-
if (argv.length !== 2) usage('Error: stasis diff takes exactly two files (lockfile or bundle)')
|
|
375
|
-
// --imports additionally diffs the resolution graphs (which import edges were
|
|
376
|
-
// added/removed/redirected); off by default since the edge list is verbose.
|
|
545
|
+
const values = parseLeadingOptions(argv, options, { onError: fail })
|
|
546
|
+
// --stat is required for now (it reports what changed, not the content); bare `stasis diff`
|
|
547
|
+
// is reserved for a future content-level diff.
|
|
548
|
+
if (!values.stat) fail('Error: stasis diff currently requires --stat')
|
|
549
|
+
if (argv.length !== 2) fail('Error: stasis diff takes exactly two files (lockfile or bundle)')
|
|
550
|
+
// --imports also diffs the resolution graphs (added/removed/redirected edges); off by default (verbose).
|
|
377
551
|
const { diffCommand } = await import('../src/cmd/diff.js')
|
|
378
552
|
const { differences } = diffCommand({ cwd: process.cwd(), left: argv[0], right: argv[1], stat: true, imports: values.imports })
|
|
379
|
-
// Exit non-zero when the artifacts differ, so
|
|
380
|
-
// (e.g. fail a build when a bundle drifts from its lockfile).
|
|
553
|
+
// Exit non-zero when the artifacts differ, so it composes in CI.
|
|
381
554
|
process.exitCode = differences ? 1 : 0
|
|
382
555
|
} else if (command === 'prune') {
|
|
383
|
-
if (argv.length > 1)
|
|
556
|
+
if (argv.length > 1) fail('Error: prune takes at most one path argument')
|
|
384
557
|
const root = argv[0] ? resolve(argv[0]) : process.cwd()
|
|
385
558
|
const { prune } = await import('@exodus/stasis-core/prune')
|
|
386
559
|
const { removed, validated } = prune({ root })
|
|
387
560
|
console.warn(`[stasis] prune: validated ${validated.length} file(s), removed ${removed.length} file(s)`)
|
|
388
561
|
} else if (command === 'audit') {
|
|
389
|
-
|
|
562
|
+
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: fail })
|
|
563
|
+
if (argv.length === 0) fail('Nothing to audit: no path to file given')
|
|
564
|
+
const repoAdvisories = Boolean(values['repo-advisories'])
|
|
565
|
+
if (repoAdvisories && !process.env.GITHUB_TOKEN) fail('Error: --repo-advisories requires a GitHub token in GITHUB_TOKEN')
|
|
566
|
+
// A Soldeer package's and a GitHub repo's advisories are their repository's, asked of GitHub with
|
|
567
|
+
// GITHUB_TOKEN where it is set; the client asks nothing for any other package by itself.
|
|
568
|
+
const github = (await import('@preventive/upstream/github.js')).createClient({ token: process.env.GITHUB_TOKEN || null })
|
|
569
|
+
// Upstream caches what it looks up (a package's GitHub repo, a month each) only where told to, and
|
|
570
|
+
// answers from it without asking: a per-user directory, never one shared with less trusted jobs.
|
|
571
|
+
const { setCacheDir } = await import('@preventive/upstream/npm.js')
|
|
572
|
+
setCacheDir(userCacheDir())
|
|
390
573
|
const { audit, printAuditReport } = await import('../src/audit.js')
|
|
391
574
|
const files = argv.map((f) => resolve(f))
|
|
392
|
-
const report = await audit(files)
|
|
575
|
+
const report = await audit(files, { why: Boolean(values.why), whyDeep: Boolean(values['why-deep']), whyFull: Boolean(values['why-full']), reason: values.reason, repoAdvisories, github })
|
|
393
576
|
printAuditReport(report)
|
|
394
577
|
process.exitCode = report.rows.length === 0 ? 0 : 1
|
|
395
578
|
} else if (command === 'sbom') {
|
|
396
|
-
const flags = []
|
|
397
|
-
const valueFlags = new Set(['--format', '--output', '-o'])
|
|
398
|
-
while (argv.length > 0 && (argv[0].startsWith('-') || valueFlags.has(flags.at(-1)))) {
|
|
399
|
-
flags.push(argv.shift())
|
|
400
|
-
}
|
|
401
579
|
const options = {
|
|
402
580
|
format: { type: 'string' },
|
|
403
581
|
output: { type: 'string', short: 'o' },
|
|
404
582
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
usage(`Error: ${cause.message}`)
|
|
410
|
-
}
|
|
411
|
-
if (!values.format) usage('Error: stasis sbom requires --format=(spdx|cyclonedx)')
|
|
412
|
-
if (!['spdx', 'cyclonedx'].includes(values.format)) usage('Error: --format must be spdx or cyclonedx')
|
|
413
|
-
if (argv.length === 0) usage('Nothing to export: no lockfile or bundle given')
|
|
583
|
+
const values = parseLeadingOptions(argv, options, { valueFlags: ['--format', '--output', '-o'], onError: fail })
|
|
584
|
+
if (!values.format) fail('Error: stasis sbom requires --format=(spdx|cyclonedx)')
|
|
585
|
+
if (!['spdx', 'cyclonedx'].includes(values.format)) fail('Error: --format must be spdx or cyclonedx')
|
|
586
|
+
if (argv.length === 0) fail('Nothing to export: no lockfile or bundle given')
|
|
414
587
|
const { sbomCommand } = await import('../src/cmd/sbom.js')
|
|
415
588
|
sbomCommand({
|
|
416
589
|
cwd: process.cwd(),
|
|
@@ -418,6 +591,8 @@ if (command === '-v' || command === '--version') {
|
|
|
418
591
|
format: values.format,
|
|
419
592
|
output: values.output,
|
|
420
593
|
})
|
|
421
|
-
} else {
|
|
594
|
+
} else if (command === undefined) {
|
|
422
595
|
usage()
|
|
596
|
+
} else {
|
|
597
|
+
fail(`Error: unknown command '${command}'`)
|
|
423
598
|
}
|