Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion stasis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
| `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` |
| `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) |
| `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 |
| `stasis github-bundle --github=owner/name [--sha=<commit>] [--directory=path] --package-manager=pnpm [src/index.js]` | statically bundle a GitHub repo at a commit (the default branch's head without `--sha`), 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`, or `soldeer` for `.sol` entries), with nothing installed and no package script run; the entries are the JS files the directory's `package.json` names (`main`, `exports`, `bin`) where none are given, resolved as the build resolves (`--conditions`, `--mainFields`, `--metro --platforms`, `--typescript`); 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) |
| `stasis github-bundle --github=owner/name [--sha=<commit>] [--directory=path] [--package-manager=pnpm] [src/index.js]` | statically bundle a GitHub repo at a commit (the default branch's head without `--sha`), 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`, 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; 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) |
| `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) |
| `stasis build --output=out.js stasis.lock.json [entry]` | same, reading + verifying sources from disk against the lockfile |
| `stasis extract app.stasis.code.br` | unpack a bundle back to sources + a `stasis.lock.json` |
Expand Down
17 changes: 10 additions & 7 deletions stasis/bin/stasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,22 @@ function usage(prefix = '') {
not with --metro-resolver;
--resources carries reached assets (e.g. --resources=png,svg) as resources instead of failing to bundle them;
--package-json auto-includes each bundled module's package.json, even ones the scan never reached)
stasis github-bundle --github=owner/name [--sha=commit] [--directory=path] --package-manager=(pnpm|yarn1|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) ...]
stasis github-bundle --github=owner/name [--sha=commit] [--directory=path] [--package-manager=(pnpm|yarn1|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) ...]
(bundles a GitHub repo at a commit, its default branch's head without --sha, 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, as
"pnpm install --frozen-lockfile --ignore-scripts" (pnpm 10, 11 or 12), "yarn install
--frozen-lockfile --ignore-scripts" (yarn 1.22) or "soldeer install" (0.12, for .sol entries)
would: nothing is installed, no package script runs, and every tarball and zip is held to the
lockfile, all cached where "stasis audit" caches; at --package-manager-version, else at the root
package.json's packageManager pin, else pnpm 10.33.4, yarn 1.22.22 or Soldeer 0.12.0. The
entries are paths in --directory, or in the repo; without them, the JS files its package.json
names as main, exports and bin, resolved as the build resolves them (--conditions, --mainFields,
--metro --platforms, --typescript); not with --metro-resolver, --cargo* or --add)
lockfile, all cached where "stasis audit" caches. Without --package-manager, the one whose
lockfile installs --directory (or the repo's root), if only one's does; at
--package-manager-version, else at the root package.json's packageManager pin, else pnpm
10.33.4, yarn 1.22.22 or Soldeer 0.12.0. The entries are paths in --directory, or in the repo;
without them, the JS files its package.json names as main, exports and bin, resolved as the
build resolves them (--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; not with --metro-resolver, --cargo* or --add)
stasis add path/to/(file|dir) ...
(adds the listed files to the project's bundle(s) with no dependency resolution;
a directory expands to its files. Requires a stasis.config.json (all fields optional).)
Expand Down Expand Up @@ -444,7 +448,6 @@ if (command === '-v' || command === '--version') {
onError: usage,
})
if (values.github === undefined) usage('Error: github-bundle requires --github=owner/name, the repo to bundle')
if (values['package-manager'] === undefined) usage('Error: github-bundle requires --package-manager=(pnpm|yarn1|soldeer)')
// Whether each option applies to the entries is buildGitHubBundle's to say.
const list = (value) => [value ?? []].flat().flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean)
let brotliQuality
Expand Down
39 changes: 21 additions & 18 deletions stasis/src/cmd/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,33 +1162,37 @@ async function buildJs({ mainFields, platforms, metro, metroResolver, ...options
return { bundle: state.sourceBundle.withReason('bundle'), lockfile: () => state.lockfile, stateBuilt: true }
}

// buildVfsBundle's options for the package manager `pm`, checked before anything is fetched (`host`
// the project's, or an empty tree's with `fetched: false`, before the project is fetched): its kind
// alone, and no metro-resolver, which reads the disk.
export function checkVfsOptions(name, pm, packageManager, options) {
if (classifyEntries(name, options) !== pm.kind) throw new Error(`${name}: only ${pm.kind === 'sol' ? 'Solidity' : 'JS'} bundles are built with ${packageManager}`)
// buildVfsBundle's checks of `options`, which hold before anything is fetched (`host` the
// project's, or an empty tree's with `fetched: false`): no metro-resolver, which reads the disk.
// -> the kind of bundle its entries make
export function checkVfsOptions(name, options) {
const kind = classifyEntries(name, options)
if (options.metroResolver) throw new Error(`${name}: metroResolver is not supported`)
return kind
}

// A bundle from the lockfile of the project held in `vfs` alone (@exodus/stasis/vfs-bundle), `cwd` a
// path there: buildBundle's JS options, resolved through the node_modules 'pnpm' or 'yarn1' would
// install, or its Solidity options, through the dependencies folder 'soldeer' would install; with
// nothing read from disk but tarballs and zips. No EXODUS_STASIS_* setting is read; `env` supplies
// a Solidity bundle's FOUNDRY_PROFILE and FOUNDRY_REMAPPINGS alone. `repo`, the informational
// `{ github, directory | root, commit }`, is the Bundle's, over what is detected in the Vfs as
// `stasis bundle` detects it on disk. `os`, `cpu` and `libc` are loadNodeModules'.
// -> { bundle: Bundle, lockfile: Lockfile (of a JS bundle), stats }
export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc, repo, env = {}, ...options } = {}) {
const { checkTarget, checkVfs, loadTree, packageManagerOf, vfsHost } = await import('../vfs-bundle/tree.js')
const pm = packageManagerOf('buildVfsBundle', packageManager)
// nothing read from disk but tarballs and zips, nor from the environment: a Solidity bundle is
// built with foundry.toml's default profile, whatever FOUNDRY_PROFILE or remappings one sets, and no
// EXODUS_STASIS_* setting is read. `repo`, the informational `{ github, directory | root, commit }`,
// is the Bundle's, over what is detected in the Vfs as `stasis bundle` detects it on disk. `os`,
// `cpu` and `libc` are loadNodeModules'. Without a `packageManager`, it is the one whose lockfile
// installs cwd, where only one's does.
// -> { bundle: Bundle, lockfile: Lockfile (of a JS bundle), stats, packageManager }
export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc, repo, ...options } = {}) {
const { checkKind, checkTarget, checkVfs, loadTree, packageManagerFor, packageManagerOf, vfsHost } = await import('../vfs-bundle/tree.js')
checkVfs('buildVfsBundle', vfs)
checkTarget('buildVfsBundle', { os, cpu, libc })
// Checked as the Bundle checks it, before anything is fetched.
if (repo !== undefined) repo = new Bundle({ repo }).repo
const project = vfsHost(vfs)
// A real path, as every file the scan reaches is.
cwd = project.realpath(posix.resolve('/', cwd))
checkVfsOptions('buildVfsBundle', pm, packageManager, { ...options, cwd, host: project })
packageManager = packageManagerFor('buildVfsBundle', project, cwd, { packageManager, packageManagerVersion })
const pm = packageManagerOf('buildVfsBundle', packageManager)
checkKind('buildVfsBundle', checkVfsOptions('buildVfsBundle', { ...options, cwd, host: project }), [packageManager])
// Checked before anything is fetched: an entry out of what the tree installs is in the project
// already. (A Solidity entry that is no .sol file is a directory, skipped where it is missing.)
for (const entry of options.entries) {
Expand All @@ -1197,13 +1201,12 @@ export async function buildVfsBundle({ vfs, packageManager, cwd = '/', packageMa
if (!posix.relative(cwd, abs).split('/').includes(pm.installs) && project.stat(abs) === null) throw new Error(`entry not found: ${abs}`)
}
const { host, stats } = await loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc })
const { bundle, lockfile, stateBuilt } = pm.kind === 'sol'
? { bundle: await buildSolidityBundle({ ...options, cwd, env, host }) }
: await buildJs({ ...options, cwd, host, env: {} })
const args = { ...options, cwd, host, env: {} }
const { bundle, lockfile, stateBuilt } = pm.kind === 'sol' ? { bundle: await buildSolidityBundle(args) } : await buildJs(args)
if (repo !== undefined) bundle.repo = repo
// Rooted at cwd, where `stasis bundle` detects its repo.
else if (!stateBuilt) bundle.repo ??= detectRepo(cwd, host)
return { bundle, lockfile: lockfile?.(), stats }
return { bundle, lockfile: lockfile?.(), stats, packageManager }
}

// Programmatic equivalent of `stasis bundle`: build and return an in-memory Bundle without
Expand Down
5 changes: 2 additions & 3 deletions stasis/src/cmd/github-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { buildGitHubBundle } from '../vfs-bundle/github.js'
// Run `stasis github-bundle`: the bundle of a GitHub repo at a commit (the default branch's head
// without one), as buildGitHubBundle builds it from `options`, written brotli-compressed to `output`
// (stasis.code.br by default, `-` for stdout), and a JS bundle's lockfile to `lockfile` where given.
// The tree is fetched with GITHUB_TOKEN from `env` where it is set.
// The tree is fetched with GITHUB_TOKEN from `env` where it is set; nothing else of `env` is read.
export async function githubBundleCommand({ cwd = process.cwd(), env = process.env, output = 'stasis.code.br', lockfile, brotliQuality, client, ...options } = {}) {
if (lockfile !== undefined && options.packageManager === 'soldeer') throw new Error('github-bundle: --lockfile is only valid for JS bundles')
const built = await buildGitHubBundle({ ...options, env, client: client ?? createClient({ token: env.GITHUB_TOKEN || null }) })
const built = await buildGitHubBundle({ ...options, lockfile, client: client ?? createClient({ token: env.GITHUB_TOKEN || null }) })
const write = (path, data) => {
const at = resolve(cwd, path)
mkdirSync(dirname(at), { recursive: true })
Expand Down
6 changes: 6 additions & 0 deletions stasis/src/loaders/foundry.js
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,12 @@ export function foundryLibs(baseDir, { env = process.env, host = diskHost } = {}
}
}

// The source directory of the Foundry project at `baseDir`: its default profile's `src`, else
// forge's (`src` unless only `contracts` exists).
export function foundrySourceDir(baseDir, { host = diskHost } = {}) {
return loadFoundryConfig(toPosix(resolve(baseDir)), 'default', { host }).src
}

// The Foundry project at `baseDir`: what `forge build` would use. `remappings` are
// `{ context, prefix, target }` relative to the root, in forge's order; `libs` the lib dirs;
// `files` the config files read (project-relative, `../` when outside the project); `envUsed` the
Expand Down
Loading
Loading