@exodus/stasis 1.0.0-beta.5 → 1.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/bin/stasis.js +30 -17
- package/package.json +14 -9
- package/src/audit-corrections.js +6 -2
- package/src/audit.js +63 -22
- package/src/bundle-from-lockfile.js +3 -5
- package/src/cmd/bundle.js +149 -69
- package/src/diff.js +16 -8
- package/src/loaders/cargo.js +26 -9
- package/src/loaders/php.js +4 -3
- package/src/metro-resolver.js +1 -1
- package/src/resolve-fields.js +18 -11
- package/src/resolve-typescript.js +175 -61
- package/src/sbom.js +3 -1
- package/src/scan.js +158 -73
- package/src/vfs-bundle/entries.js +5 -5
- package/src/vfs-bundle/github.js +15 -9
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/tree.js +18 -11
- package/src/vfs-bundle.js +4 -3
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
59
59
|
| `stasis run --lock=add --child-process app.js` | also attest modules loaded in forked child processes (e.g. Metro transform workers) |
|
|
60
60
|
| `stasis run --bundle=add --fs=sync app.js` | build a bundle that also captures sync `fs.readFileSync`/`readdirSync` reads |
|
|
61
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 |
|
|
62
|
-
| `stasis bundle src/index.js` | build a bundle statically, without executing it |
|
|
62
|
+
| `stasis bundle src/index.js` | build a bundle statically, without executing it, into the `stasis.code.br` of the project root its paths are relative to (from a workspace package, the workspace root), where `stasis run --bundle=load` finds it |
|
|
63
63
|
| `stasis bundle --add src/worker.js` | merge more entries (and their import graph) into an existing `stasis.code.br` instead of replacing it |
|
|
64
64
|
| `stasis add a.js icon.svg` | add the listed files to the project's bundles (config-driven), with no dependency resolution |
|
|
65
65
|
| `stasis add src assets` | same, expanding each directory to every file under it, minus an auto-excluded set unless explicitly named |
|
|
@@ -68,21 +68,22 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
|
|
|
68
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 |
|
|
69
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`) |
|
|
70
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.
|
|
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
|
|
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) |
|
|
71
|
+
| `stasis bundle --typescript src/index.ts` | statically bundle TS sources that import by output name, the way tsc resolves: a `./x.js` (or `.jsx`/`.mjs`/`.cjs`) specifier with no such file on disk lands on its on-disk `./x.ts` or `./x.tsx` (`.tsx`/`.mts`/`.cts`) source, and an extensionless `./x` probes `./x.ts`, `./x.tsx` and `./x/index.ts(x)`; the same substitution applies to package `main`/`exports`/`imports` targets and bare subpaths, and an existing `.js` always wins over its `.ts` twin. None of this applies from or into `node_modules`, by real path: an installed package is used as published, while a monorepo's workspace packages, linked in through `node_modules`, really live outside it and resolve like the project's own files. On without the flag when every entry is TypeScript and their relative imports of JS outputs (`./x.js`) are each only on disk as the TS source (`./x.ts`) -- tsc's convention; a project of Node-compatible `.ts` files importing `./x.ts` keeps Node's resolution. Otherwise an import that fails but would resolve under it names the file and `--typescript` in the error. Combines with `--jsx`/`--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, an extensionless target completing as tsc's does (`.ts`, `.tsx`, then `.js`, `.jsx`, then the directory's `index`); without `--tsconfig`, the `tsconfig.json` beside the importing file's `package.json` applies when present: in a monorepo (a `pnpm-workspace.yaml` or `package.json` `workspaces` at or above the cwd) the nearest named `package.json` at or above the file, else the project's one (the nearest named `package.json` at or above the cwd); never for a file in `node_modules`, and no lookup walks through one |
|
|
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, a Soldeer git dependency on GitHub fetched as the repo is; 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; its paths, and the directory its `repo` names, are the directory's, or for a JS bundle, the innermost package's at or above it that holds every file it bundles (the project root's, where one is outside it, such as a sibling workspace package or the root's `node_modules`); 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) |
|
|
74
|
+
| `stasis github-bundle --github=owner/name --generate=prisma packages/api/src/index.ts` | same, with the Prisma Client each project's `prisma generate` would write first: for each project the lockfile installs with a `prisma-client` generator in its schema (the one its Prisma config names as a string literal or `path.join` of them, else `schema.prisma` or `prisma/schema.prisma`), as the Prisma it installs (7.4.0 to 7.10.0: its `prisma`, or beside Prisma 8, the one its `@prisma/prisma7` runs) writes it, byte for byte, where the generator's `output` says, the bundle then carrying it as a source like any other. Nothing of the repo runs: the schema, the config's `schema` path, and the `tsconfig.json` and `package.json` the generator infers its module format and import extensions from are read as data, its installed `prisma` only for its version (and, for an edge runtime's client, the query compiler it ships). The client is generated by the optional `@prisma/client-generator-ts` 7.10.0 dependency and rewritten as the version installed writes it; a project it can't generate for is skipped with a warning |
|
|
74
75
|
| `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) |
|
|
75
76
|
| `stasis build --output=out.js stasis.lock.json [entry]` | same, reading + verifying sources from disk against the lockfile |
|
|
76
77
|
| `stasis extract app.stasis.code.br` | unpack a bundle back to sources + a `stasis.lock.json` |
|
|
77
78
|
| `stasis diff --stat a.lock.json b.stasis.code.br` | summarize module/file differences between two lockfiles/bundles |
|
|
78
79
|
| `stasis prune` | trim `node_modules` to the lockfile, verifying the rest |
|
|
79
80
|
| `stasis audit stasis.lock.json` | report npm advisories for a lockfile's dependencies |
|
|
80
|
-
| `stasis audit app.stasis.code.br` | report advisories for a bundle's dependencies: npm packages by npm's advisories, vendored
|
|
81
|
+
| `stasis audit app.stasis.code.br` | report advisories for a bundle's dependencies: npm packages by npm's advisories, crates vendored from a registry 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, and a crate vendored from git or with no `.cargo-checksum.json` (never sent to OSV), are listed as not audited |
|
|
81
82
|
| `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 |
|
|
82
83
|
| `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`) |
|
|
83
84
|
| `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) |
|
|
84
85
|
| `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) |
|
|
86
|
+
| `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 the one its artifact records (its `package.json`'s, see `repo` in [file formats](https://github.com/ExodusOSS/stasis/blob/main/doc/file-formats.md)), else, for an npm package a bundle records none for but carries the `package.json` of (one built before the field), the one that names, else it is looked up in npm's registry, Packagist or crates.io and 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) |
|
|
86
87
|
| `stasis sbom --format=spdx stasis.lock.json` | export an SPDX SBOM for a lockfile or bundle |
|
|
87
88
|
| `stasis sbom --format=cyclonedx app.stasis.code.br` | export a CycloneDX SBOM for a lockfile or bundle |
|
|
88
89
|
|
package/bin/stasis.js
CHANGED
|
@@ -38,8 +38,8 @@ function usage(prefix = '') {
|
|
|
38
38
|
remappings.txt, foundry.lock, soldeer.lock, .gitmodules and package.json of the project and its
|
|
39
39
|
bundled dependencies, as written: RPC/Etherscan keys and URL credentials in them included)
|
|
40
40
|
stasis bundle [--add] [--output=(path|-)] path/to/file.php ...
|
|
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) ...
|
|
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|jsx|tsx) ...
|
|
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|jsx|tsx) ...
|
|
43
43
|
stasis bundle [--add] [--output=(path|-)] path/to/file.(sh|bash) ...
|
|
44
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
45
|
(Rust: each crate's Cargo features are resolved from Cargo.toml/Cargo.lock like "cargo build" of the
|
|
@@ -52,17 +52,21 @@ function usage(prefix = '') {
|
|
|
52
52
|
out too, otherwise it is all kept; --cargo-manifests also bundles each bundled package's Cargo.toml
|
|
53
53
|
and build script (a vendored crate's .cargo-checksum.json too), the workspace Cargo.toml, Cargo.lock
|
|
54
54
|
and cargo configs, as written: tokens and URL credentials in them included)
|
|
55
|
-
(writes to stasis.code.br by default
|
|
55
|
+
(writes to stasis.code.br by default, a JS bundle's in the project root its paths are relative to,
|
|
56
|
+
where stasis run --bundle=load finds it; --output=- streams to stdout; --add merges into an
|
|
56
57
|
existing bundle instead of replacing it (not with --output=-); --brotli-quality=0..11, default 9;
|
|
57
|
-
--jsx parses JSX in .js/.cjs/.mjs files, e.g. React Native source (
|
|
58
|
+
--jsx parses JSX in .js/.cjs/.mjs files, e.g. React Native source (.jsx/.tsx files need no flag;
|
|
59
|
+
put JSX-in-TS in a .tsx file);
|
|
58
60
|
--flow strips Flow types from .js/.cjs/.mjs sources oxc can't parse (needs the optional flow-remove-types dep);
|
|
59
61
|
--typescript resolves TS the way tsc does: an import of ./x.js lands on ./x.ts when no .js is on disk
|
|
60
62
|
(likewise .mjs/.cjs -> .mts/.cts, extensionless ./x -> ./x.ts, incl. exports/imports/main targets),
|
|
61
|
-
honouring tsconfig compilerOptions.paths aliases (from
|
|
62
|
-
|
|
63
|
+
honouring tsconfig compilerOptions.paths aliases (from the tsconfig.json beside the importing
|
|
64
|
+
file's package.json -- its workspace package's in a monorepo, else the project's -- or --tsconfig=path);
|
|
65
|
+
never from or into node_modules (by real path, so linked workspace packages still are); not with --metro-resolver; implied when every entry is TS and each relative ./x.js-style import
|
|
66
|
+
in them exists only as its TS source (otherwise a failing import it would resolve names it);
|
|
63
67
|
--resources carries reached assets (e.g. --resources=png,svg) as resources instead of failing to bundle them;
|
|
64
68
|
--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) ...]
|
|
69
|
+
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) ...]
|
|
66
70
|
(bundles a GitHub repo at a commit, or the one --tag names, its default branch's head without
|
|
67
71
|
either, as "stasis bundle" bundles a clone of it once installed: the tree is fetched (with
|
|
68
72
|
GITHUB_TOKEN where set) and held to its git tree id, and the dependencies are laid out in memory
|
|
@@ -78,8 +82,14 @@ function usage(prefix = '') {
|
|
|
78
82
|
main, exports and bin, resolved as the build resolves them (--conditions, --mainFields, --metro
|
|
79
83
|
--platforms, --typescript), or for Soldeer its .sol files directly in it, under contracts/, and
|
|
80
84
|
under its source directory (foundry.toml's src, else src/), but tests, scripts, mocks and
|
|
81
|
-
dependency or build directories;
|
|
82
|
-
|
|
85
|
+
dependency or build directories; its paths, and the directory its repo names, are
|
|
86
|
+
--directory's, or of a JS bundle, the innermost package's at or above it holding every file it
|
|
87
|
+
bundles, the project root's where one is outside; not with --metro-resolver, --cargo* or --add;
|
|
88
|
+
--generate=prisma first writes, for each project with a "prisma-client" generator, the Prisma
|
|
89
|
+
Client its "prisma generate" would (Prisma 7.4.0 to 7.10.0), with the optional
|
|
90
|
+
@prisma/client-generator-ts 7.10.0 dependency: nothing of the repo runs, its schema and config
|
|
91
|
+
are read as data, and its prisma for its version alone;
|
|
92
|
+
writes to owner-name.<commit's first 7>.stasis.code.br by default,
|
|
83
93
|
owner-name.<--directory, its / made ->.<commit's first 7>.stasis.code.br with --directory, each
|
|
84
94
|
character outside [A-Za-z0-9._-] made _, and a directory too deep to fit in 255 characters cut,
|
|
85
95
|
with a hash of it; --output=- streams to stdout)
|
|
@@ -105,8 +115,9 @@ function usage(prefix = '') {
|
|
|
105
115
|
--reason=consumer shows only advisories related to that consumer, and with --why only its chains;
|
|
106
116
|
--repo-advisories also asks each package's GitHub repository for the advisories its maintainers
|
|
107
117
|
published there, before GitHub reviews them into the databases above; it needs a token in
|
|
108
|
-
GITHUB_TOKEN, and
|
|
109
|
-
|
|
118
|
+
GITHUB_TOKEN, and asks the repository each package's artifact records, else the one the
|
|
119
|
+
package.json a bundle carries of an npm package names, else looks one up and caches it for a
|
|
120
|
+
month in the user cache dir, e.g. ~/.cache/stasis)
|
|
110
121
|
stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
|
|
111
122
|
(streams to stdout by default; --output=- is explicit stdout)
|
|
112
123
|
`.trim())
|
|
@@ -289,11 +300,11 @@ if (command === '-v' || command === '--version') {
|
|
|
289
300
|
if (dirError !== null) usage(`Error: ${dirError}`)
|
|
290
301
|
const allSol = argv.every((f) => isSolidityEntry(f))
|
|
291
302
|
const allPhp = argv.every((f) => f.endsWith('.php'))
|
|
292
|
-
const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts)$/u.test(f))
|
|
303
|
+
const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts|jsx|tsx)$/u.test(f))
|
|
293
304
|
const allBash = argv.every((f) => /\.(?:sh|bash)$/u.test(f))
|
|
294
305
|
const allRust = argv.every((f) => f.endsWith('.rs'))
|
|
295
306
|
if (!allSol && !allPhp && !allJs && !allBash && !allRust) {
|
|
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')
|
|
307
|
+
usage('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')
|
|
297
308
|
}
|
|
298
309
|
if (values.mapping && !allSol) usage('Error: --mapping is only valid for .sol bundles')
|
|
299
310
|
// --manifests: carry the Solidity build's description files (foundry.toml, remappings.txt, ...).
|
|
@@ -350,8 +361,8 @@ if (command === '-v' || command === '--version') {
|
|
|
350
361
|
// extensionless) lands on its on-disk TS source (./x.ts, .mts, .cts); an existing .js always wins.
|
|
351
362
|
if (values.typescript && !allJs) usage('Error: --typescript is only valid for JS bundles')
|
|
352
363
|
const typescript = Boolean(values.typescript)
|
|
353
|
-
// --tsconfig: the config whose compilerOptions.paths aliases --typescript honours (default:
|
|
354
|
-
//
|
|
364
|
+
// --tsconfig: the config whose compilerOptions.paths aliases --typescript honours (default: the
|
|
365
|
+
// tsconfig.json beside each importing file's package.json). Meaningless without --typescript.
|
|
355
366
|
if (values.tsconfig !== undefined && !typescript) usage('Error: --tsconfig is only valid with --typescript')
|
|
356
367
|
// --mainFields: legacy package entry fields (e.g. react-native,browser,main) for the non-exports resolver.
|
|
357
368
|
if (values.mainFields !== undefined && !allJs) usage('Error: --mainFields is only valid for JS bundles')
|
|
@@ -374,7 +385,7 @@ if (command === '-v' || command === '--version') {
|
|
|
374
385
|
}
|
|
375
386
|
if ((metro || platforms.length > 0) && !allJs) usage('Error: --metro is only valid for JS bundles')
|
|
376
387
|
// --jsx: parse React Native's JSX-in-.js source in the static scanner (off by default; oxc,
|
|
377
|
-
// like tsc,
|
|
388
|
+
// like tsc, auto-enables JSX for .jsx/.tsx only). Orthogonal to the resolver, so it pairs with
|
|
378
389
|
// plain/--mainFields/--metro alike -- JS-only, since no other entry language is scanned.
|
|
379
390
|
const jsx = Boolean(values.jsx)
|
|
380
391
|
if (jsx && !allJs) usage('Error: --jsx is only valid for JS bundles')
|
|
@@ -472,8 +483,9 @@ if (command === '-v' || command === '--version') {
|
|
|
472
483
|
resources: { type: 'string' },
|
|
473
484
|
'package-json': { type: 'boolean' },
|
|
474
485
|
'brotli-quality': { type: 'string' },
|
|
486
|
+
generate: { type: 'string', multiple: true },
|
|
475
487
|
}, {
|
|
476
|
-
valueFlags: ['--github', '--sha', '--tag', '--directory', '--package-manager', '--package-manager-version', '--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--tsconfig', '--resources', '--brotli-quality', '-o'],
|
|
488
|
+
valueFlags: ['--github', '--sha', '--tag', '--directory', '--package-manager', '--package-manager-version', '--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--tsconfig', '--resources', '--brotli-quality', '--generate', '-o'],
|
|
477
489
|
onError: usage,
|
|
478
490
|
})
|
|
479
491
|
if (values.github === undefined) usage('Error: github-bundle requires --github=owner/name, the repo to bundle')
|
|
@@ -515,6 +527,7 @@ if (command === '-v' || command === '--version') {
|
|
|
515
527
|
tsconfig: values.tsconfig,
|
|
516
528
|
resources: list(values.resources),
|
|
517
529
|
packageJSON: Boolean(values['package-json']),
|
|
530
|
+
generate: list(values.generate),
|
|
518
531
|
brotliQuality,
|
|
519
532
|
})
|
|
520
533
|
} else if (command === 'add') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/stasis",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"description": "File-level source bundles and lockfiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"src/vfs-bundle.js",
|
|
69
69
|
"src/vfs-bundle/entries.js",
|
|
70
70
|
"src/vfs-bundle/github.js",
|
|
71
|
+
"src/vfs-bundle/prisma.js",
|
|
71
72
|
"src/vfs-bundle/soldeer.js",
|
|
72
73
|
"src/vfs-bundle/tree.js",
|
|
73
74
|
"src/webpack.js",
|
|
@@ -92,20 +93,24 @@
|
|
|
92
93
|
"homepage": "https://github.com/PreventiveMeasures/stasis#readme",
|
|
93
94
|
"dependencies": {
|
|
94
95
|
"@exodus/bytes": "^1.16.0",
|
|
95
|
-
"@exodus/stasis-core": "1.0.0-beta.
|
|
96
|
-
"@exodus/stasis-plugins": "1.0.0-beta.
|
|
97
|
-
"@preventive/archive": "1.0.0-beta.
|
|
98
|
-
"@preventive/deptree": "1.0.0-alpha.
|
|
99
|
-
"@preventive/lockfile": "1.0.0-alpha.
|
|
100
|
-
"@preventive/upstream": "1.0.0-alpha.
|
|
101
|
-
"@preventive/vfs": "1.0.0-beta.
|
|
102
|
-
"oxc-parser": "^0.
|
|
96
|
+
"@exodus/stasis-core": "1.0.0-beta.7",
|
|
97
|
+
"@exodus/stasis-plugins": "1.0.0-beta.7",
|
|
98
|
+
"@preventive/archive": "1.0.0-beta.4",
|
|
99
|
+
"@preventive/deptree": "1.0.0-alpha.8",
|
|
100
|
+
"@preventive/lockfile": "1.0.0-alpha.7",
|
|
101
|
+
"@preventive/upstream": "1.0.0-alpha.10",
|
|
102
|
+
"@preventive/vfs": "1.0.0-beta.4",
|
|
103
|
+
"oxc-parser": "^0.152.0"
|
|
103
104
|
},
|
|
104
105
|
"peerDependencies": {
|
|
106
|
+
"@prisma/client-generator-ts": "7.10.0",
|
|
105
107
|
"esbuild": "*",
|
|
106
108
|
"flow-remove-types": "*"
|
|
107
109
|
},
|
|
108
110
|
"peerDependenciesMeta": {
|
|
111
|
+
"@prisma/client-generator-ts": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
109
114
|
"esbuild": {
|
|
110
115
|
"optional": true
|
|
111
116
|
},
|
package/src/audit-corrections.js
CHANGED
|
@@ -48,10 +48,14 @@ function isCorrectedFile(name, version, rel) {
|
|
|
48
48
|
// alongside bundled files, but no package code ships through one. So too for the
|
|
49
49
|
// manifests the other ecosystems' bundles carry beside a dependency's code
|
|
50
50
|
// (--cargo-manifests, --manifests): its Cargo.toml and the checksums cargo vendored
|
|
51
|
-
// it with
|
|
51
|
+
// it with (a registry's copy, a git checkout's, or one whose source isn't known),
|
|
52
|
+
// its composer.json, a Solidity dependency's foundry.toml and remappings.
|
|
53
|
+
const CARGO_MANIFESTS = new Set(['Cargo.toml', 'Cargo.lock', '.cargo-checksum.json'])
|
|
52
54
|
const MANIFESTS = {
|
|
53
55
|
npm: new Set(['package.json']),
|
|
54
|
-
cargo:
|
|
56
|
+
cargo: CARGO_MANIFESTS,
|
|
57
|
+
'cargo-git': CARGO_MANIFESTS,
|
|
58
|
+
'cargo-unknown': CARGO_MANIFESTS,
|
|
55
59
|
composer: new Set(['composer.json', 'composer.lock']),
|
|
56
60
|
soldeer: new Set(['package.json', 'foundry.toml', 'remappings.txt', 'soldeer.toml']),
|
|
57
61
|
github: new Set(['package.json', 'foundry.toml', 'remappings.txt', 'soldeer.toml']),
|
package/src/audit.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { packageRepo } from '@exodus/stasis-core/bundle-util'
|
|
2
|
+
import { dependencyEcosystem, moduleFileKey, sameGithub } from '@exodus/stasis-core/util'
|
|
2
3
|
import { advisories } from '@preventive/upstream/advisories.js'
|
|
3
4
|
import { compareVersions, valid } from '@preventive/upstream/semver.js'
|
|
4
5
|
import { isEvidenceFile } from './audit-corrections.js'
|
|
5
|
-
import { parseFile } from './parse.js'
|
|
6
|
+
import { parseFile, parseFileWithKind } from './parse.js'
|
|
6
7
|
import { collectWhy, invertReason } from './why.js'
|
|
7
8
|
|
|
8
|
-
// Where advisories() asks for each ecosystem's advisories: npm's registry; OSV for crates
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// Where advisories() asks for each ecosystem's advisories: npm's registry; OSV for crates (those
|
|
10
|
+
// vendored from a registry, which OSV takes for crates.io's) and Composer packages; for Soldeer
|
|
11
|
+
// packages and GitHub repos (Foundry's lib/ submodules), the advisories their GitHub repository
|
|
12
|
+
// publishes, a Soldeer package's as Soldeer names it.
|
|
11
13
|
const SOURCES = { npm: ['npm'], cargo: ['OSV'], composer: ['OSV'], soldeer: ['Soldeer', 'GitHub'], github: ['GitHub'] }
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
const
|
|
15
|
+
// Why a vendored crate that may not be crates.io's is asked nowhere: by its name, crates.io's
|
|
16
|
+
// advisories would be another crate's, and a private git dependency's name must not reach a public
|
|
17
|
+
// database at all.
|
|
18
|
+
const UNREGISTERED_CRATES = {
|
|
19
|
+
'cargo-git': 'a crate vendored from a git repository, not crates.io',
|
|
20
|
+
'cargo-unknown': 'a vendored crate with no .cargo-checksum.json, which may be a git checkout, not crates.io\'s',
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
// A dependency's version as it is audited: a GitHub repo's `.gitmodules` branch `.`, git's for the
|
|
19
24
|
// superproject's own branch, which the bundle does not know and advisories() takes for no branch, is
|
|
@@ -31,13 +36,15 @@ const isComposerDev = (version) => /^dev-|-dev$/iu.test(version.replace(/#.*$/su
|
|
|
31
36
|
|
|
32
37
|
// Why `pkg` cannot be audited, or undefined where it can.
|
|
33
38
|
function unaudited({ ecosystem, version }) {
|
|
39
|
+
if (Object.hasOwn(UNREGISTERED_CRATES, ecosystem)) return UNREGISTERED_CRATES[ecosystem]
|
|
34
40
|
if (!Object.hasOwn(SOURCES, ecosystem)) return `no advisories are looked up for ${ecosystem}`
|
|
35
41
|
if (ecosystem === 'composer' && isComposerDev(version)) return 'a Composer dev version, which no advisory database lists'
|
|
36
42
|
return undefined
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
// Only audit installed dependencies, `{ ecosystem, name, version }`: an npm package, a vendored
|
|
40
|
-
// crate, a Composer package, a Soldeer package or a GitHub repo, as the bundle tags it
|
|
46
|
+
// crate, a Composer package, a Soldeer package or a GitHub repo, as the bundle tags it (a crate
|
|
47
|
+
// vendored from git, or from where no `.cargo-checksum.json` tells, is listed but never asked about).
|
|
41
48
|
//
|
|
42
49
|
// A package counts as present only when its REAL code is, and recorded = present:
|
|
43
50
|
// an artifact records exactly the files it ships or attested -- imported, entry,
|
|
@@ -49,34 +56,68 @@ function unaudited({ ecosystem, version }) {
|
|
|
49
56
|
// skipped, while one whose real code was bundled stays. Which consumers and import
|
|
50
57
|
// EDGES reach that code is the reason column's concern -- collectReasons and
|
|
51
58
|
// why.js apply the same evidence rule per file/edge there.
|
|
59
|
+
//
|
|
60
|
+
// A package's `github` is the repository its artifact records for it (its package.json's), where
|
|
61
|
+
// one does; else, for an npm package a bundle carries the package.json of (one built before builds
|
|
62
|
+
// recorded `repo`), the one that names, read by the rule builds record it by. A GitHub repo is its
|
|
63
|
+
// own, and has none.
|
|
52
64
|
export function collectPackagesFromFile(file) {
|
|
53
65
|
const out = []
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
const { kind, artifact } = parseFileWithKind(file)
|
|
67
|
+
for (const [dir, { name, version, ecosystem: tagged, repo, files }] of artifact.modules) {
|
|
68
|
+
const ecosystem = dependencyEcosystem(dir, tagged)
|
|
69
|
+
// First-party code is never sent to a public registry (leaks names, adds noise).
|
|
56
70
|
if (ecosystem === undefined) continue
|
|
57
71
|
if (!name || !version) continue
|
|
58
72
|
if (!Object.keys(files).some((rel) => isEvidenceFile(name, version, rel, ecosystem))) continue
|
|
59
|
-
|
|
73
|
+
let github = ecosystem === 'github' ? undefined : repo?.github
|
|
74
|
+
if (github === undefined && ecosystem === 'npm' && kind === 'bundle') github = manifestRepo(files['package.json'])
|
|
75
|
+
out.push({ ecosystem, name, version: versionOf(ecosystem, version), ...(github === undefined ? {} : { github }) })
|
|
60
76
|
}
|
|
61
77
|
return out
|
|
62
78
|
}
|
|
63
79
|
|
|
80
|
+
// The GitHub repository a bundled package.json's text names (packageRepo), or undefined where it
|
|
81
|
+
// names none or doesn't parse. Read as Node reads one, past a byte order mark (packageJSONText),
|
|
82
|
+
// which a bundle keeps in the text. A lockfile holds the file's digest alone, never asked here.
|
|
83
|
+
function manifestRepo(text) {
|
|
84
|
+
if (typeof text !== 'string') return undefined
|
|
85
|
+
try {
|
|
86
|
+
return packageRepo(JSON.parse(text.replace(/^\uFEFF/u, '')))?.github
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
// Versions in semver's order where both are semver's, else as numbers in text compare.
|
|
65
93
|
const byNumbers = new Intl.Collator('en', { numeric: true }).compare
|
|
66
94
|
const byVersion = (a, b) => (valid(a) && valid(b) ? compareVersions(a, b) : byNumbers(a, b))
|
|
67
95
|
|
|
96
|
+
// Each package once, with the GitHub repository its artifacts record for its name, which
|
|
97
|
+
// advisories() asks instead of looking one up. It takes one repository a name, so a name recorded
|
|
98
|
+
// with two, by two of its versions (a package that moved) or by two artifacts, has none, and is
|
|
99
|
+
// looked up as before.
|
|
68
100
|
export function collectPackages(files) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
101
|
+
const byKey = new Map()
|
|
102
|
+
const repos = new Map() // `ecosystem:name` -> the one repository recorded for it, or null
|
|
71
103
|
for (const file of files) {
|
|
72
|
-
for (const pkg of collectPackagesFromFile(file)) {
|
|
104
|
+
for (const { github, ...pkg } of collectPackagesFromFile(file)) {
|
|
73
105
|
const key = keyOf(pkg.ecosystem, pkg.name, pkg.version)
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
if (!byKey.has(key)) byKey.set(key, pkg)
|
|
107
|
+
if (github === undefined) continue
|
|
108
|
+
const name = `${pkg.ecosystem}:${pkg.name}`
|
|
109
|
+
const known = repos.get(name)
|
|
110
|
+
// The first spelling stands.
|
|
111
|
+
if (known === undefined) repos.set(name, github)
|
|
112
|
+
else if (known !== null && !sameGithub(known, github)) repos.set(name, null)
|
|
77
113
|
}
|
|
78
114
|
}
|
|
79
|
-
return
|
|
115
|
+
return [...byKey.values()]
|
|
116
|
+
.map((pkg) => {
|
|
117
|
+
const github = repos.get(`${pkg.ecosystem}:${pkg.name}`)
|
|
118
|
+
return github ? { ...pkg, github } : pkg
|
|
119
|
+
})
|
|
120
|
+
.toSorted((a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.name.localeCompare(b.name) || byVersion(a.version, b.version))
|
|
80
121
|
}
|
|
81
122
|
|
|
82
123
|
// Map each audited package (keyOf it) to the bundle consumers ("reasons") that recorded its files.
|
|
@@ -93,7 +134,7 @@ export function collectReasons(files) {
|
|
|
93
134
|
if (!fileReasons) continue
|
|
94
135
|
const fileToPkg = new Map()
|
|
95
136
|
for (const [dir, { name, version, ecosystem: tagged, files: modFiles }] of artifact.modules) {
|
|
96
|
-
const ecosystem =
|
|
137
|
+
const ecosystem = dependencyEcosystem(dir, tagged)
|
|
97
138
|
if (ecosystem === undefined || !name || !version) continue
|
|
98
139
|
for (const rel of Object.keys(modFiles)) {
|
|
99
140
|
if (!isEvidenceFile(name, version, rel, ecosystem)) continue
|
|
@@ -240,7 +281,7 @@ export async function audit(files, { why = false, whyDeep = false, whyFull = fal
|
|
|
240
281
|
}
|
|
241
282
|
let result
|
|
242
283
|
try {
|
|
243
|
-
result = await advisories(asked.map(({
|
|
284
|
+
result = await advisories(asked.map(({ version, ...pkg }) => ({ ...pkg, versions: [version] })), { repoAdvisories, github })
|
|
244
285
|
} catch (cause) {
|
|
245
286
|
// Refused input or a malformed answer is an assertion that says so itself; a transport or
|
|
246
287
|
// HTTP failure gets the context of which request it was.
|
|
@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
|
|
|
4
4
|
|
|
5
5
|
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
6
6
|
import { sha512integrity } from '@exodus/stasis-core/state-util'
|
|
7
|
-
import { moduleFileKey, narrowExecutable } from '@exodus/stasis-core/util'
|
|
7
|
+
import { moduleFileKey, moduleInfo, narrowExecutable } from '@exodus/stasis-core/util'
|
|
8
8
|
|
|
9
9
|
// Reconstruct an in-memory Bundle from a lockfile, reading every attested file from disk and
|
|
10
10
|
// verifying each against the lockfile's SRI digest -- fail closed on mismatch, so this is as
|
|
@@ -14,7 +14,7 @@ export function bundleFromLockfile(lockfile, { root }) {
|
|
|
14
14
|
if (!lockfile.imports) throw new Error('stasis: lockfile has no recorded import graph')
|
|
15
15
|
const formats = lockfile.formats ?? new Map()
|
|
16
16
|
const modules = new Map()
|
|
17
|
-
for (const [dir, {
|
|
17
|
+
for (const [dir, { files, ...info }] of lockfile.modules) {
|
|
18
18
|
const out = Object.create(null)
|
|
19
19
|
for (const [rel, integrity] of Object.entries(files)) {
|
|
20
20
|
const file = moduleFileKey(dir, rel)
|
|
@@ -42,9 +42,7 @@ export function bundleFromLockfile(lockfile, { root }) {
|
|
|
42
42
|
out[rel] = bytes.toString('utf8')
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
modules.set(dir,
|
|
46
|
-
? { name, version, files: out }
|
|
47
|
-
: { name, version, ecosystem, files: out })
|
|
45
|
+
modules.set(dir, moduleInfo({ ...info, files: out }))
|
|
48
46
|
}
|
|
49
47
|
return new Bundle({
|
|
50
48
|
config: lockfile.config,
|