Package map
Argus is a pnpm workspace of eight packages. Exactly one is published:
@arguslab/argus. The other seven are private: true and will never appear on the
registry.
That is a packaging decision, not an architectural one. The hexagonal split below is entirely intact in the source tree; it simply does not require eight registry entries.
| Package | Role | Rule |
|---|---|---|
@arguslab/core |
Pure domain types, ports, result-protocol parser, Hermes version/pin/asset logic | No adapter or runtime imports — not even node:path |
@arguslab/framework |
Runs inside Hermes: globals, runner, matchers, mocks, result emission | Protect the result channel |
@arguslab/rntl |
Synchronous component-testing facade, exposed through the argus alias |
A replaceable stopgap, kept out of the framework core |
@arguslab/esbuild |
Bundles polyfills + framework + tests into one IIFE | Owns syntax lowering and the virtual entry |
@arguslab/hermes |
Spawns the standalone VM, resolves engines, downloads prebuilts, builds from source | Never use stdin — stdin puts Hermes in REPL mode |
@arguslab/sourcemap |
Remaps Hermes stack frames back to original sources | Must be total: never throw during reporting |
@arguslab/reporter-cli |
Terminal output and exit-code policy | Report test failures separately from infra failures |
@arguslab/cli |
Composition root | Wires adapters; keeps the domain pure |
How eight packages become one tarball
Section titled “How eight packages become one tarball”pnpm build (scripts/build-package.ts) is a staging step, not a per-package compile.
The two halves are handled differently, and the difference is the whole design:
| Host code | Runtime assets | |
|---|---|---|
| Packages | cli, core, esbuild, hermes, sourcemap, reporter-cli |
framework, rntl |
| Runs on | Node | Hermes |
| Shipped as | One bundled ESM file, bin/argus.js |
TypeScript, copied verbatim, under runtime/<name>/src |
| Why | Nobody imports it — it is a binary, not a library, so it needs no declarations and the internal seam costs the user nothing at install time | esbuild compiles them on the user’s machine, against the engine their project pins. Compiling them here would bake in one engine’s syntax envelope and defeat the point |
The result is 30 files: 68 kB packed, 246 kB unpacked, 300 kB installed. Four dependencies
stay external because none of them survives being inlined — esbuild ships a
platform-specific native binary, @babel/core and @babel/plugin-transform-classes
resolve plugins dynamically, and source-map loads a WASM file.
framework and rntl are therefore never imported by Node at any point, in development or
once installed. Their paths are handed to esbuild as strings. That is why
packages/cli/src/paths.ts probes the disk for them instead of calling require.resolve —
and why the published layout mirrors packages/<name>/src exactly, since
rntl/src/index.ts imports ../../framework/src/lifecycle.js across the package boundary.
React and test-renderer are optional peer dependencies, resolved from the user’s
project rather than from Argus’s own tree. A pure-TypeScript suite runs with neither
installed.
Why core is this pure
Section titled “Why core is this pure”@arguslab/core holds things two sides must agree on exactly:
- Hermes tag parsing. The provisioning chain and the release pipeline must derive the same identity from the same ref, or a release is published under one name and downloaded under another.
- On-disk layout. The source-build adapter writes the cache; the provisioning chain reads it. Duplicated path segments drift into a cache written to one path and looked up at another.
- Asset names and release notes. CI publishes the assets, the chain downloads them. The one place that knows what an asset is called is the one place that describes it — which is also why the release body is generated by a function with a test, not inlined in YAML.
Paths are expressed as segments, not joined strings, because core may not import
node:path. Callers join them with their platform’s separator.
The framework is a different world
Section titled “The framework is a different world”@arguslab/framework is the only package that runs inside Hermes. Everything about it follows
from that:
- Bundled, never resolved at run time.
- Constrained to the syntax envelope.
- Index loops and captured primordials in the reporting path (why).
Host-side packages are ordinary Node and have none of these constraints. Do not carry framework discipline into the CLI, and do not carry CLI habits into the framework.
Dependency policy
Section titled “Dependency policy”@arguslab/cli and @arguslab/reporter-cli have zero external runtime dependencies. That is
deliberate: for a tool whose pitch is engine fidelity at unit-test cost, a small install is
part of the argument.
Node’s built-ins cover more than they used to:
| Need | Built-in |
|---|---|
| Argument parsing | node:util.parseArgs |
Colour, with NO_COLOR / FORCE_COLOR handling |
node:util.styleText |
| Glob discovery | fs.promises.glob |
| Loading a TypeScript config | Native type stripping |
The published package declares exactly four dependencies: esbuild (the bundler),
source-map (isolated in @arguslab/sourcemap), and @babel/core plus
@babel/plugin-transform-classes (class lowering for dependencies that ship class syntax).
React and test-renderer are optional peers, not dependencies — they belong to the project
under test.
Every added dependency needs a reason a built-in cannot satisfy.
File size
Section titled “File size”Files stay at or below roughly 500 lines; modules get split when they cross it. Precedent:
matchers.ts became matchers.ts + deep-equal.ts + show.ts + async-matchers.ts +
expect-state.ts; the runner’s helpers became jest-api.ts + hooks.ts.