6.2 KiB
Design — @suaveplan/electrobun (genesis port)
Architecture
┌───────────────────────────────────────────┐
│ src/bin/suaveplan-electrobun │
│ (process.argv → exit code) │
└───────────────────┬───────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ src/cli/cli.ts │
│ runCli(argv) → { code } │
│ createCommand / devCommand / buildCommand│
│ packageCommand / signCommand │
└───┬──────┬──────┬──────┬──────┬───────────┘
│ │ │ │ │
┌─────────┘ │ │ │ └──────────┐
▼ ▼ ▼ ▼ ▼
generator/ runner/ runner/ runner/ runner/
├─ collectFiles electrobun electrobun electrobun electrobun
├─ generate dev … build … package … sign …
│ ├─ templates/
│ ├─ icons/
│ ├─ fs/ (FsAdapter)
│ └─ config/
▼
templates/ (project + feature renderers)
Every IO-touching primitive accepts an injectable adapter:
| Adapter | Default | Used by |
|---|---|---|
FsAdapter |
nodeFsAdapter (real node:fs) |
generator, config, prompts, icons |
CommandRunner |
nodeCommandRunner (real child_process.spawn) |
runner, dev/build/package/sign |
IconRendererAdapter |
iconzRendererAdapter (real iconz) |
icons |
Prompter |
@suaveplan/cli prompt façade |
prompts |
This is the same seam pattern @suaveplan/linter uses for biome / fs / glob; the
fakes used in tests are real-behaviour fakes (temp dirs, deterministic command
output), not vi.mock stubs.
File layout
packages/devtools/tooling/electrobun/
package.json
tsconfig.lib.json
vite.config.ts
vitest.config.ts
README.md
tests/
setup/polyfills.ts
package-shape.test.ts
src/
index.ts
bin/
suaveplan-electrobun.ts
suaveplan-electrobun.md
error-codes/
error-codes.ts
error-codes.test.ts
error-codes.md
index.ts
telemetry/
telemetry.ts
telemetry.test.ts
telemetry.md
index.ts
types/
electrobun.types.ts
electrobun.types.md
index.ts
schemas/
schemas.ts
schemas.test.ts
schemas.md
index.ts
fs/
fs.ts
fs.test.ts
fs.md
index.ts
runner/
runner.ts
runner.test.ts
runner.md
index.ts
config/
config.ts
config.test.ts
config.md
index.ts
templates/
templates.ts
templates.test.ts
templates.md
features.ts
features.test.ts
features.md
index.ts
icons/
icons.ts
icons.test.ts
icons.md
index.ts
generator/
generator.ts
generator.test.ts
generator.md
index.ts
prompts/
prompts.ts
prompts.test.ts
prompts.md
index.ts
cli/
cli.ts
cli.test.ts
cli.md
index.ts
Error codes
defineCodes("ELECTROBUN", […]) with exactly:
INVALID_OPTIONS— Zod boundary failure.OUTPUT_NOT_EMPTY— refusing to overwrite a non-empty target directory.CONFIG_PARSE_FAILED—.suaveplan-electrobun.jsonexists but is malformed.ICON_SOURCE_MISSING—iconSourcepath does not exist.ICON_RENDER_FAILED—iconzraised.TEMPLATE_RENDER_FAILED— a renderer threw.FS_WRITE_FAILED—writeFilescould not commit.BINARY_UNAVAILABLE—bunx electrobun --versionreturns non-zero.RUNNER_FAILED— orchestrated command (dev/build/package/sign) returned non-zero.PROMPT_ABORTED— user cancelled the interactive prompt.UNKNOWN_FLAG—runClisaw an argv it does not recognise.
OTel surface
electrobun_projects_generated_total(counter; taggedfeatures_count).electrobun_icons_generated_total(counter; taggedoutcome=generated|skipped|failed).electrobun_runner_invocations_total(counter; taggedcommand=dev|build|package|sign|probe).electrobun_runner_failures_total(counter; taggedcommand,exit_code).electrobun_prompts_total(counter; taggedoutcome=completed|aborted).electrobun_generate_duration_ms(histogram; taggedfeatures_count).electrobun_runner_duration_ms(histogram; taggedcommand).
Test strategy
Tests run under Vitest via @suaveplan/testing/runner. Everything IO-touching uses
a real mkdtemp temp directory and the real nodeFsAdapter (no vi.mock). The
runner surface is exercised with a real-behaviour fake CommandRunner that records
the spawn args and returns a canned CommandResult; one integration scenario
exercises the real nodeCommandRunner against node --version to prove the seam
default works end-to-end. The icon renderer is exercised with a real-behaviour
fake IconRendererAdapter that asserts the option shape passed to iconz; the
real iconz path is a one-line dynamic-import call covered by the default
adapter's smoke test.
Why runner is the right level of abstraction
Electrobun's dev/build/package/sign pipeline is implemented inside the
electrobun binary that ships with the user's project. Re-implementing it here
would duplicate the upstream and silently drift. Instead the wrapper shapes the
argv, runs the binary through the CommandRunner seam, parses stdout/exit code,
and produces a structured *Report that callers (CI, IDE extensions) can act on.
This keeps the package thin enough to test exhaustively while still adding real
value over directly calling bunx electrobun build.