# Node.js SOLID Architecture Guidelines — Full Reference This document compiles all rules from the `nodejs-solid-architecture` skill into a single reference. For individual rules, see the `rules/` directory. --- ## Rule 1: File Organization — Minimal Exports Follow the **Minimal Exports** principle from [deno/std](https://github.com/denoland/std/blob/main/.github/ARCHITECTURE.md#minimal-exports): > Files are structured to minimize the number of dependencies they incur and the > amount of effort required to manage them, both for the maintainer and the user. > In most cases, only a single function or class, alongside its related types, are > exported. In other cases, functions that incur negligible dependency overhead > will be grouped together in the same file. ### One Primary API Per File Each file exports one "main thing": a single class and its types, a closely related group of pure utility functions, or a single React component and its props interface. The file name **must match** the main export. ### No `index.ts` Barrel Files ❌ Incorrect: ``` src/calibration/index.ts ← re-exports from below ``` ✅ Correct: ``` src/calibration.ts ← all calibration pure functions grouped here ``` ### Flat Directory Structure Keep the tree at most two levels deep. Avoid `src/utils/math/calibration/polynomial.ts` — prefer `src/calibration.ts`. ### When to Group vs Split **Group** functions when they are always used together, share internal logic, or have negligible dependency impact individually. **Split** when a function has a distinct dependency others don't need, the file exceeds ~200 lines, or a function is likely used in a completely different context. ### Each Source File Has a Test File ``` src/calibration.ts → src/calibration.test.ts src/modbus/client.ts → src/modbus/client.test.ts src/ui/MainScreen.tsx → src/ui/MainScreen.test.tsx ``` --- ## Rule 2: Pure Functions First Extract all business logic from React components, hooks, and service classes into **pure functions** that can be unit-tested without mocks. ### What Is a Pure Function? 1. Same inputs → same output, always. 2. No side effects (no I/O, no external state mutation, no random values). ### Naming Convention for I/O Functions | Prefix | Meaning | |--------|---------| | `load*` | Read from filesystem / storage | | `fetch*` | Network / serial / hardware request | | `send*` | Write to network / serial / hardware | | `write*` | Write to filesystem / storage | | `start*` | Begin a long-running process | | `stop*` | Terminate a long-running process | ### Arrow Functions for Non-Component Code ```ts // ✅ Arrow function /** Clamps a value to [min, max]. */ const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value)); ``` Top-level React components may use `function` declarations for DevTools display names. ### Extract Logic Out of Components ❌ Polynomial logic inline in useMemo. ✅ Extract to `applyCalibration.ts`, import in component: ```tsx const calibrated = useMemo( () => inputs.map((ch) => ({ ...ch, value: ch.calibration ? applyCalibration(ch.rawValue, ch.calibration.factors) : ch.rawValue, })), [inputs], ); ``` ### I/O Stays at the Boundary ``` ┌──────────────────────────────┐ │ I/O boundary │ loadCalibrationConfig(), fetchInputRegisters() │ ┌────────────────────────┐ │ │ │ Pure business logic │ │ applyCalibration(), clamp(), int16ToNumber() │ └────────────────────────┘ │ └──────────────────────────────┘ ``` --- ## Rule 3: SOLID Principles ### Priority Order 1. **S** — Single Responsibility (most violations occur here) 2. **D** — Dependency Inversion (critical for testability) 3. **I** — Interface Segregation (keep interfaces small) 4. **O** — Open/Closed (achieved via config-driven design) 5. **L** — Liskov Substitution (minimal inheritance used) ### S — Single Responsibility Each file/class has one reason to change. `ModbusService` currently handles connection management, polling, data transformation, output diffing, and listener management — decompose it: ```ts // connection.ts — connect/reconnect lifecycle // poller.ts — interval ticking // transform.ts — pure: int16[] → ChannelData[] // outputDiff.ts — pure: detects changed outputs ``` ### D — Dependency Inversion ```ts // ✅ Constructor injection export class ModbusService { constructor( readonly #client: IModbusClient, // injected readonly #config: CalibrationConfig, ) {} } ``` In tests, replace `IModbusClient` with `vi.fn()` mocks. ### I — Interface Segregation ```ts export interface IConnectionStatus { getConnectionStatus(): boolean; } export interface IDataSource { getInputData(): ChannelData[]; onChange(...): () => void; } export interface IOutputControl { setOutput(index: number, value: number): void; } export interface IModbusService extends IConnectionStatus, IDataSource, IOutputControl {} ``` ### O — Open/Closed Config-driven design satisfies OCP. Adding a new chip type or polynomial degree requires only a YAML change, not a code change. --- ## Rule 4: Testing Strategy ```bash pnpm test # run all tests with coverage pnpm check # type-check + lint ``` ### Pure Functions — Direct Vitest Tests ```ts import { expect, test } from "vitest"; import { applyCalibration, clamp } from "./calibration.ts"; test("applyCalibration linear", () => { expect(applyCalibration(10, [0, 2])).toBe(20); }); ``` ### React Ink Components — ink-testing-library ```tsx import { render } from "ink-testing-library"; test("displays channel values", () => { const { lastFrame } = render(); expect(lastFrame()).toContain("CH0"); }); ``` ### Services — Constructor Injection + Vi Mocks ```ts const client: IModbusClient = { connect: vi.fn(), readInputs: vi.fn().mockResolvedValue(Array(16).fill(0)), ... }; const service = new ModbusService(client, testConfig); ``` --- ## Rule 5: JSR @std/ Packages Prefer `jsr:@std/` over custom implementations. Install with `pnpm add jsr:@scope/package`. | Custom pattern | Use instead | |----------------|-------------| | Manual `setTimeout` sleep | `@std/async/delay` → `delay(ms)` | | Manual retry loop | `@std/async/retry` → `retry(fn, options)` | | `assert(cond)` in tests | `@std/assert` | | `path.join` | `@std/path/join` | | YAML parsing | `@std/yaml` | Use custom only when no `@std/` equivalent exists or performance constraints apply. --- ## Rule 6: JSDoc Requirements Every **exported** symbol requires JSDoc (English). Non-exported symbols should have JSDoc when the name alone is insufficient. ### Placement for Arrow Functions ```ts // ✅ JSDoc ABOVE the const /** Applies polynomial calibration to a raw sensor value. */ export const applyCalibration = (raw: number, factors: number[]): number => factors.reduce((acc, a, i) => acc + a * raw ** i, 0); ``` ### Full Signatures for Complex Functions Include `@param`, `@returns`, and `@example` for non-trivial functions. ### Interfaces and Types JSDoc on the type itself **and** on each field: ```ts /** Represents a single sensor or actuator channel. */ export interface ChannelData { /** Zero-based index into the raw register array. */ index: number; /** Human-readable channel identifier (e.g., "CH0"). */ id: string; /** Raw register value before calibration. */ rawValue: number; /** Calibrated value. Equals rawValue when no calibration is configured. */ value: number; } ``` --- ## Rule 7: React Ink UI Guidelines ### Thin Component Layer Components handle only rendering and local interaction state. All business logic lives in pure-function modules. ### Reusable UI Parts Extract repeated UI patterns into custom hooks or Ink components: ```ts // hooks/useChannelNavigation.ts /** Manages keyboard navigation across a list of channels. */ export const useChannelNavigation = (count: number) => { const [selected, setSelected] = useState(0); useInput((_, key) => { if (key.upArrow) setSelected((i) => Math.max(0, i - 1)); if (key.downArrow) setSelected((i) => Math.min(count - 1, i + 1)); }); return { selected, setSelected }; }; ``` ### ink Built-in Useful Components Before building a custom component, check [ink's Useful Components](https://github.com/vadimdemedes/ink?tab=readme-ov-file#useful-components): | Component | Package | Use case | |-----------|---------|----------| | `` | `@inkjs/ui` | Keyboard text input | | `` | `@inkjs/ui` | Loading indicator | | `` | `@inkjs/ui` | Value/progress visualization | | `