Skip to content

node@v1

Hygiene checks for Node.js / npm / pnpm / yarn projects. Adopt it with:

extends:
- alint://bundled/node@v1

Gated with when: facts.has_node (true if any package.json exists anywhere in the tree) plus a per-rule scope_filter: { has_ancestor: package.json } on per-file content rules so they only apply to files inside a Node package — useful in polyglot monorepos where Node packages sit alongside Rust / Python / Go subdirectories. Override has_node with your own facts: block if you need a different heuristic (e.g. detect deno.json or bun.lock).

Node project: package.json at the root is required.

A lockfile should be committed (package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lock).

  • kind: dir_absent
  • level: error
  • when: facts.has_node

node_modules/ must not be committed; add it to .gitignore.

  • kind: dir_absent
  • level: info
  • when: facts.has_node

Build-output directories shouldn’t be tracked. Set level: off if this one is intentionally shipped.

Pin the Node.js version with .nvmrc, .node-version, or .tool-versions so local and CI installs match.

Trojan Source (CVE-2021-42574): bidi override chars are rejected in JS/TS sources.

The full ruleset definition is committed at crates/alint-dsl/rulesets/v1/node.yml in the alint repo (the snapshot below is generated verbatim from that file).

# alint://bundled/node@v1
#
# Hygiene checks for Node.js / npm / pnpm / yarn projects. Adopt
# it with:
#
# extends:
# - alint://bundled/node@v1
#
# Gated with `when: facts.has_node` (true if any `package.json`
# exists anywhere in the tree) plus a per-rule
# `scope_filter: { has_ancestor: package.json }` on per-file
# content rules so they only apply to files inside a Node package
# — useful in polyglot monorepos where Node packages sit
# alongside Rust / Python / Go subdirectories. Override
# `has_node` with your own `facts:` block if you need a different
# heuristic (e.g. detect `deno.json` or `bun.lock`).
version: 1
facts:
- id: has_node
any_file_exists: [package.json, "**/package.json"]
rules:
# --- Manifest + lockfiles -----------------------------------------
- id: node-package-json-exists
when: facts.has_node
kind: file_exists
paths: package.json
root_only: true
level: error
message: "Node project: package.json at the root is required."
- id: node-has-lockfile
when: facts.has_node
# Accept any of the four common lockfiles — npm, pnpm, yarn,
# bun. At least one should be committed for reproducible installs.
kind: file_exists
paths:
- "package-lock.json"
- "pnpm-lock.yaml"
- "yarn.lock"
- "bun.lock"
- "bun.lockb"
root_only: true
level: warning
message: "A lockfile should be committed (package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lock)."
policy_url: "https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json"
# --- Build artefacts must not be tracked --------------------------
- id: node-no-tracked-node-modules
when: facts.has_node
kind: dir_absent
paths: "**/node_modules"
level: error
message: "`node_modules/` must not be committed; add it to .gitignore."
- id: node-no-tracked-dist
when: facts.has_node
# Common build-output directory names. Users with legitimate
# reasons to ship a built `dist/` (e.g. a typed-package
# preview) can set this rule's `level: off`.
#
# v0.9.18: gated on `has_ancestor: package.json` to scope the
# check per-JS-package — without this gate the rule fires on
# every `dist/` repo-wide once any `package.json` exists, which
# causes false positives in polyglot monorepos (Rust crate
# source dirs named `dist/`, etc.).
kind: dir_absent
paths: ["**/dist", "**/.next", "**/.nuxt", "**/coverage", "**/.turbo"]
scope_filter:
has_ancestor: package.json
level: info
message: >-
Build-output directories shouldn't be tracked. Set
`level: off` if this one is intentionally shipped.
# --- Node version pinning ----------------------------------------
- id: node-engine-or-nvmrc
when: facts.has_node
kind: file_exists
paths: [".nvmrc", ".node-version", ".tool-versions"]
root_only: true
level: info
message: >-
Pin the Node.js version with `.nvmrc`, `.node-version`,
or `.tool-versions` so local and CI installs match.
# --- Source-file hygiene on JS / TS sources -----------------------
- id: node-sources-final-newline
when: facts.has_node
kind: final_newline
paths: ["src/**/*.{js,jsx,ts,tsx,mjs,cjs}", "lib/**/*.{js,jsx,ts,tsx,mjs,cjs}"]
scope_filter:
has_ancestor: package.json
level: info
fix:
file_append_final_newline: {}
- id: node-sources-no-trailing-whitespace
when: facts.has_node
kind: no_trailing_whitespace
paths: ["src/**/*.{js,jsx,ts,tsx,mjs,cjs}", "lib/**/*.{js,jsx,ts,tsx,mjs,cjs}"]
scope_filter:
has_ancestor: package.json
level: info
fix:
file_trim_trailing_whitespace: {}
- id: node-sources-no-bidi
when: facts.has_node
kind: no_bidi_controls
paths: ["src/**/*.{js,jsx,ts,tsx,mjs,cjs}", "lib/**/*.{js,jsx,ts,tsx,mjs,cjs}"]
scope_filter:
has_ancestor: package.json
level: error
message: "Trojan Source (CVE-2021-42574): bidi override chars are rejected in JS/TS sources."
policy_url: "https://trojansource.codes/"