diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5378577 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Normalise line endings in the *working tree*, not just in the index. +# +# Prettier's `endOfLine` defaults to "lf" and this standard's .prettierrc.json does not override +# it, so a file checked out as CRLF fails `npm run format:check` even when the blob stored in git +# is LF. With core.autocrlf=true -- the Windows default -- that happens to every file a checkout +# or pull touches, which turns format:check red locally while CI stays green on Linux. Pinning +# the working tree to LF makes local and CI results identical on every platform. +* text=auto eol=lf + +# Hooks are executed by sh; CRLF would break them outright. +.husky/* text eol=lf diff --git a/AGENTS.md b/AGENTS.md index 2114f1a..8995944 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ > below the managed block (the "Project-specific rules" section) is yours and is never overwritten. ## Shared: Architecture + - Node packages are modular: `lib/` holds framework-independent, unit-testable core logic; `nodes/` holds one file per Node-RED node; `icons/` holds node icons. - The registered entry file (`/99-.js`) is a thin delegator that only `require`s and @@ -14,30 +15,59 @@ - Record non-trivial design decisions as an ADR in `doc/architecture/adr/`. ## Shared: Code style -- Lint: ESLint flat config (`eslint.config.js`), ESLint >= 9. Run the lint script before committing. + +- Lint: ESLint flat config (`eslint.config.js`), ESLint >= 10. Run the lint script before committing. + `eslint` and `@eslint/js` must stay on the same major: `@eslint/js@10` peers on `eslint@^10`, and + pairing `eslint@10` with `@eslint/js@9` silently keeps the v9 recommended rule set. +- ESLint 10's recommended set adds `no-unassigned-vars` and `no-useless-assignment`. Both are errors: + don't declare a binding only to pass `undefined` around, and don't assign a value no later + statement reads. - Format: Prettier (`.prettierrc.json`) — 4-space indent, single quotes, es5 trailing commas. - Target Node.js >= 20. - Avoid `var` — use `const`, or `let` only when the binding is reassigned (enforced by `no-var` / `prefer-const`). - One statement per line — don't pack multiple instructions onto a single line; keep lines simple to read (enforced by `max-statements-per-line`). -- Single exit — each function has exactly one `return`, placed as its final statement; avoid early or multiple returns. +- Keep functions short, with a single exit: + - **One exit per function.** A function leaves in exactly one place: its last statement. This + includes guard clauses — an early `return` in a precondition check is still a second exit and is + not allowed. Assign to a single result and return it as the last statement. `throw` is the one + permitted exception, because it is not a return and a `finally` still runs. + - **Validate by nesting, not by leaving.** State the precondition as the condition that must hold + and put the work inside it, with the error path in the `else`. Where the caller is code, `throw` + instead; where the caller is a Node-RED flow, the `else` calls the error path. + - **Keep functions short enough that the nesting does not matter.** The objection to nesting is + really an objection to long functions — at a readable length, one or two levels of indentation + cost nothing. If the nesting starts to hurt, extract a function; never add a second exit. + - **Most likely case first within each branch**, so a reader meets what the function normally does + before the exceptions. + - **If every path must do trailing work, put that work in `finally`** rather than repeating it + before each exit — combined with the single exit this makes the epilogue unskippable. +- No defensive programming. Do not check for states that cannot occur, and do not guard against + hypothetical future changes to code you control. Validate input at the boundary and then trust it. ## Shared: Tests + - Node's built-in test runner (`node --test`) + `node-red-node-test-helper`. Tests live in `test/` as `*.test.js`. Import `{ describe, it }` from `node:test` and assert with `node:assert`. Coverage via `c8`. +- Node's default discovery runs **every** `.js` under `test/`, whatever it is named, so shared helpers and + fixtures belong outside that directory (e.g. `test-helpers/`). The test script deliberately takes no path + arguments: a `'test/**/*.test.js'` glob would need Node >= 21 and fails on Node 20, which is still supported. ## Shared: Documentation + - `README.md` is user-facing. Architecture docs live under `doc/architecture/` (`overview.md`, `structural-design.md`, `behavioural-design.md`, `adr/`). - Update `CHANGELOG.md` (Keep a Changelog style) for every user-visible change; bump the patch version in `package.json` in the same commit. ## Shared: Workflow + - CI (`.github/workflows/node.js.yml`) must pass: lint, format:check, test, coverage. - Releases go through `.github/workflows/npm-publish.yml`. - Never bump the major version without an ADR explaining the breaking change. ## Shared: package.json scripts -`lint`, `lint:fix`, `format`, `format:check`, `test` (`node --test`), `coverage` (`c8 node --test`), `coverage:check`. + +`lint`, `lint:fix`, `format`, `format:check`, `test` (`node --test` with `--test-force-exit --test-timeout=30000 --test-concurrency=1`, no path args), `coverage` / `coverage:check` (c8 over `npm test`).