Files
2026-05-27 23:29:53 +01:00

173 lines
6.2 KiB
Markdown

# Design — @suaveplan/electrobun (genesis port)
## Architecture
```text
┌───────────────────────────────────────────┐
│ 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
```text
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.json` exists but is malformed.
- `ICON_SOURCE_MISSING` — `iconSource` path does not exist.
- `ICON_RENDER_FAILED` — `iconz` raised.
- `TEMPLATE_RENDER_FAILED` — a renderer threw.
- `FS_WRITE_FAILED` — `writeFiles` could not commit.
- `BINARY_UNAVAILABLE` — `bunx electrobun --version` returns non-zero.
- `RUNNER_FAILED` — orchestrated command (dev/build/package/sign) returned non-zero.
- `PROMPT_ABORTED` — user cancelled the interactive prompt.
- `UNKNOWN_FLAG` — `runCli` saw an argv it does not recognise.
## OTel surface
- `electrobun_projects_generated_total` (counter; tagged `features_count`).
- `electrobun_icons_generated_total` (counter; tagged `outcome=generated|skipped|failed`).
- `electrobun_runner_invocations_total` (counter; tagged `command=dev|build|package|sign|probe`).
- `electrobun_runner_failures_total` (counter; tagged `command`, `exit_code`).
- `electrobun_prompts_total` (counter; tagged `outcome=completed|aborted`).
- `electrobun_generate_duration_ms` (histogram; tagged `features_count`).
- `electrobun_runner_duration_ms` (histogram; tagged `command`).
## 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`.