@exodus/stasis 1.0.0-beta.3 → 1.0.0-beta.5

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