Initial Commit
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: electrobun
|
||||
|
||||
**Package**: `@suaveplan/electrobun`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ELECTROBUN_CODES registry MUST enumerate every typed failure path
|
||||
|
||||
The package MUST export `ELECTROBUN_CODES = defineCodes("ELECTROBUN", [...])` exposing
|
||||
exactly the following codes: `INVALID_OPTIONS`, `OUTPUT_NOT_EMPTY`,
|
||||
`CONFIG_PARSE_FAILED`, `ICON_SOURCE_MISSING`, `ICON_RENDER_FAILED`,
|
||||
`TEMPLATE_RENDER_FAILED`, `FS_WRITE_FAILED`, `BINARY_UNAVAILABLE`, `RUNNER_FAILED`,
|
||||
`PROMPT_ABORTED`, `UNKNOWN_FLAG`. Every code MUST be reachable from a call site
|
||||
exercised by a test in `src/`. The exported `ElectrobunError` MUST extend
|
||||
`SuaveplanError<ElectrobunCode>` and carry a `details: Record<string, unknown>`
|
||||
field for structured diagnostics.
|
||||
|
||||
#### Scenario: registry exposes the full code set
|
||||
|
||||
- **WHEN** the consumer imports `ELECTROBUN_CODES` and reads `Object.values(ELECTROBUN_CODES)`
|
||||
- **THEN** the result contains exactly the eleven declared codes (no more, no less)
|
||||
|
||||
#### Scenario: ElectrobunError carries code and details
|
||||
|
||||
- **WHEN** `new ElectrobunError(ELECTROBUN_CODES.INVALID_OPTIONS, "bad", { details: { field: "x" } })`
|
||||
- **THEN** the instance has `code === "ELECTROBUN.INVALID_OPTIONS"`,
|
||||
`details.field === "x"`, and `instanceof SuaveplanError === true`
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"registry exposes the full code set"` AND `src/error-codes/error-codes.test.ts::"ElectrobunError carries code and details"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Public options MUST be Zod-validated at the boundary
|
||||
|
||||
Every public entry point (`generate`, `collectFiles`, `generateIcons`, `loadConfig`,
|
||||
`runPrompts`, `createCommand`, `devCommand`, `buildCommand`, `packageCommand`,
|
||||
`signCommand`, `runCli`) MUST validate its option object against an exported Zod
|
||||
schema. A schema parse failure MUST surface as `ElectrobunError` with code
|
||||
`ELECTROBUN.INVALID_OPTIONS` and a `details.issues` array carrying the Zod issue
|
||||
list.
|
||||
|
||||
#### Scenario: ProjectOptionsSchema rejects empty name
|
||||
|
||||
- **WHEN** `ProjectOptionsSchema.safeParse({ ...validOptions, name: "" })` runs
|
||||
- **THEN** the parse fails with at least one issue on path `["name"]`
|
||||
|
||||
#### Scenario: collectFiles raises INVALID_OPTIONS on bad input
|
||||
|
||||
- **WHEN** `collectFiles({ name: "", identifier: "x", description: "y", version: "0.0.1", outputDir: "/tmp/a", features: [], networkingComplexity: "basic", r3fModules: [] })` runs
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.INVALID_OPTIONS"` is thrown
|
||||
|
||||
**Verified by:** `src/schemas/schemas.test.ts::"ProjectOptionsSchema rejects empty name"` AND `src/generator/generator.test.ts::"collectFiles raises INVALID_OPTIONS on empty name"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: collectFiles MUST return the deterministic core file list
|
||||
|
||||
`collectFiles(options)` MUST return a `GeneratedFile[]` containing — in order — the
|
||||
nine core paths `electrobun.config.ts`, `package.json`, `tsconfig.json`,
|
||||
`src/bun/index.ts`, `src/mainview/index.html`, `src/mainview/index.css`,
|
||||
`src/mainview/index.tsx`, `src/shared/types.ts`, `README.md`, followed by one file
|
||||
per selected feature in the canonical order `i18n`, `networking`, `api`,
|
||||
`api-schemas`, `storage`, `auth`, `form-builder`, `r3f`. Feature ordering MUST
|
||||
not depend on `options.features` insertion order.
|
||||
|
||||
#### Scenario: minimal options emit the nine core files
|
||||
|
||||
- **WHEN** `collectFiles({ ...validOptions, features: [] })` runs
|
||||
- **THEN** the returned `paths` equals the nine-element core list
|
||||
|
||||
#### Scenario: all eight features emit all feature paths
|
||||
|
||||
- **WHEN** `collectFiles({ ...validOptions, features: ["i18n","networking","api","api-schemas","storage","auth","form-builder","r3f"], r3fModules: ["camera"] })`
|
||||
- **THEN** the returned paths contain `src/features/i18n.ts`,
|
||||
`src/features/network.ts`, `src/features/api.ts`,
|
||||
`src/features/schemas.ts`, `src/features/storage.ts`,
|
||||
`src/features/auth.ts`, `src/features/form-builder.ts`,
|
||||
`src/features/r3f.tsx`
|
||||
|
||||
#### Scenario: feature order is canonical regardless of input order
|
||||
|
||||
- **WHEN** `collectFiles({ ...validOptions, features: ["r3f","i18n","api"] })`
|
||||
- **THEN** the feature paths in the returned list appear in the order
|
||||
`src/features/i18n.ts`, `src/features/api.ts`, `src/features/r3f.tsx`
|
||||
|
||||
**Verified by:** `src/generator/generator.test.ts::"minimal options emit the nine core files"` AND `src/generator/generator.test.ts::"all eight features emit all feature paths"` AND `src/generator/generator.test.ts::"feature order is canonical regardless of input order"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: generate MUST write files via the injectable FsAdapter
|
||||
|
||||
`generate(options, deps?)` MUST resolve `deps.fs ?? nodeFsAdapter`, write every
|
||||
file from `collectFiles(options)` to `options.outputDir` via that adapter, write
|
||||
the `.suaveplan-electrobun.json` config snapshot, optionally run the icon pipeline
|
||||
when `options.iconSource` is set, and return a `GenerateReport` carrying
|
||||
`{ outputDir, filesCreated, durationMs, icons }` where `icons` is the icon
|
||||
report (or `undefined` when no icon source was supplied). The histogram
|
||||
`electrobun_generate_duration_ms` MUST receive one observation per call.
|
||||
|
||||
#### Scenario: generate writes every file from collectFiles
|
||||
|
||||
- **WHEN** `generate({ ...validOptions, outputDir: <tmpDir> }, { fs })` runs
|
||||
- **THEN** `fs` has received one `writeFile` call per file in `collectFiles(options)`,
|
||||
plus one for `.suaveplan-electrobun.json`
|
||||
|
||||
#### Scenario: generate skips icons when iconSource is absent
|
||||
|
||||
- **WHEN** `generate({ ...validOptions, iconSource: undefined }, deps)` runs
|
||||
- **THEN** the returned `report.icons` is `undefined`
|
||||
|
||||
**Verified by:** `src/generator/generator.test.ts::"generate writes every file from collectFiles"` AND `src/generator/generator.test.ts::"generate skips icons when iconSource is absent"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: generateIcons MUST run the icon pipeline through the IconRendererAdapter
|
||||
|
||||
`generateIcons({ inputPath, outputDir }, deps?)` MUST resolve
|
||||
`deps.renderer ?? iconzRendererAdapter`, invoke `renderer.render({ input,
|
||||
output, icons })` with the canonical icon descriptor (icns / ico / png set), and
|
||||
return an `IconReport` of shape `{ generated: boolean; files: readonly string[];
|
||||
error?: string }`. A missing input path MUST surface as
|
||||
`ELECTROBUN.ICON_SOURCE_MISSING`. A renderer throw MUST surface as
|
||||
`ELECTROBUN.ICON_RENDER_FAILED` with the message preserved in `details.message`.
|
||||
|
||||
#### Scenario: missing source raises ICON_SOURCE_MISSING
|
||||
|
||||
- **WHEN** `generateIcons({ inputPath: "/does/not/exist", outputDir: "/tmp/a" })`
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.ICON_SOURCE_MISSING"`
|
||||
is thrown
|
||||
|
||||
#### Scenario: renderer throw is wrapped as ICON_RENDER_FAILED
|
||||
|
||||
- **WHEN** `generateIcons({ inputPath, outputDir }, { renderer: throwingRenderer })`
|
||||
- **THEN** the returned report has `generated === false` AND `error` contains
|
||||
the underlying message
|
||||
|
||||
**Verified by:** `src/icons/icons.test.ts::"missing source raises ICON_SOURCE_MISSING"` AND `src/icons/icons.test.ts::"renderer throw is wrapped as ICON_RENDER_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: loadConfig MUST stack defaults, file, and CLI layers
|
||||
|
||||
`loadConfig(cliArgs, deps?)` MUST resolve `deps.fs ?? nodeFsAdapter`, locate
|
||||
`.suaveplan-electrobun.json` in `cwd` or `home` via the adapter, and build an
|
||||
`ElectrobunConfig` through `@suaveplan/config`'s `createConfig`/`defaultsLayer` +
|
||||
`fileLayer` + `cliLayer` pipeline. CLI args MUST map `--name → name`,
|
||||
`--id → identifier`, `--icon → iconSource`, `--version → version`. Defaults MUST
|
||||
fill `version="0.0.1"`, `features=[]`, `networkingComplexity="basic"`,
|
||||
`r3fModules=[]`. A malformed config file MUST surface as
|
||||
`ELECTROBUN.CONFIG_PARSE_FAILED`.
|
||||
|
||||
#### Scenario: CLI args override file values
|
||||
|
||||
- **WHEN** `loadConfig(["--name", "cli-app"], { fs: fsWithFile({ name: "file-app" }) })`
|
||||
- **THEN** the returned config has `name === "cli-app"`
|
||||
|
||||
#### Scenario: malformed file raises CONFIG_PARSE_FAILED
|
||||
|
||||
- **WHEN** `loadConfig([], { fs: fsWithCorruptFile() })`
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.CONFIG_PARSE_FAILED"`
|
||||
is thrown
|
||||
|
||||
**Verified by:** `src/config/config.test.ts::"CLI args override file values"` AND `src/config/config.test.ts::"malformed file raises CONFIG_PARSE_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Templates MUST render every project and feature file deterministically
|
||||
|
||||
The `renderProjectFile(kind, options)` and `renderFeature(feature, options)`
|
||||
functions MUST cover the full upstream set: `electrobun-config`, `main-process`,
|
||||
`package-json`, `tsconfig`, `readme`, `mainview-html`, `mainview-css`,
|
||||
`mainview-entry`, `shared-types` (project) and `i18n`, `network`, `network-full`,
|
||||
`api`, `api-schemas`, `storage`, `auth`, `form-builder`, `r3f` (feature). For a
|
||||
fixed `ProjectOptions` input, each renderer MUST produce a byte-identical output
|
||||
across runs.
|
||||
|
||||
#### Scenario: renderProjectFile output is byte-stable
|
||||
|
||||
- **WHEN** `renderProjectFile("package-json", validOptions)` runs twice
|
||||
- **THEN** both invocations return identical strings
|
||||
|
||||
#### Scenario: networking feature complexity selects the correct renderer
|
||||
|
||||
- **WHEN** `renderFeature("networking", { ...opts, networkingComplexity: "full" })`
|
||||
- **THEN** the output contains `state:` AND `governance:`
|
||||
|
||||
#### Scenario: r3f renderer emits the requested modules
|
||||
|
||||
- **WHEN** `renderFeature("r3f", { ...opts, r3fModules: ["physics","particles"] })`
|
||||
- **THEN** the output contains `@react-three/rapier` AND `Sparkles`
|
||||
|
||||
**Verified by:** `src/templates/templates.test.ts::"renderProjectFile output is byte-stable"` AND `src/templates/features.test.ts::"networking feature complexity selects the correct renderer"` AND `src/templates/features.test.ts::"r3f renderer emits the requested modules"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: FsAdapter MUST expose every operation the generator needs
|
||||
|
||||
The exported `FsAdapter` contract MUST expose at minimum
|
||||
`readFile(path): Promise<string>`, `writeFile(path, content): Promise<void>`,
|
||||
`mkdir(path, opts): Promise<void>`, `stat(path): Promise<{ isDirectory(): boolean }>`,
|
||||
`readdir(path): Promise<readonly string[]>`. The `nodeFsAdapter` default MUST be
|
||||
backed by `node:fs/promises` and MUST round-trip a real file under a real
|
||||
`mkdtemp` directory. A `writeFile` failure MUST surface as
|
||||
`ELECTROBUN.FS_WRITE_FAILED`.
|
||||
|
||||
#### Scenario: nodeFsAdapter round-trips a file
|
||||
|
||||
- **WHEN** `await fs.writeFile(p, "x")` then `await fs.readFile(p)`
|
||||
- **THEN** the read returns `"x"`
|
||||
|
||||
#### Scenario: writeFiles surfaces FS_WRITE_FAILED on adapter throw
|
||||
|
||||
- **WHEN** `writeFiles(outputDir, files, { fs: throwingFs })` runs
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.FS_WRITE_FAILED"`
|
||||
is thrown
|
||||
|
||||
**Verified by:** `src/fs/fs.test.ts::"nodeFsAdapter round-trips a file"` AND `src/fs/fs.test.ts::"writeFiles surfaces FS_WRITE_FAILED on adapter throw"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CommandRunner MUST shape every electrobun shell-out
|
||||
|
||||
The exported `CommandRunner` contract MUST be
|
||||
`(input: CommandInput) => Promise<CommandResult>` where `CommandInput` carries
|
||||
`{ command, args, cwd?, env?, signal? }` and `CommandResult` carries
|
||||
`{ exitCode, stdout, stderr, durationMs }`. `probeElectrobun(runner)` MUST
|
||||
invoke the runner with `command="bunx"` and `args=["electrobun","--version"]`
|
||||
and return `{ available: boolean; version?: string }`. A non-zero exit MUST
|
||||
classify as `available === false`. The `nodeCommandRunner` default MUST spawn
|
||||
via `node:child_process.spawn` and resolve stdout / stderr as UTF-8 strings.
|
||||
|
||||
#### Scenario: probeElectrobun returns available with version on success
|
||||
|
||||
- **WHEN** `probeElectrobun(runnerReturning({ exitCode: 0, stdout: "1.2.3\n" }))`
|
||||
- **THEN** the result is `{ available: true, version: "1.2.3" }`
|
||||
|
||||
#### Scenario: probeElectrobun returns unavailable on non-zero exit
|
||||
|
||||
- **WHEN** `probeElectrobun(runnerReturning({ exitCode: 127, stdout: "" }))`
|
||||
- **THEN** the result is `{ available: false }`
|
||||
|
||||
**Verified by:** `src/runner/runner.test.ts::"probeElectrobun returns available with version on success"` AND `src/runner/runner.test.ts::"probeElectrobun returns unavailable on non-zero exit"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: dev / build / package / sign commands MUST orchestrate the binary
|
||||
|
||||
Each of `devCommand`, `buildCommand`, `packageCommand`, `signCommand` MUST:
|
||||
|
||||
1. validate its option schema (raising `ELECTROBUN.INVALID_OPTIONS` on failure);
|
||||
2. probe the electrobun binary (raising `ELECTROBUN.BINARY_UNAVAILABLE` when
|
||||
absent);
|
||||
3. spawn the binary with the canonical argv shape (`dev` → `electrobun dev`;
|
||||
`build` → `electrobun build --target <target> [--update]`; `package` →
|
||||
`electrobun package --target <target>`; `sign` → `electrobun sign --identity
|
||||
<identity> [--entitlements <path>] [--notarize]`);
|
||||
4. classify a non-zero exit as `ELECTROBUN.RUNNER_FAILED` with `details.command`,
|
||||
`details.exitCode`, and `details.stderr`;
|
||||
5. record one observation in `electrobun_runner_duration_ms` and one increment
|
||||
in `electrobun_runner_invocations_total` per call;
|
||||
6. return a `*Report` carrying `{ command, exitCode, durationMs, stdout, stderr }`.
|
||||
|
||||
#### Scenario: buildCommand passes --update when options.update is true
|
||||
|
||||
- **WHEN** `buildCommand({ cwd, target: "mac", update: true }, { runner: recordingRunner })`
|
||||
- **THEN** the recorded args include `"--update"`
|
||||
|
||||
#### Scenario: signCommand forwards identity, entitlements, and notarize
|
||||
|
||||
- **WHEN** `signCommand({ cwd, identity: "Dev ID", entitlements: "/e.plist", notarize: true }, deps)`
|
||||
- **THEN** the recorded args contain `"--identity"`, `"Dev ID"`,
|
||||
`"--entitlements"`, `"/e.plist"`, `"--notarize"`
|
||||
|
||||
#### Scenario: non-zero exit raises RUNNER_FAILED
|
||||
|
||||
- **WHEN** `devCommand({ cwd }, { runner: failingRunner })` runs
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.RUNNER_FAILED"` is
|
||||
thrown AND `details.exitCode` is preserved
|
||||
|
||||
#### Scenario: missing binary raises BINARY_UNAVAILABLE
|
||||
|
||||
- **WHEN** `buildCommand({ cwd, target: "mac" }, { runner: missingBinaryRunner })`
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.BINARY_UNAVAILABLE"`
|
||||
is thrown
|
||||
|
||||
**Verified by:** `src/cli/cli.test.ts::"buildCommand passes --update when options.update is true"` AND `src/cli/cli.test.ts::"signCommand forwards identity, entitlements, and notarize"` AND `src/cli/cli.test.ts::"non-zero exit raises RUNNER_FAILED"` AND `src/cli/cli.test.ts::"missing binary raises BINARY_UNAVAILABLE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: runPrompts MUST drive the interactive scaffolder via the injectable Prompter
|
||||
|
||||
`runPrompts(config, outputDir, prompter?)` MUST prompt for `name`, `identifier`,
|
||||
`description`, `version`, `iconSource`, `features`, `networkingComplexity` (only
|
||||
when `features` includes `"networking"`), `r3fModules` (only when `features`
|
||||
includes `"r3f"`), and a `proceed` confirmation. A negative confirmation MUST
|
||||
surface as `ELECTROBUN.PROMPT_ABORTED`. The function MUST default
|
||||
`networkingComplexity` to `"basic"` and `r3fModules` to `[]` when the gating
|
||||
feature is not selected.
|
||||
|
||||
#### Scenario: runPrompts assembles a complete ProjectOptions
|
||||
|
||||
- **WHEN** `runPrompts(emptyConfig, undefined, scriptedPrompter)` runs
|
||||
- **THEN** the returned object satisfies `ProjectOptionsSchema.safeParse(result).success === true`
|
||||
|
||||
#### Scenario: negative confirmation aborts the prompt
|
||||
|
||||
- **WHEN** `runPrompts(emptyConfig, undefined, scriptedPrompter({ proceed: false }))`
|
||||
- **THEN** an `ElectrobunError` with `code === "ELECTROBUN.PROMPT_ABORTED"`
|
||||
is thrown
|
||||
|
||||
**Verified by:** `src/prompts/prompts.test.ts::"runPrompts assembles a complete ProjectOptions"` AND `src/prompts/prompts.test.ts::"negative confirmation aborts the prompt"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: runCli MUST dispatch every subcommand and never call process.exit
|
||||
|
||||
`runCli(argv, opts?)` MUST recognise the subcommands `create`, `dev`, `build`,
|
||||
`package`, `sign`, and `--help` / `-h`. Unknown subcommands or unknown flags
|
||||
MUST raise `ELECTROBUN.UNKNOWN_FLAG`. The function MUST return
|
||||
`{ code: 0 }` on success and `{ code: 1 }` on a handled
|
||||
`ElectrobunError`. No code path inside `runCli` MUST call
|
||||
`process.exit`; only the binary entry MUST own the exit.
|
||||
|
||||
#### Scenario: --help returns code 0
|
||||
|
||||
- **WHEN** `runCli(["--help"], { out: capture })` runs
|
||||
- **THEN** the result is `{ code: 0 }` AND `capture` recorded a help string
|
||||
|
||||
#### Scenario: unknown subcommand raises UNKNOWN_FLAG
|
||||
|
||||
- **WHEN** `runCli(["zzz"])` runs
|
||||
- **THEN** the result is `{ code: 1 }` AND the captured stderr contains
|
||||
`"ELECTROBUN.UNKNOWN_FLAG"`
|
||||
|
||||
**Verified by:** `src/cli/cli.test.ts::"--help returns code 0"` AND `src/cli/cli.test.ts::"unknown subcommand raises UNKNOWN_FLAG"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Telemetry MUST emit the documented OTel surface
|
||||
|
||||
The package MUST expose `electrobun_projects_generated_total`,
|
||||
`electrobun_icons_generated_total`, `electrobun_runner_invocations_total`,
|
||||
`electrobun_runner_failures_total`, `electrobun_prompts_total` counters and
|
||||
`electrobun_generate_duration_ms`, `electrobun_runner_duration_ms` histograms,
|
||||
each on the `@suaveplan/electrobun` meter, and MUST provide `recordProject`,
|
||||
`recordIcons`, `recordRunnerInvocation`, `recordRunnerFailure`,
|
||||
`recordPromptOutcome`, `recordGenerateDuration`, `recordRunnerDuration` helpers
|
||||
that increment / observe with the documented label keys.
|
||||
|
||||
#### Scenario: recordRunnerInvocation increments the counter once per call
|
||||
|
||||
- **WHEN** `recordRunnerInvocation("build")`
|
||||
- **THEN** the in-memory `MeterProvider` reports exactly one data point on
|
||||
`electrobun_runner_invocations_total` with `command="build"`
|
||||
|
||||
#### Scenario: recordGenerateDuration writes to the histogram
|
||||
|
||||
- **WHEN** `recordGenerateDuration(123, 2)`
|
||||
- **THEN** the in-memory `MeterProvider` reports one observation of `123`
|
||||
on `electrobun_generate_duration_ms` with `features_count="2"`
|
||||
|
||||
**Verified by:** `src/telemetry/telemetry.test.ts::"recordRunnerInvocation increments the counter once per call"` AND `src/telemetry/telemetry.test.ts::"recordGenerateDuration writes to the histogram"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Package MUST be server-tier with per-feature subpath exports
|
||||
|
||||
The `package.json` MUST declare `"type": "module"`, `"sideEffects": false`,
|
||||
`bin: { "suaveplan-electrobun": "./dist/bin/suaveplan-electrobun.js" }`, and
|
||||
subpath exports for `.`, `./error-codes`, `./telemetry`, `./types`, `./schemas`,
|
||||
`./fs`, `./runner`, `./config`, `./templates`, `./icons`, `./generator`,
|
||||
`./prompts`, `./cli`. Internal deps MUST use `workspace:^`; external deps MUST
|
||||
use `catalog:*` references (`zod` → `forms`, `@opentelemetry/api` → `observability`,
|
||||
`tslib` → `build`, `iconz` → `cli`). No bare version pins.
|
||||
|
||||
#### Scenario: package.json declares the documented subpath exports
|
||||
|
||||
- **WHEN** the consumer reads `Object.keys(pkg.exports).filter(k => k.startsWith("./") && k !== "./package.json")`
|
||||
- **THEN** the result contains all thirteen documented subpaths
|
||||
|
||||
#### Scenario: bin entry resolves to the published binary
|
||||
|
||||
- **WHEN** the consumer reads `pkg.bin["suaveplan-electrobun"]`
|
||||
- **THEN** the value is `"./dist/bin/suaveplan-electrobun.js"`
|
||||
|
||||
**Verified by:** `tests/package-shape.test.ts::"package.json declares the documented subpath exports"` AND `tests/package-shape.test.ts::"bin entry resolves to the published binary"`
|
||||
Reference in New Issue
Block a user