first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
# @suaveplan/test-vite - Vite + Vitest configuration factories and validators: createViteLibraryConfig/createViteAppConfig/createVitestConfig/createVitestBrowserConfig, browser-environment shims, tree-shake validator, bundle-size snapshot, port allocator, HMR tester, custom matchers, TEST_VITE_CODES error registry, and OTel self-telemetry.
Vite + Vitest configuration factories and validators for Suaveplan: `createViteLibraryConfig`, `createViteAppConfig`, `createVitestConfig`, `createVitestBrowserConfig`, `setupBrowserEnvironment`, tree-shake validator, bundle-size snapshot, port allocator, HMR tester, custom matchers, `TEST_VITE_CODES` error registry, and OTel self-telemetry.
<!-- begin:auto-docs -->
## Documentation tree
- [browser-environment](./src/browser-environment/browser-environment.md) — `setupBrowserEnvironment` installs the DOM and Web-platform shims that suaveplan browser packages depend on when running…
- [browser-environment](./src/browser-environment/browser-environment.md) — `setupBrowserEnvironment` installs the DOM and Web-platform shims that suaveplan browser packages depend on when running…
- [bundle-size-snapshot](./src/bundle-size-snapshot/bundle-size-snapshot.md) — `snapshotBundleSize` compares the byte size of an emitted bundle against a previously-recorded snapshot file and enforc…
- [bundle-size-snapshot](./src/bundle-size-snapshot/bundle-size-snapshot.md) — `snapshotBundleSize` compares the byte size of an emitted bundle against a previously-recorded snapshot file and enforc…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` exports the canonical stable error-code registry used by every other module in `@suaveplan/test-vite`. Th…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` exports the canonical stable error-code registry used by every other module in `@suaveplan/test-vite`. Th…
- [hmr-tester](./src/hmr-tester/hmr-tester.md) — `withHMR` round-trips a single Vite HMR edit through an injectable `HmrServerLike` adapter and asserts that the expecte…
- [hmr-tester](./src/hmr-tester/hmr-tester.md) — `withHMR` round-trips a single Vite HMR edit through an injectable `HmrServerLike` adapter and asserts that the expecte…
- [matchers](./src/matchers/matchers.md) — `createMatchers` returns a bundle of typed `expect.extend` matchers used across suaveplan browser-tier test suites. The m…
- [matchers](./src/matchers/matchers.md) — `createMatchers` returns a bundle of typed `expect.extend` matchers used across suaveplan browser-tier test suites. The m…
- [port-allocator](./src/port-allocator/port-allocator.md) — `createPortAllocator` builds a deterministic port allocator over a contiguous TCP range backed by an injectable store.…
- [port-allocator](./src/port-allocator/port-allocator.md) — `createPortAllocator` builds a deterministic port allocator over a contiguous TCP range backed by an injectable store.…
- [telemetry](./src/telemetry/telemetry.md) — `telemetry` registers the OTel meter for `@suaveplan/test-vite` and exposes the counters and histogram that every other…
- [telemetry](./src/telemetry/telemetry.md) — `telemetry` registers the OTel meter for `@suaveplan/test-vite` and exposes the counters and histogram that every other…
- [tree-shake-validator](./src/tree-shake-validator/tree-shake-validator.md) — `verifyTreeShake` reads a built bundle and asserts that its set of exported symbols matches the caller-supplied allow-l…
- [tree-shake-validator](./src/tree-shake-validator/tree-shake-validator.md) — `verifyTreeShake` reads a built bundle and asserts that its set of exported symbols matches the caller-supplied allow-l…
- [vite-app-config](./src/vite-app-config/vite-app-config.md) — `createViteAppConfig` builds a Vite `UserConfig` tailored for consumer applications rather than library packages. It de…
- [vite-app-config](./src/vite-app-config/vite-app-config.md) — `createViteAppConfig` builds a Vite `UserConfig` tailored for consumer applications rather than library packages. It de…
- [vite-library-config](./src/vite-library-config/vite-library-config.md) — `createViteLibraryConfig` builds an opinionated Vite `UserConfig` suitable for any `@suaveplan/*` library package. It e…
- [vite-library-config](./src/vite-library-config/vite-library-config.md) — `createViteLibraryConfig` builds an opinionated Vite `UserConfig` suitable for any `@suaveplan/*` library package. It e…
- [vitest-browser-config](./src/vitest-browser-config/vitest-browser-config.md) — `createVitestBrowserConfig` builds a Vitest configuration that drives tests through Playwright in real browsers. The fa…
- [vitest-browser-config](./src/vitest-browser-config/vitest-browser-config.md) — `createVitestBrowserConfig` builds a Vitest configuration that drives tests through Playwright in real browsers. The fa…
- [vitest-config](./src/vitest-config/vitest-config.md) — `createVitestConfig` returns a WSL2-safe Vitest configuration with the suaveplan enterprise contract baked in. The factor…
- [vitest-config](./src/vitest-config/vitest-config.md) — `createVitestConfig` returns a WSL2-safe Vitest configuration with the suaveplan enterprise contract baked in. The factor…
<!-- end:auto-docs -->
## Why
Every browser-tier Suaveplan package needs the same Vite library build + WSL2-safe Vitest configuration. Inlining that copy across hundreds of packages is brittle — when the project learns a new safety default (Istanbul, `pool: 'forks'`, `fileParallelism`, the `external` baseline), each package has to be updated by hand. `@suaveplan/test-vite` owns those factories in one place.
## Features
- Opinionated Vite library build (`createViteLibraryConfig`) with `preserveModules`, dts emission, and the `external` baseline (`/^@suaveplan\//`, `/^node:/`, `'tslib'`).
- Vite consumer-app config (`createViteAppConfig`) with hashed assets and `envPrefix`.
- WSL2-safe Vitest config (`createVitestConfig`) — `pool: 'forks'`, Istanbul provider, 100/100/100/100 thresholds.
- Browser-mode Vitest config (`createVitestBrowserConfig`) driven by Playwright.
- DOM shims (`setupBrowserEnvironment`) — idempotent installer for `localStorage`, `matchMedia`, `ResizeObserver`, `IntersectionObserver`, `crypto`.
- Tree-shake validator (`verifyTreeShake`) — guard the exported-symbol set.
- Bundle-size snapshot (`snapshotBundleSize`) — track build-size drift with tolerance.
- Port allocator (`createPortAllocator`) — coordinate parallel servers on WSL2.
- HMR tester (`withHMR`) — round-trip a Vite HMR cycle in tests.
- Typed `expect.extend` matchers — `toBeAccessibleNode`, `toMatchDataURL`, `toHaveValidCSSVars`.
- Stable `TEST_VITE_CODES` registry + `TestViteError`.
- OTel self-telemetry — counters + histogram, no-op when no provider is registered.
## Install
```ts
import { createViteLibraryConfig } from "@suaveplan/test-vite/vite-library-config";
import { createVitestConfig } from "@suaveplan/test-vite/vitest-config";
```
Every feature module is its own subpath export for tree-shake friendliness.
## License
MIT

View File

@@ -0,0 +1,54 @@
# browser-environment
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`setupBrowserEnvironment` installs the DOM and Web-platform shims that suaveplan browser packages depend on when running under happy-dom, jsdom, or bare Node. The function is idempotent — re-running it on a target that already has real implementations is a no-op — so it is safe to call from package setup files without worrying about clobbering whatever the test runner has already installed.
## Features
- Idempotent installer: real implementations are preserved.
- Per-shim opt-out via `mock*` flags.
- Injectable `target` parameter for unit testing.
- Backed by Node's `webcrypto` for the `crypto` shim — no third-party dependency.
- Memory-backed `localStorage` implementing the standard `Storage` shape.
## Usage: Basic
```ts
import { setupBrowserEnvironment } from "@suaveplan/test-vite/browser-environment";
setupBrowserEnvironment();
```
## Usage: Advanced
Install only the shims your tests need, against a synthetic target:
```ts
import { setupBrowserEnvironment } from "@suaveplan/test-vite/browser-environment";
const target = {} as Record<string, unknown>;
setupBrowserEnvironment({
target,
mockLocalStorage: true,
mockMatchMedia: true,
mockResizeObserver: true,
mockIntersectionObserver: false,
mockCrypto: true,
});
```
## API Reference
- `setupBrowserEnvironment(opts?: BrowserEnvironmentOptions): void`.
- `BrowserEnvironmentOptions` — `{ target?, mockLocalStorage?, mockMatchMedia?, mockResizeObserver?, mockIntersectionObserver?, mockCrypto? }`. All booleans default to `true`.
- Throws `TestViteError` with code `TEST_VITE.INVALID_OPTIONS` on unknown options.
## Implementation Notes
`MemoryStorage` keeps a `Map<string, string>` rather than a plain object so insertion order is preserved across `key(index)` calls. The `matchMedia` stub returns `matches: false` for every query so tests do not accidentally trigger media-query-conditional behaviour without explicit opt-in. Observer stubs implement no-op `observe` / `unobserve` / `disconnect` methods so client code that wires them up does not crash. The crypto shim delegates to Node's standard `webcrypto` import to avoid bundling extra runtime weight.

View File

@@ -0,0 +1,60 @@
# bundle-size-snapshot
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`snapshotBundleSize` compares the byte size of an emitted bundle against a previously-recorded snapshot file and enforces a configurable growth tolerance. It is the bundle-size equivalent of a Vitest snapshot — when the bundle grows past tolerance the call throws `TestViteError(TEST_VITE.SIZE_BUDGET_EXCEEDED)` so CI fails with a clear diagnostic. Run with `update: true` from a developer machine to refresh the snapshot after an intentional change.
## Features
- Default 5% growth tolerance (`tolerance: 0.05`).
- `update: true` mode writes the current size to the snapshot file.
- Injectable filesystem adapter so the validator can run in environments without `node:fs`.
- ENOENT-aware error mapping: missing snapshot → `SNAPSHOT_MISSING`; missing bundle → `MISSING_ENTRY`.
- OTel counters: `test_vite_validator_runs_total{kind="bundle_size"}` per call, `test_vite_validator_violations_total` on budget violations.
## Usage: Basic
```ts
import { readFile, writeFile } from "node:fs/promises";
import { snapshotBundleSize } from "@suaveplan/test-vite/bundle-size-snapshot";
const fs = {
readFile: (p: string) => readFile(p),
writeFile: (p: string, data: string) => writeFile(p, data, "utf8"),
};
await snapshotBundleSize({
entry: "dist/index.js",
snapshot: "dist/index.size.snap",
fs,
});
```
## Usage: Advanced
Refresh the snapshot after a deliberate change:
```ts
await snapshotBundleSize({
entry: "dist/index.js",
snapshot: "dist/index.size.snap",
tolerance: 0.02,
update: true,
fs,
});
```
## API Reference
- `snapshotBundleSize(opts: SnapshotBundleSizeOptions): Promise<BundleSizeResult>`.
- `SnapshotBundleSizeOptions` — `{ entry, snapshot, tolerance?, update?, fs }`.
- `BundleSizeResult` — `{ updated: boolean, size: number, delta?: number }`.
- Throws `TestViteError` with `TEST_VITE.INVALID_OPTIONS`, `TEST_VITE.MISSING_ENTRY`, `TEST_VITE.SNAPSHOT_MISSING`, or `TEST_VITE.SIZE_BUDGET_EXCEEDED`.
## Implementation Notes
The snapshot file stores a single decimal integer — the byte count of the last accepted bundle. Whitespace is trimmed before parsing so editors that auto-add a trailing newline do not break the comparison. Read failures on the snapshot are uniformly classified as `SNAPSHOT_MISSING` whether the cause is ENOENT or another IO error so the diagnostic stays consistent across platforms.

View File

@@ -0,0 +1,64 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`error-codes` exports the canonical stable error-code registry used by every other module in `@suaveplan/test-vite`. The registry is built with `@suaveplan/error`'s `defineCodes` so each code value becomes a namespaced string of the form `TEST_VITE.<REASON>`. This namespace survives module boundaries, structured cloning, RPC boundaries, and JSON serialisation, so callers can discriminate on `error.code` rather than relying on prototype chains. The companion `TestViteError` class extends `SuaveplanError<TestViteCode>` so static analysis narrows `err.code` to the literal union of registered codes.
## Features
- 8-code namespaced registry: `INVALID_OPTIONS`, `MISSING_ENTRY`, `TREE_SHAKE_VIOLATION`, `SIZE_BUDGET_EXCEEDED`, `SNAPSHOT_MISSING`, `PORT_EXHAUSTED`, `PORT_NOT_HELD`, `HMR_TIMEOUT`.
- `TestViteError` class extending `SuaveplanError<TestViteCode>` with a public constructor.
- Structured `details` payload propagated as the `SuaveplanError` `context` field.
- Cause-chain support via the standard `Error.cause` constructor option.
- No marketing language — each code names a concrete, reachable failure mode.
## Usage: Basic
```ts
import { TEST_VITE_CODES, TestViteError } from "@suaveplan/test-vite/error-codes";
try {
doSomething();
} catch (err) {
if (err instanceof TestViteError && err.code === TEST_VITE_CODES.INVALID_OPTIONS) {
console.error("bad options", err.details);
}
}
```
## Usage: Advanced
Discriminate on the namespaced code value directly so the handler survives RPC and serialisation:
```ts
import { TEST_VITE_CODES } from "@suaveplan/test-vite/error-codes";
function isTestViteError(err: unknown): err is { code: string; details: Record<string, unknown> } {
return typeof err === "object" && err !== null && typeof (err as { code?: unknown }).code === "string";
}
function classify(err: unknown): "retry" | "abort" {
if (!isTestViteError(err)) return "abort";
switch (err.code) {
case TEST_VITE_CODES.PORT_EXHAUSTED:
case TEST_VITE_CODES.HMR_TIMEOUT:
return "retry";
default:
return "abort";
}
}
```
## API Reference
- `TEST_VITE_CODES` — `Readonly<Record<string, TestViteCode>>` built via `defineCodes("TEST_VITE", [...])`.
- `TestViteCode` — string literal union of every registered code.
- `TestViteError extends SuaveplanError<TestViteCode>` — `new TestViteError(code, message, { cause?, details? })`. Exposes `code: TestViteCode`, `message: string`, `details: Record<string, unknown>`, and inherits `timestamp`, `name`, `cause` from `SuaveplanError` / `Error`.
## Implementation Notes
The constructor passes the `details` payload to `SuaveplanError` as `context` so the OTel span-event adapter picks it up automatically. We mirror the payload on a public `details` field for ergonomic access from non-TypeScript consumers. `cause` is forwarded only when supplied so the standard `Error.prototype.cause` behaviour stays clean.

View File

@@ -0,0 +1,65 @@
# hmr-tester
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`withHMR` round-trips a single Vite HMR edit through an injectable `HmrServerLike` adapter and asserts that the expected events fire in order. It is designed for tests that exercise HMR-driven UI logic without spinning up a real Vite dev server — the adapter can wrap an in-memory event bus, a Playwright page, or a real `ws` connection depending on the testing strategy.
## Features
- Injectable server adapter (`send` + `onEvent`).
- Ordered event matching — events fire in the order specified in `expectEvents`.
- Configurable `timeoutMs` with `TestViteError(TEST_VITE.HMR_TIMEOUT)` on expiry.
- Injectable `Clock` for deterministic duration accounting.
- OTel `test_vite_hmr_round_trip_ms` histogram recorded on every resolution.
## Usage: Basic
```ts
import { withHMR } from "@suaveplan/test-vite/hmr-tester";
const events = await withHMR({
server: viteServerAdapter,
edit: { file: "src/widget.ts", content: "..." },
expectEvents: ["vite:beforeUpdate", "vite:afterUpdate"],
timeoutMs: 5000,
});
```
## Usage: Advanced
Drive a synthetic server in tests:
```ts
import { withHMR } from "@suaveplan/test-vite/hmr-tester";
let listener: ((event: string) => void) | undefined;
const server = {
send: () => setTimeout(() => listener?.("vite:afterUpdate"), 1),
onEvent: (cb: (event: string) => void) => {
listener = cb;
return () => { listener = undefined; };
},
};
await withHMR({
server,
edit: { file: "a.ts", content: "1" },
expectEvents: ["vite:afterUpdate"],
timeoutMs: 100,
clock: () => Date.now(),
});
```
## API Reference
- `withHMR(opts: WithHmrOptions): Promise<readonly string[]>`.
- `WithHmrOptions` — `{ server, edit: { file, content }, expectEvents, timeoutMs?, clock? }`.
- Throws `TestViteError` with `TEST_VITE.INVALID_OPTIONS` or `TEST_VITE.HMR_TIMEOUT`.
## Implementation Notes
`withHMR` matches expected events in order — non-matching events are captured but do not advance the cursor. Once the final expected event fires the promise resolves with the complete capture array, including any non-matching events that arrived along the way. The timer is cleared and the `onEvent` disposer invoked before resolution so the caller does not have to wire teardown. Test consumers can swap the clock to record deterministic durations and exercise edge cases without real wall-clock delays.

View File

@@ -0,0 +1,61 @@
# matchers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createMatchers` returns a bundle of typed `expect.extend` matchers used across suaveplan browser-tier test suites. The matchers cover three recurring assertions that come up often enough that hand-rolled equivalents drift across packages: accessibility hooks on rendered nodes, base64-encoded data URLs with a mime guard, and CSS-custom-property availability on a host element.
## Features
- `toBeAccessibleNode(node)` — passes when the node exposes `role`, `aria-label`, or `aria-labelledby` as a non-empty string.
- `toMatchDataURL(value, mimePattern)` — passes when the value is a `data:<mime>;base64,<payload>` URL and `mime` matches the supplied regex.
- `toHaveValidCSSVars(host, names)` — passes when every name in `names` is set on `host.style` as a non-empty string.
- Each matcher returns the Vitest contract `{ pass, message }` so failures surface descriptive diagnostics.
- Universal — works in node + jsdom + happy-dom without DOM lookups.
## Usage: Basic
```ts
import { expect } from "vitest";
import { createMatchers } from "@suaveplan/test-vite/matchers";
expect.extend(createMatchers());
const button = { role: "button", "aria-label": "Close" };
expect(button).toBeAccessibleNode();
```
## Usage: Advanced
Custom matchers in a server-rendered snapshot test:
```ts
import { createMatchers } from "@suaveplan/test-vite/matchers";
const matchers = createMatchers();
const widget = renderToObject();
const accessibility = matchers.toBeAccessibleNode(widget);
if (!accessibility.pass) {
throw new Error(accessibility.message());
}
const cssVars = matchers.toHaveValidCSSVars(widget, ["--brand", "--accent"]);
if (!cssVars.pass) {
throw new Error(cssVars.message());
}
```
## API Reference
- `createMatchers(): TestViteMatchers`.
- `TestViteMatchers.toBeAccessibleNode(received): MatcherResult`.
- `TestViteMatchers.toMatchDataURL(received, mimePattern): MatcherResult`.
- `TestViteMatchers.toHaveValidCSSVars(received, names): MatcherResult`.
## Implementation Notes
The data-URL parser uses a strict regex that allows only base64 payload characters (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`); URL-encoded data URLs are intentionally rejected because base64 is the only payload format suaveplan components emit. The CSS-vars matcher inspects the host's `style` record directly rather than calling `getComputedStyle`, so it works on both real DOM nodes and synthetic objects.

View File

@@ -0,0 +1,54 @@
# port-allocator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createPortAllocator` builds a deterministic port allocator over a contiguous TCP range backed by an injectable store. It is designed to coordinate parallel Playwright / Storybook / Vite-server runs on WSL2, where naïve `random-port` allocation strategies collide under load. The store is injectable so production callers can plug in a file-backed lock store to coordinate across processes while tests use the default in-memory map.
## Features
- Lowest-port-first allocation (deterministic, debuggable).
- Injectable `PortStore` for file-backed cross-process coordination.
- `PORT_EXHAUSTED` / `PORT_NOT_HELD` errors with stable codes.
- `snapshot()` returns the current set of held ports sorted ascending.
- Zod-validated boundary; bad input throws `TestViteError(TEST_VITE.INVALID_OPTIONS)`.
## Usage: Basic
```ts
import { createPortAllocator } from "@suaveplan/test-vite/port-allocator";
const allocator = createPortAllocator({ range: [5000, 5100] });
const port = allocator.allocate();
try {
await startServer(port);
} finally {
allocator.release(port);
}
```
## Usage: Advanced
Coordinate across processes via a file-backed `PortStore`:
```ts
import { createPortAllocator } from "@suaveplan/test-vite/port-allocator";
import type { PortStore } from "@suaveplan/test-vite";
const fileStore: PortStore = makeFileLockStore("/tmp/suaveplan-ports.lock");
const allocator = createPortAllocator({ range: [5000, 5100], store: fileStore });
```
## API Reference
- `createPortAllocator(opts: PortAllocatorOptions): PortAllocator`.
- `PortAllocatorOptions` — `{ range: [start, end], store? }`.
- `PortAllocator` — `{ allocate(): number, release(port: number): void, snapshot(): readonly number[] }`.
- Throws `TestViteError` with `TEST_VITE.INVALID_OPTIONS`, `TEST_VITE.PORT_EXHAUSTED`, or `TEST_VITE.PORT_NOT_HELD`.
## Implementation Notes
`MemoryPortStore` uses a `Set<number>` so `has` / `add` / `delete` are O(1). The snapshot is sorted before return so callers can rely on a stable ordering. The allocator does not bind real sockets — it is a coordination primitive, not a port prober — so callers that need defence against external bindings should wrap `allocate()` with a probing helper that retries on failure.

View File

@@ -0,0 +1,60 @@
# telemetry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`telemetry` registers the OTel meter for `@suaveplan/test-vite` and exposes the counters and histogram that every other module records against. Instruments are bound lazily through `metrics.getMeter("@suaveplan/test-vite")` so importing the module is safe when no `MeterProvider` is registered — in that case the OTel API returns a no-op meter and recordings become inexpensive no-ops. Tests register a real in-memory `MeterProvider` before module import so they can assert exported metric points.
## Features
- Four typed `Counter` instruments + one `Histogram`.
- Lazy meter binding — production callers can opt out of OTel entirely.
- Tag-aware recorders (`recordConfigBuilt(kind)`, `recordValidatorRun(kind)`) so dashboards can split by factory kind.
- Wired into every public call site so the §6 spec verification can grep for usage.
## Usage: Basic
```ts
import { recordConfigBuilt } from "@suaveplan/test-vite/telemetry";
recordConfigBuilt("vitest"); // increments test_vite_config_built_total{kind="vitest"}
```
## Usage: Advanced
Register an in-memory exporter before module import to assert metric points in tests:
```ts
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 });
metrics.setGlobalMeterProvider(new MeterProvider({ readers: [reader] }));
// Import test-vite AFTER the provider is registered so the meter binds to it.
const { createVitestConfig } = await import("@suaveplan/test-vite/vitest-config");
createVitestConfig({});
```
## API Reference
- `configBuiltCounter`, `validatorRunsCounter`, `validatorViolationsCounter`, `portAllocationsCounter` — `Counter`.
- `hmrRoundTripHistogram` — `Histogram` in milliseconds.
- `recordConfigBuilt(kind: TestViteConfigKind): void` — `kind` ∈ `"vite_library" | "vite_app" | "vitest" | "vitest_browser"`.
- `recordValidatorRun(kind: TestViteValidatorKind): void` — `kind` ∈ `"tree_shake" | "bundle_size"`.
- `recordValidatorViolation(kind: TestViteValidatorKind): void`.
- `recordPortAllocation(): void`.
- `recordHmrRoundTrip(durationMs: number): void`.
## Implementation Notes
Counters use `add(1, attrs)` so cumulative-temporality exporters report stable point values. The histogram records milliseconds (`unit: 'ms'`) so dashboards can display it directly without unit conversion. Recorders are split from the instrument constants so call sites cannot accidentally pass a wrong shape — every recorder narrows its `kind` argument to the literal-union type at the boundary.

View File

@@ -0,0 +1,55 @@
# tree-shake-validator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`verifyTreeShake` reads a built bundle and asserts that its set of exported symbols matches the caller-supplied allow-list. Tree-shake regressions are silent failures — the package compiles, tests pass, but downstream consumers suddenly ship a hundred extra kilobytes. The validator catches drift at build time by parsing the emitted bundle for `export const|function|class|let|var` statements and `export { … }` re-export lists, comparing the actual set against the expected set, and reporting `missing` / `unexpected` symbols.
## Features
- Parses both `export <kind> <name>` and `export { … }` re-export lists.
- Returns a discriminated `{ ok: true, actual }` / `{ ok: false, missing, unexpected }` result so callers can branch without `try/catch`.
- Optional `enforce: true` mode that throws `TestViteError(TEST_VITE.TREE_SHAKE_VIOLATION)` on drift.
- Injectable `readFile` so the validator never touches the filesystem directly.
- Maps `readFile` rejections to `TestViteError(TEST_VITE.MISSING_ENTRY)`.
- OTel counters: `test_vite_validator_runs_total{kind="tree_shake"}` per call, `test_vite_validator_violations_total` on drift.
## Usage: Basic
```ts
import { readFile } from "node:fs/promises";
import { verifyTreeShake } from "@suaveplan/test-vite/tree-shake-validator";
const result = await verifyTreeShake({
entry: "dist/index.js",
expectedSymbols: ["createWidget", "WidgetError"],
readFile: async (p) => (await readFile(p)).toString("utf8"),
});
```
## Usage: Advanced
CI guard that fails the build on drift:
```ts
await verifyTreeShake({
entry: "dist/index.js",
expectedSymbols: PUBLIC_API_ALLOWLIST,
readFile: async (p) => readFile(p, "utf8"),
enforce: true,
});
```
## API Reference
- `verifyTreeShake(opts: VerifyTreeShakeOptions): Promise<TreeShakeResult>`.
- `VerifyTreeShakeOptions` — `{ entry, expectedSymbols, readFile, enforce? }`.
- `TreeShakeResult` — `{ ok: true, actual } | { ok: false, actual, missing, unexpected }`.
- Throws `TestViteError` with `TEST_VITE.INVALID_OPTIONS`, `TEST_VITE.MISSING_ENTRY`, or `TEST_VITE.TREE_SHAKE_VIOLATION` (`enforce: true` only).
## Implementation Notes
The parser uses two complementary global regexes — one for inline declarations and one for `export { … }` lists — driven through `matchAll` so the regex internal state is not shared across calls. Aliases (`export { foo as bar }`) are normalised to the exported name (`bar`). The list of returned symbols is alphabetically sorted to make snapshot diffs stable.

View File

@@ -0,0 +1,54 @@
# test-vite.types
## Purpose
`types/test-vite.types.ts` is the canonical interface surface for cross-module structural types used by `@suaveplan/test-vite`. Every factory and validator references these shared types so plug-points like `FsAdapter`, `PortStore`, `HmrServerLike`, and `MatcherResult` stay consistent across feature folders. Centralising them here lets consumers depend on the structural contract without pulling in the implementation modules and keeps the package tree-shake-friendly.
## Features
- `ReadFileFn` — async byte/string reader for tree-shake validation.
- `FsAdapter` — minimal `readFile` / `writeFile` surface used by `bundle-size-snapshot`.
- `HmrServerLike` — `send` + `onEvent` contract used by `withHMR`.
- `PortStore` — pluggable store backing `createPortAllocator`.
- `PortAllocator` — return type of `createPortAllocator`.
- `TreeShakeResult` (discriminated `TreeShakeOk` | `TreeShakeDrift`) — return type of `verifyTreeShake`.
- `BundleSizeResult` — return type of `snapshotBundleSize`.
- `AccessibleNode`, `CssVarHost`, `MatcherResult` — matcher input/output shapes.
## Usage: Basic
```ts
import type { FsAdapter } from "@suaveplan/test-vite";
const fs: FsAdapter = {
async readFile(path) { /* … */ return new Uint8Array(); },
async writeFile(path, data) { /* … */ },
};
```
## Usage: Advanced
Define a custom port store backed by a file lock:
```ts
import type { PortStore } from "@suaveplan/test-vite";
class FilePortStore implements PortStore {
has(port: number): boolean { /* … */ return false; }
add(port: number): void { /* … */ }
delete(port: number): void { /* … */ }
*values(): IterableIterator<number> { /* … */ }
}
```
## API Reference
Every exported interface and type alias is structural — consumers can satisfy the contract without subclassing.
- `ReadFileFn`, `FsAdapter`, `HmrServerLike`, `PortStore`, `PortAllocator`.
- `TreeShakeOk`, `TreeShakeDrift`, `TreeShakeResult`, `BundleSizeResult`.
- `AccessibleNode`, `CssVarHost`, `MatcherResult`.
## Implementation Notes
Each interface uses the minimal viable surface so the adapters stay swappable. `FsAdapter.readFile` returns `Uint8Array` rather than `Buffer` to keep the type browser-tier compatible even though the production caller usually wraps `node:fs/promises`. The `PortStore` mirrors `Set<number>` so the default in-memory implementation can satisfy it without extra adapter code.

View File

@@ -0,0 +1,54 @@
# vite-app-config
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createViteAppConfig` builds a Vite `UserConfig` tailored for consumer applications rather than library packages. It defaults to hashed asset filenames so long-term caching works out of the box, targets `es2022` to match the project's TypeScript output, and configures `envPrefix` so the standard `VITE_*` (or any caller-supplied prefix) is the only env surface exposed to client bundles.
## Features
- Hashed asset / chunk / entry filenames under `assets/[name]-[hash][extname]`.
- `target: 'es2022'` default to match the `tsconfig.base.json` setting.
- `envPrefix` default `['VITE_']` with full opt-in override.
- Zod-validated option boundary; bad input throws `TestViteError(TEST_VITE.INVALID_OPTIONS)`.
- OTel counter `test_vite_config_built_total{kind="vite_app"}` incremented per invocation.
## Usage: Basic
```ts
// vite.config.ts
import { createViteAppConfig } from "@suaveplan/test-vite/vite-app-config";
export default createViteAppConfig({ root: __dirname });
```
## Usage: Advanced
Custom env prefix + plugins:
```ts
import react from "@vitejs/plugin-react";
import { createViteAppConfig } from "@suaveplan/test-vite/vite-app-config";
export default createViteAppConfig({
root: __dirname,
outDir: "dist",
target: "es2023",
envPrefix: ["APP_", "PUBLIC_"],
sourcemap: true,
plugins: [react()],
});
```
## API Reference
- `createViteAppConfig(opts: ViteAppConfigOptions): UserConfig`.
- `ViteAppConfigOptions` — `{ root, outDir?, target?, envPrefix?, sourcemap?, plugins? }`.
- Throws `TestViteError` with code `TEST_VITE.INVALID_OPTIONS` on Zod failure.
## Implementation Notes
The factory normalises `envPrefix` to a string array even when only one prefix is supplied so downstream tooling can iterate uniformly. Asset hashing uses the standard Rollup placeholders; consumers that need a different layout can spread the returned config and override `build.rollupOptions.output`. The factory does not register a framework plugin — pass `react()`, `vue()`, or another framework plugin through the `plugins` option so the factory stays framework-agnostic and you keep one source of truth for plugin order. The Zod boundary rejects unknown options strictly so typos surface immediately at build time rather than failing silently inside Vite.

View File

@@ -0,0 +1,63 @@
# vite-library-config
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createViteLibraryConfig` builds an opinionated Vite `UserConfig` suitable for any `@suaveplan/*` library package. It encodes the suaveplan conventions for tree-shake-friendly output, type-declaration emission via `vite-plugin-dts`, and the `external` baseline (`/^@suaveplan\//`, `/^node:/`, `'tslib'`). Every package in the project can drop a one-line `vite.config.ts` and inherit the contract: ESM-first output with `preserveModules`, named exports, and sourcemaps.
## Features
- `preserveModules: true` with `preserveModulesRoot` mirroring the source root.
- `vite-plugin-dts` plugin pre-pended ahead of caller-supplied plugins.
- `external` automatically merged with the suaveplan baseline.
- Zod-validated option boundary; bad input throws `TestViteError(TEST_VITE.INVALID_OPTIONS)`.
- OTel counter `test_vite_config_built_total{kind="vite_library"}` incremented per invocation.
## Usage: Basic
```ts
// vite.config.ts
import { createViteLibraryConfig } from "@suaveplan/test-vite/vite-library-config";
export default createViteLibraryConfig({
entries: { index: "src/index.ts" },
tsconfigPath: "./tsconfig.lib.json",
});
```
## Usage: Advanced
Multi-entry build with extra externals and caller plugins:
```ts
import path from "node:path";
import { createViteLibraryConfig } from "@suaveplan/test-vite/vite-library-config";
export default createViteLibraryConfig({
root: __dirname,
entries: {
index: "src/index.ts",
"submodule/index": "src/submodule/index.ts",
},
entryRoot: "src",
outDir: "dist",
tsconfigPath: "./tsconfig.lib.json",
external: ["zod", /^@opentelemetry\//],
plugins: [],
sourcemap: true,
emptyOutDir: true,
});
```
## API Reference
- `createViteLibraryConfig(opts: ViteLibraryConfigOptions): UserConfig`.
- `ViteLibraryConfigOptions` — `{ root?, entries, tsconfigPath?, entryRoot?, outDir?, external?, sourcemap?, emptyOutDir?, plugins? }`.
- Throws `TestViteError` with code `TEST_VITE.INVALID_OPTIONS` on Zod failure.
## Implementation Notes
The factory mutates nothing on the input; the returned config is freshly constructed. Rollup `output.entryFileNames` is set to `[name].js` so the emitted directory mirrors the source tree. The dts plugin is added with `insertTypesEntry: true` so a single `index.d.ts` shim is emitted for the package root regardless of how many entries are configured. The `external` baseline is intentionally merged via simple concatenation rather than de-duplication so caller-supplied entries always take precedence in Rollup's match order.

View File

@@ -0,0 +1,52 @@
# vitest-browser-config
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createVitestBrowserConfig` builds a Vitest configuration that drives tests through Playwright in real browsers. The factory encodes the suaveplan rules for browser-mode tests: `pool: 'forks'`, `fileParallelism: false` (required so Playwright workers do not contend), `provider: 'playwright'`, and headless-by-default. Packages opting into the browser runner inherit a known-good baseline and only have to declare the browser matrix they need.
## Features
- Playwright provider with caller-supplied browser matrix (`chromium`, `firefox`, `webkit`).
- Headless-by-default with explicit opt-out.
- `fileParallelism: false` pinned by the spec.
- Setup files accepted for environment bootstrap.
- Zod-validated boundary; bad input throws `TestViteError(TEST_VITE.INVALID_OPTIONS)`.
- OTel counter `test_vite_config_built_total{kind="vitest_browser"}` incremented per invocation.
## Usage: Basic
```ts
// vitest.browser.config.ts
import { createVitestBrowserConfig } from "@suaveplan/test-vite/vitest-browser-config";
export default createVitestBrowserConfig({});
```
## Usage: Advanced
Multi-browser matrix with custom include + setup:
```ts
import { createVitestBrowserConfig } from "@suaveplan/test-vite/vitest-browser-config";
export default createVitestBrowserConfig({
browsers: ["chromium", "firefox", "webkit"],
headless: false,
setupFiles: ["./tests/setup/browser.ts"],
include: ["src/**/*.browser.test.ts"],
});
```
## API Reference
- `createVitestBrowserConfig(opts?: VitestBrowserConfigOptions): UserConfig`.
- `VitestBrowserConfigOptions` — `{ browsers?, headless?, setupFiles?, include? }`.
- Throws `TestViteError` with code `TEST_VITE.INVALID_OPTIONS` on Zod failure (including empty browser list).
## Implementation Notes
The `browser.instances` array is built by mapping each requested browser into a `{ browser }` object so Vitest's browser runner can attach per-browser overrides if a caller spreads the result. The factory deliberately does not expose every Playwright option — packages with bespoke needs should spread the returned config and override the `browser` block. Vitest 4 requires `browser.provider` to be a provider factory imported from `@vitest/browser/providers/playwright`; to avoid a hard subpath dependency we accept that factory through `opts.provider`. When omitted we emit a tagged sentinel object that consumers can replace with a one-line spread before exporting their config.

View File

@@ -0,0 +1,55 @@
# vitest-config
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createVitestConfig` returns a WSL2-safe Vitest configuration with the suaveplan enterprise contract baked in. The factory exists so every package in the project can share the same coverage thresholds (100/100/100/100 on Istanbul), the same `pool: 'forks'` default required by Bun on WSL2, and the same setup-file plumbing without copy-pasting twenty-line config blocks.
## Features
- `pool: 'forks'` default (both `vmThreads` and `threads` fail under Bun on WSL2).
- Istanbul coverage provider with 100/100/100/100 thresholds.
- Reasonable `include` / `exclude` defaults that match the project layout.
- `setupFiles` accepted from opts so callers can wire `@suaveplan/testing/polyfills` or package-specific bootstrap.
- Configurable test/hook timeouts, `fileParallelism` switch, and `coverage.include`/`exclude` overrides.
- Zod-validated option boundary; bad input throws `TestViteError(TEST_VITE.INVALID_OPTIONS)`.
- OTel counter `test_vite_config_built_total{kind="vitest"}` incremented per invocation.
## Usage: Basic
```ts
// vitest.config.ts
import { createVitestConfig } from "@suaveplan/test-vite/vitest-config";
export default createVitestConfig({});
```
## Usage: Advanced
DOM tests with package-specific setup file:
```ts
import { createVitestConfig } from "@suaveplan/test-vite/vitest-config";
export default createVitestConfig({
environment: "happy-dom",
setupFiles: ["@suaveplan/testing/polyfills", "./tests/setup/dom.ts"],
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
fileParallelism: false,
testTimeout: 30000,
hookTimeout: 30000,
});
```
## API Reference
- `createVitestConfig(opts?: VitestConfigOptions): UserConfig` (from `vitest/config`).
- `VitestConfigOptions` — `{ environment?, setupFiles?, include?, exclude?, fileParallelism?, coverageInclude?, coverageExclude?, testTimeout?, hookTimeout? }`.
- Throws `TestViteError` with code `TEST_VITE.INVALID_OPTIONS` on Zod failure.
## Implementation Notes
Coverage `exclude` defaults are aligned with the package layout — `src/**/*.test.ts`, `src/**/*.types.ts`, `src/**/index.ts`, `src/index.ts`, and `tests/**` so barrel files and type-only modules do not artificially inflate or deflate coverage. Override `coverageExclude` to opt back in to those paths when measuring a specific module.