173 lines
6.2 KiB
Markdown
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`.
|