Bun and TypeScript Coding Conventions
VibeFlow uses Bun 1.4 for development, dependency installation, builds, and tests.
The published vf artifact is still a Node-targeted ESM bundle. Code must satisfy both
boundaries: fast Bun-native tooling and the declared Node runtime.
These rules are executable where practical. tsconfig.json, biome.json,
bun.lock, the file-size and waiver scripts, focused contract tests, and vf verify
are the authorities. This page explains why those gates exist.
Runtime and modules
- Use ESM,
node:built-in imports,.jssuffixes for relative source imports, andimport typefor type-only dependencies. - Source compiled into
dist/cli.jsmust remain Node-compatible. Use a Bun-only API only behind a deliberate adapter or external build boundary with focused tests. - Keep TypeScript strict. The project additionally enforces isolated modules, consistent path casing, total returns, checked indexed access and side-effect imports, override markers, and switch fallthrough protection.
- Parse external data as
unknown. Prove shape, membership, bounds, and state before narrowing. Newany, blind double casts, and non-null assertions require a real integration constraint and focused evidence.
Bun’s current TypeScript guidance recommends strict mode, bundler resolution,
verbatimModuleSyntax, noUncheckedIndexedAccess, noImplicitOverride, and
noFallthroughCasesInSwitch. VibeFlow keeps target: ES2022 instead of Bun’s
ESNext suggestion because the released bundle deliberately targets Node rather than
the Bun runtime. See the official Bun TypeScript guide.
Constants, unions, and hardcoded values
Do not add TypeScript enum or const enum. Biome rejects both. For closed values that
must exist at runtime, use one plain-JavaScript authority and derive the type:
export const TASK_STATE = Object.freeze({
QUEUED: "queued",
RUNNING: "running",
FAILED: "failed",
} as const);
export type TaskState = (typeof TASK_STATE)[keyof typeof TASK_STATE];
export const TASK_STATE_LABEL = Object.freeze({
[TASK_STATE.QUEUED]: "Queued",
[TASK_STATE.RUNNING]: "Running",
[TASK_STATE.FAILED]: "Failed",
} satisfies Record<TaskState, string>);
This matches Biome’s recommendation to use JavaScript objects or TypeScript unions instead
of emitted enums. See the official noEnum rule.
Centralize a value when it is a closed vocabulary, bound, field name, exit code,
environment key, lifecycle state, or wire literal shared by multiple layers. Import that
authority in storage, validators, services, CLI, and UI. Do not duplicate the same values
as a handwritten union or create a miscellaneous constants.ts dumping ground.
A local one-use branch label, prose string, fixture payload, or UI sentence is not a protocol. Leaving it inline is clearer than manufacturing a constant with no authority.
Use the typed-protocol-contracts skill for persisted/API/config/CLI/UI vocabularies.
Structure and ownership
- Keep ordinary source files at or below 400 lines. Split along authority and responsibility boundaries, not into arbitrary helper shards.
- New circular dependencies are not acceptable. Contract modules stay dependency-light and browser-safe when the UI consumes them.
- Public collections and protocol maps are immutable. Prefer
Readonly,ReadonlyArray,Object.freeze, and totalsatisfies Record<...>projections. - A file-size waiver needs an issue, owner, reason, and expiry. A waiver does not replace a concrete split plan.
Processes, paths, and cleanup
- Launch executable plus argv; keep
shell: falseunless a proved platform shim requires a shell. Never interpolate untrusted input into a command string. - Reuse the tracked launcher and owned-process abstractions instead of introducing an ad
hoc
Bun.spawn. - Persist PID plus process-start identity. PID alone is not ownership.
- Make POSIX and Windows path semantics explicit. Avoid assuming slash, drive, UNC, symlink, reparse-point, hardlink, process-group, or signal behavior is portable.
- Every acquired resource has deterministic success, failure, cancellation, and crash cleanup. Preserve the primary error when cleanup also fails. Bound reads, waits, retries, output, and aggregate memory.
Use runtime-portable-process-fixtures for launcher/process work and
deterministic-async-teardown for lifecycle-heavy async changes.
Tests
Use bun:test with descriptive behavior names and specific matchers. Cover success,
rejection, bounds, invalid external values, cleanup, and relevant platforms. Prefer
table-driven cases for vocabularies and boundary matrices.
Tests must be deterministic. Do not add retries, sleeps, global state, or weaker assertions to hide a race. Native and platform adapters need injected seams locally plus a real same-SHA CI job on the target OS. Bun documents test isolation, filtering, randomized-order reproduction, teardown, and coverage in its official test runner guide.
Dependencies and reproducibility
packageManager and bun.lock define the Bun toolchain. CI uses bun ci, which is
equivalent to a frozen-lockfile install and fails when declarations and the lock disagree.
See the official Bun install guide.
Do not add a dependency when a small standard-library implementation is clearer. A tool upgrade must update its declaration, lockfiles, compatibility documentation, and focused contract evidence in the same change.
Required verification
Run focused tests while iterating, then:
bun run typecheck
bun run lint
bun run file-size:check
bun run waiver:check
bun run build
git diff --check
Run affected UI, landing, E2E, native, or normative gates as applicable. Before claiming
completion after any code edit, run whole-repo vf verify and require confidence 1.0.