Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
36 changes: 33 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,67 @@
> 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 (`<pkg>/99-<name>.js`) is a thin delegator that only `require`s and
registers the modules in `nodes/`. Keep runtime glue thin.
- 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`).

<!-- END node-red-standards:managed -->

Expand Down
Loading