From c7ed0cee637117f51b53fb013d795b2cb7962573 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 12:56:52 +0800 Subject: [PATCH 01/33] chore: flat-config workspace, node:test, changesets, GitHub Actions Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .changeset/config.json | 8 + .github/workflows/ci.yml | 17 ++ .github/workflows/release.yml | 26 +++ .gitignore | 2 + .travis.yml | 36 ---- .../plans/2026-11-10-eslint-config-2026.md | 160 ++++++++++++++++++ .../2026-11-10-eslint-config-2026-design.md | 72 ++++++++ eslint.config.js | 8 + package.json | 58 ++++--- packages/eslint-config-base/.babelrc | 3 - packages/eslint-config-base/.eslintrc | 4 - packages/eslint-config-base/.npmrc | 1 - packages/eslint-config-base/index.js | 11 -- packages/eslint-config-base/package.json | 87 ++++------ packages/eslint-config-base/rules/airbnb.js | 42 ----- .../rules/eslint-comment.js | 31 ---- packages/eslint-config-base/rules/hapi.js | 42 ----- packages/eslint-config-base/rules/import.js | 61 ------- packages/eslint-config-base/src/index.js | 1 + packages/eslint-config-base/test/base.test.js | 36 ---- packages/eslint-config-base/test/requires.js | 13 -- .../eslint-config-base/test/smoke.test.js | 7 + packages/eslint-config-react/.babelrc | 3 - packages/eslint-config-react/.eslintrc | 4 - packages/eslint-config-react/.npmrc | 1 - packages/eslint-config-react/hooks.js | 5 - packages/eslint-config-react/index.js | 15 -- packages/eslint-config-react/package.json | 93 +++------- packages/eslint-config-react/rules/format.js | 96 ----------- packages/eslint-config-react/rules/react.js | 51 ------ .../eslint-config-react/test/base.test.js | 36 ---- packages/eslint-config-react/test/requires.js | 15 -- 32 files changed, 392 insertions(+), 653 deletions(-) create mode 100644 .changeset/config.json create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml create mode 100644 docs/superpowers/plans/2026-11-10-eslint-config-2026.md create mode 100644 docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md create mode 100644 eslint.config.js delete mode 100644 packages/eslint-config-base/.babelrc delete mode 100644 packages/eslint-config-base/.eslintrc delete mode 120000 packages/eslint-config-base/.npmrc delete mode 100644 packages/eslint-config-base/index.js delete mode 100644 packages/eslint-config-base/rules/airbnb.js delete mode 100644 packages/eslint-config-base/rules/eslint-comment.js delete mode 100644 packages/eslint-config-base/rules/hapi.js delete mode 100644 packages/eslint-config-base/rules/import.js create mode 100644 packages/eslint-config-base/src/index.js delete mode 100644 packages/eslint-config-base/test/base.test.js delete mode 100644 packages/eslint-config-base/test/requires.js create mode 100644 packages/eslint-config-base/test/smoke.test.js delete mode 100644 packages/eslint-config-react/.babelrc delete mode 100644 packages/eslint-config-react/.eslintrc delete mode 120000 packages/eslint-config-react/.npmrc delete mode 100644 packages/eslint-config-react/hooks.js delete mode 100644 packages/eslint-config-react/index.js delete mode 100644 packages/eslint-config-react/rules/format.js delete mode 100644 packages/eslint-config-react/rules/react.js delete mode 100644 packages/eslint-config-react/test/base.test.js delete mode 100644 packages/eslint-config-react/test/requires.js diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..221bd13 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..24d9f28 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: + push: + branches: [main] + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm test + - run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2d9acf0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Release +on: + push: + branches: [main] +permissions: + contents: write + pull-requests: write + id-token: write +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org + - run: npm ci + - uses: changesets/action@v1 + with: + publish: npm run release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3c3629e..84888f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.DS_Store +.superpowers diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 933ddc0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ -language: node_js -node_js: - - "14" - - "12" - - "10" -before_install: - - 'nvm install-latest-npm' -install: - - 'if [ -n "${PACKAGE-}" ]; then cd "packages/${PACKAGE}"; fi' - - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.9" ]; then nvm install --latest-npm 0.8 && npm install && nvm use "${TRAVIS_NODE_VERSION}"; else npm install; fi;' - - 'if [ -n "${ESLINT}" ]; then npm install --no-save "eslint@${ESLINT}"; fi' -script: - - 'if [ -n "${PREPUBLISH-}" ]; then npm run pretravis && npm run prepublish && npm run posttravis; else npm run travis; fi' -sudo: false -env: - matrix: - - 'TEST=true ESLINT=7 PACKAGE=eslint-config-base' - - 'TEST=true ESLINT=7 PACKAGE=eslint-config-react' -matrix: - fast_finish: true - include: - - node_js: "lts/*" - env: PREPUBLISH=true ESLINT=7 PACKAGE=eslint-config-base - - node_js: "lts/*" - env: PREPUBLISH=true ESLINT=7 PACKAGE=eslint-config-react - - node_js: "lts/*" - env: LINT=true - exclude: - - node_js: "5" - env: TEST=true ESLINT=7 PACKAGE=eslint-config-base - - node_js: "5" - env: TEST=true ESLINT=7 PACKAGE=eslint-config-react - allow_failures: - - node_js: "11" - - env: PREPUBLISH=true ESLINT=7 PACKAGE=eslint-config-base - - env: PREPUBLISH=true ESLINT=7 PACKAGE=eslint-config-react diff --git a/docs/superpowers/plans/2026-11-10-eslint-config-2026.md b/docs/superpowers/plans/2026-11-10-eslint-config-2026.md new file mode 100644 index 0000000..edfd001 --- /dev/null +++ b/docs/superpowers/plans/2026-11-10-eslint-config-2026.md @@ -0,0 +1,160 @@ +# @cospired ESLint configs 2026 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the legacy airbnb/eslintrc `@cospired/eslint-config-*` packages with flat-config ESLint 10 packages (`base`, `react`, each with JS and TypeScript variants, plus a node entry), tested for completeness, released as 8.0.0. + +**Architecture:** npm workspace with two ESM packages. Each package exports config(s) built from small rule modules in `src/rules/`. A shared test helper lints fixture code through the ESLint Node API. Rule decisions that conflict are placeholders until the interactive rule review (Task 8). + +**Tech Stack:** ESLint 10, `@eslint/js`, typescript-eslint 8, eslint-plugin-import-x, `@eslint-community/eslint-plugin-eslint-comments`, eslint-plugin-todo-plz, `@stylistic/eslint-plugin`, eslint-config-prettier, eslint-plugin-react-hooks, -react-refresh, -jsx-a11y, -formatjs, `node:test`, changesets, GitHub Actions. + +**Spec:** `docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md` (read it first, including "Findings from eventured consumers"). + +## Global Constraints + +- Flat config, ESLint 10, typescript-eslint 8+, ESM only. No airbnb, no hapi plugin, no Babel/tape/`eslint-find-rules`/`in-publish`/`safe-publish-latest`/Travis. +- Packages: `@cospired/eslint-config-base` (`.` JS, `./typescript`, `./node`) and `@cospired/eslint-config-react` (`.` JS, `./typescript`). Both released as `8.0.0`. Delete `packages/eslint-config-typescript`. +- `react` depends on `base` (regular dependency); `react/typescript` extends `base/typescript`. `base` has no React rules and no React peers. +- Peer dependencies are ranges, not exact pins; they must include ESLint 10, typescript-eslint 8.72, TypeScript 6, Prettier 3.9. +- Carry over: `eslint-disable` needs `-- reason`; TODOs need `#\d+` ticket (`todo-plz/ticket-ref`); alphabetized `import-x/order`; devDependency imports only in test/config/script files (eventured's glob list, overridable via option). +- Prettier owns formatting; `eslint-config-prettier` goes last. +- Warnings are failures in consumers (`--max-warnings 0`): prefer `error`/`off`. +- Defaults moved into the shared TS configs (Dirk-approved): `@typescript-eslint/explicit-function-return-type` (`{ allowExpressions: true }`, non-test files only), `@typescript-eslint/no-misused-promises` (`{ checksConditionals: { flagUnions: 'strict' } }`), `@typescript-eslint/no-generated-empty-object-type: 'error'`. +- `tsconfigPath` accepts a string or array; `tsconfigRootDir` stays an option; `import-x/resolver` typescript settings with `noWarnOnMultipleProjects` follow eventured's `node.js`. +- Rule conflicts are decided one by one with Dirk (Task 8); until then keep eventured's setting and list the rule as open. +- Eventured source: `/Users/dh/projects/eventured/eventured/packages/eslint-config/src/{base,node,react}.js`. Old rules: `packages/eslint-config-{base,react}/rules/*.js` (read via `git show main:` once the packages are rewritten). + +## Review Focus + +- A TS file not covered by any given tsconfig: lint reports a clear parse error for that file, no crash or hang (Task 4). +- `.mjs`/`.cjs`/`.js` config and script files under the TS configs: linted without type-aware errors, node globals where intended (Task 4). +- `#alias` subpath imports and relative workspace imports: no `import-x/no-unresolved` false positives, and `importOrder.pathGroups` option groups them (Task 4). +- Test files: exempt from `explicit-function-return-type` and allowed devDependency imports; non-test files are not (Task 4). +- Calling a `create*Config` function twice with different options returns independent arrays (no shared mutated state), and JSX parses in `.jsx`/`.tsx` under the react configs (Tasks 4, 6). + +--- + +### Task 1: Workspace and tooling scaffold + +**Files:** +- Modify: `package.json` (root: `private`, `type: module`, `workspaces: ["packages/*"]`, scripts `test`, `lint`, `changeset`, `release`; drop `preinstall` and `travis*`) +- Delete: `packages/eslint-config-typescript/`, per-package `.babelrc`, `.eslintrc`, `.npmrc`, `test/requires.js`, `test/base.test.js`, `rules/airbnb.js`, `rules/hapi.js` (rules/import.js and eslint-comment.js stay until Tasks 3/6 port them) +- Create: `.changeset/config.json`, `.github/workflows/ci.yml`, `.github/workflows/release.yml`, `eslint.config.js` (lints the repo itself), `.gitignore` entries +- Modify: `packages/eslint-config-{base,react}/package.json` (name kept, `version` stays 7.x until changeset, `type: module`, `exports` per Global Constraints, `files: ["src"]`, `scripts.test: "node --test"`, `engines.node` matching ESLint 10's) + +**Interfaces:** +- Produces: `npm test` at the root runs `node --test` in each workspace; workspaces `@cospired/eslint-config-base` and `@cospired/eslint-config-react` installable. + +- [ ] **Step 1:** Write `packages/eslint-config-base/test/smoke.test.js` asserting `import('../src/index.js')` resolves and its default export is an array (test name `base JS entry exports a flat config array`). +- [ ] **Step 2:** Run `npm test -w @cospired/eslint-config-base`; expected FAIL (module missing). +- [ ] **Step 3:** Apply the file changes above; create `src/index.js` exporting `export default []`. Install peers/devDeps with current versions (`npm install -D eslint@10 @eslint/js typescript-eslint …`); look up the real latest versions on npm instead of copying eventured's pins unverified. +- [ ] **Step 4:** Run `npm test`; expected PASS. Run `npx eslint .` at root; expected no errors. +- [ ] **Step 5:** CI workflow runs `npm ci && npm test && npm run lint` on push/PR; release workflow uses `changesets/action`. Commit: `chore: flat-config workspace, node:test, changesets, GitHub Actions`. + +### Task 2: Lint test helper + +**Files:** +- Create: `test-utils/lint.js`, `test-utils/lint.test.js`, `test-utils/fixtures/ts-project/{tsconfig.json,src/ok.ts}` + +**Interfaces:** +- Produces: `lintText(config: Linter.Config[], code: string, opts?: { filePath?: string }): Promise>` — creates `new ESLint({ overrideConfigFile: true, overrideConfig: config })`, lints with `filePath` default `fixtures/ts-project/src/sample.ts` (absolute, inside the fixture project), returns the flattened messages. +- Produces: `ruleIds(messages): string[]` (sorted unique non-null ruleIds). +- Produces: `loadedRules(config): Set` — every rule id in the config array with severity not `off`. + +- [ ] **Step 1:** Test `lintText reports no-debugger for "debugger;"` with config `[{ rules: { 'no-debugger': 'error' } }]` and `filePath` `x.js`; asserts `ruleIds` equals `['no-debugger']`. Test `loadedRules ignores off rules`. +- [ ] **Step 2:** Run `node --test test-utils`; expected FAIL. +- [ ] **Step 3:** Implement the three functions in `test-utils/lint.js`. +- [ ] **Step 4:** Run; expected PASS. Commit: `test: lint helper for config fixtures`. + +### Task 3: Base JS config + +**Files:** +- Create: `packages/eslint-config-base/src/{index.js,ignores.js}`, `src/rules/{comments.js,imports.js,style.js}`, `test/{load.test.js,rules-exist.test.js,fixtures.test.js}` +- Delete: `rules/eslint-comment.js`, `rules/import.js` after porting + +**Interfaces:** +- Consumes: `lintText`, `ruleIds`, `loadedRules` (Task 2). +- Produces: `default export: Linter.Config[]` from `src/index.js` (recommended + comments + imports + style + prettier last); `export const globalIgnores: Linter.Config` from `src/ignores.js` (eventured's ignore list); each `src/rules/*.js` default-exports `Linter.Config` (plugin registrations plus `rules`). +- Produces: `devDependencyGlobs(): string[]` and `createImportsConfig(options?: { devDependencyGlobs?: string[]; pathGroups?: object[] }): Linter.Config` in `src/rules/imports.js` (eventured's glob list, both anchored spellings). + +- [ ] **Step 1:** Write tests, each with the spec's exact values: + - `load.test.js`: `base JS config lints a trivial file without errors` (`const a = 1;\nexport default a;\n`, expects `[]`). + - `rules-exist.test.js`: for every rule id in the config, assert the plugin's `rules` object (or ESLint's `builtinRules`) contains it — name `every configured rule exists`. Also assert no rule id starts with `react/`. + - `fixtures.test.js`: `requires a description on disable comments` (`// eslint-disable-next-line no-console` → `@eslint-community/eslint-comments/require-description`); `requires a ticket on TODO` (`// TODO: fix` → `todo-plz/ticket-ref`; `// TODO(#12): fix` passes); `alphabetizes imports` (`import b from 'b';import a from 'a';` → `import-x/order`); `rejects devDependency import in src, allows in *.test.js`; `no-var`, `prefer-const` still flagged. +- [ ] **Step 2:** Run `npm test -w @cospired/eslint-config-base`; expected FAIL. +- [ ] **Step 3:** Port eventured's `createBaseConfig` JS parts into the rule modules. Before writing `style.js`, spike: install `@stylistic/eslint-plugin`, confirm `padding-line-between-statements` (blank line before `return`, after imports) works with `eslint-config-prettier` last and that Prettier does not rewrite the result (`npx prettier --check` on a fixture). If not, apply the spec's fallback and tell Dirk. Express `newline-before-return` and `max-statements-per-line` intent with the same rule; hapi `scope-start`/`no-arrowception` are not ported yet (Task 8). +- [ ] **Step 4:** Run; expected PASS. Commit: `feat(base): flat-config JS ruleset`. + +### Task 4: Base TypeScript config + +**Files:** +- Create: `packages/eslint-config-base/src/typescript.js`, `src/rules/typescript.js`, `test/typescript.test.js` +- Create: `packages/eslint-config-base/test/fixtures/ts-project/{tsconfig.json,tsconfig.test.json,src/a.ts,test/a.test.ts}` + +**Interfaces:** +- Consumes: Task 3 exports. +- Produces: `createTypescriptConfig(options?: { tsconfigPath?: string | string[]; tsconfigRootDir?: string; files?: string[]; devDependencyGlobs?: string[]; importOrder?: { pathGroups?: object[] } }): Linter.Config[]` default-exported helpers re-exporting `globalIgnores`. Includes recommendedTypeChecked + stylisticTypeChecked + the three default rules from Global Constraints + import-x typescript resolver settings (`noWarnOnMultipleProjects: true`). + +- [ ] **Step 1:** Tests in `typescript.test.js`, using the fixture project and `tsconfigRootDir`: + - `flags missing return type in src, not in *.test.ts` (uses `allowExpressions`: `const f = () => 1` inside call expression passes, `export function f() { return 1; }` fails). + - `no-misused-promises flags async callback in condition` and `no-generated-empty-object-type`. + - `accepts an array of tsconfigs` (src/ in `tsconfig.json`, test/ in `tsconfig.test.json`, both lint without parse errors). + - `file outside every tsconfig reports a parsing error, does not throw` (Review Focus 1). + - `.mjs config file is linted without type-aware errors` (Review Focus 2). + - `#shared/x import: no no-unresolved error with a package.json imports map; pathGroups option places it before relative imports` (Review Focus 3). + - `two calls return independent arrays` (Review Focus 5). +- [ ] **Step 2:** Run; expected FAIL. +- [ ] **Step 3:** Implement `createTypescriptConfig`; use `parserOptions.project` when `tsconfigPath` is given, else `projectService: true`. Apply type-aware rules only to `files` and non-JS files get `tseslint.configs.disableTypeChecked`. +- [ ] **Step 4:** Run; expected PASS. Commit: `feat(base): TypeScript config`. + +### Task 5: Base node entry + +**Files:** +- Create: `packages/eslint-config-base/src/node.js`, `test/node.test.js` + +**Interfaces:** +- Produces: `createNodeConfig(options?: same options as createTypescriptConfig): Linter.Config[]` — typescript config plus node/es2022 globals, `ecmaVersion: 2022`, `sourceType: 'module'`, `@typescript-eslint/prefer-nullish-coalescing` with `ignorePrimitives: { string: true }` (eventured's reasoning comment kept), `scripts/**/*.{mjs,js}` with node globals and `no-console` off. + +- [ ] **Step 1:** Tests: `process is a known global` (`process.exit(1)` no `no-undef`), `string || fallback is allowed, number || fallback is flagged`, `no-console allowed in scripts/ but not src/`. +- [ ] **Step 2:** Run; expected FAIL. **Step 3:** Implement. **Step 4:** Run; expected PASS. Commit: `feat(base): node config`. + +### Task 6: React package (JS and TypeScript) + +**Files:** +- Create: `packages/eslint-config-react/src/{index.js,typescript.js}`, `src/rules/{react.js,hooks.js,a11y.js,formatjs.js}`, `test/{load.test.js,rules-exist.test.js,fixtures.test.js}` +- Delete: old `rules/*.js`, `index.js`, `hooks.js` after porting (hooks convenience export dropped, mention in migration notes) + +**Interfaces:** +- Consumes: base default config and `createTypescriptConfig`. +- Produces: `default export: Linter.Config[]` (base + react + react-hooks + react-refresh + jsx-a11y + formatjs + browser globals) from `src/index.js`; `createReactTypescriptConfig(options?: same as createTypescriptConfig): Linter.Config[]` from `src/typescript.js`. formatjs excludes `**/*.{test,spec}.{ts,tsx,js,jsx}`. jsx-a11y inclusion follows Task 8's decision; ship enabled until then. + +- [ ] **Step 1:** Tests: `JSX parses in .jsx and .tsx`; `hooks rule flags conditional hook call`; `formatjs/enforce-default-message flags intl.formatMessage without defaultMessage in src, not in tests`; `no react rules in base JS or TS configs`; `jsx-a11y rule flags without alt`; rules-exist over all react rule ids (including `formatjs/*` against the installed plugin); `React JS config contains no @typescript-eslint rules`. +- [ ] **Step 2:** Run; expected FAIL. **Step 3:** Port eventured `react.js` and formatjs/a11y rules, reconciling per spec (placeholder: eventured's frontend formatjs settings). **Step 4:** Run; expected PASS. Commit: `feat(react): React JS and TypeScript configs`. + +### Task 7: Rule comparison table + +**Files:** +- Create: `scripts/rule-table.js`, `scripts/rule-table.test.js`, generated `docs/rules.md` + +**Interfaces:** +- Produces: `buildRuleTable(config: Linter.Config[], baseline: Linter.Config[]): Array<{ rule: string; ours: unknown; recommended: unknown; source: 'cospired' | 'eventured' | 'airbnb-override' | 'hapi' | 'new' }>`, listing only rules where ours differs from baseline (`@eslint/js` + typescript-eslint recommended/stylistic). `source` comes from a hand-written `scripts/rule-sources.json` seeded from the old and eventured files. + +- [ ] **Step 1:** Test: given a config that sets `no-console: 'error'` against baseline `warn`, the table has one row with both values. **Step 2:** FAIL. **Step 3:** Implement and generate `docs/rules.md` (one table per config). **Step 4:** PASS. Commit: `docs: rule comparison table`. + +### Task 8: Interactive rule review with Dirk (gate) + +**Files:** modify `src/rules/*.js` and the fixture tests per decision; update `docs/rules.md` with a "Decision" column. + +- [ ] **Step 1:** Present `docs/rules.md` rows to Dirk grouped by theme, one row/conflict at a time, in this order: hapi `scope-start` and `no-arrowception` replacements; `no-unused-vars` severity; `import/order` newlines (`always` vs `always-and-inside-groups`); `no-console`; React `no-unused-expressions` relaxations; severity policy for `warn`; `#` alias default; jsx-a11y keep/opt-in/drop; formatjs preset values; then every remaining row. Do not decide for Dirk. +- [ ] **Step 2:** For each decision, update the rule, add or adjust a fixture test asserting the new behavior, rerun `npm test`, record the decision in `docs/rules.md`. +- [ ] **Step 3:** When no open rows remain, commit: `feat: apply rule review decisions`. + +### Task 9: Docs, changeset, eventured acceptance + +**Files:** +- Modify: `README.md` (root), `packages/*/README.md`; create `.changeset/.md` (major for both packages), `MIGRATION.md` + +- [ ] **Step 1:** READMEs show usage for each entry (JS, TS, node, react JS/TS), options, the `no-restricted-imports` local-pattern example, and the peer install commands. `MIGRATION.md` lists removed airbnb/hapi rules, dropped `hooks` export, new peers. +- [ ] **Step 2:** Add the major changeset. Run `npx changeset version --snapshot` in a scratch branch to confirm both packages become 8.0.0; do not publish. +- [ ] **Step 3:** Acceptance: in a scratch copy of eventured (not the real repo), rewrite its three `eslint.config.js` files to use the new packages with only the local additions, run `eslint .`, and report to Dirk the list of rule-level differences versus the current config (lint errors themselves are not a concern). +- [ ] **Step 4:** Commit: `docs: usage, migration, 8.0.0 changeset`. Publishing is Dirk's call and is not part of this plan. diff --git a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md new file mode 100644 index 0000000..208f836 --- /dev/null +++ b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md @@ -0,0 +1,72 @@ +# @cospired ESLint configs for 2026/2027 — design + +## Intent + +Cospired projects currently copy a hand-built TypeScript ESLint config between repos (the one in `eventured/packages/eslint-config`) because the published `@cospired/eslint-config-*` packages are stuck on ESLint 8, the legacy `.eslintrc` format and airbnb, and have no working TypeScript support. Goal: one published package family that every project installs, replacing both. Released as a new major version. + +## Goals + +- Flat config, ESLint 10, typescript-eslint 8+, ESM. No airbnb (`eslint-config-airbnb-base`/`-typescript` are unmaintained and eslintrc-only). +- Four configs, in two packages (**my call**, keeps existing package names): + - `@cospired/eslint-config-base` — `.` (JS), `./typescript` (TS) + - `@cospired/eslint-config-react` — `.` (JS), `./typescript` (TS); react-hooks, jsx-a11y, formatjs. Replaces the old `eslint-config-typescript` package branch work. +- Rule set built on `@eslint/js` / typescript-eslint recommended (+ stylistic, type-checked for the TS variant). Every custom rule is compared against what recommended already sets and kept only on clean-code merit, decided with Dirk one by one (see "Rule review"). +- Carry over from eventured: `eslint-disable` needs a `-- reason`, TODOs need a ticket ref (`todo-plz`), alphabetized `import-x/order`, devDependency imports only in test/config/script files. +- Carry over from the old packages: eslint-comments rules, formatjs (i18n) rules, jsx-a11y extras, the useful import rules, `no-unused-vars` with `^_` ignore. +- Replace the hapi plugin. Keep its readability intent (blank line after function open / before return) via a non-hapi rule — candidate: `padding-line-between-statements` from `@stylistic/eslint-plugin` (to verify it coexists with Prettier; core version is deprecated). `no-arrowception` replacement: a core rule such as `no-confusing-arrow` / `no-return-assign` family, or drop. Decide during review. +- Tests that prove completeness: every config loads in ESLint 10; every configured rule exists in the installed plugin versions (replaces `eslint-find-rules`); fixture files assert expected errors/non-errors (including blank-line, disable-reason, TODO-ticket, import-order, and a11y/formatjs cases); JS configs contain no TS-only rules; base contains no React rules. +- Tooling modernized: drop Babel, tape, `eslint-find-rules`, `in-publish`, `safe-publish-latest`, Travis scripts. Use `node:test` + the ESLint Node API, npm workspaces, changesets for versioning/publish. Turbo only if it earns its place for two packages (**my call: no**). + +## Non-goals + +- No formatting rules that Prettier owns (`eslint-config-prettier` stays last). +- No support for ESLint < 10 or legacy `.eslintrc`. +- No new rule categories beyond what is listed (no security, unicorn, sonarjs, etc.) in this pass. +- No migration of consuming projects (eventured etc.); only a migration note in the README. + +## Anti-goals + +- A config that loads but silently misses rules (typos, renamed/removed rules, wrong plugin namespace). +- Rules kept only because airbnb or hapi had them. +- Forcing React/formatjs/a11y peer dependencies onto non-React users. + +## Constraints + +- Node/ESLint/typescript-eslint floors: ESLint 10, current Node LTS at release (builder: confirm against ESLint 10's own `engines`). +- Peer dependencies are ranges, not the exact pins eventured uses. +- Type-aware linting options (`tsconfigRootDir`, project paths, multiple tsconfigs, import-x resolver settings) from eventured's `base.js`/`node.js` must survive in the TS variant. +- New major; old packages' last 7.x line stays on npm unchanged. + +## Rule review (process, decided with Dirk) + +Builder produces a table of every custom rule: source (airbnb-override / cospired / eventured / hapi), our setting, recommended's setting, difference. Conflicts and divergences are presented to Dirk and decided one at a time on clean-code grounds; the decisions are recorded in the repo (rule comments or `docs/rules.md`). Hapi `scope-start`/`no-arrowception` and the eventured-vs-cospired conflicts (e.g. `no-unused-vars` severity, `import/order` newlines, `no-console`, React `no-unused-expressions` relaxations) are the first items. + +## Delivery phases and settled details + +- **Two phases.** (1) Infrastructure: workspace, tooling, loader/existence tests, a first port of eventured's rules, rule-comparison table. (2) Interactive rule review with Dirk, one rule/conflict at a time; fixture tests are written alongside each decision, not before. The builder never ships rule decisions by default; until reviewed, conflicts keep eventured's setting as a placeholder and are listed as open. +- **Sources.** Eventured config: `/Users/dh/projects/eventured/eventured/packages/eslint-config/src/{base,node,react}.js` (local sibling checkout, readable). Old rules: `packages/eslint-config-{base,react}/rules/*.js` on `main`. +- **Versioning (my call).** Both packages release as `8.0.0`. The unpublished `eslint-config-typescript` (0.1.0) package is deleted; its work is superseded by the `./typescript` exports. +- **Layering (my call).** `react` depends on `base` as a regular dependency and composes it; `react/typescript` extends `base/typescript`. `base` contains no React rules and no React peer dependencies; React/formatjs/a11y plugins are peers of `react` only. +- **devDependency globs (my call).** Default to eventured's glob list (test/spec files, `test/`, `__tests__/`, `scripts/`, `*.config.*`), overridable via an option. +- **Prettier fallback (my call).** If `@stylistic` blank-line rules conflict with Prettier, fall back to core `padding-line-between-statements` if still present in ESLint 10; otherwise drop the blank-line rule and tell Dirk. +- **CI/publish (my call).** Repo is on GitHub (`cospired/javascript`): GitHub Actions running tests, plus the changesets release action. Dirk supplies the npm token / trusted-publishing setup. + +## Findings from eventured consumers (apps/api, apps/frontend, packages/api-contracts) + +What the three consumer configs add on top of the shared package, and what that means for the new package: + +- **Node config is needed.** `apps/api` uses `createNodeConfig` (type-aware, node globals, `prefer-nullish-coalescing` with string `||` allowed, import-x resolver with `noWarnOnMultipleProjects`). The two-package layout had no node variant. **My call:** `@cospired/eslint-config-base/node` (TS only), built on base/typescript. Dirk to confirm. +- **Multiple tsconfigs + absolute paths + `tsconfigRootDir`.** api lints with three tsconfigs (`tsconfig.json`, `tsconfig.lint.json`, `test/tsconfig.json`) from both package root (Turbo) and repo root (lint-staged). `tsconfigPath` must accept an array; `tsconfigRootDir` stays an option. Test this scenario. +- **Import order needs extensible alias groups.** api overrides `import-x/order` to place `#shared/#data/#domain/#services/#entrypoints` aliases as an `internal` group before relative imports. The config should take `importOrder.pathGroups` (or treat Node `#` subpath imports as internal by default — decide in rule review) so consumers stop copy-pasting the whole rule. +- **`explicit-function-return-type`.** Base turns it off; api and api-contracts both turn it back on (`allowExpressions: true`, not for tests), and frontend uses disables for it (4×). Candidate to become a default (non-test files). Rule review item. +- **Type-aware extras both consumers add:** `no-misused-promises` (`checksConditionals: {flagUnions: 'strict'}`), `no-generated-empty-object-type`. Candidates for defaults. +- **formatjs is configured locally in frontend** from `formatjs.configs.recommended` plus: `no-invalid-icu` error, `no-camel-case` error, `enforce-description` off, test files excluded, `no-literal-string-in-jsx` left at warn. Old cospired `format.js` differs (enforce-description on, camel-case off, many extras). The new react config should own one reconciled formatjs preset, test files excluded by default. Rule review item. +- **No jsx-a11y in eventured frontend** (UI is react-aria based). Old cospired react config enables jsx-a11y. Open question for rule review: keep jsx-a11y by default, make it opt-in, or drop. react-hooks and react-refresh are used in eventured (React 19, Vite) but absent from the old react package; add both. +- **`no-restricted-imports` stays local.** All uses (icon subpath imports, layered-architecture bans, `#` alias rules) are project-specific; the package does not ship them, but the README should show the pattern. +- **Warnings are failures.** All lint scripts run `--report-unused-disable-directives --max-warnings 0`, so `warn` severity == `error` in practice. Rule review should settle a policy (prefer `error`/`off`; `warn` only for rules needing a transition period). +- **Misc:** CommonJS override for `*.config.{js,mjs}` (api-contracts), local `ignores`, `**/*.d.ts` globally ignored, `scripts/**` gets node globals with `no-console` off. `no-console` is disabled inline 10×, so the default stays `warn`/`error` with scripts exempted. Consumers pin exact versions via pnpm and use ESLint 10, typescript-eslint 8.72, TypeScript 6, Prettier 3.9; the package peers should be ranges that include these. +- **Acceptance check:** after the new package exists, the three eventured configs should be reproducible from it with only the local additions above, lint output on the eventured repo should match the current one except for deliberate rule decisions (diff list produced for Dirk). + +## Left to the builder + +File layout, test harness details, fixture format, changesets config, CI (existing is Travis; choose a current setup), README/migration docs, exact plugin versions. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2e737f0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,8 @@ +import js from '@eslint/js'; +import globals from 'globals'; + +export default [ + { ignores: ['node_modules', '**/fixtures/**'] }, + js.configs.recommended, + { languageOptions: { globals: globals.node } }, +]; diff --git a/package.json b/package.json index 56cc60e..c6807a3 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,22 @@ { "name": "@cospired/javascript", - "version": "5.0.1", - "description": "A slightly opinionated approach to JavaScript.", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "A slightly opinionated approach to JavaScript and TypeScript.", + "workspaces": [ + "packages/*" + ], "scripts": { - "preinstall": "npm run install:config:base && npm run install:config:react", - "install:config:base": "cd packages/eslint-config-base && npm prune && npm install", - "install:config:react": "cd packages/eslint-config-react && npm prune && npm install", - "test": "npm run --silent test:config:base && npm run --silent test:config:react", - "test:config:base": "cd packages/eslint-config-base; npm test", - "test:config:react": "cd packages/eslint-config-react; npm test", - "travis": "npm run --silent travis:config:base && npm run --silent travis:config:react", - "travis:config:base": "cd packages/eslint-config-base; npm run travis", - "travis:config:react": "cd packages/eslint-config-react; npm run travis" + "test": "npm test --workspaces --if-present", + "lint": "eslint .", + "changeset": "changeset", + "release": "changeset publish" }, "repository": { "type": "git", "url": "git+https://github.com/cospired/javascript.git" }, - "keywords": [ - "style guide", - "lint", - "cospired", - "es6", - "es2015", - "es2016", - "es2017", - "es2018", - "react", - "jsx" - ], "contributors": [ { "name": "Dirk Haage", @@ -46,5 +34,27 @@ "bugs": { "url": "https://github.com/cospired/javascript/issues" }, - "homepage": "https://github.com/cospired/javascript#readme" + "homepage": "https://github.com/cospired/javascript#readme", + "engines": { + "node": ">=22.13" + }, + "devDependencies": { + "@changesets/cli": "^3.0.3", + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "@types/node": "^24", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.72.2" + } } diff --git a/packages/eslint-config-base/.babelrc b/packages/eslint-config-base/.babelrc deleted file mode 100644 index c43c556..0000000 --- a/packages/eslint-config-base/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["airbnb"] -} \ No newline at end of file diff --git a/packages/eslint-config-base/.eslintrc b/packages/eslint-config-base/.eslintrc deleted file mode 100644 index 0544d45..0000000 --- a/packages/eslint-config-base/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./index.js", - "rules": {} -} \ No newline at end of file diff --git a/packages/eslint-config-base/.npmrc b/packages/eslint-config-base/.npmrc deleted file mode 120000 index cba44bb..0000000 --- a/packages/eslint-config-base/.npmrc +++ /dev/null @@ -1 +0,0 @@ -../../.npmrc \ No newline at end of file diff --git a/packages/eslint-config-base/index.js b/packages/eslint-config-base/index.js deleted file mode 100644 index b836546..0000000 --- a/packages/eslint-config-base/index.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - extends: [ - 'airbnb-base', - ].concat([ - './rules/airbnb', - './rules/eslint-comment', - './rules/hapi', - './rules/import', - ].map(require.resolve)), - rules: {}, -}; diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index 82344f6..cf8d7fc 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -1,71 +1,46 @@ { "name": "@cospired/eslint-config-base", - "version": "6.0.0", - "description": "Slightly opinionated eslint config based on airbnb's config", - "main": "index.js", - "scripts": { - "lint": "eslint --report-unused-disable-directives .", - "prepublishOnly": "eslint-find-rules --unused && npm test && safe-publish-latest", - "prepublish": "not-in-publish || npm run prepublishOnly", - "pretest": "npm run --silent lint", - "pretests-only": "node ./test/requires", - "test": "npm run --silent tests-only", - "tests-only": "babel-tape-runner ./test/*.test.js", - "pretravis": ":", - "travis": "npm run --silent test", - "posttravis": ":" - }, + "version": "7.0.2", + "description": "Slightly opinionated ESLint flat config for JavaScript and TypeScript", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "author": "Dirk Haage ", "repository": { "type": "git", "url": "https://github.com/cospired/javascript" }, - "contributors": [ - { - "name": "Dirk Haage", - "email": "dirk@cospired.com", - "url": "https://cospired.com/" - }, - { - "name": "Jan Schmidle", - "email": "jan@cospired.com", - "url": "https://cospired.com/" - } - ], - "author": "Dirk Haage ", "keywords": [ "eslint", "eslintconfig", - "config", - "cospired", - "javascript", - "styleguide", - "es2015", - "es2016", - "es2017", - "es2018" + "flat-config", + "typescript", + "cospired" ], - "license": "BSD-3-Clause-Clear", - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.0" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "devDependencies": { - "@babel/runtime": "^7.17.2", - "@hapi/eslint-plugin": "^5.1.0", - "babel-preset-airbnb": "^5.0.0", - "babel-tape-runner": "^3.0.0", - "eslint": "8.9.0", - "eslint-find-rules": "^4.1.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "2.25.4", - "in-publish": "^2.0.1", - "safe-publish-latest": "^2.0.0", - "tape": "^5.5.2" + "exports": { + ".": "./src/index.js", + "./typescript": "./src/typescript.js", + "./node": "./src/node.js" + }, + "scripts": { + "test": "node --test" }, "peerDependencies": { - "@hapi/eslint-plugin": "^5.1.0", - "eslint": "^8.9.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.25.4" + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.0.0", + "typescript": ">=5.4 <6.1", + "typescript-eslint": "^8.57.0" } } diff --git a/packages/eslint-config-base/rules/airbnb.js b/packages/eslint-config-base/rules/airbnb.js deleted file mode 100644 index 28e015a..0000000 --- a/packages/eslint-config-base/rules/airbnb.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - rules: { - /* - restrict the number of statements per line - https://eslint.org/docs/rules/max-statements-per-line - */ - 'max-statements-per-line': [ - 'warn', - { - max: 1, - }, - ], - - /* - visually separate returns from other code. Makes early returns - easy to see and helps with overall readability - https://eslint.org/docs/rules/newline-before-return - */ - 'newline-before-return': 'warn', - - /* - warn for unused vars, but allow args vars to be listed if not used - https://eslint.org/docs/rules/no-unused-vars - */ - 'no-unused-vars': [ - 'warn', - { - args: 'all', - argsIgnorePattern: '^_', - vars: 'all', - }, - ], - - /* - Prefer Object.hasOwn() over Object.prototype.hasOwnProperty.call() - https://eslint.org/docs/rules/prefer-object-has-own - TODO: part of but not enabled yet as of airbnb-base@15.0.0, - remove when enabled - */ - 'prefer-object-has-own': 'warn', - }, -}; diff --git a/packages/eslint-config-base/rules/eslint-comment.js b/packages/eslint-config-base/rules/eslint-comment.js deleted file mode 100644 index 5d60bd7..0000000 --- a/packages/eslint-config-base/rules/eslint-comment.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - extends: [ - 'plugin:eslint-comments/recommended', - ], - rules: { - /* - disallow `eslint-disable` comments about specific rules - https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/no-restricted-disable.html - */ - 'eslint-comments/no-restricted-disable': 0, - - /* - disallow unused `eslint-disable` comments - (missing in recommended config, https://github.com/mysticatea/eslint-plugin-eslint-comments/issues/22) - https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/no-unused-disable.html - */ - 'eslint-comments/no-unused-disable': 'error', - - /* - disallow ESLint directive-comments - https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/no-use.html - */ - 'eslint-comments/no-use': 0, - - /* - enforce description for disable comments - https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/require-description.html - */ - 'eslint-comments/require-description': 'error', - }, -}; diff --git a/packages/eslint-config-base/rules/hapi.js b/packages/eslint-config-base/rules/hapi.js deleted file mode 100644 index 8356683..0000000 --- a/packages/eslint-config-base/rules/hapi.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - plugins: [ - '@hapi/eslint-plugin', - ], - rules: { - /* - ESLint rule to enforce the capitalization of imported module variables. - https://github.com/hapijs/hapi-capitalize-modules - */ - '@hapi/capitalize-modules': 0, - - /* - hapi style guide ESLint rule dealing with for loops. - https://github.com/hapijs/rule-for-loop - */ - '@hapi/for-loop': 0, - - /* - Enforces the usage of var declarations only in try-catch scope. - https://github.com/hapijs/hapi-no-var - */ - '@hapi/no-var': 0, - - /* - visually separate the function body from the function declaration - https://github.com/hapijs/hapi-scope-start - */ - '@hapi/scope-start': 'warn', - - /* - ESLint rule preventing arrow functions that implicitly return arrow functions. - https://github.com/hapijs/no-arrowception - */ - '@hapi/no-arrowception': 'warn', - - /* - conflicts with hapi/hapi-scope-start - we also want to allow extra empty lines when readability improves by it - */ - 'padded-blocks': 0, - }, -}; diff --git a/packages/eslint-config-base/rules/import.js b/packages/eslint-config-base/rules/import.js deleted file mode 100644 index 20ff594..0000000 --- a/packages/eslint-config-base/rules/import.js +++ /dev/null @@ -1,61 +0,0 @@ -// import to extend rule instead of overwriting it -const airbnbRules = require('eslint-config-airbnb-base/rules/imports'); - -module.exports = { - plugins: [ - 'import', - ], - rules: { - /* - visually group all exports - https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/newline-after-import.md - */ - 'import/newline-after-import': 'error', - - /* - based on the default airbnb configuration - in addition to that we allow peer dependency imports - https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md - */ - 'import/no-extraneous-dependencies': [ - 'error', - Object.assign(airbnbRules.rules['import/no-extraneous-dependencies'][1], { peerDependencies: true }), - ], - - /* - Reports the use of import declarations with CommonJS exports in any module except for the - main module. - https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-import-module-exports.md - TODO: not yet part of airbnb-base@14.2.1 - revisit when published - */ - 'import/no-import-module-exports': 'error', - - /* - prevent importing packages through relative paths. - https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-relative-packages.md - TODO: not yet part of airbnb-base@14.2.1 - revisit when published - */ - 'import/no-relative-packages': 'error', - - /* - group imports depending on source and visually group them - https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md - */ - 'import/order': [ - 'error', - { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - ], - 'newlines-between': 'always-and-inside-groups', - }, - ], - }, -}; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js new file mode 100644 index 0000000..d6d1738 --- /dev/null +++ b/packages/eslint-config-base/src/index.js @@ -0,0 +1 @@ +export default []; diff --git a/packages/eslint-config-base/test/base.test.js b/packages/eslint-config-base/test/base.test.js deleted file mode 100644 index 6603b79..0000000 --- a/packages/eslint-config-base/test/base.test.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import test from 'tape'; - -import index from '..'; - -const files = { ...{ index } }; // object spread is to test parsing - -fs.readdirSync(path.join(__dirname, '../rules')).forEach((name) => { - - // eslint-disable-next-line import/no-dynamic-require -- iterate rules files for test - files[name] = require(`../rules/${name}`); // eslint-disable-line global-require -- see above -}); - -Object.keys(files).forEach(( - name, // trailing function comma is to test parsing -) => { - - const config = files[name]; - - test(`${name}: does not reference react`, (t) => { - - t.plan(2); - - // scan plugins for react and fail if it is found - const hasReactPlugin = Object.prototype.hasOwnProperty.call(config, 'plugins') - && config.plugins.indexOf('react') !== -1; - t.notOk(hasReactPlugin, 'there is no react plugin'); - - // scan rules for react/ and fail if any exist - const reactRuleIds = Object.keys(config.rules) - .filter((ruleId) => ruleId.indexOf('react/') === 0); - t.deepEquals(reactRuleIds, [], 'there are no react/ rules'); - }); -}); diff --git a/packages/eslint-config-base/test/requires.js b/packages/eslint-config-base/test/requires.js deleted file mode 100644 index 4714f0b..0000000 --- a/packages/eslint-config-base/test/requires.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint - strict: 0, - global-require: 0, -- requires for test - */ - -const test = require('tape'); - -test('all entry points parse', (t) => { - - t.doesNotThrow(() => require('..'), 'index does not throw'); - - t.end(); -}); diff --git a/packages/eslint-config-base/test/smoke.test.js b/packages/eslint-config-base/test/smoke.test.js new file mode 100644 index 0000000..44200dd --- /dev/null +++ b/packages/eslint-config-base/test/smoke.test.js @@ -0,0 +1,7 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +test('base JS entry exports a flat config array', async () => { + const { default: config } = await import('../src/index.js'); + assert.ok(Array.isArray(config)); +}); diff --git a/packages/eslint-config-react/.babelrc b/packages/eslint-config-react/.babelrc deleted file mode 100644 index c43c556..0000000 --- a/packages/eslint-config-react/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["airbnb"] -} \ No newline at end of file diff --git a/packages/eslint-config-react/.eslintrc b/packages/eslint-config-react/.eslintrc deleted file mode 100644 index 0544d45..0000000 --- a/packages/eslint-config-react/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./index.js", - "rules": {} -} \ No newline at end of file diff --git a/packages/eslint-config-react/.npmrc b/packages/eslint-config-react/.npmrc deleted file mode 120000 index cba44bb..0000000 --- a/packages/eslint-config-react/.npmrc +++ /dev/null @@ -1 +0,0 @@ -../../.npmrc \ No newline at end of file diff --git a/packages/eslint-config-react/hooks.js b/packages/eslint-config-react/hooks.js deleted file mode 100644 index 1106d86..0000000 --- a/packages/eslint-config-react/hooks.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - extends: [ - 'airbnb/hooks', - ], -}; diff --git a/packages/eslint-config-react/index.js b/packages/eslint-config-react/index.js deleted file mode 100644 index 5ff006c..0000000 --- a/packages/eslint-config-react/index.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - env: { - browser: true, - }, - extends: [ - 'airbnb', - ].concat([ - '@cospired/eslint-config-base/rules/airbnb', - '@cospired/eslint-config-base/rules/eslint-comment', - '@cospired/eslint-config-base/rules/hapi', - '@cospired/eslint-config-base/rules/import', - './rules/react', - './rules/format', - ].map(require.resolve)), -}; diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index b295ceb..b5a7958 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -1,82 +1,41 @@ { "name": "@cospired/eslint-config-react", - "version": "6.0.0", - "description": "Slightly opinionated eslint config based on airbnb's config", - "main": "index.js", - "scripts": { - "lint": "eslint .", - "posttravis": ":", - "prepublishOnly": "eslint-find-rules --unused && npm test && safe-publish-latest", - "prepublish": "not-in-publish || npm run prepublishOnly", - "pretest": "npm run --silent lint", - "pretests-only": "node ./test/requires", - "pretravis": ":", - "test": "npm run --silent tests-only", - "tests-only": "babel-tape-runner ./test/*.test.js", - "travis": "npm run --silent test" - }, + "version": "7.0.0", + "description": "Slightly opinionated ESLint flat config for React (JavaScript and TypeScript)", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "author": "Dirk Haage ", "repository": { "type": "git", "url": "https://github.com/cospired/javascript" }, - "contributors": [ - { - "name": "Dirk Haage", - "email": "dirk@cospired.com", - "url": "https://cospired.com/" - }, - { - "name": "Jan Schmidle", - "email": "jan@cospired.com", - "url": "https://cospired.com/" - } - ], - "author": "Dirk Haage ", "keywords": [ "eslint", "eslintconfig", - "config", - "cospired", - "javascript", - "styleguide", - "es2015", - "es2016", - "es2017", - "es2018", - "react", - "jsx" + "flat-config", + "typescript", + "cospired" ], - "license": "BSD-3-Clause-Clear", - "dependencies": { - "@cospired/eslint-config-base": "^6.0.0", - "eslint-config-airbnb": "^19.0.4", - "object.assign": "^4.1.1" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "exports": { + ".": "./src/index.js", + "./typescript": "./src/typescript.js" }, - "devDependencies": { - "@hapi/eslint-plugin": "^5.1.0", - "babel-preset-airbnb": "^5.0.0", - "babel-tape-runner": "^3.0.0", - "eslint": "^8.9.0", - "eslint-find-rules": "^4.1.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-formatjs": "^3.0.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0", - "in-publish": "^2.0.1", - "react": "^16.13.1", - "safe-publish-latest": "^2.0.0", - "tape": "^5.5.2" + "scripts": { + "test": "node --test" + }, + "dependencies": { + "@cospired/eslint-config-base": "*" }, "peerDependencies": { - "@hapi/eslint-plugin": "^5.1.0", - "eslint": "^8.9.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-formatjs": "^3.0.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0" + "eslint": "^10.0.0", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.5.0" } } diff --git a/packages/eslint-config-react/rules/format.js b/packages/eslint-config-react/rules/format.js deleted file mode 100644 index a2c7996..0000000 --- a/packages/eslint-config-react/rules/format.js +++ /dev/null @@ -1,96 +0,0 @@ -module.exports = { - plugins: [ - 'formatjs', - ], - rules: { - /* - This blacklists usage of specific elements in ICU message. - https://formatjs.io/docs/tooling/linter#blocklist-elements - */ - 'formatjs/blocklist-elements': ['off'], - - /* - Enforce description in messages descriptors - https://formatjs.io/docs/tooling/linter#enforce-description - */ - 'formatjs/enforce-description': ['error', 'literal'], - - /* - Enforce default message in messages descriptors - https://formatjs.io/docs/tooling/linter#enforce-default-message - */ - 'formatjs/enforce-default-message': ['error', 'literal'], - - /* - Makes sure all values are passed in if message has placeholders - (number/date/time/plural/select/selectordinal). - This requires values to be passed in as literal object (not a variable). - https://formatjs.io/docs/tooling/linter#enforce-placeholders - */ - 'formatjs/enforce-placeholders': ['error'], - - /* - Enforce certain plural rules to always be specified/forbidden in a message. - https://formatjs.io/docs/tooling/linter#enforce-plural-rules - */ - 'formatjs/enforce-plural-rules': [ - 'warn', - { - one: true, - other: true, - zero: false, - }, - ], - - /* - This make sure placeholders are not camel-case. - https://formatjs.io/docs/tooling/linter#no-camel-case - */ - 'formatjs/no-camel-case': ['off'], - - /* - Make sure a sentence is not too complex. Complexity is determined by how many strings are - produced when we try to flatten the sentence given its selectors. - https://formatjs.io/docs/tooling/linter/#no-complex-selectors - */ - 'formatjs/no-complex-selectors': ['error'], - - /* - This prevents usage of emoji in message. - https://formatjs.io/docs/tooling/linter#no-emoji - */ - 'formatjs/no-emoji': ['off'], - - /* - This prevents specifying multiple plurals in your message. - https://formatjs.io/docs/tooling/linter#no-multiple-plurals - */ - 'formatjs/no-multiple-plurals': ['error'], - - /* - This prevents specifying multiple whitespaces in your message. - https://formatjs.io/docs/tooling/linter#no-multiple-whitespaces - */ - 'formatjs/no-multiple-whitespaces': ['error'], - - /* - This prevents specifying offset in plural rules in your message. - https://formatjs.io/docs/tooling/linter#no-offset - */ - 'formatjs/no-offset': ['error'], - - /* - Some pipelines enforce automatic ID generation during transpilation using - babel-plugin-react-intl or @formatjs/ts-transformer so manual IDs cause issues of - mismatching during runtime. - https://formatjs.io/docs/tooling/linter#no-id - */ - 'formatjs/no-id': ['off'], - - /* - This enforces generated ID to be set in MessageDescriptor. - https://formatjs.io/docs/tooling/linter#enforce-id - */ - 'formatjs/enforce-id': ['off'], - }, -}; diff --git a/packages/eslint-config-react/rules/react.js b/packages/eslint-config-react/rules/react.js deleted file mode 100644 index b21f7b0..0000000 --- a/packages/eslint-config-react/rules/react.js +++ /dev/null @@ -1,51 +0,0 @@ -module.exports = { - rules: { - /* - Enforce spaces inside of curly braces in JSX attributes for better readability - https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md - */ - 'react/jsx-curly-spacing': [ - 'error', - 'always', - ], - - /* - overriding airbnb rule that allows .jsx extension - */ - 'react/jsx-filename-extension': [ - 'warn', - { - extensions: [ - '.js', - ], - }, - ], - - /* - Prevent usage of JSX strings in general - strings should always be translatables, hence simple strings are not allowed - We keep this a 'warn' to allow strings during dev - https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-literals.md - */ - 'react/jsx-no-literals': [ - 'warn', - { - noStrings: true, - // TODO: add all literals required for a11y and other rules - allowedStrings: ['button', 'submit'], - }, - ], - - /* - Disallow JSX props spreading - we allow react-intl component here as the only exception - https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-props-no-spreading.md - */ - 'react/jsx-props-no-spreading': ['error', { - html: 'enforce', - custom: 'enforce', - explicitSpread: 'ignore', - exceptions: ['FormattedMessage'], - }], - }, -}; diff --git a/packages/eslint-config-react/test/base.test.js b/packages/eslint-config-react/test/base.test.js deleted file mode 100644 index 4ce597c..0000000 --- a/packages/eslint-config-react/test/base.test.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import test from 'tape'; - -const files = {}; - -fs.readdirSync(path.join(__dirname, '../rules')).forEach((name) => { - - if (name === 'react.js' || name === 'react-a11y.js') { - return; - } - - // eslint-disable-next-line import/no-dynamic-require -- iterate rules files for test - files[name] = require(`../rules/${name}`); // eslint-disable-line global-require -- see above -}); - -Object.keys(files).forEach((name) => { - - const config = files[name]; - - test(`${name}: does not reference react`, (t) => { - - t.plan(2); - - // scan plugins for react and fail if it is found - const hasReactPlugin = Object.hasOwn(config, 'plugins') - && config.plugins.indexOf('react') !== -1; - t.notOk(hasReactPlugin, 'there is no react plugin'); - - // scan rules for react/ and fail if any exist - const reactRuleIds = Object.keys(config.rules) - .filter((ruleId) => ruleId.indexOf('react/') === 0); - t.deepEquals(reactRuleIds, [], 'there are no react/ rules'); - }); -}); diff --git a/packages/eslint-config-react/test/requires.js b/packages/eslint-config-react/test/requires.js deleted file mode 100644 index 81b35ec..0000000 --- a/packages/eslint-config-react/test/requires.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint - strict: 0, - global-require: 0, -- requires for test - */ - -'use strict'; - -const test = require('tape'); - -test('all entry points parse', (t) => { - - t.doesNotThrow(() => require('..'), 'index does not throw'); - - t.end(); -}); From c9139b159b79998366fece062f0a8601b5bb5563 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 12:57:21 +0800 Subject: [PATCH 02/33] test: lint helper for config fixtures Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 3 +- .github/workflows/release.yml | 3 +- package.json | 2 +- test-utils/fixtures/ts-project/src/ok.ts | 1 + test-utils/fixtures/ts-project/tsconfig.json | 11 ++++++ test-utils/lint.js | 39 ++++++++++++++++++++ test-utils/lint.test.js | 21 +++++++++++ 7 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 test-utils/fixtures/ts-project/src/ok.ts create mode 100644 test-utils/fixtures/ts-project/tsconfig.json create mode 100644 test-utils/lint.js create mode 100644 test-utils/lint.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24d9f28..c2b7d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: npm - - run: npm ci + - run: npm install --no-audit --no-fund - run: npm test - run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d9acf0..9cb009d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,9 +14,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: npm registry-url: https://registry.npmjs.org - - run: npm ci + - run: npm install --no-audit --no-fund - uses: changesets/action@v1 with: publish: npm run release diff --git a/package.json b/package.json index c6807a3..cee8561 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "packages/*" ], "scripts": { - "test": "npm test --workspaces --if-present", + "test": "npm test --workspaces --if-present && node --test test-utils", "lint": "eslint .", "changeset": "changeset", "release": "changeset publish" diff --git a/test-utils/fixtures/ts-project/src/ok.ts b/test-utils/fixtures/ts-project/src/ok.ts new file mode 100644 index 0000000..d6ca325 --- /dev/null +++ b/test-utils/fixtures/ts-project/src/ok.ts @@ -0,0 +1 @@ +export const ok = 1; diff --git a/test-utils/fixtures/ts-project/tsconfig.json b/test-utils/fixtures/ts-project/tsconfig.json new file mode 100644 index 0000000..f0c7975 --- /dev/null +++ b/test-utils/fixtures/ts-project/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/test-utils/lint.js b/test-utils/lint.js new file mode 100644 index 0000000..7b71ef5 --- /dev/null +++ b/test-utils/lint.js @@ -0,0 +1,39 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { ESLint } from 'eslint'; + +const here = path.dirname(fileURLToPath(import.meta.url)); + +export const fixtureProject = path.join(here, 'fixtures', 'ts-project'); + +/** Lint `code` with a flat config array; returns the flattened messages. */ +export async function lintText(config, code, { filePath = 'src/sample.ts' } = {}) { + filePath = path.resolve(fixtureProject, filePath); + const eslint = new ESLint({ + overrideConfigFile: true, + overrideConfig: config, + cwd: path.dirname(filePath), + }); + const [result] = await eslint.lintText(code, { filePath }); + return result.messages; +} + +export const ruleIds = (messages) => [...new Set(messages.map((m) => m.ruleId).filter(Boolean))].sort(); + +const isOn = (entry) => { + const level = Array.isArray(entry) ? entry[0] : entry; + return level !== 'off' && level !== 0; +}; + +/** Every rule id in a config array whose severity is not off. */ +export function loadedRules(config) { + const rules = new Set(); + for (const item of config) { + for (const [id, entry] of Object.entries(item.rules ?? {})) { + if (isOn(entry)) rules.add(id); + else rules.delete(id); + } + } + return rules; +} diff --git a/test-utils/lint.test.js b/test-utils/lint.test.js new file mode 100644 index 0000000..a0239de --- /dev/null +++ b/test-utils/lint.test.js @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, lintText, ruleIds } from './lint.js'; + +test('lintText reports no-debugger for "debugger;"', async () => { + const messages = await lintText( + [{ rules: { 'no-debugger': 'error' } }], + 'debugger;', + { filePath: 'x.js' }, + ); + assert.deepEqual(ruleIds(messages), ['no-debugger']); +}); + +test('loadedRules ignores off rules', () => { + const rules = loadedRules([ + { rules: { 'no-debugger': 'error', 'no-var': 'off', eqeqeq: ['warn'] } }, + { rules: { 'no-console': 0 } }, + ]); + assert.deepEqual([...rules].sort(), ['eqeqeq', 'no-debugger']); +}); From 9b19c53c23336ef978c1a6e736df4c57865ef8b9 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 12:58:37 +0800 Subject: [PATCH 03/33] feat(base): flat-config JS ruleset Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/ignores.js | 18 ++++++++ packages/eslint-config-base/src/index.js | 14 +++++- .../eslint-config-base/src/rules/comments.js | 13 ++++++ .../eslint-config-base/src/rules/general.js | 14 ++++++ .../eslint-config-base/src/rules/imports.js | 43 +++++++++++++++++++ .../eslint-config-base/src/rules/style.js | 14 ++++++ .../eslint-config-base/test/fixtures.test.js | 38 ++++++++++++++++ packages/eslint-config-base/test/load.test.js | 10 +++++ .../test/rules-exist.test.js | 14 ++++++ .../eslint-config-base/test/smoke.test.js | 7 --- test-utils/lint.js | 15 +++++++ test-utils/lint.test.js | 9 +++- 12 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 packages/eslint-config-base/src/ignores.js create mode 100644 packages/eslint-config-base/src/rules/comments.js create mode 100644 packages/eslint-config-base/src/rules/general.js create mode 100644 packages/eslint-config-base/src/rules/imports.js create mode 100644 packages/eslint-config-base/src/rules/style.js create mode 100644 packages/eslint-config-base/test/fixtures.test.js create mode 100644 packages/eslint-config-base/test/load.test.js create mode 100644 packages/eslint-config-base/test/rules-exist.test.js delete mode 100644 packages/eslint-config-base/test/smoke.test.js diff --git a/packages/eslint-config-base/src/ignores.js b/packages/eslint-config-base/src/ignores.js new file mode 100644 index 0000000..2153462 --- /dev/null +++ b/packages/eslint-config-base/src/ignores.js @@ -0,0 +1,18 @@ +/** Global ignore patterns shared by every config. */ +export const globalIgnores = { + ignores: [ + '**/dist/**', + '**/build/**', + '**/node_modules/**', + '**/*.d.ts', + '**/*.js.map', + '**/*.d.ts.map', + '**/coverage/**', + '**/.eslintcache', + '**/*.log', + '**/.cache/**', + '**/.turbo/**', + 'eslint.config.js', + 'eslint-*.config.js', + ], +}; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index d6d1738..b320fd2 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -1 +1,13 @@ -export default []; +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; + +import comments from './rules/comments.js'; +import general from './rules/general.js'; +import { createImportsConfig } from './rules/imports.js'; +import style from './rules/style.js'; + +export { globalIgnores } from './ignores.js'; +export { createImportsConfig, defaultDevDependencyGlobs } from './rules/imports.js'; + +/** JavaScript config: ESLint recommended + cospired rules. Prettier last. */ +export default [js.configs.recommended, comments, createImportsConfig(), style, general, prettier]; diff --git a/packages/eslint-config-base/src/rules/comments.js b/packages/eslint-config-base/src/rules/comments.js new file mode 100644 index 0000000..1f432d0 --- /dev/null +++ b/packages/eslint-config-base/src/rules/comments.js @@ -0,0 +1,13 @@ +import { recommended } from '@eslint-community/eslint-plugin-eslint-comments/configs'; +import todoPlz from 'eslint-plugin-todo-plz'; + +/** Directive comments need a reason, TODOs need a ticket. */ +export default { + plugins: { ...recommended.plugins, 'todo-plz': todoPlz }, + linterOptions: { reportUnusedDisableDirectives: 'error' }, + rules: { + ...recommended.rules, + '@eslint-community/eslint-comments/require-description': 'error', + 'todo-plz/ticket-ref': ['error', { pattern: '#\\d+' }], + }, +}; diff --git a/packages/eslint-config-base/src/rules/general.js b/packages/eslint-config-base/src/rules/general.js new file mode 100644 index 0000000..94644f9 --- /dev/null +++ b/packages/eslint-config-base/src/rules/general.js @@ -0,0 +1,14 @@ +/** General JavaScript rules beyond `@eslint/js` recommended. Severities are placeholders until the rule review. */ +export default { + rules: { + 'logical-assignment-operators': ['error', 'always', { enforceForIfStatements: true }], + 'no-console': 'warn', + 'no-unused-expressions': 'error', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'prefer-object-has-own': 'error', + }, +}; diff --git a/packages/eslint-config-base/src/rules/imports.js b/packages/eslint-config-base/src/rules/imports.js new file mode 100644 index 0000000..11ef6ff --- /dev/null +++ b/packages/eslint-config-base/src/rules/imports.js @@ -0,0 +1,43 @@ +import importX from 'eslint-plugin-import-x'; + +/** + * Where a devDependency import is allowed. Anchored rather than `**`-prefixed so a path like + * `src/lib/scripts/x.ts` can't claim one; import-x matches against the lint cwd, which is the + * package root under Turbo and the repo root under lint-staged, hence both spellings. + */ +export const defaultDevDependencyGlobs = [ + '**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}', + 'test/**', + 'src/test/**', + 'src/__tests__/**', + 'scripts/**', + '*.config.{ts,js,mjs,cjs}', +].flatMap((glob) => [glob, `{apps,packages}/*/${glob}`]); + +/** + * @param {{ devDependencyGlobs?: string[], pathGroups?: object[] }} [options] `pathGroups` is passed to + * `import-x/order`, e.g. to place `#alias/**` imports in the `internal` group. + */ +export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyGlobs, pathGroups } = {}) { + return { + plugins: { 'import-x': importX }, + rules: { + ...importX.flatConfigs.recommended.rules, + 'import-x/newline-after-import': 'error', + 'import-x/no-duplicates': 'error', + 'import-x/no-empty-named-blocks': 'error', + 'import-x/no-extraneous-dependencies': ['error', { devDependencies: devDependencyGlobs, peerDependencies: true }], + 'import-x/no-import-module-exports': 'error', + 'import-x/no-relative-packages': 'error', + 'import-x/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + ...(pathGroups && { pathGroups, pathGroupsExcludedImportTypes: ['builtin'] }), + }, + ], + }, + }; +} diff --git a/packages/eslint-config-base/src/rules/style.js b/packages/eslint-config-base/src/rules/style.js new file mode 100644 index 0000000..64035f8 --- /dev/null +++ b/packages/eslint-config-base/src/rules/style.js @@ -0,0 +1,14 @@ +import stylistic from '@stylistic/eslint-plugin'; + +/** Readability: blank lines around returns and declarations. Prettier does not touch these. */ +export default { + plugins: { '@stylistic': stylistic }, + rules: { + '@stylistic/padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + ], + }, +}; diff --git a/packages/eslint-config-base/test/fixtures.test.js b/packages/eslint-config-base/test/fixtures.test.js new file mode 100644 index 0000000..2561b8a --- /dev/null +++ b/packages/eslint-config-base/test/fixtures.test.js @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import config from '../src/index.js'; + +const lint = async (code, filePath = 'src/a.js') => ruleIds(await lintText(config, code, { filePath })); + +test('requires a description on disable comments', async () => { + assert.ok((await lint('// eslint-disable-next-line no-console\nconsole.log(1);\n')).includes('@eslint-community/eslint-comments/require-description')); + assert.ok(!(await lint('// eslint-disable-next-line no-console -- cli output\nconsole.log(1);\n')).includes('@eslint-community/eslint-comments/require-description')); +}); + +test('requires a ticket on TODO', async () => { + assert.ok((await lint('// TODO: fix\nexport const a = 1;\n')).includes('todo-plz/ticket-ref')); + assert.ok(!(await lint('// TODO(#12): fix\nexport const a = 1;\n')).includes('todo-plz/ticket-ref')); +}); + +test('alphabetizes imports', async () => { + assert.ok((await lint("import b from 'node:path';\nimport a from 'node:fs';\n\nexport { a, b };\n")).includes('import-x/order')); +}); + +test('devDependency imports: rejected in src, allowed in *.test.js', async () => { + const code = "import { test } from 'eslint';\n\nexport default test;\n"; + assert.ok((await lint(code, 'src/a.js')).includes('import-x/no-extraneous-dependencies')); + assert.ok(!(await lint(code, 'src/a.test.js')).includes('import-x/no-extraneous-dependencies')); +}); + +test('blank line before return is required', async () => { + const code = 'export function f(a) {\n const b = a + 1;\n return b;\n}\n'; + assert.ok((await lint(code)).includes('@stylistic/padding-line-between-statements')); + assert.ok(!(await lint('export function f(a) {\n const b = a + 1;\n\n return b;\n}\n')).includes('@stylistic/padding-line-between-statements')); +}); + +test('no-var and prefer-const are flagged', async () => { + const ids = await lint('var a = 1;\nlet b = 2;\n\nexport { a, b };\n'); + assert.ok(ids.includes('no-var') && ids.includes('prefer-const')); +}); diff --git a/packages/eslint-config-base/test/load.test.js b/packages/eslint-config-base/test/load.test.js new file mode 100644 index 0000000..9244c62 --- /dev/null +++ b/packages/eslint-config-base/test/load.test.js @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { lintText } from '../../../test-utils/lint.js'; +import config from '../src/index.js'; + +test('base JS config lints a trivial file without errors', async () => { + const messages = await lintText(config, 'const a = 1;\n\nexport default a;\n', { filePath: 'a.js' }); + assert.deepEqual(messages, []); +}); diff --git a/packages/eslint-config-base/test/rules-exist.test.js b/packages/eslint-config-base/test/rules-exist.test.js new file mode 100644 index 0000000..04263fe --- /dev/null +++ b/packages/eslint-config-base/test/rules-exist.test.js @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, missingRules } from '../../../test-utils/lint.js'; +import config from '../src/index.js'; + +test('every configured rule exists', () => { + assert.deepEqual(missingRules(config), []); +}); + +test('base contains no react rules', () => { + const react = [...loadedRules(config)].filter((id) => id.startsWith('react/') || id.startsWith('react-hooks/')); + assert.deepEqual(react, []); +}); diff --git a/packages/eslint-config-base/test/smoke.test.js b/packages/eslint-config-base/test/smoke.test.js deleted file mode 100644 index 44200dd..0000000 --- a/packages/eslint-config-base/test/smoke.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -test('base JS entry exports a flat config array', async () => { - const { default: config } = await import('../src/index.js'); - assert.ok(Array.isArray(config)); -}); diff --git a/test-utils/lint.js b/test-utils/lint.js index 7b71ef5..68293c9 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -2,6 +2,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ESLint } from 'eslint'; +import { builtinRules } from 'eslint/use-at-your-own-risk'; const here = path.dirname(fileURLToPath(import.meta.url)); @@ -37,3 +38,17 @@ export function loadedRules(config) { } return rules; } + +/** Rule ids in a config whose rule (or whose plugin) is not installed. */ +export function missingRules(config) { + const plugins = Object.assign({}, ...config.map((item) => item.plugins ?? {})); + const ids = loadedRules(config); + + return [...ids].filter((id) => { + const slash = id.lastIndexOf('/'); + if (slash === -1) return !builtinRules.has(id); + const plugin = plugins[id.slice(0, slash)]; + + return !plugin?.rules?.[id.slice(slash + 1)]; + }); +} diff --git a/test-utils/lint.test.js b/test-utils/lint.test.js index a0239de..e6dab98 100644 --- a/test-utils/lint.test.js +++ b/test-utils/lint.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { loadedRules, lintText, ruleIds } from './lint.js'; +import { loadedRules, lintText, missingRules, ruleIds } from './lint.js'; test('lintText reports no-debugger for "debugger;"', async () => { const messages = await lintText( @@ -19,3 +19,10 @@ test('loadedRules ignores off rules', () => { ]); assert.deepEqual([...rules].sort(), ['eqeqeq', 'no-debugger']); }); + +test('missingRules reports ids whose plugin or rule does not exist', () => { + const config = [ + { plugins: { p: { rules: { real: {} } } }, rules: { 'p/real': 'error', 'p/fake': 'error', 'q/x': 'error', 'no-debugger': 'error', 'no-nope': 'error', 'gone/off': 'off', 'gone/zero': [0] } }, + ]; + assert.deepEqual(missingRules(config).sort(), ['no-nope', 'p/fake', 'q/x']); +}); From c8004c99e918072145f2efbf9315251065349fbd Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 13:00:27 +0800 Subject: [PATCH 04/33] feat(base): TypeScript config Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/index.js | 12 ++- .../src/rules/typescript.js | 33 ++++++++ packages/eslint-config-base/src/typescript.js | 46 ++++++++++ .../test/fixtures/ts-project/package.json | 1 + .../test/fixtures/ts-project/src/a.ts | 1 + .../test/fixtures/ts-project/src/b.ts | 1 + .../test/fixtures/ts-project/src/shared/x.ts | 1 + .../test/fixtures/ts-project/test/a.test.ts | 1 + .../test/fixtures/ts-project/tsconfig.json | 1 + .../fixtures/ts-project/tsconfig.test.json | 1 + .../test/typescript.test.js | 83 +++++++++++++++++++ test-utils/lint.js | 9 ++ 12 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-config-base/src/rules/typescript.js create mode 100644 packages/eslint-config-base/src/typescript.js create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/package.json create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/src/a.ts create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/src/b.ts create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json create mode 100644 packages/eslint-config-base/test/typescript.test.js diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index b320fd2..6b9b261 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -9,5 +9,13 @@ import style from './rules/style.js'; export { globalIgnores } from './ignores.js'; export { createImportsConfig, defaultDevDependencyGlobs } from './rules/imports.js'; -/** JavaScript config: ESLint recommended + cospired rules. Prettier last. */ -export default [js.configs.recommended, comments, createImportsConfig(), style, general, prettier]; +/** + * Language-agnostic rules. Prettier last. + * @param {Parameters[0]} [importOptions] + */ +export function createBaseConfig(importOptions) { + return [js.configs.recommended, comments, createImportsConfig(importOptions), style, general, prettier]; +} + +/** JavaScript config: ESLint recommended + cospired rules. */ +export default createBaseConfig(); diff --git a/packages/eslint-config-base/src/rules/typescript.js b/packages/eslint-config-base/src/rules/typescript.js new file mode 100644 index 0000000..5cd1c0d --- /dev/null +++ b/packages/eslint-config-base/src/rules/typescript.js @@ -0,0 +1,33 @@ +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; +import { flatConfigs as importX } from 'eslint-plugin-import-x'; + +export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; +export const testGlobs = ['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']; + +/** Rules on top of typescript-eslint's recommended/stylistic type-checked sets. Severities are placeholders until the rule review. */ +export const typescriptRules = { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-generated-empty-object-type': 'error', + '@typescript-eslint/no-misused-promises': ['error', { checksConditionals: { flagUnions: 'strict' } }], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'], +}; + +/** Rules typescript-eslint's recommended sets leave off (or do not set) that cospired wants on for source files. */ +export const sourceOnlyRules = { + '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], +}; + +export const importXTypescript = importX.typescript; + +/** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ +export function importSettings(project) { + return { + 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs', ...tsExtensions], + 'import-x/parsers': { '@typescript-eslint/parser': tsExtensions }, + 'import-x/resolver-next': [ + // Several projects is the intended shape (app + test-only tsconfigs); the suggested `references` fix needs `composite`. + createTypeScriptImportResolver({ alwaysTryTypes: true, noWarnOnMultipleProjects: true, ...(project && { project }) }), + ], + }; +} diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js new file mode 100644 index 0000000..ddc724d --- /dev/null +++ b/packages/eslint-config-base/src/typescript.js @@ -0,0 +1,46 @@ +import prettier from 'eslint-config-prettier'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +import { createBaseConfig } from './index.js'; +import { importSettings, importXTypescript, sourceOnlyRules, testGlobs, typescriptRules } from './rules/typescript.js'; + +export { globalIgnores } from './ignores.js'; + +/** + * TypeScript config: base rules + typescript-eslint (type-checked) + cospired TS rules. + * + * @param {object} [options] + * @param {string | string[]} [options.tsconfigPath] tsconfig(s) covering every linted TS file (including test-only projects). + * Without it typescript-eslint's project service finds them. + * @param {string} [options.tsconfigRootDir] Pass `import.meta.dirname` when one ESLint run loads several workspace configs. + * @param {string[]} [options.files] TS files to apply type-aware rules to. + * @param {string[]} [options.devDependencyGlobs] Files that may import devDependencies. + * @param {{ pathGroups?: object[] }} [options.importOrder] Extra `import-x/order` path groups, e.g. `#alias/**` as `internal`. + * @returns {import('eslint').Linter.Config[]} + */ +export function createTypescriptConfig({ + tsconfigPath, + tsconfigRootDir, + files = ['**/*.{ts,tsx,mts,cts}'], + devDependencyGlobs, + importOrder = {}, +} = {}) { + const parserOptions = tsconfigPath + ? { project: tsconfigPath, tsconfigRootDir } + : { projectService: true, tsconfigRootDir }; + + return [ + ...createBaseConfig({ devDependencyGlobs, pathGroups: importOrder.pathGroups }), + ...defineConfig({ + files, + extends: [tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, importXTypescript], + languageOptions: { parserOptions }, + settings: importSettings(tsconfigPath), + rules: typescriptRules, + }), + { files, ignores: testGlobs, rules: sourceOnlyRules }, + { files: ['**/*.{js,mjs,cjs}'], ...tseslint.configs.disableTypeChecked }, + prettier, + ]; +} diff --git a/packages/eslint-config-base/test/fixtures/ts-project/package.json b/packages/eslint-config-base/test/fixtures/ts-project/package.json new file mode 100644 index 0000000..8f75309 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/package.json @@ -0,0 +1 @@ +{ "name": "fixture", "private": true, "type": "module", "imports": { "#shared/*": "./src/shared/*.ts" } } diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/a.ts b/packages/eslint-config-base/test/fixtures/ts-project/src/a.ts new file mode 100644 index 0000000..73ea5dd --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/src/a.ts @@ -0,0 +1 @@ +export const y = 1; diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/b.ts b/packages/eslint-config-base/test/fixtures/ts-project/src/b.ts new file mode 100644 index 0000000..b85663f --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/src/b.ts @@ -0,0 +1 @@ +export const b = 1; diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts b/packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts new file mode 100644 index 0000000..ad1d380 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts @@ -0,0 +1 @@ +export const x = 1; diff --git a/packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts b/packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts new file mode 100644 index 0000000..fe74263 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts @@ -0,0 +1 @@ +export const t = 1; diff --git a/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json new file mode 100644 index 0000000..7dba039 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json b/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json new file mode 100644 index 0000000..fc1e6f5 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json @@ -0,0 +1 @@ +{ "extends": "./tsconfig.json", "include": ["test"] } diff --git a/packages/eslint-config-base/test/typescript.test.js b/packages/eslint-config-base/test/typescript.test.js new file mode 100644 index 0000000..f304ff2 --- /dev/null +++ b/packages/eslint-config-base/test/typescript.test.js @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds, ruleSeverity } from '../../../test-utils/lint.js'; +import { createTypescriptConfig } from '../src/typescript.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const abs = (file) => path.join(root, file); +const options = { tsconfigPath: [abs('tsconfig.json'), abs('tsconfig.test.json')], tsconfigRootDir: root }; +const config = createTypescriptConfig(options); +const lint = async (code, file = 'src/a.ts', cfg = config) => lintText(cfg, code, { filePath: abs(file) }); +const ids = async (...args) => { + const messages = await lint(...args); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); + + return ruleIds(messages); +}; + +test('explicit-function-return-type: required in src, not in *.test.ts; expressions allowed', async () => { + const rule = '@typescript-eslint/explicit-function-return-type'; + assert.ok((await ids('export function f() {\n return 1;\n}\n')).includes(rule)); + assert.ok(!(await ids('export function f() {\n return 1;\n}\n', 'test/a.test.ts')).includes(rule)); + assert.ok(!(await ids('export const n = [1].map((v) => v + 1);\n')).includes(rule)); +}); + +test('no-misused-promises flags an async callback used as a condition', async () => { + const code = 'const p = async (): Promise => true;\n\nif (p()) {\n console.info(1);\n}\n'; + assert.ok((await ids(code)).includes('@typescript-eslint/no-misused-promises')); +}); + +test('no-generated-empty-object-type is an error for TS files', async () => { + assert.equal(await ruleSeverity(config, '@typescript-eslint/no-generated-empty-object-type', abs('src/a.ts')), 2); +}); + +test('accepts an array of tsconfigs: src and test files both lint without parse errors', async () => { + for (const file of ['src/a.ts', 'test/a.test.ts']) { + const messages = await lint('export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); + } +}); + +test('a file outside every tsconfig reports a parsing error and does not throw', async () => { + const messages = await lint('export const v = 1;\n', 'other/b.ts'); + assert.ok(messages.some((m) => m.fatal && /parsing error/i.test(m.message))); +}); + +test('.mjs config files are linted without type-aware errors', async () => { + const messages = await lint('export default {};\n', 'eslint.config.mjs'); + assert.deepEqual(messages.filter((m) => m.fatal), []); +}); + +test('#alias imports resolve as internal, before relative imports', async () => { + const code = "import { x } from '#shared/x';\n\nimport { y } from './a.js';\n\nexport const z = x + y;\n"; + const found = await ids(code, 'src/b.ts'); + assert.ok(!found.includes('import-x/no-unresolved')); + assert.ok(!found.includes('import-x/order')); +}); + +test('importOrder.pathGroups moves an alias into another group', async () => { + const code = "import { x } from '#shared/x';\nimport { ESLint } from 'eslint';\n\nexport const z = [x, ESLint];\n"; + const misordered = async (cfg) => (await lint(code, 'src/b.ts', cfg)).some((m) => /should occur before/.test(m.message)); + assert.ok(await misordered(config)); + const grouped = createTypescriptConfig({ ...options, importOrder: { pathGroups: [{ pattern: '#shared/**', group: 'external', position: 'before' }] } }); + assert.ok(!(await misordered(grouped))); +}); + +test('two calls return independent arrays', () => { + const a = createTypescriptConfig(options); + const b = createTypescriptConfig({ ...options, files: ['x/**/*.ts'] }); + assert.notEqual(a, b); + assert.notDeepEqual(a, b); + a.length = 0; + assert.ok(createTypescriptConfig(options).length > 0); +}); + +test('every rule exists and JS configs hold no TypeScript rules', async () => { + const { missingRules, loadedRules } = await import('../../../test-utils/lint.js'); + assert.deepEqual(missingRules(config), []); + const { default: js } = await import('../src/index.js'); + assert.deepEqual([...loadedRules(js)].filter((id) => id.startsWith('@typescript-eslint/')), []); +}); diff --git a/test-utils/lint.js b/test-utils/lint.js index 68293c9..5835daa 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -52,3 +52,12 @@ export function missingRules(config) { return !plugin?.rules?.[id.slice(slash + 1)]; }); } + +/** The severity (0|1|2) a rule has for `filePath` once all config objects are merged. */ +export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts') { + filePath = path.resolve(fixtureProject, filePath); + const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: path.dirname(filePath) }); + const entry = (await eslint.calculateConfigForFile(filePath)).rules?.[ruleId]; + + return Array.isArray(entry) ? entry[0] : entry; +} From d3d111645e394647fbdc1eaab4c521385ef9771c Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 13:01:19 +0800 Subject: [PATCH 05/33] feat(base): node config Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/node.js | 30 +++++++++++++++ packages/eslint-config-base/test/node.test.js | 38 +++++++++++++++++++ test-utils/lint.js | 4 +- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-config-base/src/node.js create mode 100644 packages/eslint-config-base/test/node.test.js diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js new file mode 100644 index 0000000..ac6352e --- /dev/null +++ b/packages/eslint-config-base/src/node.js @@ -0,0 +1,30 @@ +import globals from 'globals'; + +import { createTypescriptConfig } from './typescript.js'; + +export { globalIgnores } from './ignores.js'; + +/** + * Node.js/backend TypeScript config: `createTypescriptConfig` plus node globals. + * Takes the same options. + * + * @param {Parameters[0]} [options] + * @returns {import('eslint').Linter.Config[]} + */ +export function createNodeConfig(options = {}) { + const { files = ['**/*.{ts,tsx,mts,cts}'] } = options; + + return [ + ...createTypescriptConfig(options), + { languageOptions: { sourceType: 'module', globals: globals.node } }, + { + files, + rules: { + // String `||` is deliberate empty-string fallthrough (env vars, trimmed input) that `??` would silently drop. + '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], + }, + }, + // CLI output is expected in scripts. + { files: ['scripts/**/*.{mjs,js,ts}'], rules: { 'no-console': 'off' } }, + ]; +} diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js new file mode 100644 index 0000000..aebb136 --- /dev/null +++ b/packages/eslint-config-base/test/node.test.js @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { createNodeConfig } from '../src/node.js'; +import { createTypescriptConfig } from '../src/typescript.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const options = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; +const node = createNodeConfig(options); +const ids = async (cfg, code, file) => { + const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); + + return ruleIds(messages); +}; + +test('node globals are defined (process is not no-undef)', async () => { + const code = 'process.exit(1);\n'; + assert.ok((await ids(createTypescriptConfig(options), code, 'src/x.mjs')).includes('no-undef')); + assert.ok(!(await ids(node, code, 'src/x.mjs')).includes('no-undef')); +}); + +test('string || fallback is allowed, number || fallback is flagged', async () => { + const rule = '@typescript-eslint/prefer-nullish-coalescing'; + const str = "export function f(s: string | undefined): string {\n return s || 'x';\n}\n"; + const num = 'export function f(n: number | undefined): number {\n return n || 1;\n}\n'; + assert.ok(!(await ids(node, str, 'src/a.ts')).includes(rule)); + assert.ok((await ids(node, num, 'src/a.ts')).includes(rule)); +}); + +test('no-console is allowed in scripts/ but not in src/', async () => { + const code = 'console.log(1);\n'; + assert.ok(!(await ids(node, code, 'scripts/run.mjs')).includes('no-console')); + assert.ok((await ids(node, code, 'src/run.mjs')).includes('no-console')); +}); diff --git a/test-utils/lint.js b/test-utils/lint.js index 5835daa..524604b 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -9,12 +9,12 @@ const here = path.dirname(fileURLToPath(import.meta.url)); export const fixtureProject = path.join(here, 'fixtures', 'ts-project'); /** Lint `code` with a flat config array; returns the flattened messages. */ -export async function lintText(config, code, { filePath = 'src/sample.ts' } = {}) { +export async function lintText(config, code, { filePath = 'src/sample.ts', cwd } = {}) { filePath = path.resolve(fixtureProject, filePath); const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, - cwd: path.dirname(filePath), + cwd: cwd ?? path.dirname(filePath), }); const [result] = await eslint.lintText(code, { filePath }); return result.messages; From c4f38849802f6d22f1d760d39be49dd6b06ae245 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 13:02:23 +0800 Subject: [PATCH 06/33] feat(react): React JS and TypeScript configs Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-react/package.json | 4 ++- packages/eslint-config-react/src/index.js | 10 ++++++ .../eslint-config-react/src/rules/formatjs.js | 15 ++++++++ .../eslint-config-react/src/rules/react.js | 21 +++++++++++ .../eslint-config-react/src/typescript.js | 17 +++++++++ .../eslint-config-react/test/fixtures.test.js | 35 +++++++++++++++++++ .../test/fixtures/ts-project/package.json | 1 + .../test/fixtures/ts-project/src/a.test.tsx | 1 + .../test/fixtures/ts-project/src/a.tsx | 1 + .../test/fixtures/ts-project/tsconfig.json | 1 + packages/eslint-config-react/test/helpers.js | 19 ++++++++++ .../eslint-config-react/test/load.test.js | 13 +++++++ .../test/rules-exist.test.js | 22 ++++++++++++ 13 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-config-react/src/index.js create mode 100644 packages/eslint-config-react/src/rules/formatjs.js create mode 100644 packages/eslint-config-react/src/rules/react.js create mode 100644 packages/eslint-config-react/src/typescript.js create mode 100644 packages/eslint-config-react/test/fixtures.test.js create mode 100644 packages/eslint-config-react/test/fixtures/ts-project/package.json create mode 100644 packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx create mode 100644 packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx create mode 100644 packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json create mode 100644 packages/eslint-config-react/test/helpers.js create mode 100644 packages/eslint-config-react/test/load.test.js create mode 100644 packages/eslint-config-react/test/rules-exist.test.js diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index b5a7958..089ed96 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -34,8 +34,10 @@ }, "peerDependencies": { "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-formatjs": "^8.1.4", "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-react-refresh": "^0.5.0" + "eslint-plugin-react-refresh": "^0.5.0", + "globals": "^17.0.0" } } diff --git a/packages/eslint-config-react/src/index.js b/packages/eslint-config-react/src/index.js new file mode 100644 index 0000000..4dab03e --- /dev/null +++ b/packages/eslint-config-react/src/index.js @@ -0,0 +1,10 @@ +import baseConfig, { globalIgnores } from '@cospired/eslint-config-base'; +import prettier from 'eslint-config-prettier'; + +import formatjs from './rules/formatjs.js'; +import react from './rules/react.js'; + +export { globalIgnores }; + +/** React JavaScript config: base JS + React rules. */ +export default [...baseConfig, ...react, formatjs, prettier]; diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js new file mode 100644 index 0000000..02c041d --- /dev/null +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -0,0 +1,15 @@ +import formatjs from 'eslint-plugin-formatjs'; + +export const testGlobs = ['**/*.{test,spec}.{js,jsx,ts,tsx}']; + +/** i18n: formatjs `recommended` plus extras. Test files are exempt. Values are placeholders until the rule review. */ +export default { + plugins: formatjs.configs.recommended.plugins, + ignores: testGlobs, + rules: { + ...formatjs.configs.recommended.rules, + 'formatjs/enforce-description': 'off', + 'formatjs/no-camel-case': 'error', + 'formatjs/no-invalid-icu': 'error', + }, +}; diff --git a/packages/eslint-config-react/src/rules/react.js b/packages/eslint-config-react/src/rules/react.js new file mode 100644 index 0000000..fb8d048 --- /dev/null +++ b/packages/eslint-config-react/src/rules/react.js @@ -0,0 +1,21 @@ +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; + +/** React: hooks, refresh (Vite HMR), browser globals, JSX in plain JS. Values are placeholders until the rule review. */ +export default [ + // ESLint lints only .js/.mjs/.cjs by default; .jsx must be opted in. + { files: ['**/*.jsx'] }, + reactHooks.configs.flat.recommended, + { + plugins: { 'react-refresh': reactRefresh }, + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + rules: { + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }], + }, + }, +]; diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js new file mode 100644 index 0000000..31b4042 --- /dev/null +++ b/packages/eslint-config-react/src/typescript.js @@ -0,0 +1,17 @@ +import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; +import prettier from 'eslint-config-prettier'; + +import formatjs from './rules/formatjs.js'; +import react from './rules/react.js'; + +export { globalIgnores }; + +/** + * React TypeScript config: base TypeScript + React rules. Takes the same options as `createTypescriptConfig`. + * + * @param {Parameters[0]} [options] + * @returns {import('eslint').Linter.Config[]} + */ +export function createReactTypescriptConfig(options = {}) { + return [...createTypescriptConfig(options), ...react, formatjs, prettier]; +} diff --git a/packages/eslint-config-react/test/fixtures.test.js b/packages/eslint-config-react/test/fixtures.test.js new file mode 100644 index 0000000..f6895ec --- /dev/null +++ b/packages/eslint-config-react/test/fixtures.test.js @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ids, jsConfig, tsConfig } from './helpers.js'; + +const hook = 'function useThing() {\n return 1;\n}\n\nexport function A({ c }) {\n if (c) {\n useThing();\n }\n\n return null;\n}\n'; +const message = 'export const m = intl.formatMessage({ id: "a" });\n'; + +test('hooks rule flags a conditional hook call', async () => { + assert.ok((await ids(jsConfig, hook, 'src/a.jsx')).includes('react-hooks/rules-of-hooks')); +}); + +test('formatjs flags a missing defaultMessage in src, not in tests', async () => { + const typed = 'declare const intl: { formatMessage: (d: { id: string }) => string };\n\n' + message; + assert.ok((await ids(tsConfig, typed, 'src/a.tsx')).includes('formatjs/enforce-default-message')); + assert.ok(!(await ids(tsConfig, typed, 'src/a.test.tsx')).includes('formatjs/enforce-default-message')); +}); + +test('browser globals are defined', async () => { + assert.ok(!(await ids(jsConfig, 'export const w = window.innerWidth;\n', 'src/a.js')).includes('no-undef')); +}); + +test('react-refresh flags a non-component export next to a component', async () => { + const code = 'export function A() {\n return
;\n}\n\nexport function helper() {\n return 1;\n}\n'; + assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('react-refresh/only-export-components')); +}); + +test('two calls to createReactTypescriptConfig return independent arrays', async () => { + const { createReactTypescriptConfig } = await import('../src/typescript.js'); + const a = createReactTypescriptConfig({ tsconfigRootDir: '/a' }); + const b = createReactTypescriptConfig({ tsconfigRootDir: '/b' }); + assert.notEqual(a, b); + a.length = 0; + assert.ok(b.length > 0); +}); diff --git a/packages/eslint-config-react/test/fixtures/ts-project/package.json b/packages/eslint-config-react/test/fixtures/ts-project/package.json new file mode 100644 index 0000000..6a2659b --- /dev/null +++ b/packages/eslint-config-react/test/fixtures/ts-project/package.json @@ -0,0 +1 @@ +{ "name": "fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx b/packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx @@ -0,0 +1 @@ +export const a = 1; diff --git a/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx b/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx @@ -0,0 +1 @@ +export const a = 1; diff --git a/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json new file mode 100644 index 0000000..eb46ae9 --- /dev/null +++ b/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-react/test/helpers.js b/packages/eslint-config-react/test/helpers.js new file mode 100644 index 0000000..149d70a --- /dev/null +++ b/packages/eslint-config-react/test/helpers.js @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import jsConfig from '../src/index.js'; +import { createReactTypescriptConfig } from '../src/typescript.js'; + +export const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +export const tsConfig = createReactTypescriptConfig({ tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }); +export { jsConfig }; + +/** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ +export async function ids(cfg, code, file) { + const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); + + return ruleIds(messages); +} diff --git a/packages/eslint-config-react/test/load.test.js b/packages/eslint-config-react/test/load.test.js new file mode 100644 index 0000000..f8260d7 --- /dev/null +++ b/packages/eslint-config-react/test/load.test.js @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ids, jsConfig, tsConfig } from './helpers.js'; + +test('JSX parses in .jsx under the JS config', async () => { + assert.deepEqual(await ids(jsConfig, 'export const A = () =>
;\n', 'src/a.jsx'), []); +}); + +test('JSX parses in .tsx under the TS config', async () => { + const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/a.tsx'); + assert.ok(Array.isArray(found)); +}); diff --git a/packages/eslint-config-react/test/rules-exist.test.js b/packages/eslint-config-react/test/rules-exist.test.js new file mode 100644 index 0000000..47b5e10 --- /dev/null +++ b/packages/eslint-config-react/test/rules-exist.test.js @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, missingRules } from '../../../test-utils/lint.js'; + +import { jsConfig, tsConfig } from './helpers.js'; + +test('every configured rule exists (JS and TS)', () => { + assert.deepEqual(missingRules(jsConfig), []); + assert.deepEqual(missingRules(tsConfig), []); +}); + +test('React JS config contains no @typescript-eslint rules', () => { + assert.deepEqual([...loadedRules(jsConfig)].filter((id) => id.startsWith('@typescript-eslint/')), []); +}); + +test('React configs enable hooks, refresh and formatjs rules', () => { + for (const cfg of [jsConfig, tsConfig]) { + const rules = loadedRules(cfg); + for (const id of ['react-hooks/rules-of-hooks', 'react-refresh/only-export-components', 'formatjs/enforce-default-message']) assert.ok(rules.has(id), id); + } +}); From d7a862991e17d91c36ff225fd257c04b615aa596 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 13:03:03 +0800 Subject: [PATCH 07/33] docs: rule comparison table Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- docs/rules.md | 185 +++++++++++++++++++++++++++++++++++++ package.json | 5 +- scripts/rule-sources.json | 31 +++++++ scripts/rule-table.js | 73 +++++++++++++++ scripts/rule-table.test.js | 26 ++++++ 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 docs/rules.md create mode 100644 scripts/rule-sources.json create mode 100644 scripts/rule-table.js create mode 100644 scripts/rule-table.test.js diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 0000000..64aaf4b --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,185 @@ +# Rule comparison + +Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review. + +## Base JS (vs. @eslint/js recommended) + +| Rule | Ours | Recommended | Source | Decision | +|---|---|---|---|---| +| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | open | +| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | open | +| `@eslint-community/eslint-comments/require-description` | error | - | eventured | open | +| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | open | +| `import-x/default` | error | - | new | open | +| `import-x/export` | error | - | new | open | +| `import-x/named` | error | - | new | open | +| `import-x/namespace` | error | - | new | open | +| `import-x/newline-after-import` | error | - | cospired | open | +| `import-x/no-duplicates` | error | - | new | open | +| `import-x/no-empty-named-blocks` | error | - | cospired | open | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | open | +| `import-x/no-import-module-exports` | error | - | cospired | open | +| `import-x/no-named-as-default` | warn | - | new | open | +| `import-x/no-named-as-default-member` | warn | - | new | open | +| `import-x/no-relative-packages` | error | - | cospired | open | +| `import-x/no-unresolved` | error | - | new | open | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}} | - | eventured | open | +| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | +| `no-console` | warn | - | eventured | open | +| `no-unexpected-multiline` | off | error | new | open | +| `no-unused-expressions` | error | - | eventured | open | +| `no-unused-vars` | error {"argsIgnorePattern":"^_"} | error | cospired | open | +| `no-var` | error | - | eventured | open | +| `object-shorthand` | error | - | eventured | open | +| `prefer-arrow-callback` | error | - | eventured | open | +| `prefer-const` | error | - | eventured | open | +| `prefer-object-has-own` | error | - | cospired | open | +| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | + +## Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked) + +| Rule | Ours | Recommended | Source | Decision | +|---|---|---|---|---| +| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | open | +| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | open | +| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | open | +| `@eslint-community/eslint-comments/require-description` | error | - | eventured | open | +| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | open | +| `@typescript-eslint/await-thenable` | off | error | new | open | +| `@typescript-eslint/dot-notation` | off | error | new | open | +| `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | open | +| `@typescript-eslint/no-array-delete` | off | error | new | open | +| `@typescript-eslint/no-base-to-string` | off | error | new | open | +| `@typescript-eslint/no-duplicate-type-constituents` | off | error | new | open | +| `@typescript-eslint/no-explicit-any` | warn | error | eventured | open | +| `@typescript-eslint/no-floating-promises` | off | error | new | open | +| `@typescript-eslint/no-for-in-array` | off | error | new | open | +| `@typescript-eslint/no-implied-eval` | off | error | new | open | +| `@typescript-eslint/no-misused-promises` | off | error | eventured | open | +| `@typescript-eslint/no-redundant-type-constituents` | off | error | new | open | +| `@typescript-eslint/no-unnecessary-type-assertion` | off | error | new | open | +| `@typescript-eslint/no-unsafe-argument` | off | error | new | open | +| `@typescript-eslint/no-unsafe-assignment` | off | error | new | open | +| `@typescript-eslint/no-unsafe-call` | off | error | new | open | +| `@typescript-eslint/no-unsafe-enum-comparison` | off | error | new | open | +| `@typescript-eslint/no-unsafe-member-access` | off | error | new | open | +| `@typescript-eslint/no-unsafe-return` | off | error | new | open | +| `@typescript-eslint/no-unsafe-unary-minus` | off | error | new | open | +| `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_"} | error | eventured | open | +| `@typescript-eslint/non-nullable-type-assertion-style` | off | error | new | open | +| `@typescript-eslint/only-throw-error` | off | error | new | open | +| `@typescript-eslint/prefer-find` | off | error | new | open | +| `@typescript-eslint/prefer-includes` | off | error | new | open | +| `@typescript-eslint/prefer-nullish-coalescing` | off | error | eventured | open | +| `@typescript-eslint/prefer-optional-chain` | off | error | new | open | +| `@typescript-eslint/prefer-promise-reject-errors` | off | error | new | open | +| `@typescript-eslint/prefer-regexp-exec` | off | error | new | open | +| `@typescript-eslint/prefer-string-starts-ends-with` | off | error | new | open | +| `@typescript-eslint/require-await` | off | error | new | open | +| `@typescript-eslint/restrict-plus-operands` | off | error | new | open | +| `@typescript-eslint/restrict-template-expressions` | off | error | new | open | +| `@typescript-eslint/unbound-method` | off | error | new | open | +| `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | open | +| `import-x/default` | error | - | new | open | +| `import-x/export` | error | - | new | open | +| `import-x/namespace` | error | - | new | open | +| `import-x/newline-after-import` | error | - | cospired | open | +| `import-x/no-duplicates` | error | - | new | open | +| `import-x/no-empty-named-blocks` | error | - | cospired | open | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | open | +| `import-x/no-import-module-exports` | error | - | cospired | open | +| `import-x/no-named-as-default` | warn | - | new | open | +| `import-x/no-named-as-default-member` | warn | - | new | open | +| `import-x/no-relative-packages` | error | - | cospired | open | +| `import-x/no-unresolved` | error | - | new | open | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}} | - | eventured | open | +| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | +| `no-console` | warn | - | eventured | open | +| `no-unexpected-multiline` | off | error | new | open | +| `object-shorthand` | error | - | eventured | open | +| `prefer-arrow-callback` | error | - | eventured | open | +| `prefer-object-has-own` | error | - | cospired | open | +| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | + +## React JS (vs. Base JS) + +| Rule | Ours | Recommended | Source | Decision | +|---|---|---|---|---| +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | open | +| `formatjs/enforce-default-message` | error "literal" | - | new | open | +| `formatjs/enforce-placeholders` | error | - | new | open | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | open | +| `formatjs/no-camel-case` | error | - | eventured | open | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | open | +| `formatjs/no-emoji` | error | - | new | open | +| `formatjs/no-invalid-icu` | error | - | eventured | open | +| `formatjs/no-literal-string-in-jsx` | warn {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | open | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | open | +| `formatjs/no-multiple-plurals` | error | - | new | open | +| `formatjs/no-multiple-whitespaces` | error | - | new | open | +| `formatjs/no-offset` | error | - | new | open | +| `formatjs/no-useless-message` | error | - | new | open | +| `formatjs/prefer-full-sentence` | error | - | new | open | +| `formatjs/prefer-pound-in-plural` | error | - | new | open | +| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true} | error | eventured | open | +| `react-hooks/config` | error | - | new | open | +| `react-hooks/error-boundaries` | error | - | new | open | +| `react-hooks/exhaustive-deps` | warn | - | new | open | +| `react-hooks/gating` | error | - | new | open | +| `react-hooks/globals` | error | - | new | open | +| `react-hooks/immutability` | error | - | new | open | +| `react-hooks/incompatible-library` | warn | - | new | open | +| `react-hooks/preserve-manual-memoization` | error | - | new | open | +| `react-hooks/purity` | error | - | new | open | +| `react-hooks/refs` | error | - | new | open | +| `react-hooks/rules-of-hooks` | error | - | new | open | +| `react-hooks/set-state-in-effect` | error | - | new | open | +| `react-hooks/set-state-in-render` | error | - | new | open | +| `react-hooks/static-components` | error | - | new | open | +| `react-hooks/unsupported-syntax` | warn | - | new | open | +| `react-hooks/use-memo` | error | - | new | open | +| `react-refresh/only-export-components` | warn {"allowConstantExport":true} | - | eventured | open | + +## React TypeScript (vs. Base TypeScript) + +| Rule | Ours | Recommended | Source | Decision | +|---|---|---|---|---| +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | open | +| `formatjs/enforce-default-message` | error "literal" | - | new | open | +| `formatjs/enforce-placeholders` | error | - | new | open | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | open | +| `formatjs/no-camel-case` | error | - | eventured | open | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | open | +| `formatjs/no-emoji` | error | - | new | open | +| `formatjs/no-invalid-icu` | error | - | eventured | open | +| `formatjs/no-literal-string-in-jsx` | warn {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | open | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | open | +| `formatjs/no-multiple-plurals` | error | - | new | open | +| `formatjs/no-multiple-whitespaces` | error | - | new | open | +| `formatjs/no-offset` | error | - | new | open | +| `formatjs/no-useless-message` | error | - | new | open | +| `formatjs/prefer-full-sentence` | error | - | new | open | +| `formatjs/prefer-pound-in-plural` | error | - | new | open | +| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true} | off | eventured | open | +| `react-hooks/config` | error | - | new | open | +| `react-hooks/error-boundaries` | error | - | new | open | +| `react-hooks/exhaustive-deps` | warn | - | new | open | +| `react-hooks/gating` | error | - | new | open | +| `react-hooks/globals` | error | - | new | open | +| `react-hooks/immutability` | error | - | new | open | +| `react-hooks/incompatible-library` | warn | - | new | open | +| `react-hooks/preserve-manual-memoization` | error | - | new | open | +| `react-hooks/purity` | error | - | new | open | +| `react-hooks/refs` | error | - | new | open | +| `react-hooks/rules-of-hooks` | error | - | new | open | +| `react-hooks/set-state-in-effect` | error | - | new | open | +| `react-hooks/set-state-in-render` | error | - | new | open | +| `react-hooks/static-components` | error | - | new | open | +| `react-hooks/unsupported-syntax` | warn | - | new | open | +| `react-hooks/use-memo` | error | - | new | open | +| `react-refresh/only-export-components` | warn {"allowConstantExport":true} | - | eventured | open | diff --git a/package.json b/package.json index cee8561..152a858 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "packages/*" ], "scripts": { - "test": "npm test --workspaces --if-present && node --test test-utils", + "test": "npm test --workspaces --if-present && node --test test-utils scripts", "lint": "eslint .", "changeset": "changeset", - "release": "changeset publish" + "release": "changeset publish", + "rules": "node scripts/rule-table.js" }, "repository": { "type": "git", diff --git a/scripts/rule-sources.json b/scripts/rule-sources.json new file mode 100644 index 0000000..e578dc4 --- /dev/null +++ b/scripts/rule-sources.json @@ -0,0 +1,31 @@ +{ + "logical-assignment-operators": "cospired", + "prefer-object-has-own": "cospired", + "@stylistic/padding-line-between-statements": "hapi", + "import-x/consistent-type-specifier-style": "cospired", + "import-x/no-empty-named-blocks": "cospired", + "import-x/no-import-module-exports": "cospired", + "import-x/no-relative-packages": "cospired", + "import-x/newline-after-import": "cospired", + "import-x/no-extraneous-dependencies": "eventured", + "import-x/order": "eventured", + "no-console": "eventured", + "no-unused-expressions": "eventured", + "no-unused-vars": "cospired", + "no-var": "eventured", + "object-shorthand": "eventured", + "prefer-arrow-callback": "eventured", + "prefer-const": "eventured", + "@eslint-community/eslint-comments/require-description": "eventured", + "todo-plz/ticket-ref": "eventured", + "@typescript-eslint/explicit-function-return-type": "eventured", + "@typescript-eslint/no-misused-promises": "eventured", + "@typescript-eslint/no-generated-empty-object-type": "eventured", + "@typescript-eslint/no-explicit-any": "eventured", + "@typescript-eslint/no-unused-vars": "eventured", + "@typescript-eslint/prefer-nullish-coalescing": "eventured", + "react-refresh/only-export-components": "eventured", + "formatjs/enforce-description": "eventured", + "formatjs/no-camel-case": "eventured", + "formatjs/no-invalid-icu": "eventured" +} diff --git a/scripts/rule-table.js b/scripts/rule-table.js new file mode 100644 index 0000000..b64a3fd --- /dev/null +++ b/scripts/rule-table.js @@ -0,0 +1,73 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const LEVELS = { 0: 'off', 1: 'warn', 2: 'error' }; + +const level = (entry) => { + const raw = Array.isArray(entry) ? entry[0] : entry; + + return LEVELS[raw] ?? raw; +}; + +const format = (entry) => { + if (entry === undefined) return '-'; + const options = Array.isArray(entry) ? entry.slice(1) : []; + + return [level(entry), ...options.map((o) => JSON.stringify(o))].join(' '); +}; + +/** Last-wins merge of every `rules` object in a config array (ignores `files` scoping). */ +const effective = (config) => Object.assign({}, ...config.map((item) => item.rules ?? {})); + +/** + * Rows for every rule where `config` is on and differs from `baseline`. + * @returns {Array<{ rule: string, ours: string, recommended: string, source: string }>} + */ +export function buildRuleTable(config, baseline, sources = {}) { + const ours = effective(config); + const base = effective(baseline); + + return Object.keys(ours) + .sort() + .filter((rule) => level(ours[rule]) !== 'off' || base[rule] !== undefined) + .filter((rule) => format(ours[rule]) !== format(base[rule])) + .map((rule) => ({ rule, ours: format(ours[rule]), recommended: format(base[rule]), source: sources[rule] ?? 'new' })); +} + +export function renderTable(title, rows) { + const lines = rows.map((r) => `| \`${r.rule}\` | ${r.ours} | ${r.recommended} | ${r.source} | open |`); + + return [`## ${title}`, '', '| Rule | Ours | Recommended | Source | Decision |', '|---|---|---|---|---|', ...lines, ''].join('\n'); +} + +async function main() { + const [{ default: js }, { default: tseslint }, base, baseTs, react, reactTs] = await Promise.all([ + import('@eslint/js'), + import('typescript-eslint'), + import('../packages/eslint-config-base/src/index.js'), + import('../packages/eslint-config-base/src/typescript.js'), + import('../packages/eslint-config-react/src/index.js'), + import('../packages/eslint-config-react/src/typescript.js'), + ]); + const sources = JSON.parse(fs.readFileSync(path.join(here, 'rule-sources.json'), 'utf8')); + const recommended = [js.configs.recommended]; + const tsRecommended = [...recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked]; + const tsConfig = baseTs.createTypescriptConfig(); + const reactTsConfig = reactTs.createReactTypescriptConfig(); + + const out = [ + '# Rule comparison', + '', + 'Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review.', + '', + renderTable('Base JS (vs. @eslint/js recommended)', buildRuleTable(base.default, recommended, sources)), + renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', buildRuleTable(tsConfig, tsRecommended, sources)), + renderTable('React JS (vs. Base JS)', buildRuleTable(react.default, base.default, sources)), + renderTable('React TypeScript (vs. Base TypeScript)', buildRuleTable(reactTsConfig, tsConfig, sources)), + ].join('\n'); + fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) await main(); diff --git a/scripts/rule-table.test.js b/scripts/rule-table.test.js new file mode 100644 index 0000000..ae0f56a --- /dev/null +++ b/scripts/rule-table.test.js @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { buildRuleTable, renderTable } from './rule-table.js'; + +const sources = { 'no-console': 'eventured' }; + +test('buildRuleTable lists only rules where ours differs from the baseline', () => { + const table = buildRuleTable( + [{ rules: { 'no-console': 'error', eqeqeq: 'error', 'no-var': ['error'], 'no-debugger': 'off' } }], + [{ rules: { 'no-console': 'warn', eqeqeq: 'error', 'no-var': 'error' } }], + sources, + ); + assert.deepEqual(table, [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); +}); + +test('buildRuleTable includes rules the baseline does not set, marked source "new" when unknown', () => { + const table = buildRuleTable([{ rules: { 'prefer-const': ['error', { destructuring: 'all' }] } }], [{ rules: {} }], sources); + assert.deepEqual(table, [{ rule: 'prefer-const', ours: 'error {"destructuring":"all"}', recommended: '-', source: 'new' }]); +}); + +test('renderTable renders a markdown table with a Decision column', () => { + const md = renderTable('Base JS', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); + assert.match(md, /^## Base JS/m); + assert.match(md, /\| `no-console` \| error \| warn \| eventured \| open \|/); +}); From 7ea93e3cc90689946e08acbd52c3def425cbe863 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 13:03:44 +0800 Subject: [PATCH 08/33] docs: resolve rule table per file Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- docs/rules.md | 60 ++++++++++++------------------------------- scripts/rule-table.js | 30 +++++++++++++++++----- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index 64aaf4b..9b8a1c7 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -26,16 +26,16 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `import-x/no-named-as-default-member` | warn | - | new | open | | `import-x/no-relative-packages` | error | - | cospired | open | | `import-x/no-unresolved` | error | - | new | open | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}} | - | eventured | open | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | open | | `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | -| `no-console` | warn | - | eventured | open | +| `no-console` | warn {} | - | eventured | open | | `no-unexpected-multiline` | off | error | new | open | -| `no-unused-expressions` | error | - | eventured | open | -| `no-unused-vars` | error {"argsIgnorePattern":"^_"} | error | cospired | open | +| `no-unused-expressions` | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | - | eventured | open | +| `no-unused-vars` | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_"} | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | open | | `no-var` | error | - | eventured | open | -| `object-shorthand` | error | - | eventured | open | -| `prefer-arrow-callback` | error | - | eventured | open | -| `prefer-const` | error | - | eventured | open | +| `object-shorthand` | error "always" | - | eventured | open | +| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | open | +| `prefer-const` | error {"destructuring":"any","ignoreReadBeforeAssign":false} | - | eventured | open | | `prefer-object-has-own` | error | - | cospired | open | | `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | @@ -50,40 +50,11 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | open | | `@eslint-community/eslint-comments/require-description` | error | - | eventured | open | | `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | open | -| `@typescript-eslint/await-thenable` | off | error | new | open | -| `@typescript-eslint/dot-notation` | off | error | new | open | | `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | open | -| `@typescript-eslint/no-array-delete` | off | error | new | open | -| `@typescript-eslint/no-base-to-string` | off | error | new | open | -| `@typescript-eslint/no-duplicate-type-constituents` | off | error | new | open | | `@typescript-eslint/no-explicit-any` | warn | error | eventured | open | -| `@typescript-eslint/no-floating-promises` | off | error | new | open | -| `@typescript-eslint/no-for-in-array` | off | error | new | open | -| `@typescript-eslint/no-implied-eval` | off | error | new | open | -| `@typescript-eslint/no-misused-promises` | off | error | eventured | open | -| `@typescript-eslint/no-redundant-type-constituents` | off | error | new | open | -| `@typescript-eslint/no-unnecessary-type-assertion` | off | error | new | open | -| `@typescript-eslint/no-unsafe-argument` | off | error | new | open | -| `@typescript-eslint/no-unsafe-assignment` | off | error | new | open | -| `@typescript-eslint/no-unsafe-call` | off | error | new | open | -| `@typescript-eslint/no-unsafe-enum-comparison` | off | error | new | open | -| `@typescript-eslint/no-unsafe-member-access` | off | error | new | open | -| `@typescript-eslint/no-unsafe-return` | off | error | new | open | -| `@typescript-eslint/no-unsafe-unary-minus` | off | error | new | open | +| `@typescript-eslint/no-generated-empty-object-type` | error | - | eventured | open | +| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | open | | `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_"} | error | eventured | open | -| `@typescript-eslint/non-nullable-type-assertion-style` | off | error | new | open | -| `@typescript-eslint/only-throw-error` | off | error | new | open | -| `@typescript-eslint/prefer-find` | off | error | new | open | -| `@typescript-eslint/prefer-includes` | off | error | new | open | -| `@typescript-eslint/prefer-nullish-coalescing` | off | error | eventured | open | -| `@typescript-eslint/prefer-optional-chain` | off | error | new | open | -| `@typescript-eslint/prefer-promise-reject-errors` | off | error | new | open | -| `@typescript-eslint/prefer-regexp-exec` | off | error | new | open | -| `@typescript-eslint/prefer-string-starts-ends-with` | off | error | new | open | -| `@typescript-eslint/require-await` | off | error | new | open | -| `@typescript-eslint/restrict-plus-operands` | off | error | new | open | -| `@typescript-eslint/restrict-template-expressions` | off | error | new | open | -| `@typescript-eslint/unbound-method` | off | error | new | open | | `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | open | | `import-x/default` | error | - | new | open | | `import-x/export` | error | - | new | open | @@ -97,12 +68,13 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `import-x/no-named-as-default-member` | warn | - | new | open | | `import-x/no-relative-packages` | error | - | cospired | open | | `import-x/no-unresolved` | error | - | new | open | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}} | - | eventured | open | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | open | | `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | -| `no-console` | warn | - | eventured | open | +| `no-console` | warn {} | - | eventured | open | | `no-unexpected-multiline` | off | error | new | open | -| `object-shorthand` | error | - | eventured | open | -| `prefer-arrow-callback` | error | - | eventured | open | +| `no-unused-vars` | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_"} | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | open | +| `object-shorthand` | error "always" | - | eventured | open | +| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | open | | `prefer-object-has-own` | error | - | cospired | open | | `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | @@ -126,7 +98,7 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `formatjs/no-useless-message` | error | - | new | open | | `formatjs/prefer-full-sentence` | error | - | new | open | | `formatjs/prefer-pound-in-plural` | error | - | new | open | -| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true} | error | eventured | open | +| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | eventured | open | | `react-hooks/config` | error | - | new | open | | `react-hooks/error-boundaries` | error | - | new | open | | `react-hooks/exhaustive-deps` | warn | - | new | open | @@ -165,7 +137,7 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `formatjs/no-useless-message` | error | - | new | open | | `formatjs/prefer-full-sentence` | error | - | new | open | | `formatjs/prefer-pound-in-plural` | error | - | new | open | -| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true} | off | eventured | open | +| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | off {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | eventured | open | | `react-hooks/config` | error | - | new | open | | `react-hooks/error-boundaries` | error | - | new | open | | `react-hooks/exhaustive-deps` | warn | - | new | open | diff --git a/scripts/rule-table.js b/scripts/rule-table.js index b64a3fd..df49d0d 100644 --- a/scripts/rule-table.js +++ b/scripts/rule-table.js @@ -2,6 +2,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { ESLint } from 'eslint'; + const here = path.dirname(fileURLToPath(import.meta.url)); const LEVELS = { 0: 'off', 1: 'warn', 2: 'error' }; @@ -42,8 +44,16 @@ export function renderTable(title, rows) { return [`## ${title}`, '', '| Rule | Ours | Recommended | Source | Decision |', '|---|---|---|---|---|', ...lines, ''].join('\n'); } +/** Rules as ESLint resolves them for one file (honours `files`/`ignores` scoping), as a one-item config array. */ +async function resolved(config, file) { + const filePath = path.join(here, '..', file); + const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: path.dirname(filePath) }); + + return [{ rules: (await eslint.calculateConfigForFile(filePath)).rules }]; +} + async function main() { - const [{ default: js }, { default: tseslint }, base, baseTs, react, reactTs] = await Promise.all([ + const [{ default: js }, { default: tseslint }, base, baseTsModule, react, reactTs] = await Promise.all([ import('@eslint/js'), import('typescript-eslint'), import('../packages/eslint-config-base/src/index.js'), @@ -54,18 +64,26 @@ async function main() { const sources = JSON.parse(fs.readFileSync(path.join(here, 'rule-sources.json'), 'utf8')); const recommended = [js.configs.recommended]; const tsRecommended = [...recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked]; - const tsConfig = baseTs.createTypescriptConfig(); + const tsConfig = baseTsModule.createTypescriptConfig(); const reactTsConfig = reactTs.createReactTypescriptConfig(); + const [baseJs, recJs, baseTsRules, recTs, reactJs, reactTsRules] = await Promise.all([ + resolved(base.default, 'src/a.js'), + resolved(recommended, 'src/a.js'), + resolved(tsConfig, 'src/a.ts'), + resolved(tsRecommended, 'src/a.ts'), + resolved(react.default, 'src/a.jsx'), + resolved(reactTsConfig, 'src/a.tsx'), + ]); const out = [ '# Rule comparison', '', 'Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review.', '', - renderTable('Base JS (vs. @eslint/js recommended)', buildRuleTable(base.default, recommended, sources)), - renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', buildRuleTable(tsConfig, tsRecommended, sources)), - renderTable('React JS (vs. Base JS)', buildRuleTable(react.default, base.default, sources)), - renderTable('React TypeScript (vs. Base TypeScript)', buildRuleTable(reactTsConfig, tsConfig, sources)), + renderTable('Base JS (vs. @eslint/js recommended)', buildRuleTable(baseJs, recJs, sources)), + renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', buildRuleTable(baseTsRules, recTs, sources)), + renderTable('React JS (vs. Base JS)', buildRuleTable(reactJs, baseJs, sources)), + renderTable('React TypeScript (vs. Base TypeScript)', buildRuleTable(reactTsRules, baseTsRules, sources)), ].join('\n'); fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); } From 83fd2b5f1cda6e12597561f81a431cacdf9dd56d Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 15:49:08 +0800 Subject: [PATCH 09/33] feat: apply rule review decisions Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- docs/rules.md | 282 +++++++++--------- packages/eslint-config-base/src/index.js | 17 +- packages/eslint-config-base/src/node.js | 2 - .../eslint-config-base/src/rules/general.js | 16 +- .../eslint-config-base/src/rules/imports.js | 3 + .../src/rules/typescript.js | 12 +- packages/eslint-config-base/src/typescript.js | 5 +- .../eslint-config-base/test/decisions.test.js | 66 ++++ packages/eslint-config-react/src/index.js | 5 +- .../eslint-config-react/src/rules/errors.js | 11 + .../eslint-config-react/src/rules/formatjs.js | 4 +- .../eslint-config-react/src/rules/react.js | 9 +- .../eslint-config-react/src/typescript.js | 4 +- .../test/decisions.test.js | 28 ++ scripts/rule-decisions.json | 54 ++++ scripts/rule-table.js | 19 +- scripts/rule-table.test.js | 5 + test-utils/lint.js | 9 + 18 files changed, 391 insertions(+), 160 deletions(-) create mode 100644 packages/eslint-config-base/test/decisions.test.js create mode 100644 packages/eslint-config-react/src/rules/errors.js create mode 100644 packages/eslint-config-react/test/decisions.test.js create mode 100644 scripts/rule-decisions.json diff --git a/docs/rules.md b/docs/rules.md index 9b8a1c7..81b60d6 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -6,152 +6,166 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | open | -| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | open | -| `@eslint-community/eslint-comments/require-description` | error | - | eventured | open | -| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | open | -| `import-x/default` | error | - | new | open | -| `import-x/export` | error | - | new | open | -| `import-x/named` | error | - | new | open | -| `import-x/namespace` | error | - | new | open | -| `import-x/newline-after-import` | error | - | cospired | open | -| `import-x/no-duplicates` | error | - | new | open | -| `import-x/no-empty-named-blocks` | error | - | cospired | open | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | open | -| `import-x/no-import-module-exports` | error | - | cospired | open | -| `import-x/no-named-as-default` | warn | - | new | open | -| `import-x/no-named-as-default-member` | warn | - | new | open | -| `import-x/no-relative-packages` | error | - | cospired | open | -| `import-x/no-unresolved` | error | - | new | open | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | open | -| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | -| `no-console` | warn {} | - | eventured | open | -| `no-unexpected-multiline` | off | error | new | open | -| `no-unused-expressions` | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | - | eventured | open | -| `no-unused-vars` | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_"} | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | open | -| `no-var` | error | - | eventured | open | -| `object-shorthand` | error "always" | - | eventured | open | -| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | open | -| `prefer-const` | error {"destructuring":"any","ignoreReadBeforeAssign":false} | - | eventured | open | -| `prefer-object-has-own` | error | - | cospired | open | -| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | +| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | +| `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | +| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | +| `consistent-return` | error {"treatUndefinedAsUnspecified":false} | - | new | adopted, JS only | +| `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | +| `default-param-last` | error | - | new | adopted | +| `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | +| `import-x/default` | error | - | new | plugin preset, unchanged | +| `import-x/export` | error | - | new | plugin preset, unchanged | +| `import-x/named` | error | - | new | plugin preset, unchanged | +| `import-x/namespace` | error | - | new | plugin preset, unchanged | +| `import-x/newline-after-import` | error | - | cospired | kept | +| `import-x/no-duplicates` | error | - | new | kept | +| `import-x/no-empty-named-blocks` | error | - | cospired | kept | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | +| `import-x/no-import-module-exports` | error | - | cospired | kept | +| `import-x/no-relative-packages` | error | - | cospired | kept | +| `import-x/no-unresolved` | error | - | new | plugin preset, unchanged | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | +| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | +| `no-await-in-loop` | error | - | new | adopted | +| `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | +| `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | +| `no-nested-ternary` | error | - | new | adopted | +| `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | +| `no-shadow` | error {"allow":[],"builtinGlobals":false,"hoist":"functions","ignoreOnInitialization":false,"ignoreTypeValueShadow":true,"ignoreFunctionTypeParameterNameValueShadow":true} | - | new | adopted (TS: typescript-eslint version) | +| `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | +| `no-unused-expressions` | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | - | eventured | strict, no React relaxations | +| `no-unused-vars` | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | error, `_` prefix ignored | +| `no-use-before-define` | error {"classes":true,"functions":true,"variables":true,"allowNamedExports":false,"enums":true,"typedefs":true,"ignoreTypeReferences":true} | - | new | adopted, strict | +| `no-var` | error | - | eventured | kept | +| `object-shorthand` | error "always" | - | eventured | kept | +| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | +| `prefer-const` | error {"destructuring":"any","ignoreReadBeforeAssign":false} | - | eventured | kept | +| `prefer-object-has-own` | error | - | cospired | kept | +| `prefer-template` | error | - | new | adopted | +| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | ## Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked) | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | open | -| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | open | -| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | open | -| `@eslint-community/eslint-comments/require-description` | error | - | eventured | open | -| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | open | -| `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | open | -| `@typescript-eslint/no-explicit-any` | warn | error | eventured | open | -| `@typescript-eslint/no-generated-empty-object-type` | error | - | eventured | open | -| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | open | -| `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_"} | error | eventured | open | -| `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | open | -| `import-x/default` | error | - | new | open | -| `import-x/export` | error | - | new | open | -| `import-x/namespace` | error | - | new | open | -| `import-x/newline-after-import` | error | - | cospired | open | -| `import-x/no-duplicates` | error | - | new | open | -| `import-x/no-empty-named-blocks` | error | - | cospired | open | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | open | -| `import-x/no-import-module-exports` | error | - | cospired | open | -| `import-x/no-named-as-default` | warn | - | new | open | -| `import-x/no-named-as-default-member` | warn | - | new | open | -| `import-x/no-relative-packages` | error | - | cospired | open | -| `import-x/no-unresolved` | error | - | new | open | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | open | -| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | open | -| `no-console` | warn {} | - | eventured | open | -| `no-unexpected-multiline` | off | error | new | open | -| `no-unused-vars` | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_"} | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | open | -| `object-shorthand` | error "always" | - | eventured | open | -| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | open | -| `prefer-object-has-own` | error | - | cospired | open | -| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | open | +| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | +| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | +| `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | +| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | +| `@typescript-eslint/default-param-last` | error | - | new | adopted | +| `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | default on for non-test files, allowExpressions | +| `@typescript-eslint/no-generated-empty-object-type` | error | - | eventured | default on | +| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | default on, flagUnions strict | +| `@typescript-eslint/no-shadow` | error | - | new | adopted | +| `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error | eventured | error, `_` prefix ignored | +| `@typescript-eslint/no-use-before-define` | error | - | new | adopted, strict | +| `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | +| `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | +| `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | kept: prefer-inline | +| `import-x/default` | error | - | new | plugin preset, unchanged | +| `import-x/export` | error | - | new | plugin preset, unchanged | +| `import-x/namespace` | error | - | new | plugin preset, unchanged | +| `import-x/newline-after-import` | error | - | cospired | kept | +| `import-x/no-duplicates` | error | - | new | kept | +| `import-x/no-empty-named-blocks` | error | - | cospired | kept | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | +| `import-x/no-import-module-exports` | error | - | cospired | kept | +| `import-x/no-relative-packages` | error | - | cospired | kept | +| `import-x/no-unresolved` | error | - | new | plugin preset, unchanged | +| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | +| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | +| `no-await-in-loop` | error | - | new | adopted | +| `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | +| `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | +| `no-nested-ternary` | error | - | new | adopted | +| `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | +| `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | +| `no-unused-vars` | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | error, `_` prefix ignored | +| `object-shorthand` | error "always" | - | eventured | kept | +| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | +| `prefer-object-has-own` | error | - | cospired | kept | +| `prefer-template` | error | - | new | adopted | +| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | ## React JS (vs. Base JS) | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | open | -| `formatjs/enforce-default-message` | error "literal" | - | new | open | -| `formatjs/enforce-placeholders` | error | - | new | open | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | open | -| `formatjs/no-camel-case` | error | - | eventured | open | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | open | -| `formatjs/no-emoji` | error | - | new | open | -| `formatjs/no-invalid-icu` | error | - | eventured | open | -| `formatjs/no-literal-string-in-jsx` | warn {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | open | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | open | -| `formatjs/no-multiple-plurals` | error | - | new | open | -| `formatjs/no-multiple-whitespaces` | error | - | new | open | -| `formatjs/no-offset` | error | - | new | open | -| `formatjs/no-useless-message` | error | - | new | open | -| `formatjs/prefer-full-sentence` | error | - | new | open | -| `formatjs/prefer-pound-in-plural` | error | - | new | open | -| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | eventured | open | -| `react-hooks/config` | error | - | new | open | -| `react-hooks/error-boundaries` | error | - | new | open | -| `react-hooks/exhaustive-deps` | warn | - | new | open | -| `react-hooks/gating` | error | - | new | open | -| `react-hooks/globals` | error | - | new | open | -| `react-hooks/immutability` | error | - | new | open | -| `react-hooks/incompatible-library` | warn | - | new | open | -| `react-hooks/preserve-manual-memoization` | error | - | new | open | -| `react-hooks/purity` | error | - | new | open | -| `react-hooks/refs` | error | - | new | open | -| `react-hooks/rules-of-hooks` | error | - | new | open | -| `react-hooks/set-state-in-effect` | error | - | new | open | -| `react-hooks/set-state-in-render` | error | - | new | open | -| `react-hooks/static-components` | error | - | new | open | -| `react-hooks/unsupported-syntax` | warn | - | new | open | -| `react-hooks/use-memo` | error | - | new | open | -| `react-refresh/only-export-components` | warn {"allowConstantExport":true} | - | eventured | open | +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | plugin preset, unchanged | +| `formatjs/enforce-default-message` | error "literal" | - | new | plugin preset, unchanged | +| `formatjs/enforce-placeholders` | error | - | new | plugin preset, unchanged | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | plugin preset, unchanged | +| `formatjs/no-camel-case` | error | - | eventured | error (eventured) | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | plugin preset, unchanged | +| `formatjs/no-emoji` | error | - | new | plugin preset, unchanged | +| `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | +| `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | plugin preset, unchanged | +| `formatjs/no-multiple-plurals` | error | - | new | plugin preset, unchanged | +| `formatjs/no-multiple-whitespaces` | error | - | new | plugin preset, unchanged | +| `formatjs/no-offset` | error | - | new | plugin preset, unchanged | +| `formatjs/no-useless-message` | error | - | new | plugin preset, unchanged | +| `formatjs/prefer-full-sentence` | error | - | new | plugin preset, unchanged | +| `formatjs/prefer-pound-in-plural` | error | - | new | plugin preset, unchanged | +| `react-hooks/config` | error | - | new | plugin preset, unchanged | +| `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | +| `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | +| `react-hooks/gating` | error | - | new | plugin preset, unchanged | +| `react-hooks/globals` | error | - | new | plugin preset, unchanged | +| `react-hooks/immutability` | error | - | new | plugin preset, unchanged | +| `react-hooks/incompatible-library` | error | - | new | error (no warn policy) | +| `react-hooks/preserve-manual-memoization` | error | - | new | plugin preset, unchanged | +| `react-hooks/purity` | error | - | new | plugin preset, unchanged | +| `react-hooks/refs` | error | - | new | plugin preset, unchanged | +| `react-hooks/rules-of-hooks` | error | - | new | plugin preset, unchanged | +| `react-hooks/set-state-in-effect` | error | - | new | plugin preset, unchanged | +| `react-hooks/set-state-in-render` | error | - | new | plugin preset, unchanged | +| `react-hooks/static-components` | error | - | new | plugin preset, unchanged | +| `react-hooks/unsupported-syntax` | error | - | new | error (no warn policy) | +| `react-hooks/use-memo` | error | - | new | plugin preset, unchanged | +| `react-refresh/only-export-components` | error {"allowConstantExport":true} | - | eventured | error (no warn policy) | ## React TypeScript (vs. Base TypeScript) | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | open | -| `formatjs/enforce-default-message` | error "literal" | - | new | open | -| `formatjs/enforce-placeholders` | error | - | new | open | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | open | -| `formatjs/no-camel-case` | error | - | eventured | open | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | open | -| `formatjs/no-emoji` | error | - | new | open | -| `formatjs/no-invalid-icu` | error | - | eventured | open | -| `formatjs/no-literal-string-in-jsx` | warn {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | open | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | open | -| `formatjs/no-multiple-plurals` | error | - | new | open | -| `formatjs/no-multiple-whitespaces` | error | - | new | open | -| `formatjs/no-offset` | error | - | new | open | -| `formatjs/no-useless-message` | error | - | new | open | -| `formatjs/prefer-full-sentence` | error | - | new | open | -| `formatjs/prefer-pound-in-plural` | error | - | new | open | -| `no-unused-expressions` | error {"allowShortCircuit":true,"allowTernary":true,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | off {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | eventured | open | -| `react-hooks/config` | error | - | new | open | -| `react-hooks/error-boundaries` | error | - | new | open | -| `react-hooks/exhaustive-deps` | warn | - | new | open | -| `react-hooks/gating` | error | - | new | open | -| `react-hooks/globals` | error | - | new | open | -| `react-hooks/immutability` | error | - | new | open | -| `react-hooks/incompatible-library` | warn | - | new | open | -| `react-hooks/preserve-manual-memoization` | error | - | new | open | -| `react-hooks/purity` | error | - | new | open | -| `react-hooks/refs` | error | - | new | open | -| `react-hooks/rules-of-hooks` | error | - | new | open | -| `react-hooks/set-state-in-effect` | error | - | new | open | -| `react-hooks/set-state-in-render` | error | - | new | open | -| `react-hooks/static-components` | error | - | new | open | -| `react-hooks/unsupported-syntax` | warn | - | new | open | -| `react-hooks/use-memo` | error | - | new | open | -| `react-refresh/only-export-components` | warn {"allowConstantExport":true} | - | eventured | open | +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | plugin preset, unchanged | +| `formatjs/enforce-default-message` | error "literal" | - | new | plugin preset, unchanged | +| `formatjs/enforce-placeholders` | error | - | new | plugin preset, unchanged | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | plugin preset, unchanged | +| `formatjs/no-camel-case` | error | - | eventured | error (eventured) | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | plugin preset, unchanged | +| `formatjs/no-emoji` | error | - | new | plugin preset, unchanged | +| `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | +| `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | plugin preset, unchanged | +| `formatjs/no-multiple-plurals` | error | - | new | plugin preset, unchanged | +| `formatjs/no-multiple-whitespaces` | error | - | new | plugin preset, unchanged | +| `formatjs/no-offset` | error | - | new | plugin preset, unchanged | +| `formatjs/no-useless-message` | error | - | new | plugin preset, unchanged | +| `formatjs/prefer-full-sentence` | error | - | new | plugin preset, unchanged | +| `formatjs/prefer-pound-in-plural` | error | - | new | plugin preset, unchanged | +| `react-hooks/config` | error | - | new | plugin preset, unchanged | +| `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | +| `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | +| `react-hooks/gating` | error | - | new | plugin preset, unchanged | +| `react-hooks/globals` | error | - | new | plugin preset, unchanged | +| `react-hooks/immutability` | error | - | new | plugin preset, unchanged | +| `react-hooks/incompatible-library` | error | - | new | error (no warn policy) | +| `react-hooks/preserve-manual-memoization` | error | - | new | plugin preset, unchanged | +| `react-hooks/purity` | error | - | new | plugin preset, unchanged | +| `react-hooks/refs` | error | - | new | plugin preset, unchanged | +| `react-hooks/rules-of-hooks` | error | - | new | plugin preset, unchanged | +| `react-hooks/set-state-in-effect` | error | - | new | plugin preset, unchanged | +| `react-hooks/set-state-in-render` | error | - | new | plugin preset, unchanged | +| `react-hooks/static-components` | error | - | new | plugin preset, unchanged | +| `react-hooks/unsupported-syntax` | error | - | new | error (no warn policy) | +| `react-hooks/use-memo` | error | - | new | plugin preset, unchanged | +| `react-refresh/only-export-components` | error {"allowConstantExport":true} | - | eventured | error (no warn policy) | diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index 6b9b261..3d26136 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -9,12 +9,27 @@ import style from './rules/style.js'; export { globalIgnores } from './ignores.js'; export { createImportsConfig, defaultDevDependencyGlobs } from './rules/imports.js'; +/** + * eslint-config-prettier disables `curly`, so it must be re-enabled after it. Every config ends with these. + * @type {import('eslint').Linter.Config[]} + */ +export const prettierLast = [prettier, { rules: { curly: ['error', 'multi-line'] } }]; + /** * Language-agnostic rules. Prettier last. * @param {Parameters[0]} [importOptions] */ export function createBaseConfig(importOptions) { - return [js.configs.recommended, comments, createImportsConfig(importOptions), style, general, prettier]; + return [ + js.configs.recommended, + comments, + createImportsConfig(importOptions), + style, + general, + // CLI output is expected in scripts. + { files: ['scripts/**/*.{mjs,js,ts}'], rules: { 'no-console': 'off' } }, + ...prettierLast, + ]; } /** JavaScript config: ESLint recommended + cospired rules. */ diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index ac6352e..b4774a1 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -24,7 +24,5 @@ export function createNodeConfig(options = {}) { '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], }, }, - // CLI output is expected in scripts. - { files: ['scripts/**/*.{mjs,js,ts}'], rules: { 'no-console': 'off' } }, ]; } diff --git a/packages/eslint-config-base/src/rules/general.js b/packages/eslint-config-base/src/rules/general.js index 94644f9..d50cf4a 100644 --- a/packages/eslint-config-base/src/rules/general.js +++ b/packages/eslint-config-base/src/rules/general.js @@ -1,14 +1,24 @@ -/** General JavaScript rules beyond `@eslint/js` recommended. Severities are placeholders until the rule review. */ +/** General JavaScript rules beyond `@eslint/js` recommended. Every rule here was decided in the rule review (docs/rules.md). */ export default { rules: { + 'consistent-return': 'error', + 'default-param-last': 'error', + eqeqeq: ['error', 'always', { null: 'ignore' }], 'logical-assignment-operators': ['error', 'always', { enforceForIfStatements: true }], - 'no-console': 'warn', + 'no-await-in-loop': 'error', + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-else-return': ['error', { allowElseIf: false }], + 'no-nested-ternary': 'error', + 'no-param-reassign': ['error', { props: false }], + 'no-shadow': 'error', 'no-unused-expressions': 'error', - 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + 'no-use-before-define': 'error', 'no-var': 'error', 'object-shorthand': 'error', 'prefer-arrow-callback': 'error', 'prefer-const': 'error', 'prefer-object-has-own': 'error', + 'prefer-template': 'error', }, }; diff --git a/packages/eslint-config-base/src/rules/imports.js b/packages/eslint-config-base/src/rules/imports.js index 11ef6ff..74842be 100644 --- a/packages/eslint-config-base/src/rules/imports.js +++ b/packages/eslint-config-base/src/rules/imports.js @@ -23,6 +23,9 @@ export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyG plugins: { 'import-x': importX }, rules: { ...importX.flatConfigs.recommended.rules, + // Plugins default-export objects whose members also ship as named exports. + 'import-x/no-named-as-default': 'off', + 'import-x/no-named-as-default-member': 'off', 'import-x/newline-after-import': 'error', 'import-x/no-duplicates': 'error', 'import-x/no-empty-named-blocks': 'error', diff --git a/packages/eslint-config-base/src/rules/typescript.js b/packages/eslint-config-base/src/rules/typescript.js index 5cd1c0d..173fe49 100644 --- a/packages/eslint-config-base/src/rules/typescript.js +++ b/packages/eslint-config-base/src/rules/typescript.js @@ -6,10 +6,18 @@ export const testGlobs = ['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests /** Rules on top of typescript-eslint's recommended/stylistic type-checked sets. Severities are placeholders until the rule review. */ export const typescriptRules = { - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-generated-empty-object-type': 'error', '@typescript-eslint/no-misused-promises': ['error', { checksConditionals: { flagUnions: 'strict' } }], - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + '@typescript-eslint/no-use-before-define': 'error', + // Core versions replaced by the typescript-eslint ones above; the compiler's noImplicitReturns covers consistent-return. + 'consistent-return': 'off', + 'default-param-last': 'off', + 'no-shadow': 'off', + 'no-use-before-define': 'off', 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'], }; diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index ddc724d..a4f4fc3 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -1,8 +1,7 @@ -import prettier from 'eslint-config-prettier'; import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; -import { createBaseConfig } from './index.js'; +import { createBaseConfig, prettierLast } from './index.js'; import { importSettings, importXTypescript, sourceOnlyRules, testGlobs, typescriptRules } from './rules/typescript.js'; export { globalIgnores } from './ignores.js'; @@ -41,6 +40,6 @@ export function createTypescriptConfig({ }), { files, ignores: testGlobs, rules: sourceOnlyRules }, { files: ['**/*.{js,mjs,cjs}'], ...tseslint.configs.disableTypeChecked }, - prettier, + ...prettierLast, ]; } diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js new file mode 100644 index 0000000..2a35607 --- /dev/null +++ b/packages/eslint-config-base/test/decisions.test.js @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { loadedRules, lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; +import config from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; +import { createTypescriptConfig } from '../src/typescript.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const tsOptions = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; +const tsConfig = createTypescriptConfig(tsOptions); +const lint = async (code, file = 'src/a.js', cfg = config) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); + +// [rule, code that must be flagged, code that must pass] +const cases = [ + ['eqeqeq', 'export const a = (x, y) => x == y;\n', 'export const a = (x) => x == null;\n'], + ['no-param-reassign', 'export function f(a) {\n a = 1;\n\n return a;\n}\n', 'export function f(a) {\n a.x = 1;\n}\n'], + ['consistent-return', 'export function f(a) {\n if (a) {\n return 1;\n }\n}\n', 'export function f(a) {\n if (a) {\n return 1;\n }\n\n return 2;\n}\n'], + ['no-shadow', 'const a = 1;\n\nexport function f() {\n const a = 2;\n\n return a;\n}\n\nexport { a };\n', 'const a = 1;\n\nexport function f() {\n const b = 2;\n\n return b;\n}\n\nexport { a };\n'], + ['prefer-template', "export const s = (b) => 'a' + b;\n", 'export const s = (b) => `a${b}`;\n'], + ['no-nested-ternary', 'export const s = (a, b) => (a ? 1 : b ? 2 : 3);\n', 'export const s = (a) => (a ? 1 : 2);\n'], + ['no-else-return', 'export function f(a) {\n if (a) {\n return 1;\n } else {\n return 2;\n }\n}\n', 'export function f(a) {\n if (a) {\n return 1;\n }\n\n return 2;\n}\n'], + ['curly', 'export function f(a) {\n if (a)\n return 1;\n\n return 2;\n}\n', 'export function f(a) {\n if (a) return 1;\n\n return 2;\n}\n'], + ['no-use-before-define', 'export const r = f();\n\nfunction f() {\n return 1;\n}\n', 'function f() {\n return 1;\n}\n\nexport const r = f();\n'], + ['default-param-last', 'export function f(a = 1, b) {\n return [a, b];\n}\n', 'export function f(b, a = 1) {\n return [a, b];\n}\n'], + ['no-await-in-loop', 'export async function f(xs) {\n for (const x of xs) {\n await x;\n }\n}\n', 'export async function f(xs) {\n await Promise.all(xs);\n}\n'], + ['no-unused-vars', 'export function f(a) {\n return 1;\n}\n', 'export function f(_a) {\n return 1;\n}\n'], + ['no-unused-vars', 'export function f() {\n const x = 1;\n}\n', 'export function f() {\n const _x = 1;\n}\n'], + ['no-console', 'console.log(1);\n', 'console.warn(1);\nconsole.error(2);\n'], +]; + +for (const [rule, bad, good] of cases) { + test(`${rule}: flagged / accepted`, async () => { + assert.ok((await lint(bad)).includes(rule), `${rule} should flag`); + assert.ok(!(await lint(good)).includes(rule), `${rule} should accept`); + }); +} + +test('no-plusplus is not enabled; hapi plugin is gone', () => { + const rules = loadedRules(config); + assert.ok(!rules.has('no-plusplus')); + assert.deepEqual([...rules].filter((id) => id.startsWith('@hapi/')), []); +}); + +test('functions need no blank line at the start of the body (Prettier-safe)', async () => { + assert.ok(!(await lint('export function f() {\n return 1;\n}\n')).includes('@stylistic/padding-line-between-statements')); +}); + +test('import-x named-as-default rules are off', () => { + const rules = loadedRules(config); + assert.ok(!rules.has('import-x/no-named-as-default') && !rules.has('import-x/no-named-as-default-member')); +}); + +test('no rule resolves to "warn" in any base config', async () => { + assert.deepEqual(await warnRules(config, path.join(root, 'src/a.js'), root), []); + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.ts'), root), []); + assert.deepEqual(await warnRules(createNodeConfig(tsOptions), path.join(root, 'src/a.ts'), root), []); +}); + +test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { + const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts')); + for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); + for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); +}); diff --git a/packages/eslint-config-react/src/index.js b/packages/eslint-config-react/src/index.js index 4dab03e..070961c 100644 --- a/packages/eslint-config-react/src/index.js +++ b/packages/eslint-config-react/src/index.js @@ -1,5 +1,4 @@ -import baseConfig, { globalIgnores } from '@cospired/eslint-config-base'; -import prettier from 'eslint-config-prettier'; +import baseConfig, { globalIgnores, prettierLast } from '@cospired/eslint-config-base'; import formatjs from './rules/formatjs.js'; import react from './rules/react.js'; @@ -7,4 +6,4 @@ import react from './rules/react.js'; export { globalIgnores }; /** React JavaScript config: base JS + React rules. */ -export default [...baseConfig, ...react, formatjs, prettier]; +export default [...baseConfig, ...react, formatjs, ...prettierLast]; diff --git a/packages/eslint-config-react/src/rules/errors.js b/packages/eslint-config-react/src/rules/errors.js new file mode 100644 index 0000000..717f3b5 --- /dev/null +++ b/packages/eslint-config-react/src/rules/errors.js @@ -0,0 +1,11 @@ +/** The shipped configs never use `warn`: upgrade every warn in a preset's rules to error. */ +export function errorOnly(rules) { + return Object.fromEntries( + Object.entries(rules).map(([id, entry]) => { + if (entry === 'warn' || entry === 1) return [id, 'error']; + if (Array.isArray(entry) && (entry[0] === 'warn' || entry[0] === 1)) return [id, ['error', ...entry.slice(1)]]; + + return [id, entry]; + }), + ); +} diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js index 02c041d..792ff17 100644 --- a/packages/eslint-config-react/src/rules/formatjs.js +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -1,5 +1,7 @@ import formatjs from 'eslint-plugin-formatjs'; +import { errorOnly } from './errors.js'; + export const testGlobs = ['**/*.{test,spec}.{js,jsx,ts,tsx}']; /** i18n: formatjs `recommended` plus extras. Test files are exempt. Values are placeholders until the rule review. */ @@ -7,7 +9,7 @@ export default { plugins: formatjs.configs.recommended.plugins, ignores: testGlobs, rules: { - ...formatjs.configs.recommended.rules, + ...errorOnly(formatjs.configs.recommended.rules), 'formatjs/enforce-description': 'off', 'formatjs/no-camel-case': 'error', 'formatjs/no-invalid-icu': 'error', diff --git a/packages/eslint-config-react/src/rules/react.js b/packages/eslint-config-react/src/rules/react.js index fb8d048..5378e4a 100644 --- a/packages/eslint-config-react/src/rules/react.js +++ b/packages/eslint-config-react/src/rules/react.js @@ -2,11 +2,15 @@ import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; import globals from 'globals'; +import { errorOnly } from './errors.js'; + +const hooks = reactHooks.configs.flat.recommended; + /** React: hooks, refresh (Vite HMR), browser globals, JSX in plain JS. Values are placeholders until the rule review. */ export default [ // ESLint lints only .js/.mjs/.cjs by default; .jsx must be opted in. { files: ['**/*.jsx'] }, - reactHooks.configs.flat.recommended, + { ...hooks, rules: errorOnly(hooks.rules) }, { plugins: { 'react-refresh': reactRefresh }, languageOptions: { @@ -14,8 +18,7 @@ export default [ parserOptions: { ecmaFeatures: { jsx: true } }, }, rules: { - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - 'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }], + 'react-refresh/only-export-components': ['error', { allowConstantExport: true }], }, }, ]; diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js index 31b4042..6044bdf 100644 --- a/packages/eslint-config-react/src/typescript.js +++ b/packages/eslint-config-react/src/typescript.js @@ -1,5 +1,5 @@ +import { prettierLast } from '@cospired/eslint-config-base'; import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; -import prettier from 'eslint-config-prettier'; import formatjs from './rules/formatjs.js'; import react from './rules/react.js'; @@ -13,5 +13,5 @@ export { globalIgnores }; * @returns {import('eslint').Linter.Config[]} */ export function createReactTypescriptConfig(options = {}) { - return [...createTypescriptConfig(options), ...react, formatjs, prettier]; + return [...createTypescriptConfig(options), ...react, formatjs, ...prettierLast]; } diff --git a/packages/eslint-config-react/test/decisions.test.js b/packages/eslint-config-react/test/decisions.test.js new file mode 100644 index 0000000..08d73d0 --- /dev/null +++ b/packages/eslint-config-react/test/decisions.test.js @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; + +import { ruleSeverity, warnRules } from '../../../test-utils/lint.js'; + +import { ids, jsConfig, root, tsConfig } from './helpers.js'; + +test('no rule resolves to "warn" in the React configs', async () => { + assert.deepEqual(await warnRules(jsConfig, path.join(root, 'src/a.jsx'), root), []); + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.tsx'), root), []); +}); + +test('hooks exhaustive-deps, refresh and formatjs literal-string rules are errors', async () => { + for (const id of ['react-hooks/exhaustive-deps', 'react-refresh/only-export-components', 'formatjs/no-literal-string-in-jsx']) { + assert.equal(await ruleSeverity(jsConfig, id, path.join(root, 'src/a.jsx')), 2, id); + } +}); + +test('no-unused-expressions is strict in React code (no short-circuit statements)', async () => { + const code = 'export function f(a, g) {\n a && g();\n}\n'; + assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('no-unused-expressions')); +}); + +test('formatjs: enforce-description is off, no-camel-case is an error', async () => { + assert.equal(await ruleSeverity(jsConfig, 'formatjs/enforce-description', path.join(root, 'src/a.jsx')), 0); + assert.equal(await ruleSeverity(jsConfig, 'formatjs/no-camel-case', path.join(root, 'src/a.jsx')), 2); +}); diff --git a/scripts/rule-decisions.json b/scripts/rule-decisions.json new file mode 100644 index 0000000..119dc06 --- /dev/null +++ b/scripts/rule-decisions.json @@ -0,0 +1,54 @@ +{ + "@stylistic/padding-line-between-statements": "kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines)", + "no-unused-vars": "error, `_` prefix ignored", + "@typescript-eslint/no-unused-vars": "error, `_` prefix ignored", + "import-x/order": "always + alphabetize", + "no-console": "error, allow warn/error; scripts/** exempt", + "no-unused-expressions": "strict, no React relaxations", + "object-shorthand": "kept", + "prefer-arrow-callback": "kept", + "prefer-const": "kept", + "no-var": "kept", + "prefer-object-has-own": "kept", + "logical-assignment-operators": "kept (incl. if statements)", + "import-x/no-duplicates": "kept", + "import-x/no-relative-packages": "kept", + "import-x/no-import-module-exports": "kept", + "import-x/no-empty-named-blocks": "kept", + "import-x/newline-after-import": "kept", + "import-x/consistent-type-specifier-style": "kept: prefer-inline", + "@eslint-community/eslint-comments/disable-enable-pair": "kept", + "@eslint-community/eslint-comments/no-aggregating-enable": "kept", + "@eslint-community/eslint-comments/no-duplicate-disable": "kept", + "@eslint-community/eslint-comments/no-unlimited-disable": "kept", + "@eslint-community/eslint-comments/no-unused-enable": "kept", + "@eslint-community/eslint-comments/require-description": "kept", + "import-x/no-extraneous-dependencies": "kept (eventured globs, overridable)", + "todo-plz/ticket-ref": "kept", + "eqeqeq": "adopted, `== null` allowed", + "no-param-reassign": "adopted, props: false", + "consistent-return": "adopted, JS only", + "no-shadow": "adopted (TS: typescript-eslint version)", + "@typescript-eslint/no-shadow": "adopted", + "prefer-template": "adopted", + "no-nested-ternary": "adopted", + "no-else-return": "adopted, allowElseIf: false", + "curly": "adopted, multi-line (re-enabled after Prettier)", + "no-use-before-define": "adopted, strict", + "@typescript-eslint/no-use-before-define": "adopted, strict", + "default-param-last": "adopted", + "@typescript-eslint/default-param-last": "adopted", + "no-await-in-loop": "adopted", + "@typescript-eslint/explicit-function-return-type": "default on for non-test files, allowExpressions", + "@typescript-eslint/no-misused-promises": "default on, flagUnions strict", + "@typescript-eslint/no-generated-empty-object-type": "default on", + "@typescript-eslint/no-explicit-any": "error (no warn policy)", + "react-refresh/only-export-components": "error (no warn policy)", + "formatjs/enforce-description": "off (eventured)", + "formatjs/no-camel-case": "error (eventured)", + "formatjs/no-invalid-icu": "error (eventured)", + "formatjs/no-literal-string-in-jsx": "error (no warn policy)", + "react-hooks/exhaustive-deps": "error (no warn policy)", + "react-hooks/incompatible-library": "error (no warn policy)", + "react-hooks/unsupported-syntax": "error (no warn policy)" +} diff --git a/scripts/rule-table.js b/scripts/rule-table.js index df49d0d..ea73b6d 100644 --- a/scripts/rule-table.js +++ b/scripts/rule-table.js @@ -38,8 +38,8 @@ export function buildRuleTable(config, baseline, sources = {}) { .map((rule) => ({ rule, ours: format(ours[rule]), recommended: format(base[rule]), source: sources[rule] ?? 'new' })); } -export function renderTable(title, rows) { - const lines = rows.map((r) => `| \`${r.rule}\` | ${r.ours} | ${r.recommended} | ${r.source} | open |`); +export function renderTable(title, rows, decisions = {}) { + const lines = rows.map((r) => `| \`${r.rule}\` | ${r.ours} | ${r.recommended} | ${r.source} | ${decisions[r.rule] ?? 'open'} |`); return [`## ${title}`, '', '| Rule | Ours | Recommended | Source | Decision |', '|---|---|---|---|---|', ...lines, ''].join('\n'); } @@ -62,6 +62,7 @@ async function main() { import('../packages/eslint-config-react/src/typescript.js'), ]); const sources = JSON.parse(fs.readFileSync(path.join(here, 'rule-sources.json'), 'utf8')); + const decisions = JSON.parse(fs.readFileSync(path.join(here, 'rule-decisions.json'), 'utf8')); const recommended = [js.configs.recommended]; const tsRecommended = [...recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked]; const tsConfig = baseTsModule.createTypescriptConfig(); @@ -75,15 +76,21 @@ async function main() { resolved(react.default, 'src/a.jsx'), resolved(reactTsConfig, 'src/a.tsx'), ]); + // Rules nobody listed in rule-decisions.json come straight from a plugin's own preset. + const table = (config, baseline) => { + const rows = buildRuleTable(config, baseline, sources); + + return [rows, Object.fromEntries(rows.map((r) => [r.rule, decisions[r.rule] ?? 'plugin preset, unchanged']))]; + }; const out = [ '# Rule comparison', '', 'Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review.', '', - renderTable('Base JS (vs. @eslint/js recommended)', buildRuleTable(baseJs, recJs, sources)), - renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', buildRuleTable(baseTsRules, recTs, sources)), - renderTable('React JS (vs. Base JS)', buildRuleTable(reactJs, baseJs, sources)), - renderTable('React TypeScript (vs. Base TypeScript)', buildRuleTable(reactTsRules, baseTsRules, sources)), + renderTable('Base JS (vs. @eslint/js recommended)', ...table(baseJs, recJs)), + renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', ...table(baseTsRules, recTs)), + renderTable('React JS (vs. Base JS)', ...table(reactJs, baseJs)), + renderTable('React TypeScript (vs. Base TypeScript)', ...table(reactTsRules, baseTsRules)), ].join('\n'); fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); } diff --git a/scripts/rule-table.test.js b/scripts/rule-table.test.js index ae0f56a..532d694 100644 --- a/scripts/rule-table.test.js +++ b/scripts/rule-table.test.js @@ -24,3 +24,8 @@ test('renderTable renders a markdown table with a Decision column', () => { assert.match(md, /^## Base JS/m); assert.match(md, /\| `no-console` \| error \| warn \| eventured \| open \|/); }); + +test('renderTable fills the Decision column from the decisions map', () => { + const md = renderTable('X', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }], { 'no-console': 'error, allow warn/error' }); + assert.match(md, /\| `no-console` \| error \| warn \| eventured \| error, allow warn\/error \|/); +}); diff --git a/test-utils/lint.js b/test-utils/lint.js index 524604b..7d30ecd 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -61,3 +61,12 @@ export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts') { return Array.isArray(entry) ? entry[0] : entry; } + +/** Rule ids that resolve to severity "warn" for `filePath`. The shipped configs must have none. */ +export async function warnRules(config, filePath = 'src/sample.ts', cwd) { + filePath = path.resolve(fixtureProject, filePath); + const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: cwd ?? path.dirname(filePath) }); + const { rules = {} } = await eslint.calculateConfigForFile(filePath); + + return Object.entries(rules).filter(([, entry]) => (Array.isArray(entry) ? entry[0] : entry) === 1).map(([id]) => id).sort(); +} From 9fa123fdb153ace0dadd772203f2d81841fa976e Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 15:49:40 +0800 Subject: [PATCH 10/33] docs: usage, migration, 8.0.0 changeset Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .changeset/flat-config-rewrite.md | 6 ++ MIGRATION.md | 12 ++++ README.md | 27 +++++++- packages/eslint-config-base/README.md | 88 ++++++++++---------------- packages/eslint-config-react/README.md | 81 +++++++----------------- 5 files changed, 99 insertions(+), 115 deletions(-) create mode 100644 .changeset/flat-config-rewrite.md create mode 100644 MIGRATION.md diff --git a/.changeset/flat-config-rewrite.md b/.changeset/flat-config-rewrite.md new file mode 100644 index 0000000..d8ca9f2 --- /dev/null +++ b/.changeset/flat-config-rewrite.md @@ -0,0 +1,6 @@ +--- +'@cospired/eslint-config-base': major +'@cospired/eslint-config-react': major +--- + +Rewrite for ESLint 10 flat config with JavaScript and TypeScript entries, replacing airbnb and the hapi plugin. See MIGRATION.md. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..25045d5 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,12 @@ +# Migrating from 7.x to 8.0 + +8.0 is a rewrite. Both packages keep their names. + +- **Flat config only, ESLint 10.** `.eslintrc` is gone; use `eslint.config.js` (see the package READMEs). +- **No airbnb.** `eslint-config-airbnb(-base)` and `airbnb-typescript` are no longer used or needed. The airbnb rules we kept are listed in [docs/rules.md](docs/rules.md); the rest are dropped. New rule set: ESLint and typescript-eslint recommended plus the rules in that table. +- **No hapi plugin.** `@hapi/scope-start` (a blank line at the start of function bodies) cannot coexist with Prettier and is dropped. `@hapi/no-arrowception` is dropped. Blank lines before `return` and after declarations are enforced via `@stylistic/padding-line-between-statements`. +- **TypeScript is a first-class entry:** `@cospired/eslint-config-base/typescript` (and `/node`, `@cospired/eslint-config-react/typescript`). The unpublished `@cospired/eslint-config-typescript-base` package is gone. +- **No `warn`.** Every rule is `error` or off. +- **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. +- **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. +- **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket `#123`; imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template`, `no-await-in-loop` and others are errors; TypeScript files require explicit return types outside tests. diff --git a/README.md b/README.md index 17f8a92..82725ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,25 @@ -[![Build Status](https://travis-ci.com/cospired/javascript.svg?branch=master)](https://travis-ci.com/cospired/javascript) +# @cospired/javascript -# javascript -basic packages for JS development +Slightly opinionated ESLint flat configs (ESLint 10) for JavaScript and TypeScript. + +| Package | Entry | For | +|---|---|---| +| [`@cospired/eslint-config-base`](packages/eslint-config-base) | `.` | JavaScript | +| | `./typescript` | TypeScript (type-aware) | +| | `./node` | Node/backend TypeScript | +| [`@cospired/eslint-config-react`](packages/eslint-config-react) | `.` | React, JavaScript | +| | `./typescript` | React, TypeScript | + +Every rule that differs from ESLint/typescript-eslint recommended, with its source and the decision taken, is listed in [docs/rules.md](docs/rules.md). Upgrading from 7.x: [MIGRATION.md](MIGRATION.md). + +## Development + +```sh +npm install +npm test # node:test over every package, the test helpers and the rule-table script +npm run lint +npm run rules # regenerate docs/rules.md +npx changeset # describe a change for the next release +``` + +Releases are cut by the changesets GitHub Action on `main`. diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index ef16f54..7f85548 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -1,70 +1,52 @@ -[![Build Status](https://travis-ci.com/cospired/javascript.svg?branch=master)](https://travis-ci.com/cospired/javascript) - # @cospired/eslint-config-base -This package provides cospired's base JS .eslintrc (without React plugins) as an extensible shared config. - -## Usage - -We export one ESLint configurations for your usage. - -### @cospired/eslint-config-base - -Our default export contains all of our ESLint rules, including ECMAScript 6+. It requires `eslint`, `eslint-plugin-eslint-comment`, `eslint-plugin-import` and `@hapi/eslint-plugin-hapi`. - -If you use yarn, run `npm info "@cospired/eslint-config-base@latest" peerDependencies` to list the peer dependencies and versions, then run `yarn add --dev @` for each listed peer dependency. See below for npm instructions. +Slightly opinionated ESLint flat config for JavaScript and TypeScript. Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. -1. Install the correct versions of each package, which are listed by the command: +## Install - ```sh - npm info "@cospired/eslint-config-base@latest" peerDependencies - ``` +```sh +npm i -D @cospired/eslint-config-base eslint @eslint/js typescript-eslint typescript globals \ + @eslint-community/eslint-plugin-eslint-comments @stylistic/eslint-plugin eslint-config-prettier \ + eslint-import-resolver-typescript eslint-plugin-import-x eslint-plugin-todo-plz +``` - If using **npm 5+**, use this shortcut - - ```sh - npx install-peerdeps --dev @cospired/eslint-config-base - ``` - - If using **npm < 5**, Linux/OSX users can run - - ```sh - ( - export PKG=@cospired/eslint-config-base; - npm info "$PKG@latest" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "$PKG@latest" - ) - ``` - - Which produces and runs a command like: - - ```sh - npm install --save-dev @cospired/eslint-config-base eslint@^#.#.# eslint-plugin-import@^#.#.# - ``` +## Usage - If using **npm < 5**, Windows users can either install all the peer dependencies manually, or use the [install-peerdeps](https://github.com/nathanhleung/install-peerdeps) cli tool. +JavaScript: - ```sh - npm install -g install-peerdeps - install-peerdeps --dev @cospired/eslint-config-base - ``` +```js +import config, { globalIgnores } from '@cospired/eslint-config-base'; - The cli will produce and run a command like: +export default [globalIgnores, ...config]; +``` - ```sh - npm install --save-dev @cospired/eslint-config-base eslint@^#.#.# eslint-plugin-import@^#.#.# - ``` +TypeScript (type-aware): -2. Add `"extends": "@cospired/eslint-config-base"` to your .eslintrc. +```js +import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; +export default [ + globalIgnores, + ...createTypescriptConfig({ + tsconfigRootDir: import.meta.dirname, + tsconfigPath: ['tsconfig.json', 'test/tsconfig.json'], // optional; every linted file must belong to one + importOrder: { pathGroups: [{ pattern: '#shared/**', group: 'internal', position: 'before' }] }, // optional + }), +]; +``` -## Improving this config +Node backends: `createNodeConfig` from `@cospired/eslint-config-base/node` (same options, plus Node globals). -Consider adding test cases if you're making complicated rules changes, like anything involving regexes. Perhaps in a distant future, we could use literate programming to structure our README as test cases for our .eslintrc? +Options: `tsconfigPath` (string or array), `tsconfigRootDir`, `files`, `devDependencyGlobs` (files allowed to import devDependencies; defaults to tests, `scripts/`, `*.config.*`), `importOrder.pathGroups`. -You can run tests with `npm test`. +## What it enforces -You can make sure this module lints with itself using `npm run lint`. +Highlights; the complete table is in [docs/rules.md](../../docs/rules.md). -# Acknowledgment +- `eslint-disable` comments need a reason (`-- why`); TODOs need a ticket (`TODO(#123): ...`). +- Alphabetized imports in groups separated by a blank line; devDependency imports only in test/config/script files. +- Blank line before `return` and after declarations. +- No `warn` severities: rules are `error` or off. Downgrade locally if you need to. +- TypeScript: type-checked recommended + stylistic, explicit return types in non-test files, `no-misused-promises`. -This package is based on the [airbnb](https://github.com/airbnb/javascript) javascript styleguide and eslint packages. +Project-specific rules (for example `no-restricted-imports` for layer boundaries) belong in your own `eslint.config.js`. diff --git a/packages/eslint-config-react/README.md b/packages/eslint-config-react/README.md index 5dddc50..3e06a2e 100644 --- a/packages/eslint-config-react/README.md +++ b/packages/eslint-config-react/README.md @@ -1,70 +1,33 @@ -[![Build Status](https://travis-ci.com/cospired/javascript.svg?branch=master)](https://travis-ci.com/cospired/javascript) - # @cospired/eslint-config-react -This package provides cospired's react JS .eslintrc as an extensible shared config. - -## Usage - -We export one ESLint configurations for your usage. - -### @cospired/eslint-config-react - -Our default export contains all of our ESLint rules, including ECMAScript 6+. It requires `eslint`, `eslint-plugin-eslint-comment`, `@hapi/eslint-plugin-hapi`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y` and `eslint-plugin-react`. If you don't need React, see [@cospired/eslint-config-react](https://www.npmjs.com/package/@cospired/eslint-config-base). - -If you use yarn, run `npm info "@cospired/eslint-config-react@latest" peerDependencies` to list the peer dependencies and versions, then run `yarn add --dev @` for each listed peer dependency. See below for npm instructions. - -1. Install the correct versions of each package, which are listed by the command: - - ```sh - npm info "@cospired/eslint-config-react@latest" peerDependencies - ``` - - If using **npm 5+**, use this shortcut +React flat config on top of [`@cospired/eslint-config-base`](../eslint-config-base): react-hooks, react-refresh (Vite), formatjs (i18n) and browser globals. Requires ESLint 10. - ```sh - npx install-peerdeps --dev @cospired/eslint-config-react - ``` +## Install - If using **npm < 5**, Linux/OSX users can run +```sh +npm i -D @cospired/eslint-config-react eslint eslint-config-prettier globals \ + eslint-plugin-react-hooks eslint-plugin-react-refresh eslint-plugin-formatjs +``` - ```sh - ( - export PKG=@cospired/eslint-config-react; - npm info "$PKG@latest" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "$PKG@latest" - ) - ``` +The peer dependencies of `@cospired/eslint-config-base` are required as well (see its README). - Which produces and runs a command like: - - ```sh - npm install --save-dev @cospired/eslint-config-react eslint@^#.#.# eslint-plugin-import@^#.#.# - ``` - - If using **npm < 5**, Windows users can either install all the peer dependencies manually, or use the [install-peerdeps](https://github.com/nathanhleung/install-peerdeps) cli tool. - - ```sh - npm install -g install-peerdeps - install-peerdeps --dev @cospired/eslint-config-react - ``` - - The cli will produce and run a command like: - - ```sh - npm install --save-dev @cospired/eslint-config-react eslint@^#.#.# eslint-plugin-import@^#.#.# - ``` - -2. Add `"extends": "@cospired/eslint-config-react"` to your .eslintrc. - - -## Improving this config +## Usage -Consider adding test cases if you're making complicated rules changes, like anything involving regexes. Perhaps in a distant future, we could use literate programming to structure our README as test cases for our .eslintrc? +```js +// JavaScript +import config, { globalIgnores } from '@cospired/eslint-config-react'; -You can run tests with `npm test`. +export default [globalIgnores, ...config]; +``` -You can make sure this module lints with itself using `npm run lint`. +```js +// TypeScript: same options as createTypescriptConfig in the base package +import { createReactTypescriptConfig, globalIgnores } from '@cospired/eslint-config-react/typescript'; -# Acknowledgment +export default [ + globalIgnores, + ...createReactTypescriptConfig({ tsconfigRootDir: import.meta.dirname, tsconfigPath: ['tsconfig.app.json'] }), +]; +``` -This package is based on the [airbnb](https://github.com/airbnb/javascript) javascript styleguide and eslint packages. +formatjs rules apply to all files except `*.test`/`*.spec`. jsx-a11y is not included yet: `eslint-plugin-jsx-a11y` does not support ESLint 10. From ff3c5b31a6dcf85140e3af7307e270844a442f80 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 15:51:23 +0800 Subject: [PATCH 11/33] chore: commit lockfile so changesets and npm ci work Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 3 +- .github/workflows/release.yml | 3 +- .npmrc | 1 - package-lock.json | 2908 +++++++++++++++++++++++++++++++++ 4 files changed, 2912 insertions(+), 3 deletions(-) delete mode 100644 .npmrc create mode 100644 package-lock.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2b7d52..24d9f28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - - run: npm install --no-audit --no-fund + cache: npm + - run: npm ci - run: npm test - run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cb009d..2d9acf0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 + cache: npm registry-url: https://registry.npmjs.org - - run: npm install --no-audit --no-fund + - run: npm ci - uses: changesets/action@v1 with: publish: npm run release diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 9cf9495..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..95a282a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2908 @@ +{ + "name": "@cospired/javascript", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@cospired/javascript", + "version": "0.0.0", + "license": "BSD-3-Clause-Clear", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@changesets/cli": "^3.0.3", + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "@types/node": "^24", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.72.2" + }, + "engines": { + "node": ">=22.13" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-compilation-targets": "^7.29.13", + "@babel/helper-module-transforms": "^7.29.13", + "@babel/helpers": "^7.29.13", + "@babel/parser": "^7.29.13", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.13", + "@babel/types": "^7.29.11", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.13" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.13", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.13", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.13", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@changesets/apply-release-plan": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^4.0.1", + "@changesets/format": "^0.1.2", + "@changesets/git": "^4.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/cli": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^8.1.1", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.1", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.1", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.1", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.1" + }, + "bin": { + "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/config": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.7" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/errors": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/parse": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/pre": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/read": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^4.0.1", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/types": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/write": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@clack/core": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^2.0.0" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.6.0", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^2.0.0" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@cospired/eslint-config-base": { + "resolved": "packages/eslint-config-base", + "link": true + }, + "node_modules/@cospired/eslint-config-react": { + "resolved": "packages/eslint-config-react", + "link": true + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.8.1", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "ignore": "^7.0.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.3", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.23", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.13" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.13", + "license": "MIT" + }, + "node_modules/@formatjs/ts-transformer": { + "version": "4.5.3", + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "3.5.23", + "@types/babel__core": "7", + "@types/node": "22 || 24 || 26", + "json-stable-stringify": "1", + "typescript": "^5.6 || 6 || 7" + }, + "engines": { + "node": ">= 20.12.0" + }, + "peerDependencies": { + "ts-jest": "29" + }, + "peerDependenciesMeta": { + "ts-jest": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@manypkg/find-root": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/picomatch": { + "version": "4.0.3", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/type-utils": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.2", + "@typescript-eslint/types": "^8.72.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.2", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.2", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.2", + "@typescript-eslint/tsconfig-utils": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unicode/unicode-17.0.0": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.13.0.tgz", + "integrity": "sha512-60E5mvmIiW+i1E/HYeSX4iE4A8s9NJSGYPu7UM0DJght1c2tTkWwuyDyAwzsEXhKM/XbbndfnTiXdfrf27d2hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.13.0.tgz", + "integrity": "sha512-qlh/JR3Tuy1cJ7xtf5pR4ykvLiK6os7X7EL2+zXZbrVc/s+yB8DoFdQrCAW9/a2TOxPVbR7QVNpkPWh7UWIsEQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.13.0", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.13.0.tgz", + "integrity": "sha512-t8PXE0Ip9tmGyhRlfVsnEbzGM5iSvJIeNIXciUACjzpg0mmPs13fGv2x7W8sBa0S8zbIiohxKXS9fL/LXW/ERQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.13.0.tgz", + "integrity": "sha512-qdtl9GDWDoDx6rhzF4RU5SJutV3lcLOiTiyqD6nBxYfEO6lAgLNukfrA+HJ2u2OiyWvV8JW5iEWD3LoONw7lYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.13.0.tgz", + "integrity": "sha512-VrMUVwdrScTarsh4B6aUhTtziUavSlglo4Hi8XZ6CpAe4+j3nquY+XoP+tYkiSBDsXZ8386ne4pv4YYYBNi02w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.13.0.tgz", + "integrity": "sha512-zSbTVwaxiTC+zYJ9xVNTC6c+BYO/kOV/odcTI3bHuFFXa+XmjLoUkb/8UxsZeU5YF4K000m17wrXoTblNWuzOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.13.0.tgz", + "integrity": "sha512-Z/Rax87moZHsuvJ8awdfP+zrMlsCt3d7mggUPIiPe32KpeagimyzJWnZSMQoV67an9uVQghyh92Pc0VprITnYA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.13.0.tgz", + "integrity": "sha512-oUzXz8hNvFLNEAag8lBKJe1euiV2dmGHAR+eQ5fNpsZO0F1K7IMRb4J6pqYsd7uLMRQNPiu5A1WKUJHbFLWbbw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.13.0.tgz", + "integrity": "sha512-BxhGYvZBg0YrvNOeSEGR3eNklwXYy0mUJk7oeGJvZoQMKM68zYvbbf52+fQI3Le4X0fW276FTKIoTwjpMob80A==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.13.0.tgz", + "integrity": "sha512-nZcIuup9Ar3gAlqoZfX/KW6x83TMI11tLpCjldZG5M1KeY8qNUPNJJ71+sPvG8HMBpoh+ix1pAm5gkMFvcSAcQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.13.0.tgz", + "integrity": "sha512-AG6AgdDrkRNjAPwD/cftFJGgZUWCiJX+E8vpYCDEnwFHLTHZ7ppBiU6/vyPPwrfjcPUPQCDbbxE/eZUXsx49jQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.13.0.tgz", + "integrity": "sha512-6vOqYGcAmzpRWpUURWutzvpf1qJ6eOu5cfYObdtPJnzZ0+Kw4cw+ZkZJqIYEh8fPuTBwSYDL/FwlUhma1G/JVQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.13.0.tgz", + "integrity": "sha512-4HeIn0S2KVUowK0A+EweXrDvZ8CKHEfTtif0kx9vt3ORNsqcXNFFIFnHE0tizma3/Gt5OGrU2ovYNuj+D/bN4g==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.13.0.tgz", + "integrity": "sha512-JfkvqY7NZbxyhexLE+g6Z8Hsx4utHOGaVazAVghPiBn8Upw2iERZPpKYnW1FBVU4WStv/BiIsoK/Muke0E7asA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.13.0.tgz", + "integrity": "sha512-iwjLeVdZo/8v5OKAeRIC+4VQV045t1EPZr5B/AVLgUQ04IdtNthWzg6not2cNjZgH2gHpSf4fOXMmN83aLO1FA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.13.0.tgz", + "integrity": "sha512-TOh1fqOjyq1OPoht3BDLd7Y+HeMJRwd5g2VNmCQclknEbjp4py5GFRmT4L/rOybF5Af8q0LT9zHs2WGkwlqtrw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.13.0.tgz", + "integrity": "sha512-3T2MNtF2dd1yjaXCVqrsi+/3VZeksEzU1hygPzqqkBLVVjn+2iJzXEkxxCKIVFrWnpmEbazeHJp/ruYD0RUVWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.13.0.tgz", + "integrity": "sha512-hKWfSLiN3L4cIxg0jrpyP/66AXLTcyD37jmSn//rGdOUYGlizYxTEFdJ+GZbp2NnaML3YS/A73nCysze8FaJdg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.13.0.tgz", + "integrity": "sha512-N4gDKkDoiyIQEcn3vTqe2ksRxUAtBvQ8c5TiT+WonSa6FHgCAY5+RggnZ134hXJoRj57JEjv4/m+i/pAKHWrTQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.13.0.tgz", + "integrity": "sha512-DpLMlAqlrk4zBVK3DiDjwKD0oq7pZNy+8BiIBRuByqb6dm5jsdoLMhIiyh/xR7+WejaW/1Ui+XgbgMNa4+zWJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.19.0", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.35", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.5", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001823", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cjs-module-lexer": { + "version": "2.3.0", + "license": "MIT" + }, + "node_modules/comment-parser": { + "version": "1.4.9", + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.479", + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.14.0", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.6", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.5", + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-formatjs": { + "version": "8.1.4", + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "3.5.23", + "@formatjs/ts-transformer": "4.5.3", + "@types/estree-jsx": "1", + "@types/picomatch": "4", + "@unicode/unicode-17.0.0": "2", + "magic-string": "1", + "picomatch": "2 || 3 || 4" + }, + "peerDependencies": { + "eslint": "9 || 10" + } + }, + "node_modules/eslint-plugin-import-x": { + "version": "4.18.1", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.56.0", + "cjs-module-lexer": "^2.2.1", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "es-module-lexer": "^3.0.3", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2 || ^11.0.1", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.7", + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-plugin-todo-plz": { + "version": "1.4.1", + "license": "MIT", + "peerDependencies": { + "eslint": ">=7.3.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.14.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "license": "MIT" + }, + "node_modules/human-id": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/ignore": { + "version": "7.0.14", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/jju": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonify": { + "version": "0.0.1", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/launch-editor": { + "version": "2.14.2", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "1.4.5", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.8", + "license": "MIT", + "bin": { + "napi-patch": "lib/patch.js", + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.63", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.8.6", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.2", + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.2", + "@typescript-eslint/parser": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.13.0", + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.7" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.13.0", + "@unrs/resolver-binding-android-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-x64": "1.13.0", + "@unrs/resolver-binding-freebsd-x64": "1.13.0", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-arm64-musl": "1.13.0", + "@unrs/resolver-binding-linux-loong64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-loong64-musl": "1.13.0", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-musl": "1.13.0", + "@unrs/resolver-binding-linux-s390x-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-musl": "1.13.0", + "@unrs/resolver-binding-openharmony-arm64": "1.13.0", + "@unrs/resolver-binding-win32-arm64-msvc": "1.13.0", + "@unrs/resolver-binding-win32-ia32-msvc": "1.13.0", + "@unrs/resolver-binding-win32-x64-msvc": "1.13.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.1", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "packages/eslint-config-base": { + "name": "@cospired/eslint-config-base", + "version": "7.0.2", + "license": "BSD-3-Clause-Clear", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.0.0", + "typescript": ">=5.4 <6.1", + "typescript-eslint": "^8.57.0" + } + }, + "packages/eslint-config-react": { + "name": "@cospired/eslint-config-react", + "version": "7.0.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@cospired/eslint-config-base": "*" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.5.0", + "globals": "^17.0.0" + } + } + } +} From 34e2ef8574b2d042fd8df2ef344aab96b18e5ecb Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 15:53:19 +0800 Subject: [PATCH 12/33] fix(base): keep TS-only rule objects off JS files matched by files Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/node.js | 2 ++ packages/eslint-config-base/src/rules/typescript.js | 2 ++ packages/eslint-config-base/src/typescript.js | 6 +++--- packages/eslint-config-base/test/node.test.js | 7 +++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index b4774a1..7eac799 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,5 +1,6 @@ import globals from 'globals'; +import { jsFiles } from './rules/typescript.js'; import { createTypescriptConfig } from './typescript.js'; export { globalIgnores } from './ignores.js'; @@ -19,6 +20,7 @@ export function createNodeConfig(options = {}) { { languageOptions: { sourceType: 'module', globals: globals.node } }, { files, + ignores: jsFiles, rules: { // String `||` is deliberate empty-string fallthrough (env vars, trimmed input) that `??` would silently drop. '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], diff --git a/packages/eslint-config-base/src/rules/typescript.js b/packages/eslint-config-base/src/rules/typescript.js index 173fe49..785c454 100644 --- a/packages/eslint-config-base/src/rules/typescript.js +++ b/packages/eslint-config-base/src/rules/typescript.js @@ -2,6 +2,8 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip import { flatConfigs as importX } from 'eslint-plugin-import-x'; export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; +/** JS files can be matched by a consumer's `files`; they are never type-checked. */ +export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; export const testGlobs = ['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']; /** Rules on top of typescript-eslint's recommended/stylistic type-checked sets. Severities are placeholders until the rule review. */ diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index a4f4fc3..feb41d8 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -2,7 +2,7 @@ import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import { createBaseConfig, prettierLast } from './index.js'; -import { importSettings, importXTypescript, sourceOnlyRules, testGlobs, typescriptRules } from './rules/typescript.js'; +import { importSettings, importXTypescript, jsFiles, sourceOnlyRules, testGlobs, typescriptRules } from './rules/typescript.js'; export { globalIgnores } from './ignores.js'; @@ -38,8 +38,8 @@ export function createTypescriptConfig({ settings: importSettings(tsconfigPath), rules: typescriptRules, }), - { files, ignores: testGlobs, rules: sourceOnlyRules }, - { files: ['**/*.{js,mjs,cjs}'], ...tseslint.configs.disableTypeChecked }, + { files, ignores: [...testGlobs, ...jsFiles], rules: sourceOnlyRules }, + { files: jsFiles, ...tseslint.configs.disableTypeChecked }, ...prettierLast, ]; } diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js index aebb136..ad240b2 100644 --- a/packages/eslint-config-base/test/node.test.js +++ b/packages/eslint-config-base/test/node.test.js @@ -36,3 +36,10 @@ test('no-console is allowed in scripts/ but not in src/', async () => { assert.ok(!(await ids(node, code, 'scripts/run.mjs')).includes('no-console')); assert.ok((await ids(node, code, 'src/run.mjs')).includes('no-console')); }); + +test('JS files matched by `files` are linted without type-aware rules (no crash)', async () => { + const cfg = createNodeConfig({ ...options, files: ['src/**/*.{ts,js}', '*.{ts,js}'] }); + const found = await ids(cfg, 'export const a = (x) => x || 1;\n', 'src/c.js'); + assert.ok(!found.includes('@typescript-eslint/prefer-nullish-coalescing')); + assert.ok(!found.includes('@typescript-eslint/explicit-function-return-type')); +}); From c007462c1b09e36301933eec1dd22e0182c2d35c Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 15:54:01 +0800 Subject: [PATCH 13/33] fix(base): node globals for scripts/ in every config; migration note on tsconfigs Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- MIGRATION.md | 1 + packages/eslint-config-base/src/index.js | 5 +++-- packages/eslint-config-base/test/decisions.test.js | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 25045d5..a79302a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,3 +10,4 @@ - **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. - **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. - **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket `#123`; imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template`, `no-await-in-loop` and others are errors; TypeScript files require explicit return types outside tests. +- **Type-aware linting needs tsconfigs:** the TypeScript configs (including `react/typescript`) lint with type information, so every linted file, tests included, must belong to a tsconfig. Pass them all via `tsconfigPath` (an array is fine), for example a test-only `tsconfig.test.json`. Files outside every tsconfig report a parsing error. diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index 3d26136..0d72b4b 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -1,5 +1,6 @@ import js from '@eslint/js'; import prettier from 'eslint-config-prettier'; +import globals from 'globals'; import comments from './rules/comments.js'; import general from './rules/general.js'; @@ -26,8 +27,8 @@ export function createBaseConfig(importOptions) { createImportsConfig(importOptions), style, general, - // CLI output is expected in scripts. - { files: ['scripts/**/*.{mjs,js,ts}'], rules: { 'no-console': 'off' } }, + // Build/CLI scripts run on Node and print to the console. + { files: ['scripts/**/*.{mjs,js,ts}'], languageOptions: { globals: globals.node }, rules: { 'no-console': 'off' } }, ...prettierLast, ]; } diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index 2a35607..ca87942 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -64,3 +64,11 @@ test('TS config uses the typescript-eslint variants and drops consistent-return' for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); }); + +test('scripts/ files get node globals in every base config', async () => { + for (const [name, cfg] of [['js', config], ['ts', tsConfig]]) { + const found = await lint('process.exit(1);\n', 'scripts/run.mjs', cfg); + assert.ok(!found.includes('no-undef'), `${name}: process should be defined in scripts/`); + } + assert.ok((await lint('process.exit(1);\n', 'src/run.mjs', tsConfig)).includes('no-undef')); +}); From 896cba3b34baf453d0f9225f00ac667a132dd3d4 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:12:12 +0800 Subject: [PATCH 14/33] feat: per-plugin rule files, completeness tests, review fixes Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/globs.js | 9 + packages/eslint-config-base/src/index.js | 24 +-- packages/eslint-config-base/src/node.js | 2 +- .../rules/{comments.js => eslint-comments.js} | 9 +- .../eslint-config-base/src/rules/eslint.js | 156 ++++++++++++++++++ .../eslint-config-base/src/rules/general.js | 24 --- .../eslint-config-base/src/rules/import-x.js | 106 ++++++++++++ .../eslint-config-base/src/rules/imports.js | 46 ------ .../eslint-config-base/src/rules/style.js | 14 -- .../eslint-config-base/src/rules/stylistic.js | 30 ++++ .../eslint-config-base/src/rules/todo-plz.js | 8 + .../src/rules/typescript-eslint.js | 56 +++++++ .../src/rules/typescript.js | 43 ----- packages/eslint-config-base/src/typescript.js | 8 +- .../test/completeness.test.js | 25 +++ .../eslint-config-base/test/decisions.test.js | 14 ++ packages/eslint-config-react/src/index.js | 4 +- .../eslint-config-react/src/rules/formatjs.js | 11 +- .../src/rules/react-hooks.js | 27 +++ .../src/rules/react-refresh.js | 8 + .../eslint-config-react/src/rules/react.js | 21 +-- .../eslint-config-react/src/typescript.js | 4 +- .../test/completeness.test.js | 22 +++ .../test/decisions.test.js | 6 + test-utils/completeness.test.js | 23 +++ test-utils/lint.js | 38 ++++- test-utils/plugins.js | 27 +++ 27 files changed, 595 insertions(+), 170 deletions(-) create mode 100644 packages/eslint-config-base/src/globs.js rename packages/eslint-config-base/src/rules/{comments.js => eslint-comments.js} (52%) create mode 100644 packages/eslint-config-base/src/rules/eslint.js delete mode 100644 packages/eslint-config-base/src/rules/general.js create mode 100644 packages/eslint-config-base/src/rules/import-x.js delete mode 100644 packages/eslint-config-base/src/rules/imports.js delete mode 100644 packages/eslint-config-base/src/rules/style.js create mode 100644 packages/eslint-config-base/src/rules/stylistic.js create mode 100644 packages/eslint-config-base/src/rules/todo-plz.js create mode 100644 packages/eslint-config-base/src/rules/typescript-eslint.js delete mode 100644 packages/eslint-config-base/src/rules/typescript.js create mode 100644 packages/eslint-config-base/test/completeness.test.js create mode 100644 packages/eslint-config-react/src/rules/react-hooks.js create mode 100644 packages/eslint-config-react/src/rules/react-refresh.js create mode 100644 packages/eslint-config-react/test/completeness.test.js create mode 100644 test-utils/completeness.test.js create mode 100644 test-utils/plugins.js diff --git a/packages/eslint-config-base/src/globs.js b/packages/eslint-config-base/src/globs.js new file mode 100644 index 0000000..6b6fa7d --- /dev/null +++ b/packages/eslint-config-base/src/globs.js @@ -0,0 +1,9 @@ +/** File globs shared by the configs. */ +export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; +/** Patterns are relative to the eslint config's directory; the `{apps,packages}/*` twins cover one root config linting a monorepo. */ +export const withMonorepoTwins = (globs) => globs.flatMap((glob) => [glob, `{apps,packages}/*/${glob}`]); +export const testGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']); +/** Any language: the same test files for rules that also apply to JS (formatjs, devDependency imports). */ +export const anyTestGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', 'test/**', 'tests/**', '**/__tests__/**']); +/** JS files can be matched by a consumer's `files`; they are never type-checked. */ +export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index 0d72b4b..b17b760 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -1,14 +1,16 @@ -import js from '@eslint/js'; import prettier from 'eslint-config-prettier'; import globals from 'globals'; -import comments from './rules/comments.js'; -import general from './rules/general.js'; -import { createImportsConfig } from './rules/imports.js'; -import style from './rules/style.js'; +import { withMonorepoTwins } from './globs.js'; +import eslintCore from './rules/eslint.js'; +import eslintComments from './rules/eslint-comments.js'; +import { createImportsConfig } from './rules/import-x.js'; +import stylistic from './rules/stylistic.js'; +import todoPlz from './rules/todo-plz.js'; +export { anyTestGlobs, testGlobs, withMonorepoTwins } from './globs.js'; export { globalIgnores } from './ignores.js'; -export { createImportsConfig, defaultDevDependencyGlobs } from './rules/imports.js'; +export { createImportsConfig, defaultDevDependencyGlobs } from './rules/import-x.js'; /** * eslint-config-prettier disables `curly`, so it must be re-enabled after it. Every config ends with these. @@ -22,13 +24,13 @@ export const prettierLast = [prettier, { rules: { curly: ['error', 'multi-line'] */ export function createBaseConfig(importOptions) { return [ - js.configs.recommended, - comments, + ...eslintCore, + eslintComments, createImportsConfig(importOptions), - style, - general, + todoPlz, + stylistic, // Build/CLI scripts run on Node and print to the console. - { files: ['scripts/**/*.{mjs,js,ts}'], languageOptions: { globals: globals.node }, rules: { 'no-console': 'off' } }, + { files: withMonorepoTwins(['scripts/**/*.{js,mjs,cjs,ts,mts,cts}']), languageOptions: { globals: globals.node }, rules: { 'no-console': 'off' } }, ...prettierLast, ]; } diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 7eac799..389c5c4 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,6 +1,6 @@ import globals from 'globals'; -import { jsFiles } from './rules/typescript.js'; +import { jsFiles } from './globs.js'; import { createTypescriptConfig } from './typescript.js'; export { globalIgnores } from './ignores.js'; diff --git a/packages/eslint-config-base/src/rules/comments.js b/packages/eslint-config-base/src/rules/eslint-comments.js similarity index 52% rename from packages/eslint-config-base/src/rules/comments.js rename to packages/eslint-config-base/src/rules/eslint-comments.js index 1f432d0..1283869 100644 --- a/packages/eslint-config-base/src/rules/comments.js +++ b/packages/eslint-config-base/src/rules/eslint-comments.js @@ -1,13 +1,14 @@ import { recommended } from '@eslint-community/eslint-plugin-eslint-comments/configs'; -import todoPlz from 'eslint-plugin-todo-plz'; -/** Directive comments need a reason, TODOs need a ticket. */ +/** Directive comments stay narrow, named and explained. */ export default { - plugins: { ...recommended.plugins, 'todo-plz': todoPlz }, + name: 'cospired/eslint-comments', + plugins: recommended.plugins, linterOptions: { reportUnusedDisableDirectives: 'error' }, rules: { ...recommended.rules, '@eslint-community/eslint-comments/require-description': 'error', - 'todo-plz/ticket-ref': ['error', { pattern: '#\\d+' }], + '@eslint-community/eslint-comments/no-restricted-disable': 'off', + '@eslint-community/eslint-comments/no-use': 'off', }, }; diff --git a/packages/eslint-config-base/src/rules/eslint.js b/packages/eslint-config-base/src/rules/eslint.js new file mode 100644 index 0000000..feb5ee9 --- /dev/null +++ b/packages/eslint-config-base/src/rules/eslint.js @@ -0,0 +1,156 @@ +import js from '@eslint/js'; + +/** + * ESLint core. `js.configs.recommended` first; below it only rules recommended does not set (or sets differently). + * Every non-deprecated core rule is configured: here, in recommended, or by eslint-config-prettier (see test/completeness). + */ +export default [ + js.configs.recommended, + { + name: 'cospired/eslint', + rules: { + // --- on: possible bugs + 'array-callback-return': 'error', + 'default-case-last': 'error', + 'guard-for-in': 'error', + 'no-array-constructor': 'error', + 'no-await-in-loop': 'error', + 'no-constructor-return': 'error', + 'no-loop-func': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-promise-executor-return': 'error', + 'no-self-compare': 'error', + 'no-template-curly-in-string': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unreachable-loop': 'error', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + 'no-use-before-define': 'error', + radix: 'error', + 'symbol-description': 'error', + + // --- on: unsafe or discouraged features + 'no-alert': 'error', + 'no-caller': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-implied-eval': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': 'error', + 'no-multi-assign': 'error', + 'no-multi-str': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': ['error', { props: false }], + 'no-proto': 'error', + 'no-return-assign': 'error', + 'no-script-url': 'error', + 'no-sequences': 'error', + 'no-shadow': 'error', + 'no-undef-init': 'error', + 'no-throw-literal': 'error', + 'no-var': 'error', + + // --- on: clarity and consistency + 'accessor-pairs': 'error', + 'arrow-body-style': ['error', 'as-needed'], + camelcase: ['error', { properties: 'never' }], + 'consistent-return': 'error', + 'default-param-last': 'error', + 'dot-notation': 'error', + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'func-name-matching': 'error', + 'grouped-accessor-pairs': ['error', 'getBeforeSet'], + 'logical-assignment-operators': ['error', 'always', { enforceForIfStatements: true }], + 'new-cap': ['error', { capIsNew: false }], + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-else-return': ['error', { allowElseIf: false }], + 'no-empty-function': ['error', { allow: ['arrowFunctions', 'functions', 'methods'] }], + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-invalid-this': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-nested-ternary': 'error', + 'no-object-constructor': 'error', + 'no-unneeded-ternary': 'error', + 'no-unused-expressions': 'error', + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'object-shorthand': 'error', + 'one-var': ['error', 'never'], + 'operator-assignment': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'prefer-exponentiation-operator': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-object-has-own': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-regex-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'require-await': 'error', + yoda: 'error', + + // --- off: opinion, thresholds, or covered elsewhere (import-x, TypeScript, Prettier, todo-plz) + 'block-scoped-var': 'off', + 'capitalized-comments': 'off', + 'class-methods-use-this': 'off', + complexity: 'off', + 'consistent-this': 'off', + 'default-case': 'off', + 'func-names': 'off', + 'func-style': 'off', + 'id-denylist': 'off', + 'id-length': 'off', + 'id-match': 'off', + 'init-declarations': 'off', + 'max-classes-per-file': 'off', + 'max-depth': 'off', + 'max-lines': 'off', + 'max-lines-per-function': 'off', + 'max-nested-callbacks': 'off', + 'max-params': 'off', + 'max-statements': 'off', + 'no-bitwise': 'off', + 'no-continue': 'off', + 'no-div-regex': 'off', + 'no-duplicate-imports': 'off', + 'no-eq-null': 'off', + 'no-implicit-coercion': 'off', + 'no-implicit-globals': 'off', + 'no-inline-comments': 'off', + 'no-inner-declarations': 'off', + 'no-magic-numbers': 'off', + 'no-negated-condition': 'off', + 'no-new': 'off', + 'no-plusplus': 'off', + 'no-restricted-exports': 'off', + 'no-restricted-globals': 'off', + 'no-restricted-imports': 'off', + 'no-restricted-properties': 'off', + 'no-restricted-syntax': 'off', + 'no-ternary': 'off', + 'no-undefined': 'off', + 'no-underscore-dangle': 'off', + 'no-void': 'off', + 'no-warning-comments': 'off', + 'prefer-destructuring': 'off', + 'prefer-named-capture-group': 'off', + 'require-atomic-updates': 'off', + 'require-unicode-regexp': 'off', + 'sort-imports': 'off', + 'sort-keys': 'off', + 'sort-vars': 'off', + strict: 'off', + 'unicode-bom': 'off', + 'vars-on-top': 'off', + }, + }, +]; diff --git a/packages/eslint-config-base/src/rules/general.js b/packages/eslint-config-base/src/rules/general.js deleted file mode 100644 index d50cf4a..0000000 --- a/packages/eslint-config-base/src/rules/general.js +++ /dev/null @@ -1,24 +0,0 @@ -/** General JavaScript rules beyond `@eslint/js` recommended. Every rule here was decided in the rule review (docs/rules.md). */ -export default { - rules: { - 'consistent-return': 'error', - 'default-param-last': 'error', - eqeqeq: ['error', 'always', { null: 'ignore' }], - 'logical-assignment-operators': ['error', 'always', { enforceForIfStatements: true }], - 'no-await-in-loop': 'error', - 'no-console': ['error', { allow: ['warn', 'error'] }], - 'no-else-return': ['error', { allowElseIf: false }], - 'no-nested-ternary': 'error', - 'no-param-reassign': ['error', { props: false }], - 'no-shadow': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], - 'no-use-before-define': 'error', - 'no-var': 'error', - 'object-shorthand': 'error', - 'prefer-arrow-callback': 'error', - 'prefer-const': 'error', - 'prefer-object-has-own': 'error', - 'prefer-template': 'error', - }, -}; diff --git a/packages/eslint-config-base/src/rules/import-x.js b/packages/eslint-config-base/src/rules/import-x.js new file mode 100644 index 0000000..1e6419f --- /dev/null +++ b/packages/eslint-config-base/src/rules/import-x.js @@ -0,0 +1,106 @@ +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; +import importX, { flatConfigs } from 'eslint-plugin-import-x'; + +import { tsExtensions, withMonorepoTwins } from '../globs.js'; + +/** + * Where a devDependency import is allowed. Patterns are relative to the directory of the eslint config file; + * the `{apps,packages}/*` twins cover one root config linting a whole monorepo (e.g. from lint-staged). + */ +export const defaultDevDependencyGlobs = withMonorepoTwins([ + '**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}', + 'test/**', + 'tests/**', + '**/__tests__/**', + 'scripts/**', + '*.config.{ts,js,mjs,cjs}', +]); + +/** + * @param {{ devDependencyGlobs?: string[], pathGroups?: object[] }} [options] `pathGroups` is passed to + * `import-x/order`, e.g. to place `#alias/**` imports in another group. + */ +export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyGlobs, pathGroups } = {}) { + return { + name: 'cospired/import-x', + plugins: { 'import-x': importX }, + rules: { + ...flatConfigs.recommended.rules, + + // --- on + 'import-x/first': 'error', + 'import-x/newline-after-import': 'error', + 'import-x/no-absolute-path': 'error', + 'import-x/no-amd': 'error', + 'import-x/no-cycle': ['error', { ignoreExternal: true }], + 'import-x/no-deprecated': 'error', + 'import-x/no-duplicates': 'error', + 'import-x/no-dynamic-require': 'error', + 'import-x/no-empty-named-blocks': 'error', + 'import-x/no-extraneous-dependencies': ['error', { devDependencies: devDependencyGlobs, peerDependencies: true }], + 'import-x/no-import-module-exports': 'error', + 'import-x/no-mutable-exports': 'error', + 'import-x/no-named-default': 'error', + 'import-x/no-relative-packages': 'error', + 'import-x/no-self-import': 'error', + 'import-x/no-useless-path-segments': ['error', { noUselessIndex: true }], + 'import-x/no-webpack-loader-syntax': 'error', + 'import-x/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + ...(pathGroups && { pathGroups, pathGroupsExcludedImportTypes: ['builtin'] }), + }, + ], + + // --- off: plugins default-export objects whose members also ship as named exports + 'import-x/no-named-as-default': 'off', + 'import-x/no-named-as-default-member': 'off', + + // --- off: opinion, project-specific, or too costly for a shared config + 'import-x/avoid-barrel-files': 'off', + 'import-x/consistent-type-specifier-style': 'off', + 'import-x/avoid-importing-barrel-files': 'off', + 'import-x/avoid-namespace-import': 'off', + 'import-x/avoid-re-export-all': 'off', + 'import-x/dynamic-import-chunkname': 'off', + 'import-x/exports-last': 'off', + 'import-x/extensions': 'off', + 'import-x/group-exports': 'off', + 'import-x/max-dependencies': 'off', + 'import-x/no-anonymous-default-export': 'off', + 'import-x/no-commonjs': 'off', + 'import-x/no-default-export': 'off', + 'import-x/no-internal-modules': 'off', + 'import-x/no-named-export': 'off', + 'import-x/no-namespace': 'off', + 'import-x/no-nodejs-modules': 'off', + 'import-x/no-relative-parent-imports': 'off', + 'import-x/no-rename-default': 'off', + 'import-x/no-restricted-paths': 'off', + 'import-x/no-unassigned-import': 'off', + 'import-x/no-unused-modules': 'off', + 'import-x/prefer-default-export': 'off', + 'import-x/prefer-namespace-import': 'off', + 'import-x/unambiguous': 'off', + }, + }; +} + +/** TypeScript-only import-x config: the typescript preset and a type-import style that matches `consistent-type-imports`. */ +export const importXTypescript = flatConfigs.typescript; +export const importXTypescriptRules = { 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'] }; + +/** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ +export function importSettings(project) { + return { + 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs', ...tsExtensions], + 'import-x/parsers': { '@typescript-eslint/parser': tsExtensions }, + 'import-x/resolver-next': [ + // Several projects is the intended shape (app + test-only tsconfigs); the suggested `references` fix needs `composite`. + createTypeScriptImportResolver({ alwaysTryTypes: true, noWarnOnMultipleProjects: true, ...(project && { project }) }), + ], + }; +} diff --git a/packages/eslint-config-base/src/rules/imports.js b/packages/eslint-config-base/src/rules/imports.js deleted file mode 100644 index 74842be..0000000 --- a/packages/eslint-config-base/src/rules/imports.js +++ /dev/null @@ -1,46 +0,0 @@ -import importX from 'eslint-plugin-import-x'; - -/** - * Where a devDependency import is allowed. Anchored rather than `**`-prefixed so a path like - * `src/lib/scripts/x.ts` can't claim one; import-x matches against the lint cwd, which is the - * package root under Turbo and the repo root under lint-staged, hence both spellings. - */ -export const defaultDevDependencyGlobs = [ - '**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}', - 'test/**', - 'src/test/**', - 'src/__tests__/**', - 'scripts/**', - '*.config.{ts,js,mjs,cjs}', -].flatMap((glob) => [glob, `{apps,packages}/*/${glob}`]); - -/** - * @param {{ devDependencyGlobs?: string[], pathGroups?: object[] }} [options] `pathGroups` is passed to - * `import-x/order`, e.g. to place `#alias/**` imports in the `internal` group. - */ -export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyGlobs, pathGroups } = {}) { - return { - plugins: { 'import-x': importX }, - rules: { - ...importX.flatConfigs.recommended.rules, - // Plugins default-export objects whose members also ship as named exports. - 'import-x/no-named-as-default': 'off', - 'import-x/no-named-as-default-member': 'off', - 'import-x/newline-after-import': 'error', - 'import-x/no-duplicates': 'error', - 'import-x/no-empty-named-blocks': 'error', - 'import-x/no-extraneous-dependencies': ['error', { devDependencies: devDependencyGlobs, peerDependencies: true }], - 'import-x/no-import-module-exports': 'error', - 'import-x/no-relative-packages': 'error', - 'import-x/order': [ - 'error', - { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], - 'newlines-between': 'always', - alphabetize: { order: 'asc', caseInsensitive: true }, - ...(pathGroups && { pathGroups, pathGroupsExcludedImportTypes: ['builtin'] }), - }, - ], - }, - }; -} diff --git a/packages/eslint-config-base/src/rules/style.js b/packages/eslint-config-base/src/rules/style.js deleted file mode 100644 index 64035f8..0000000 --- a/packages/eslint-config-base/src/rules/style.js +++ /dev/null @@ -1,14 +0,0 @@ -import stylistic from '@stylistic/eslint-plugin'; - -/** Readability: blank lines around returns and declarations. Prettier does not touch these. */ -export default { - plugins: { '@stylistic': stylistic }, - rules: { - '@stylistic/padding-line-between-statements': [ - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, - { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, - ], - }, -}; diff --git a/packages/eslint-config-base/src/rules/stylistic.js b/packages/eslint-config-base/src/rules/stylistic.js new file mode 100644 index 0000000..15620fd --- /dev/null +++ b/packages/eslint-config-base/src/rules/stylistic.js @@ -0,0 +1,30 @@ +import stylistic from '@stylistic/eslint-plugin'; + +/** + * Prettier owns formatting; eslint-config-prettier switches the formatting rules off. Only readability + * rules Prettier does not touch are on (blank lines, comment spacing); everything else is off. + */ +export default { + name: 'cospired/stylistic', + plugins: { '@stylistic': stylistic }, + rules: { + '@stylistic/jsx-self-closing-comp': 'error', + '@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + '@stylistic/padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + ], + '@stylistic/spaced-comment': ['error', 'always', { markers: ['/'] }], + + '@stylistic/curly-newline': 'off', + '@stylistic/exp-jsx-props-style': 'off', + '@stylistic/exp-list-style': 'off', + '@stylistic/jsx-curly-brace-presence': 'off', + '@stylistic/jsx-function-call-newline': 'off', + '@stylistic/jsx-pascal-case': 'off', + '@stylistic/line-comment-position': 'off', + '@stylistic/multiline-comment-style': 'off', + }, +}; diff --git a/packages/eslint-config-base/src/rules/todo-plz.js b/packages/eslint-config-base/src/rules/todo-plz.js new file mode 100644 index 0000000..60724f6 --- /dev/null +++ b/packages/eslint-config-base/src/rules/todo-plz.js @@ -0,0 +1,8 @@ +import todoPlz from 'eslint-plugin-todo-plz'; + +/** TODOs need a ticket: `TODO(#123): ...`. */ +export default { + name: 'cospired/todo-plz', + plugins: { 'todo-plz': todoPlz }, + rules: { 'todo-plz/ticket-ref': ['error', { pattern: '#\\d+' }] }, +}; diff --git a/packages/eslint-config-base/src/rules/typescript-eslint.js b/packages/eslint-config-base/src/rules/typescript-eslint.js new file mode 100644 index 0000000..55f2c97 --- /dev/null +++ b/packages/eslint-config-base/src/rules/typescript-eslint.js @@ -0,0 +1,56 @@ +/** + * typescript-eslint, on top of `recommendedTypeChecked` and `stylisticTypeChecked` (added in ../typescript.js). + * Every non-deprecated rule is configured: here or by a preset (see test/completeness). + */ +export const typescriptRules = { + // --- on + '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], + '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-generated-empty-object-type': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-invalid-this': 'error', + '@typescript-eslint/no-invalid-void-type': 'error', + '@typescript-eslint/no-misused-promises': ['error', { checksConditionals: { flagUnions: 'strict' } }], + '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-unnecessary-parameter-property-assignment': 'error', + '@typescript-eslint/no-unused-private-class-members': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + '@typescript-eslint/no-use-before-define': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-useless-empty-export': 'error', + '@typescript-eslint/prefer-literal-enum-member': 'error', + '@typescript-eslint/unified-signatures': 'error', + + // --- core rules replaced by the typescript-eslint versions above (or covered by the compiler) + 'consistent-return': 'off', + 'default-param-last': 'off', + 'no-invalid-this': 'off', + 'no-shadow': 'off', + 'no-unused-private-class-members': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', + + // --- off: opinion, thresholds, or the compiler already reports it + '@typescript-eslint/class-methods-use-this': 'off', + '@typescript-eslint/explicit-member-accessibility': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/init-declarations': 'off', + '@typescript-eslint/max-params': 'off', + '@typescript-eslint/member-ordering': 'off', + '@typescript-eslint/method-signature-style': 'off', + '@typescript-eslint/no-dupe-class-members': 'off', + '@typescript-eslint/no-extraneous-class': 'off', + '@typescript-eslint/no-magic-numbers': 'off', + '@typescript-eslint/no-redeclare': 'off', + '@typescript-eslint/no-restricted-types': 'off', + '@typescript-eslint/parameter-properties': 'off', + '@typescript-eslint/prefer-enum-initializers': 'off', +}; + +/** Rules for source files only; tests are exempt. */ +export const sourceOnlyRules = { + '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], +}; diff --git a/packages/eslint-config-base/src/rules/typescript.js b/packages/eslint-config-base/src/rules/typescript.js deleted file mode 100644 index 785c454..0000000 --- a/packages/eslint-config-base/src/rules/typescript.js +++ /dev/null @@ -1,43 +0,0 @@ -import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; -import { flatConfigs as importX } from 'eslint-plugin-import-x'; - -export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; -/** JS files can be matched by a consumer's `files`; they are never type-checked. */ -export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; -export const testGlobs = ['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']; - -/** Rules on top of typescript-eslint's recommended/stylistic type-checked sets. Severities are placeholders until the rule review. */ -export const typescriptRules = { - '@typescript-eslint/default-param-last': 'error', - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-generated-empty-object-type': 'error', - '@typescript-eslint/no-misused-promises': ['error', { checksConditionals: { flagUnions: 'strict' } }], - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], - '@typescript-eslint/no-use-before-define': 'error', - // Core versions replaced by the typescript-eslint ones above; the compiler's noImplicitReturns covers consistent-return. - 'consistent-return': 'off', - 'default-param-last': 'off', - 'no-shadow': 'off', - 'no-use-before-define': 'off', - 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'], -}; - -/** Rules typescript-eslint's recommended sets leave off (or do not set) that cospired wants on for source files. */ -export const sourceOnlyRules = { - '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], -}; - -export const importXTypescript = importX.typescript; - -/** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ -export function importSettings(project) { - return { - 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs', ...tsExtensions], - 'import-x/parsers': { '@typescript-eslint/parser': tsExtensions }, - 'import-x/resolver-next': [ - // Several projects is the intended shape (app + test-only tsconfigs); the suggested `references` fix needs `composite`. - createTypeScriptImportResolver({ alwaysTryTypes: true, noWarnOnMultipleProjects: true, ...(project && { project }) }), - ], - }; -} diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index feb41d8..b858c0f 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -2,7 +2,9 @@ import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import { createBaseConfig, prettierLast } from './index.js'; -import { importSettings, importXTypescript, jsFiles, sourceOnlyRules, testGlobs, typescriptRules } from './rules/typescript.js'; +import { jsFiles, testGlobs } from './globs.js'; +import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x.js'; +import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; export { globalIgnores } from './ignores.js'; @@ -36,9 +38,9 @@ export function createTypescriptConfig({ extends: [tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, importXTypescript], languageOptions: { parserOptions }, settings: importSettings(tsconfigPath), - rules: typescriptRules, }), - { files, ignores: [...testGlobs, ...jsFiles], rules: sourceOnlyRules }, + { name: 'cospired/typescript', files, rules: { ...typescriptRules, ...importXTypescriptRules } }, + { name: 'cospired/typescript-eslint', files, ignores: [...testGlobs, ...jsFiles], rules: sourceOnlyRules }, { files: jsFiles, ...tseslint.configs.disableTypeChecked }, ...prettierLast, ]; diff --git a/packages/eslint-config-base/test/completeness.test.js b/packages/eslint-config-base/test/completeness.test.js new file mode 100644 index 0000000..036e237 --- /dev/null +++ b/packages/eslint-config-base/test/completeness.test.js @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; +import { baseNamespaces, pluginRules, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import jsConfig from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; +import { createTypescriptConfig } from '../src/typescript.js'; + +// A rule counts as configured when a recommended preset or one of our files sets it (on or off). +const cases = [ + ['base JS', jsConfig, baseNamespaces], + ['base TypeScript', createTypescriptConfig(), typescriptNamespaces], + ['node', createNodeConfig(), typescriptNamespaces], +]; + +for (const [name, config, namespaces] of cases) { + test(`${name}: every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(config, namespaces, pluginRules), []); + }); + + test(`${name}: our overrides only repeat nothing the presets already set`, () => { + assert.deepEqual(redundantRules(config), []); + }); +} diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index ca87942..ab527d8 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -72,3 +72,17 @@ test('scripts/ files get node globals in every base config', async () => { } assert.ok((await lint('process.exit(1);\n', 'src/run.mjs', tsConfig)).includes('no-undef')); }); + +test('test-file globs also match from a monorepo root config', async () => { + const { testGlobs } = await import('../src/globs.js'); + for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts}']) { + assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); + } +}); + +test('console is allowed in scripts of every extension', async () => { + for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { + const found = await lint('console.log(1);\n', `scripts/x.${ext}`, createTypescriptConfig(tsOptions)); + assert.ok(!found.includes('no-console'), ext); + } +}); diff --git a/packages/eslint-config-react/src/index.js b/packages/eslint-config-react/src/index.js index 070961c..22f2384 100644 --- a/packages/eslint-config-react/src/index.js +++ b/packages/eslint-config-react/src/index.js @@ -2,8 +2,10 @@ import baseConfig, { globalIgnores, prettierLast } from '@cospired/eslint-config import formatjs from './rules/formatjs.js'; import react from './rules/react.js'; +import reactHooks from './rules/react-hooks.js'; +import reactRefresh from './rules/react-refresh.js'; export { globalIgnores }; /** React JavaScript config: base JS + React rules. */ -export default [...baseConfig, ...react, formatjs, ...prettierLast]; +export default [...baseConfig, ...react, ...reactHooks, reactRefresh, formatjs, ...prettierLast]; diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js index 792ff17..9c01de2 100644 --- a/packages/eslint-config-react/src/rules/formatjs.js +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -1,16 +1,21 @@ +import { anyTestGlobs } from '@cospired/eslint-config-base'; import formatjs from 'eslint-plugin-formatjs'; import { errorOnly } from './errors.js'; -export const testGlobs = ['**/*.{test,spec}.{js,jsx,ts,tsx}']; - /** i18n: formatjs `recommended` plus extras. Test files are exempt. Values are placeholders until the rule review. */ export default { plugins: formatjs.configs.recommended.plugins, - ignores: testGlobs, + ignores: anyTestGlobs, + name: 'cospired/formatjs', rules: { ...errorOnly(formatjs.configs.recommended.rules), 'formatjs/enforce-description': 'off', + 'formatjs/enforce-id': 'off', + 'formatjs/enforce-message-types': 'off', + 'formatjs/no-id': 'off', + 'formatjs/no-literal-string-in-object': 'off', + 'formatjs/prefer-formatted-message': 'error', 'formatjs/no-camel-case': 'error', 'formatjs/no-invalid-icu': 'error', }, diff --git a/packages/eslint-config-react/src/rules/react-hooks.js b/packages/eslint-config-react/src/rules/react-hooks.js new file mode 100644 index 0000000..39087dd --- /dev/null +++ b/packages/eslint-config-react/src/rules/react-hooks.js @@ -0,0 +1,27 @@ +import reactHooks from 'eslint-plugin-react-hooks'; + +import { errorOnly } from './errors.js'; + +const recommended = reactHooks.configs.flat.recommended; + +/** react-hooks `recommended` (warnings upgraded to errors) plus an explicit off for the experimental/internal compiler diagnostics. */ +export default [ + { ...recommended, rules: errorOnly(recommended.rules) }, + { + name: 'cospired/react-hooks', + rules: { + 'react-hooks/capitalized-calls': 'off', + 'react-hooks/exhaustive-effect-dependencies': 'off', + 'react-hooks/fbt': 'off', + 'react-hooks/hooks': 'off', + 'react-hooks/invariant': 'off', + 'react-hooks/memo-dependencies': 'off', + 'react-hooks/memoized-effect-dependencies': 'off', + 'react-hooks/no-deriving-state-in-effects': 'off', + 'react-hooks/rule-suppression': 'off', + 'react-hooks/syntax': 'off', + 'react-hooks/todo': 'off', + 'react-hooks/void-use-memo': 'off', + }, + }, +]; diff --git a/packages/eslint-config-react/src/rules/react-refresh.js b/packages/eslint-config-react/src/rules/react-refresh.js new file mode 100644 index 0000000..5d383ec --- /dev/null +++ b/packages/eslint-config-react/src/rules/react-refresh.js @@ -0,0 +1,8 @@ +import reactRefresh from 'eslint-plugin-react-refresh'; + +/** Fast Refresh (Vite HMR): files exporting components may export nothing else but constants. */ +export default { + name: 'cospired/react-refresh', + plugins: { 'react-refresh': reactRefresh }, + rules: { 'react-refresh/only-export-components': ['error', { allowConstantExport: true }] }, +}; diff --git a/packages/eslint-config-react/src/rules/react.js b/packages/eslint-config-react/src/rules/react.js index 5378e4a..cce1692 100644 --- a/packages/eslint-config-react/src/rules/react.js +++ b/packages/eslint-config-react/src/rules/react.js @@ -1,24 +1,11 @@ -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; import globals from 'globals'; -import { errorOnly } from './errors.js'; - -const hooks = reactHooks.configs.flat.recommended; - -/** React: hooks, refresh (Vite HMR), browser globals, JSX in plain JS. Values are placeholders until the rule review. */ +/** Browser globals, and JSX in plain JS files. */ export default [ // ESLint lints only .js/.mjs/.cjs by default; .jsx must be opted in. - { files: ['**/*.jsx'] }, - { ...hooks, rules: errorOnly(hooks.rules) }, + { name: 'cospired/react-files', files: ['**/*.jsx'] }, { - plugins: { 'react-refresh': reactRefresh }, - languageOptions: { - globals: globals.browser, - parserOptions: { ecmaFeatures: { jsx: true } }, - }, - rules: { - 'react-refresh/only-export-components': ['error', { allowConstantExport: true }], - }, + name: 'cospired/react', + languageOptions: { globals: globals.browser, parserOptions: { ecmaFeatures: { jsx: true } } }, }, ]; diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js index 6044bdf..eea6ab9 100644 --- a/packages/eslint-config-react/src/typescript.js +++ b/packages/eslint-config-react/src/typescript.js @@ -3,6 +3,8 @@ import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-b import formatjs from './rules/formatjs.js'; import react from './rules/react.js'; +import reactHooks from './rules/react-hooks.js'; +import reactRefresh from './rules/react-refresh.js'; export { globalIgnores }; @@ -13,5 +15,5 @@ export { globalIgnores }; * @returns {import('eslint').Linter.Config[]} */ export function createReactTypescriptConfig(options = {}) { - return [...createTypescriptConfig(options), ...react, formatjs, ...prettierLast]; + return [...createTypescriptConfig(options), ...react, ...reactHooks, reactRefresh, formatjs, ...prettierLast]; } diff --git a/packages/eslint-config-react/test/completeness.test.js b/packages/eslint-config-react/test/completeness.test.js new file mode 100644 index 0000000..06e30ce --- /dev/null +++ b/packages/eslint-config-react/test/completeness.test.js @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; +import { baseNamespaces, pluginRules, reactNamespaces, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import jsConfig from '../src/index.js'; +import { createReactTypescriptConfig } from '../src/typescript.js'; + +const cases = [ + ['react JS', jsConfig, [...baseNamespaces, ...reactNamespaces]], + ['react TypeScript', createReactTypescriptConfig(), [...typescriptNamespaces, ...reactNamespaces]], +]; + +for (const [name, config, namespaces] of cases) { + test(`${name}: every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(config, namespaces, pluginRules), []); + }); + + test(`${name}: our overrides only repeat nothing the presets already set`, () => { + assert.deepEqual(redundantRules(config), []); + }); +} diff --git a/packages/eslint-config-react/test/decisions.test.js b/packages/eslint-config-react/test/decisions.test.js index 08d73d0..0cf435d 100644 --- a/packages/eslint-config-react/test/decisions.test.js +++ b/packages/eslint-config-react/test/decisions.test.js @@ -26,3 +26,9 @@ test('formatjs: enforce-description is off, no-camel-case is an error', async () assert.equal(await ruleSeverity(jsConfig, 'formatjs/enforce-description', path.join(root, 'src/a.jsx')), 0); assert.equal(await ruleSeverity(jsConfig, 'formatjs/no-camel-case', path.join(root, 'src/a.jsx')), 2); }); + +test('formatjs exempts the same test files as the base config', async () => { + const sev = (file) => ruleSeverity(jsConfig, 'formatjs/enforce-default-message', path.join(root, file), root); + assert.equal(await sev('src/a.jsx'), 2); + for (const file of ['src/a.test.jsx', 'test/helpers.jsx', 'src/__tests__/a.jsx']) assert.notEqual(await sev(file), 2, file); +}); diff --git a/test-utils/completeness.test.js b/test-utils/completeness.test.js new file mode 100644 index 0000000..2c2668f --- /dev/null +++ b/test-utils/completeness.test.js @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from './lint.js'; + +const plugins = { '': { a: {}, b: {}, gone: { meta: { deprecated: true } } }, p: { x: {}, y: {} } }; + +test('unconfiguredRules lists non-deprecated rules no config item sets (on or off)', () => { + const config = [{ rules: { a: 'error', 'p/x': 'off' } }]; + assert.deepEqual(unconfiguredRules(config, ['', 'p'], plugins), ['b', 'p/y']); +}); + +test('unconfiguredRules only checks the requested namespaces', () => { + assert.deepEqual(unconfiguredRules([], [''], plugins), ['a', 'b']); +}); + +test('redundantRules flags a named config item that repeats the value already in effect', () => { + const config = [ + { rules: { a: ['error', { x: 1 }], b: 'warn' } }, + { name: 'cospired/test', rules: { a: [2, { x: 1 }], b: 'error', c: 'off' } }, + ]; + assert.deepEqual(redundantRules(config), ['a']); +}); diff --git a/test-utils/lint.js b/test-utils/lint.js index 7d30ecd..3b4ca0a 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -54,9 +54,9 @@ export function missingRules(config) { } /** The severity (0|1|2) a rule has for `filePath` once all config objects are merged. */ -export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts') { +export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts', cwd) { filePath = path.resolve(fixtureProject, filePath); - const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: path.dirname(filePath) }); + const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: cwd ?? path.dirname(filePath) }); const entry = (await eslint.calculateConfigForFile(filePath)).rules?.[ruleId]; return Array.isArray(entry) ? entry[0] : entry; @@ -70,3 +70,37 @@ export async function warnRules(config, filePath = 'src/sample.ts', cwd) { return Object.entries(rules).filter(([, entry]) => (Array.isArray(entry) ? entry[0] : entry) === 1).map(([id]) => id).sort(); } + +/** Non-deprecated rules of `namespaces` that no config item sets, whether on or off. */ +export function unconfiguredRules(config, namespaces, pluginRules) { + const configured = new Set(config.flatMap((item) => Object.keys(item.rules ?? {}))); + + return namespaces.flatMap((ns) => + Object.entries(pluginRules[ns]) + .filter(([, rule]) => !rule.meta?.deprecated) + .map(([name]) => (ns ? `${ns}/${name}` : name)) + .filter((id) => !configured.has(id)), + ); +} + +const normalize = (entry) => { + const [level, ...options] = Array.isArray(entry) ? entry : [entry]; + const severity = { off: 0, warn: 1, error: 2 }[level] ?? level; + + return JSON.stringify([severity, ...options]); +}; + +/** Rules set by a config item named `cospired/*` to the value that was already in effect (an override that changes nothing). */ +export function redundantRules(config) { + const effective = {}; + const redundant = new Set(); + + for (const item of config) { + for (const [id, entry] of Object.entries(item.rules ?? {})) { + if (item.name?.startsWith('cospired/') && id in effective && normalize(effective[id]) === normalize(entry)) redundant.add(id); + effective[id] = entry; + } + } + + return [...redundant].sort(); +} diff --git a/test-utils/plugins.js b/test-utils/plugins.js new file mode 100644 index 0000000..314e324 --- /dev/null +++ b/test-utils/plugins.js @@ -0,0 +1,27 @@ +import comments from '@eslint-community/eslint-plugin-eslint-comments'; +import stylistic from '@stylistic/eslint-plugin'; +import { builtinRules } from 'eslint/use-at-your-own-risk'; +import formatjs from 'eslint-plugin-formatjs'; +import importX from 'eslint-plugin-import-x'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import todoPlz from 'eslint-plugin-todo-plz'; +import tseslint from 'typescript-eslint'; + +/** Every plugin the configs use, keyed by rule namespace ('' = ESLint core). */ +export const pluginRules = { + '': Object.fromEntries(builtinRules), + '@eslint-community/eslint-comments': comments.rules, + '@stylistic': stylistic.rules, + '@typescript-eslint': tseslint.plugin.rules, + formatjs: formatjs.rules, + 'import-x': importX.rules, + 'react-hooks': reactHooks.rules, + 'react-refresh': reactRefresh.rules, + 'todo-plz': todoPlz.rules, +}; + +/** Namespaces each shipped config must cover completely. */ +export const baseNamespaces = ['', '@eslint-community/eslint-comments', '@stylistic', 'import-x', 'todo-plz']; +export const typescriptNamespaces = [...baseNamespaces, '@typescript-eslint']; +export const reactNamespaces = ['react-hooks', 'react-refresh', 'formatjs']; From 798d4191057f1ef8bc3fc906da869901ce485972 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:14:08 +0800 Subject: [PATCH 15/33] feat: zero-option default exports, project service defaults Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/package.json | 2 +- packages/eslint-config-base/src/index.js | 5 +- packages/eslint-config-base/src/node.js | 6 ++- packages/eslint-config-base/src/typescript.js | 9 +++- .../eslint-config-base/test/defaults.test.js | 46 +++++++++++++++++++ .../test/fixtures/ts-project/vitest.config.ts | 1 + packages/eslint-config-react/package.json | 2 +- .../eslint-config-react/src/typescript.js | 3 ++ 8 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 packages/eslint-config-base/test/defaults.test.js create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index cf8d7fc..5f7bba0 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -28,7 +28,7 @@ "./node": "./src/node.js" }, "scripts": { - "test": "node --test" + "test": "node --test 'test/*.test.js'" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index b17b760..da17d01 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -2,6 +2,7 @@ import prettier from 'eslint-config-prettier'; import globals from 'globals'; import { withMonorepoTwins } from './globs.js'; +import { globalIgnores } from './ignores.js'; import eslintCore from './rules/eslint.js'; import eslintComments from './rules/eslint-comments.js'; import { createImportsConfig } from './rules/import-x.js'; @@ -35,5 +36,5 @@ export function createBaseConfig(importOptions) { ]; } -/** JavaScript config: ESLint recommended + cospired rules. */ -export default createBaseConfig(); +/** JavaScript config: ESLint recommended + cospired rules, ready to use as `export { default } from '@cospired/eslint-config-base'`. */ +export default [globalIgnores, ...createBaseConfig()]; diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 389c5c4..05bfa42 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,9 +1,10 @@ import globals from 'globals'; import { jsFiles } from './globs.js'; -import { createTypescriptConfig } from './typescript.js'; export { globalIgnores } from './ignores.js'; +import { createTypescriptConfig } from './typescript.js'; + /** * Node.js/backend TypeScript config: `createTypescriptConfig` plus node globals. @@ -28,3 +29,6 @@ export function createNodeConfig(options = {}) { }, ]; } + +/** Node TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node'`. */ +export default createNodeConfig(); diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index b858c0f..eaaaf82 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -1,12 +1,13 @@ import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; +import { globalIgnores } from './ignores.js'; import { createBaseConfig, prettierLast } from './index.js'; import { jsFiles, testGlobs } from './globs.js'; import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x.js'; import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; -export { globalIgnores } from './ignores.js'; +export { globalIgnores }; /** * TypeScript config: base rules + typescript-eslint (type-checked) + cospired TS rules. @@ -29,9 +30,10 @@ export function createTypescriptConfig({ } = {}) { const parserOptions = tsconfigPath ? { project: tsconfigPath, tsconfigRootDir } - : { projectService: true, tsconfigRootDir }; + : { projectService: { allowDefaultProject: ['*.config.{ts,mts,cts}'] }, tsconfigRootDir }; return [ + globalIgnores, ...createBaseConfig({ devDependencyGlobs, pathGroups: importOrder.pathGroups }), ...defineConfig({ files, @@ -45,3 +47,6 @@ export function createTypescriptConfig({ ...prettierLast, ]; } + +/** TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/typescript'`. */ +export default createTypescriptConfig(); diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js new file mode 100644 index 0000000..ec326ce --- /dev/null +++ b/packages/eslint-config-base/test/defaults.test.js @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import jsDefault from '../src/index.js'; +import nodeDefault from '../src/node.js'; +import tsDefault from '../src/typescript.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + +test('default exports are ready-made config arrays that include the global ignores', () => { + for (const cfg of [jsDefault, tsDefault, nodeDefault]) { + assert.ok(Array.isArray(cfg)); + assert.ok(cfg.some((item) => item.ignores?.includes('**/dist/**'))); + } +}); + +test('zero options: TS files lint via the project service, no parse errors', async () => { + for (const cfg of [tsDefault, nodeDefault]) { + for (const file of ['src/a.ts', 'src/b.ts']) { + const messages = await lint(cfg, 'export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); + } + } +}); + +test('zero options: a *.config.ts outside every tsconfig still lints (default project)', async () => { + // typescript-eslint resolves allowDefaultProject globs against the cwd, i.e. the package being linted. + const previous = process.cwd(); + process.chdir(root); + try { + const messages = await lint(nodeDefault, 'export default { test: true };\n', 'vitest.config.ts'); + assert.deepEqual(messages.filter((m) => m.fatal), []); + assert.ok(!ruleIds(messages).includes('@typescript-eslint/explicit-function-return-type')); + } finally { + process.chdir(previous); + } +}); + +test('zero options: dist output is ignored', async () => { + const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); + assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); +}); diff --git a/packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts b/packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts new file mode 100644 index 0000000..0e811a8 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts @@ -0,0 +1 @@ +export default { test: true }; diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index 089ed96..d368361 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -27,7 +27,7 @@ "./typescript": "./src/typescript.js" }, "scripts": { - "test": "node --test" + "test": "node --test 'test/*.test.js'" }, "dependencies": { "@cospired/eslint-config-base": "*" diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js index eea6ab9..e76dd71 100644 --- a/packages/eslint-config-react/src/typescript.js +++ b/packages/eslint-config-react/src/typescript.js @@ -17,3 +17,6 @@ export { globalIgnores }; export function createReactTypescriptConfig(options = {}) { return [...createTypescriptConfig(options), ...react, ...reactHooks, reactRefresh, formatjs, ...prettierLast]; } + +/** React TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-react/typescript'`. */ +export default createReactTypescriptConfig(); From a2db23491ea0c8c96ab2e4f934598a1561237c6a Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:16:55 +0800 Subject: [PATCH 16/33] feat: untyped tests/configs by default, typedTests option; formatjs prefer-formatted-message off Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/globs.js | 6 ++++ packages/eslint-config-base/src/node.js | 6 ++-- packages/eslint-config-base/src/typescript.js | 9 ++++-- .../eslint-config-base/test/defaults.test.js | 28 +++++++++++-------- .../test/fixtures/ts-project/other/c.test.ts | 1 + .../eslint-config-react/src/rules/formatjs.js | 2 +- 6 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts diff --git a/packages/eslint-config-base/src/globs.js b/packages/eslint-config-base/src/globs.js index 6b6fa7d..2a9840b 100644 --- a/packages/eslint-config-base/src/globs.js +++ b/packages/eslint-config-base/src/globs.js @@ -7,3 +7,9 @@ export const testGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts}', export const anyTestGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', 'test/**', 'tests/**', '**/__tests__/**']); /** JS files can be matched by a consumer's `files`; they are never type-checked. */ export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; + +/** + * Files linted without type information unless `typedTests: true`: they are often not part of any tsconfig + * (build configs exclude them), and type-aware linting reports a parsing error for such files. + */ +export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.config.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 05bfa42..7eb951f 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,6 +1,6 @@ import globals from 'globals'; -import { jsFiles } from './globs.js'; +import { jsFiles, untypedGlobs } from './globs.js'; export { globalIgnores } from './ignores.js'; import { createTypescriptConfig } from './typescript.js'; @@ -14,14 +14,14 @@ import { createTypescriptConfig } from './typescript.js'; * @returns {import('eslint').Linter.Config[]} */ export function createNodeConfig(options = {}) { - const { files = ['**/*.{ts,tsx,mts,cts}'] } = options; + const { files = ['**/*.{ts,tsx,mts,cts}'], typedTests = false } = options; return [ ...createTypescriptConfig(options), { languageOptions: { sourceType: 'module', globals: globals.node } }, { files, - ignores: jsFiles, + ignores: typedTests ? jsFiles : [...jsFiles, ...untypedGlobs], rules: { // String `||` is deliberate empty-string fallthrough (env vars, trimmed input) that `??` would silently drop. '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index eaaaf82..1f254bd 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -3,7 +3,7 @@ import tseslint from 'typescript-eslint'; import { globalIgnores } from './ignores.js'; import { createBaseConfig, prettierLast } from './index.js'; -import { jsFiles, testGlobs } from './globs.js'; +import { jsFiles, testGlobs, untypedGlobs } from './globs.js'; import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x.js'; import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; @@ -17,6 +17,7 @@ export { globalIgnores }; * Without it typescript-eslint's project service finds them. * @param {string} [options.tsconfigRootDir] Pass `import.meta.dirname` when one ESLint run loads several workspace configs. * @param {string[]} [options.files] TS files to apply type-aware rules to. + * @param {boolean} [options.typedTests] Lint test, config and script files with type information too (they must then belong to a tsconfig). * @param {string[]} [options.devDependencyGlobs] Files that may import devDependencies. * @param {{ pathGroups?: object[] }} [options.importOrder] Extra `import-x/order` path groups, e.g. `#alias/**` as `internal`. * @returns {import('eslint').Linter.Config[]} @@ -27,10 +28,11 @@ export function createTypescriptConfig({ files = ['**/*.{ts,tsx,mts,cts}'], devDependencyGlobs, importOrder = {}, + typedTests = false, } = {}) { const parserOptions = tsconfigPath ? { project: tsconfigPath, tsconfigRootDir } - : { projectService: { allowDefaultProject: ['*.config.{ts,mts,cts}'] }, tsconfigRootDir }; + : { projectService: true, tsconfigRootDir }; return [ globalIgnores, @@ -43,7 +45,8 @@ export function createTypescriptConfig({ }), { name: 'cospired/typescript', files, rules: { ...typescriptRules, ...importXTypescriptRules } }, { name: 'cospired/typescript-eslint', files, ignores: [...testGlobs, ...jsFiles], rules: sourceOnlyRules }, - { files: jsFiles, ...tseslint.configs.disableTypeChecked }, + { name: 'cospired/untyped-js', files: jsFiles, ...tseslint.configs.disableTypeChecked }, + ...(typedTests ? [] : [{ name: 'cospired/untyped', files: untypedGlobs, ...tseslint.configs.disableTypeChecked }]), ...prettierLast, ]; } diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index ec326ce..c68c894 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -3,10 +3,10 @@ import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { lintText } from '../../../test-utils/lint.js'; import jsDefault from '../src/index.js'; import nodeDefault from '../src/node.js'; -import tsDefault from '../src/typescript.js'; +import tsDefault, { createTypescriptConfig } from '../src/typescript.js'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); @@ -27,19 +27,23 @@ test('zero options: TS files lint via the project service, no parse errors', asy } }); -test('zero options: a *.config.ts outside every tsconfig still lints (default project)', async () => { - // typescript-eslint resolves allowDefaultProject globs against the cwd, i.e. the package being linted. - const previous = process.cwd(); - process.chdir(root); - try { - const messages = await lint(nodeDefault, 'export default { test: true };\n', 'vitest.config.ts'); - assert.deepEqual(messages.filter((m) => m.fatal), []); - assert.ok(!ruleIds(messages).includes('@typescript-eslint/explicit-function-return-type')); - } finally { - process.chdir(previous); +test('zero options: test and config files outside every tsconfig lint without type information', async () => { + for (const file of ['other/c.test.ts', 'vitest.config.ts']) { + for (const cfg of [tsDefault, nodeDefault]) { + const messages = await lint(cfg, 'export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); + } } }); +test('typedTests: true lints tests with type information (they must be in a tsconfig)', async () => { + const typed = createTypescriptConfig({ typedTests: true, tsconfigRootDir: root }); + const outside = await lint(typed, 'export const v = 1;\n', 'other/c.test.ts'); + assert.ok(outside.some((m) => m.fatal), 'a test outside every tsconfig reports a parsing error'); + const inside = await lint(typed, 'export const v = 1;\n', 'src/a.ts'); + assert.deepEqual(inside.filter((m) => m.fatal), []); +}); + test('zero options: dist output is ignored', async () => { const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); diff --git a/packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts b/packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts new file mode 100644 index 0000000..351d3e8 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts @@ -0,0 +1 @@ +export const c = 1; diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js index 9c01de2..f5d9a6e 100644 --- a/packages/eslint-config-react/src/rules/formatjs.js +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -15,7 +15,7 @@ export default { 'formatjs/enforce-message-types': 'off', 'formatjs/no-id': 'off', 'formatjs/no-literal-string-in-object': 'off', - 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-formatted-message': 'off', 'formatjs/no-camel-case': 'error', 'formatjs/no-invalid-icu': 'error', }, From 490e50535a9c2889217c24e91b802f13212f24f3 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:18:44 +0800 Subject: [PATCH 17/33] feat: package-root TS files untyped, import-x/first off in tests Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/src/globs.js | 7 ++++--- packages/eslint-config-base/src/index.js | 3 ++- packages/eslint-config-base/test/decisions.test.js | 2 +- packages/eslint-config-base/test/defaults.test.js | 9 ++++++++- .../test/fixtures/ts-project/vitest.shared.ts | 1 + packages/eslint-config-base/test/typescript.test.js | 4 ++-- 6 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts diff --git a/packages/eslint-config-base/src/globs.js b/packages/eslint-config-base/src/globs.js index 2a9840b..504cece 100644 --- a/packages/eslint-config-base/src/globs.js +++ b/packages/eslint-config-base/src/globs.js @@ -9,7 +9,8 @@ export const anyTestGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; /** - * Files linted without type information unless `typedTests: true`: they are often not part of any tsconfig - * (build configs exclude them), and type-aware linting reports a parsing error for such files. + * Files linted without type information unless `typedTests: true`: tests, TypeScript files at the package root + * (vitest.config.ts, drizzle.config.ts, ...) and scripts. They are often not part of any tsconfig (build configs + * exclude them), and type-aware linting reports a parsing error for such files. */ -export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.config.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; +export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index da17d01..dc79dfa 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -1,7 +1,7 @@ import prettier from 'eslint-config-prettier'; import globals from 'globals'; -import { withMonorepoTwins } from './globs.js'; +import { anyTestGlobs, withMonorepoTwins } from './globs.js'; import { globalIgnores } from './ignores.js'; import eslintCore from './rules/eslint.js'; import eslintComments from './rules/eslint-comments.js'; @@ -28,6 +28,7 @@ export function createBaseConfig(importOptions) { ...eslintCore, eslintComments, createImportsConfig(importOptions), + { name: 'cospired/tests', files: anyTestGlobs, rules: { 'import-x/first': 'off' } }, // vi.mock()/jest.mock() hoisting runs before imports todoPlz, stylistic, // Build/CLI scripts run on Node and print to the console. diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index ab527d8..60e5f6a 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -60,7 +60,7 @@ test('no rule resolves to "warn" in any base config', async () => { }); test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { - const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts')); + const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts'), root); for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); }); diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index c68c894..50f5938 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -9,6 +9,7 @@ import nodeDefault from '../src/node.js'; import tsDefault, { createTypescriptConfig } from '../src/typescript.js'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); test('default exports are ready-made config arrays that include the global ignores', () => { @@ -28,7 +29,7 @@ test('zero options: TS files lint via the project service, no parse errors', asy }); test('zero options: test and config files outside every tsconfig lint without type information', async () => { - for (const file of ['other/c.test.ts', 'vitest.config.ts']) { + for (const file of ['other/c.test.ts', 'vitest.config.ts', 'vitest.shared.ts']) { for (const cfg of [tsDefault, nodeDefault]) { const messages = await lint(cfg, 'export const v = 1;\n', file); assert.deepEqual(messages.filter((m) => m.fatal), [], file); @@ -48,3 +49,9 @@ test('zero options: dist output is ignored', async () => { const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); }); + +test('import-x/first is off in tests (vi.mock hoisting) and on in source', async () => { + const code = "vi.mock('./a.js');\n\nimport { a } from './a.js';\n\nexport const v = a;\n"; + assert.ok(!(await ruleIdsFor(tsDefault, code, 'other/c.test.ts')).includes('import-x/first')); + assert.ok((await ruleIdsFor(tsDefault, code, 'src/b.ts')).includes('import-x/first')); +}); diff --git a/packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts b/packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts new file mode 100644 index 0000000..cf20619 --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts @@ -0,0 +1 @@ +export const shared = 1; diff --git a/packages/eslint-config-base/test/typescript.test.js b/packages/eslint-config-base/test/typescript.test.js index f304ff2..dc31e16 100644 --- a/packages/eslint-config-base/test/typescript.test.js +++ b/packages/eslint-config-base/test/typescript.test.js @@ -10,7 +10,7 @@ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', const abs = (file) => path.join(root, file); const options = { tsconfigPath: [abs('tsconfig.json'), abs('tsconfig.test.json')], tsconfigRootDir: root }; const config = createTypescriptConfig(options); -const lint = async (code, file = 'src/a.ts', cfg = config) => lintText(cfg, code, { filePath: abs(file) }); +const lint = async (code, file = 'src/a.ts', cfg = config) => lintText(cfg, code, { filePath: abs(file), cwd: root }); const ids = async (...args) => { const messages = await lint(...args); assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); @@ -31,7 +31,7 @@ test('no-misused-promises flags an async callback used as a condition', async () }); test('no-generated-empty-object-type is an error for TS files', async () => { - assert.equal(await ruleSeverity(config, '@typescript-eslint/no-generated-empty-object-type', abs('src/a.ts')), 2); + assert.equal(await ruleSeverity(config, '@typescript-eslint/no-generated-empty-object-type', abs('src/a.ts'), root), 2); }); test('accepts an array of tsconfigs: src and test files both lint without parse errors', async () => { From 21e0c8d5feb0824d80a93d08e350e0d6b6385e71 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:20:25 +0800 Subject: [PATCH 18/33] feat(base): node-js entry Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-base/package.json | 3 +- packages/eslint-config-base/src/node-js.js | 48 +++++++++++++++++++ .../test/completeness.test.js | 2 + .../test/fixtures/cjs-project/package.json | 1 + .../test/fixtures/cjs-project/src/a.js | 1 + .../eslint-config-base/test/node-js.test.js | 32 +++++++++++++ 6 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-config-base/src/node-js.js create mode 100644 packages/eslint-config-base/test/fixtures/cjs-project/package.json create mode 100644 packages/eslint-config-base/test/fixtures/cjs-project/src/a.js create mode 100644 packages/eslint-config-base/test/node-js.test.js diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index 5f7bba0..8a9c070 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -25,7 +25,8 @@ "exports": { ".": "./src/index.js", "./typescript": "./src/typescript.js", - "./node": "./src/node.js" + "./node": "./src/node.js", + "./node-js": "./src/node-js.js" }, "scripts": { "test": "node --test 'test/*.test.js'" diff --git a/packages/eslint-config-base/src/node-js.js b/packages/eslint-config-base/src/node-js.js new file mode 100644 index 0000000..82ae327 --- /dev/null +++ b/packages/eslint-config-base/src/node-js.js @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import globals from 'globals'; + +import baseConfig, { globalIgnores } from './index.js'; + +export { globalIgnores }; + +/** `module` when the nearest package.json at or above `cwd` says `"type": "module"`, otherwise `commonjs`. */ +function detectSourceType(cwd) { + for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) { + const file = path.join(dir, 'package.json'); + + if (fs.existsSync(file)) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')).type === 'module' ? 'module' : 'commonjs'; + } catch { + return 'commonjs'; + } + } + if (path.dirname(dir) === dir) return 'commonjs'; + } +} + +/** + * Node.js JavaScript config: base JS rules, Node globals, and `.js` files parsed as CommonJS or ESM depending on + * the package's `"type"`. `.cjs`/`.mjs` keep ESLint's own defaults. + * + * @param {{ cwd?: string }} [options] Directory whose package.json decides the module type; defaults to the working directory. + * @returns {import('eslint').Linter.Config[]} + */ +export function createNodeJsConfig({ cwd = process.cwd() } = {}) { + const sourceType = detectSourceType(cwd); + + return [ + ...baseConfig, + { + name: 'cospired/node-js', + // CommonJS wrappers (require, module, __dirname) are only defined in CommonJS packages. + languageOptions: { globals: sourceType === 'module' ? globals.nodeBuiltin : globals.node }, + }, + { name: 'cospired/node-js-source-type', files: ['**/*.js'], languageOptions: { sourceType } }, + ]; +} + +/** Node JavaScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node-js'`. */ +export default createNodeJsConfig(); diff --git a/packages/eslint-config-base/test/completeness.test.js b/packages/eslint-config-base/test/completeness.test.js index 036e237..e5419ac 100644 --- a/packages/eslint-config-base/test/completeness.test.js +++ b/packages/eslint-config-base/test/completeness.test.js @@ -5,6 +5,7 @@ import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; import { baseNamespaces, pluginRules, typescriptNamespaces } from '../../../test-utils/plugins.js'; import jsConfig from '../src/index.js'; import { createNodeConfig } from '../src/node.js'; +import nodeJs from '../src/node-js.js'; import { createTypescriptConfig } from '../src/typescript.js'; // A rule counts as configured when a recommended preset or one of our files sets it (on or off). @@ -12,6 +13,7 @@ const cases = [ ['base JS', jsConfig, baseNamespaces], ['base TypeScript', createTypescriptConfig(), typescriptNamespaces], ['node', createNodeConfig(), typescriptNamespaces], + ['node JS', nodeJs, baseNamespaces], ]; for (const [name, config, namespaces] of cases) { diff --git a/packages/eslint-config-base/test/fixtures/cjs-project/package.json b/packages/eslint-config-base/test/fixtures/cjs-project/package.json new file mode 100644 index 0000000..bd135cf --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/cjs-project/package.json @@ -0,0 +1 @@ +{ "name": "cjs-fixture", "private": true } diff --git a/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js b/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/packages/eslint-config-base/test/node-js.test.js b/packages/eslint-config-base/test/node-js.test.js new file mode 100644 index 0000000..6d24743 --- /dev/null +++ b/packages/eslint-config-base/test/node-js.test.js @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import nodeJsDefault, { createNodeJsConfig } from '../src/node-js.js'; + +const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures'); +const cjs = path.join(fixtures, 'cjs-project'); +const esm = path.join(fixtures, 'ts-project'); +const cjsCode = "const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"; +const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(createNodeJsConfig({ cwd }), code, { filePath: path.join(cwd, file), cwd })); + +test('CommonJS package (no "type"): require/module/process are defined', async () => { + const found = await ids(cjs, cjsCode); + assert.ok(!found.includes('no-undef'), found.join()); +}); + +test('ESM package ("type": "module"): CommonJS globals are not defined', async () => { + assert.ok((await ids(esm, cjsCode)).includes('no-undef')); +}); + +test('node globals exist either way', async () => { + const code = 'export const c = process.cwd();\n'; + assert.ok(!(await ids(esm, code)).includes('no-undef')); +}); + +test('default export is a ready-made config array with the global ignores', () => { + assert.ok(Array.isArray(nodeJsDefault)); + assert.ok(nodeJsDefault.some((item) => item.ignores?.includes('**/dist/**'))); +}); From 5d3753f6fa78b0f4d575039f59127bf1021d0073 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:27:27 +0800 Subject: [PATCH 19/33] chore: dogfood base config, peer floors + pack smoke scripts, package metadata, noUselessIndex off Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- eslint.config.js | 19 +++++-- package.json | 3 +- packages/eslint-config-base/package.json | 23 ++++++-- packages/eslint-config-base/src/index.js | 2 +- packages/eslint-config-base/src/node.js | 2 +- .../eslint-config-base/src/rules/eslint.js | 2 +- .../eslint-config-base/src/rules/import-x.js | 2 +- packages/eslint-config-base/src/typescript.js | 5 +- .../test/completeness.test.js | 2 +- .../eslint-config-base/test/decisions.test.js | 14 +++++ .../eslint-config-base/test/defaults.test.js | 8 ++- .../eslint-config-base/test/fixtures.test.js | 3 + packages/eslint-config-base/test/load.test.js | 1 + .../eslint-config-base/test/node-js.test.js | 2 + packages/eslint-config-base/test/node.test.js | 5 ++ .../test/rules-exist.test.js | 1 + .../test/typescript.test.js | 14 ++++- packages/eslint-config-react/package.json | 25 +++++--- packages/eslint-config-react/src/index.js | 2 +- .../eslint-config-react/src/typescript.js | 2 +- .../test/decisions.test.js | 2 + .../eslint-config-react/test/fixtures.test.js | 5 +- packages/eslint-config-react/test/helpers.js | 1 + .../eslint-config-react/test/load.test.js | 1 + .../test/rules-exist.test.js | 1 + scripts/peer-floors.js | 18 ++++++ scripts/rule-table.js | 1 + scripts/rule-table.test.js | 4 ++ scripts/smoke-pack.js | 57 +++++++++++++++++++ test-utils/completeness.test.js | 2 + test-utils/lint.js | 17 ++++-- test-utils/lint.test.js | 3 + 32 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 scripts/peer-floors.js create mode 100644 scripts/smoke-pack.js diff --git a/eslint.config.js b/eslint.config.js index 2e737f0..9b89e71 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,17 @@ -import js from '@eslint/js'; -import globals from 'globals'; +import nodeJs from './packages/eslint-config-base/src/node-js.js'; +// The repo lints itself with the config it ships. export default [ - { ignores: ['node_modules', '**/fixtures/**'] }, - js.configs.recommended, - { languageOptions: { globals: globals.node } }, + ...nodeJs, + { ignores: ['**/fixtures/**', '.superpowers/**'] }, + { + // Test helpers import the plugins they check; they are devDependencies of the repo. + files: ['test-utils/**'], + rules: { 'import-x/no-extraneous-dependencies': ['error', { devDependencies: true }] }, + }, + { + // Tests and scripts import the shared helpers through relative paths across package boundaries. + files: ['**/test/**', 'test-utils/**', 'scripts/**'], + rules: { 'import-x/no-relative-packages': 'off', 'no-await-in-loop': 'off' }, + }, ]; diff --git a/package.json b/package.json index 152a858..2f85015 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lint": "eslint .", "changeset": "changeset", "release": "changeset publish", - "rules": "node scripts/rule-table.js" + "rules": "node scripts/rule-table.js", + "smoke": "node scripts/smoke-pack.js" }, "repository": { "type": "git", diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index 8a9c070..f4d2676 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -10,7 +10,8 @@ "author": "Dirk Haage ", "repository": { "type": "git", - "url": "https://github.com/cospired/javascript" + "url": "git+https://github.com/cospired/javascript.git", + "directory": "packages/eslint-config-base" }, "keywords": [ "eslint", @@ -26,7 +27,8 @@ ".": "./src/index.js", "./typescript": "./src/typescript.js", "./node": "./src/node.js", - "./node-js": "./src/node-js.js" + "./node-js": "./src/node-js.js", + "./package.json": "./package.json" }, "scripts": { "test": "node --test 'test/*.test.js'" @@ -40,8 +42,17 @@ "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.18.1", "eslint-plugin-todo-plz": "^1.4.1", - "globals": "^17.0.0", - "typescript": ">=5.4 <6.1", - "typescript-eslint": "^8.57.0" - } + "globals": "^17.14.0", + "typescript": ">=5.4.2 <6.1", + "typescript-eslint": "^8.72.2" + }, + "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-base#readme", + "bugs": { + "url": "https://github.com/cospired/javascript/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "sideEffects": false } diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index dc79dfa..48d74e8 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -3,8 +3,8 @@ import globals from 'globals'; import { anyTestGlobs, withMonorepoTwins } from './globs.js'; import { globalIgnores } from './ignores.js'; -import eslintCore from './rules/eslint.js'; import eslintComments from './rules/eslint-comments.js'; +import eslintCore from './rules/eslint.js'; import { createImportsConfig } from './rules/import-x.js'; import stylistic from './rules/stylistic.js'; import todoPlz from './rules/todo-plz.js'; diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 7eb951f..9f749bd 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,9 +1,9 @@ import globals from 'globals'; import { jsFiles, untypedGlobs } from './globs.js'; +import { createTypescriptConfig } from './typescript.js'; export { globalIgnores } from './ignores.js'; -import { createTypescriptConfig } from './typescript.js'; /** diff --git a/packages/eslint-config-base/src/rules/eslint.js b/packages/eslint-config-base/src/rules/eslint.js index feb5ee9..855addb 100644 --- a/packages/eslint-config-base/src/rules/eslint.js +++ b/packages/eslint-config-base/src/rules/eslint.js @@ -98,7 +98,7 @@ export default [ 'require-await': 'error', yoda: 'error', - // --- off: opinion, thresholds, or covered elsewhere (import-x, TypeScript, Prettier, todo-plz) + // --- off: opinion, thresholds, or covered elsewhere (import-x, TypeScript, Prettier, the ticket-ref plugin) 'block-scoped-var': 'off', 'capitalized-comments': 'off', 'class-methods-use-this': 'off', diff --git a/packages/eslint-config-base/src/rules/import-x.js b/packages/eslint-config-base/src/rules/import-x.js index 1e6419f..eea9836 100644 --- a/packages/eslint-config-base/src/rules/import-x.js +++ b/packages/eslint-config-base/src/rules/import-x.js @@ -43,7 +43,7 @@ export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyG 'import-x/no-named-default': 'error', 'import-x/no-relative-packages': 'error', 'import-x/no-self-import': 'error', - 'import-x/no-useless-path-segments': ['error', { noUselessIndex: true }], + 'import-x/no-useless-path-segments': 'error', 'import-x/no-webpack-loader-syntax': 'error', 'import-x/order': [ 'error', diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-base/src/typescript.js index 1f254bd..6f7c6c1 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-base/src/typescript.js @@ -1,12 +1,13 @@ import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; -import { globalIgnores } from './ignores.js'; -import { createBaseConfig, prettierLast } from './index.js'; import { jsFiles, testGlobs, untypedGlobs } from './globs.js'; +import { globalIgnores } from './ignores.js'; import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x.js'; import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; +import { createBaseConfig, prettierLast } from './index.js'; + export { globalIgnores }; /** diff --git a/packages/eslint-config-base/test/completeness.test.js b/packages/eslint-config-base/test/completeness.test.js index e5419ac..3ce1536 100644 --- a/packages/eslint-config-base/test/completeness.test.js +++ b/packages/eslint-config-base/test/completeness.test.js @@ -4,8 +4,8 @@ import test from 'node:test'; import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; import { baseNamespaces, pluginRules, typescriptNamespaces } from '../../../test-utils/plugins.js'; import jsConfig from '../src/index.js'; -import { createNodeConfig } from '../src/node.js'; import nodeJs from '../src/node-js.js'; +import { createNodeConfig } from '../src/node.js'; import { createTypescriptConfig } from '../src/typescript.js'; // A rule counts as configured when a recommended preset or one of our files sets it (on or off). diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index 60e5f6a..bc8b846 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -14,6 +14,7 @@ const tsConfig = createTypescriptConfig(tsOptions); const lint = async (code, file = 'src/a.js', cfg = config) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); // [rule, code that must be flagged, code that must pass] +/* eslint-disable no-template-curly-in-string -- fixture code is written as plain strings */ const cases = [ ['eqeqeq', 'export const a = (x, y) => x == y;\n', 'export const a = (x) => x == null;\n'], ['no-param-reassign', 'export function f(a) {\n a = 1;\n\n return a;\n}\n', 'export function f(a) {\n a.x = 1;\n}\n'], @@ -30,6 +31,7 @@ const cases = [ ['no-unused-vars', 'export function f() {\n const x = 1;\n}\n', 'export function f() {\n const _x = 1;\n}\n'], ['no-console', 'console.log(1);\n', 'console.warn(1);\nconsole.error(2);\n'], ]; +/* eslint-enable no-template-curly-in-string -- end of fixture table */ for (const [rule, bad, good] of cases) { test(`${rule}: flagged / accepted`, async () => { @@ -40,6 +42,7 @@ for (const [rule, bad, good] of cases) { test('no-plusplus is not enabled; hapi plugin is gone', () => { const rules = loadedRules(config); + assert.ok(!rules.has('no-plusplus')); assert.deepEqual([...rules].filter((id) => id.startsWith('@hapi/')), []); }); @@ -50,6 +53,7 @@ test('functions need no blank line at the start of the body (Prettier-safe)', as test('import-x named-as-default rules are off', () => { const rules = loadedRules(config); + assert.ok(!rules.has('import-x/no-named-as-default') && !rules.has('import-x/no-named-as-default-member')); }); @@ -61,6 +65,7 @@ test('no rule resolves to "warn" in any base config', async () => { test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts'), root); + for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); }); @@ -68,6 +73,7 @@ test('TS config uses the typescript-eslint variants and drops consistent-return' test('scripts/ files get node globals in every base config', async () => { for (const [name, cfg] of [['js', config], ['ts', tsConfig]]) { const found = await lint('process.exit(1);\n', 'scripts/run.mjs', cfg); + assert.ok(!found.includes('no-undef'), `${name}: process should be defined in scripts/`); } assert.ok((await lint('process.exit(1);\n', 'src/run.mjs', tsConfig)).includes('no-undef')); @@ -75,6 +81,7 @@ test('scripts/ files get node globals in every base config', async () => { test('test-file globs also match from a monorepo root config', async () => { const { testGlobs } = await import('../src/globs.js'); + for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts}']) { assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); } @@ -83,6 +90,13 @@ test('test-file globs also match from a monorepo root config', async () => { test('console is allowed in scripts of every extension', async () => { for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { const found = await lint('console.log(1);\n', `scripts/x.${ext}`, createTypescriptConfig(tsOptions)); + assert.ok(!found.includes('no-console'), ext); } }); + +test('explicit index.js imports are not "useless" (ESM has no directory imports)', async () => { + const found = await lint("import { a } from './index.js';\n\nexport const b = a;\n", 'src/b.js'); + + assert.ok(!found.includes('import-x/no-useless-path-segments')); +}); diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index 50f5938..bbeecb4 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -9,8 +9,8 @@ import nodeDefault from '../src/node.js'; import tsDefault, { createTypescriptConfig } from '../src/typescript.js'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); -const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); +const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; test('default exports are ready-made config arrays that include the global ignores', () => { for (const cfg of [jsDefault, tsDefault, nodeDefault]) { @@ -23,6 +23,7 @@ test('zero options: TS files lint via the project service, no parse errors', asy for (const cfg of [tsDefault, nodeDefault]) { for (const file of ['src/a.ts', 'src/b.ts']) { const messages = await lint(cfg, 'export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); } } @@ -32,6 +33,7 @@ test('zero options: test and config files outside every tsconfig lint without ty for (const file of ['other/c.test.ts', 'vitest.config.ts', 'vitest.shared.ts']) { for (const cfg of [tsDefault, nodeDefault]) { const messages = await lint(cfg, 'export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); } } @@ -40,18 +42,22 @@ test('zero options: test and config files outside every tsconfig lint without ty test('typedTests: true lints tests with type information (they must be in a tsconfig)', async () => { const typed = createTypescriptConfig({ typedTests: true, tsconfigRootDir: root }); const outside = await lint(typed, 'export const v = 1;\n', 'other/c.test.ts'); + assert.ok(outside.some((m) => m.fatal), 'a test outside every tsconfig reports a parsing error'); const inside = await lint(typed, 'export const v = 1;\n', 'src/a.ts'); + assert.deepEqual(inside.filter((m) => m.fatal), []); }); test('zero options: dist output is ignored', async () => { const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); + assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); }); test('import-x/first is off in tests (vi.mock hoisting) and on in source', async () => { const code = "vi.mock('./a.js');\n\nimport { a } from './a.js';\n\nexport const v = a;\n"; + assert.ok(!(await ruleIdsFor(tsDefault, code, 'other/c.test.ts')).includes('import-x/first')); assert.ok((await ruleIdsFor(tsDefault, code, 'src/b.ts')).includes('import-x/first')); }); diff --git a/packages/eslint-config-base/test/fixtures.test.js b/packages/eslint-config-base/test/fixtures.test.js index 2561b8a..2191f6c 100644 --- a/packages/eslint-config-base/test/fixtures.test.js +++ b/packages/eslint-config-base/test/fixtures.test.js @@ -22,17 +22,20 @@ test('alphabetizes imports', async () => { test('devDependency imports: rejected in src, allowed in *.test.js', async () => { const code = "import { test } from 'eslint';\n\nexport default test;\n"; + assert.ok((await lint(code, 'src/a.js')).includes('import-x/no-extraneous-dependencies')); assert.ok(!(await lint(code, 'src/a.test.js')).includes('import-x/no-extraneous-dependencies')); }); test('blank line before return is required', async () => { const code = 'export function f(a) {\n const b = a + 1;\n return b;\n}\n'; + assert.ok((await lint(code)).includes('@stylistic/padding-line-between-statements')); assert.ok(!(await lint('export function f(a) {\n const b = a + 1;\n\n return b;\n}\n')).includes('@stylistic/padding-line-between-statements')); }); test('no-var and prefer-const are flagged', async () => { const ids = await lint('var a = 1;\nlet b = 2;\n\nexport { a, b };\n'); + assert.ok(ids.includes('no-var') && ids.includes('prefer-const')); }); diff --git a/packages/eslint-config-base/test/load.test.js b/packages/eslint-config-base/test/load.test.js index 9244c62..d50eef1 100644 --- a/packages/eslint-config-base/test/load.test.js +++ b/packages/eslint-config-base/test/load.test.js @@ -6,5 +6,6 @@ import config from '../src/index.js'; test('base JS config lints a trivial file without errors', async () => { const messages = await lintText(config, 'const a = 1;\n\nexport default a;\n', { filePath: 'a.js' }); + assert.deepEqual(messages, []); }); diff --git a/packages/eslint-config-base/test/node-js.test.js b/packages/eslint-config-base/test/node-js.test.js index 6d24743..665126b 100644 --- a/packages/eslint-config-base/test/node-js.test.js +++ b/packages/eslint-config-base/test/node-js.test.js @@ -14,6 +14,7 @@ const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(creat test('CommonJS package (no "type"): require/module/process are defined', async () => { const found = await ids(cjs, cjsCode); + assert.ok(!found.includes('no-undef'), found.join()); }); @@ -23,6 +24,7 @@ test('ESM package ("type": "module"): CommonJS globals are not defined', async ( test('node globals exist either way', async () => { const code = 'export const c = process.cwd();\n'; + assert.ok(!(await ids(esm, code)).includes('no-undef')); }); diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js index ad240b2..dd284dd 100644 --- a/packages/eslint-config-base/test/node.test.js +++ b/packages/eslint-config-base/test/node.test.js @@ -12,6 +12,7 @@ const options = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRoot const node = createNodeConfig(options); const ids = async (cfg, code, file) => { const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); return ruleIds(messages); @@ -19,6 +20,7 @@ const ids = async (cfg, code, file) => { test('node globals are defined (process is not no-undef)', async () => { const code = 'process.exit(1);\n'; + assert.ok((await ids(createTypescriptConfig(options), code, 'src/x.mjs')).includes('no-undef')); assert.ok(!(await ids(node, code, 'src/x.mjs')).includes('no-undef')); }); @@ -27,12 +29,14 @@ test('string || fallback is allowed, number || fallback is flagged', async () => const rule = '@typescript-eslint/prefer-nullish-coalescing'; const str = "export function f(s: string | undefined): string {\n return s || 'x';\n}\n"; const num = 'export function f(n: number | undefined): number {\n return n || 1;\n}\n'; + assert.ok(!(await ids(node, str, 'src/a.ts')).includes(rule)); assert.ok((await ids(node, num, 'src/a.ts')).includes(rule)); }); test('no-console is allowed in scripts/ but not in src/', async () => { const code = 'console.log(1);\n'; + assert.ok(!(await ids(node, code, 'scripts/run.mjs')).includes('no-console')); assert.ok((await ids(node, code, 'src/run.mjs')).includes('no-console')); }); @@ -40,6 +44,7 @@ test('no-console is allowed in scripts/ but not in src/', async () => { test('JS files matched by `files` are linted without type-aware rules (no crash)', async () => { const cfg = createNodeConfig({ ...options, files: ['src/**/*.{ts,js}', '*.{ts,js}'] }); const found = await ids(cfg, 'export const a = (x) => x || 1;\n', 'src/c.js'); + assert.ok(!found.includes('@typescript-eslint/prefer-nullish-coalescing')); assert.ok(!found.includes('@typescript-eslint/explicit-function-return-type')); }); diff --git a/packages/eslint-config-base/test/rules-exist.test.js b/packages/eslint-config-base/test/rules-exist.test.js index 04263fe..4fdc671 100644 --- a/packages/eslint-config-base/test/rules-exist.test.js +++ b/packages/eslint-config-base/test/rules-exist.test.js @@ -10,5 +10,6 @@ test('every configured rule exists', () => { test('base contains no react rules', () => { const react = [...loadedRules(config)].filter((id) => id.startsWith('react/') || id.startsWith('react-hooks/')); + assert.deepEqual(react, []); }); diff --git a/packages/eslint-config-base/test/typescript.test.js b/packages/eslint-config-base/test/typescript.test.js index dc31e16..bc9be4e 100644 --- a/packages/eslint-config-base/test/typescript.test.js +++ b/packages/eslint-config-base/test/typescript.test.js @@ -10,9 +10,10 @@ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', const abs = (file) => path.join(root, file); const options = { tsconfigPath: [abs('tsconfig.json'), abs('tsconfig.test.json')], tsconfigRootDir: root }; const config = createTypescriptConfig(options); -const lint = async (code, file = 'src/a.ts', cfg = config) => lintText(cfg, code, { filePath: abs(file), cwd: root }); +const lint = (code, file = 'src/a.ts', cfg = config) => lintText(cfg, code, { filePath: abs(file), cwd: root }); const ids = async (...args) => { const messages = await lint(...args); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); return ruleIds(messages); @@ -20,6 +21,7 @@ const ids = async (...args) => { test('explicit-function-return-type: required in src, not in *.test.ts; expressions allowed', async () => { const rule = '@typescript-eslint/explicit-function-return-type'; + assert.ok((await ids('export function f() {\n return 1;\n}\n')).includes(rule)); assert.ok(!(await ids('export function f() {\n return 1;\n}\n', 'test/a.test.ts')).includes(rule)); assert.ok(!(await ids('export const n = [1].map((v) => v + 1);\n')).includes(rule)); @@ -27,6 +29,7 @@ test('explicit-function-return-type: required in src, not in *.test.ts; expressi test('no-misused-promises flags an async callback used as a condition', async () => { const code = 'const p = async (): Promise => true;\n\nif (p()) {\n console.info(1);\n}\n'; + assert.ok((await ids(code)).includes('@typescript-eslint/no-misused-promises')); }); @@ -37,23 +40,27 @@ test('no-generated-empty-object-type is an error for TS files', async () => { test('accepts an array of tsconfigs: src and test files both lint without parse errors', async () => { for (const file of ['src/a.ts', 'test/a.test.ts']) { const messages = await lint('export const v = 1;\n', file); + assert.deepEqual(messages.filter((m) => m.fatal), [], file); } }); test('a file outside every tsconfig reports a parsing error and does not throw', async () => { const messages = await lint('export const v = 1;\n', 'other/b.ts'); + assert.ok(messages.some((m) => m.fatal && /parsing error/i.test(m.message))); }); test('.mjs config files are linted without type-aware errors', async () => { const messages = await lint('export default {};\n', 'eslint.config.mjs'); + assert.deepEqual(messages.filter((m) => m.fatal), []); }); test('#alias imports resolve as internal, before relative imports', async () => { const code = "import { x } from '#shared/x';\n\nimport { y } from './a.js';\n\nexport const z = x + y;\n"; const found = await ids(code, 'src/b.ts'); + assert.ok(!found.includes('import-x/no-unresolved')); assert.ok(!found.includes('import-x/order')); }); @@ -61,14 +68,17 @@ test('#alias imports resolve as internal, before relative imports', async () => test('importOrder.pathGroups moves an alias into another group', async () => { const code = "import { x } from '#shared/x';\nimport { ESLint } from 'eslint';\n\nexport const z = [x, ESLint];\n"; const misordered = async (cfg) => (await lint(code, 'src/b.ts', cfg)).some((m) => /should occur before/.test(m.message)); + assert.ok(await misordered(config)); const grouped = createTypescriptConfig({ ...options, importOrder: { pathGroups: [{ pattern: '#shared/**', group: 'external', position: 'before' }] } }); + assert.ok(!(await misordered(grouped))); }); test('two calls return independent arrays', () => { const a = createTypescriptConfig(options); const b = createTypescriptConfig({ ...options, files: ['x/**/*.ts'] }); + assert.notEqual(a, b); assert.notDeepEqual(a, b); a.length = 0; @@ -77,7 +87,9 @@ test('two calls return independent arrays', () => { test('every rule exists and JS configs hold no TypeScript rules', async () => { const { missingRules, loadedRules } = await import('../../../test-utils/lint.js'); + assert.deepEqual(missingRules(config), []); const { default: js } = await import('../src/index.js'); + assert.deepEqual([...loadedRules(js)].filter((id) => id.startsWith('@typescript-eslint/')), []); }); diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index d368361..7dc91c5 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -10,7 +10,8 @@ "author": "Dirk Haage ", "repository": { "type": "git", - "url": "https://github.com/cospired/javascript" + "url": "git+https://github.com/cospired/javascript.git", + "directory": "packages/eslint-config-react" }, "keywords": [ "eslint", @@ -24,20 +25,30 @@ }, "exports": { ".": "./src/index.js", - "./typescript": "./src/typescript.js" + "./typescript": "./src/typescript.js", + "./package.json": "./package.json" }, "scripts": { "test": "node --test 'test/*.test.js'" }, "dependencies": { - "@cospired/eslint-config-base": "*" + "@cospired/eslint-config-base": "^7.0.2" }, "peerDependencies": { "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-formatjs": "^8.1.4", - "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-react-refresh": "^0.5.0", - "globals": "^17.0.0" - } + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "globals": "^17.14.0" + }, + "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-react#readme", + "bugs": { + "url": "https://github.com/cospired/javascript/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "sideEffects": false } diff --git a/packages/eslint-config-react/src/index.js b/packages/eslint-config-react/src/index.js index 22f2384..c8f86e6 100644 --- a/packages/eslint-config-react/src/index.js +++ b/packages/eslint-config-react/src/index.js @@ -1,9 +1,9 @@ import baseConfig, { globalIgnores, prettierLast } from '@cospired/eslint-config-base'; import formatjs from './rules/formatjs.js'; -import react from './rules/react.js'; import reactHooks from './rules/react-hooks.js'; import reactRefresh from './rules/react-refresh.js'; +import react from './rules/react.js'; export { globalIgnores }; diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js index e76dd71..39cef64 100644 --- a/packages/eslint-config-react/src/typescript.js +++ b/packages/eslint-config-react/src/typescript.js @@ -2,9 +2,9 @@ import { prettierLast } from '@cospired/eslint-config-base'; import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; import formatjs from './rules/formatjs.js'; -import react from './rules/react.js'; import reactHooks from './rules/react-hooks.js'; import reactRefresh from './rules/react-refresh.js'; +import react from './rules/react.js'; export { globalIgnores }; diff --git a/packages/eslint-config-react/test/decisions.test.js b/packages/eslint-config-react/test/decisions.test.js index 0cf435d..38ae37a 100644 --- a/packages/eslint-config-react/test/decisions.test.js +++ b/packages/eslint-config-react/test/decisions.test.js @@ -19,6 +19,7 @@ test('hooks exhaustive-deps, refresh and formatjs literal-string rules are error test('no-unused-expressions is strict in React code (no short-circuit statements)', async () => { const code = 'export function f(a, g) {\n a && g();\n}\n'; + assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('no-unused-expressions')); }); @@ -29,6 +30,7 @@ test('formatjs: enforce-description is off, no-camel-case is an error', async () test('formatjs exempts the same test files as the base config', async () => { const sev = (file) => ruleSeverity(jsConfig, 'formatjs/enforce-default-message', path.join(root, file), root); + assert.equal(await sev('src/a.jsx'), 2); for (const file of ['src/a.test.jsx', 'test/helpers.jsx', 'src/__tests__/a.jsx']) assert.notEqual(await sev(file), 2, file); }); diff --git a/packages/eslint-config-react/test/fixtures.test.js b/packages/eslint-config-react/test/fixtures.test.js index f6895ec..d219158 100644 --- a/packages/eslint-config-react/test/fixtures.test.js +++ b/packages/eslint-config-react/test/fixtures.test.js @@ -11,7 +11,8 @@ test('hooks rule flags a conditional hook call', async () => { }); test('formatjs flags a missing defaultMessage in src, not in tests', async () => { - const typed = 'declare const intl: { formatMessage: (d: { id: string }) => string };\n\n' + message; + const typed = `declare const intl: { formatMessage: (d: { id: string }) => string };\n\n${ message}`; + assert.ok((await ids(tsConfig, typed, 'src/a.tsx')).includes('formatjs/enforce-default-message')); assert.ok(!(await ids(tsConfig, typed, 'src/a.test.tsx')).includes('formatjs/enforce-default-message')); }); @@ -22,6 +23,7 @@ test('browser globals are defined', async () => { test('react-refresh flags a non-component export next to a component', async () => { const code = 'export function A() {\n return
;\n}\n\nexport function helper() {\n return 1;\n}\n'; + assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('react-refresh/only-export-components')); }); @@ -29,6 +31,7 @@ test('two calls to createReactTypescriptConfig return independent arrays', async const { createReactTypescriptConfig } = await import('../src/typescript.js'); const a = createReactTypescriptConfig({ tsconfigRootDir: '/a' }); const b = createReactTypescriptConfig({ tsconfigRootDir: '/b' }); + assert.notEqual(a, b); a.length = 0; assert.ok(b.length > 0); diff --git a/packages/eslint-config-react/test/helpers.js b/packages/eslint-config-react/test/helpers.js index 149d70a..3a4fdf7 100644 --- a/packages/eslint-config-react/test/helpers.js +++ b/packages/eslint-config-react/test/helpers.js @@ -13,6 +13,7 @@ export { jsConfig }; /** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ export async function ids(cfg, code, file) { const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); return ruleIds(messages); diff --git a/packages/eslint-config-react/test/load.test.js b/packages/eslint-config-react/test/load.test.js index f8260d7..b617182 100644 --- a/packages/eslint-config-react/test/load.test.js +++ b/packages/eslint-config-react/test/load.test.js @@ -9,5 +9,6 @@ test('JSX parses in .jsx under the JS config', async () => { test('JSX parses in .tsx under the TS config', async () => { const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/a.tsx'); + assert.ok(Array.isArray(found)); }); diff --git a/packages/eslint-config-react/test/rules-exist.test.js b/packages/eslint-config-react/test/rules-exist.test.js index 47b5e10..0c7da21 100644 --- a/packages/eslint-config-react/test/rules-exist.test.js +++ b/packages/eslint-config-react/test/rules-exist.test.js @@ -17,6 +17,7 @@ test('React JS config contains no @typescript-eslint rules', () => { test('React configs enable hooks, refresh and formatjs rules', () => { for (const cfg of [jsConfig, tsConfig]) { const rules = loadedRules(cfg); + for (const id of ['react-hooks/rules-of-hooks', 'react-refresh/only-export-components', 'formatjs/enforce-default-message']) assert.ok(rules.has(id), id); } }); diff --git a/scripts/peer-floors.js b/scripts/peer-floors.js new file mode 100644 index 0000000..c7fa762 --- /dev/null +++ b/scripts/peer-floors.js @@ -0,0 +1,18 @@ +// CI only: rewrites the root devDependencies to the lowest version each package's peerDependencies allow and +// reinstalls, so the test run proves the floors are real. Leaves package.json modified; run on a throwaway checkout. +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; + +const floor = (range) => range.match(/\d+(\.\d+){0,2}/)[0].split('.').concat(['0', '0']).slice(0, 3).join('.'); +const peers = {}; + +for (const dir of fs.readdirSync('packages')) { + const { peerDependencies = {} } = JSON.parse(fs.readFileSync(`packages/${dir}/package.json`, 'utf8')); + + Object.assign(peers, peerDependencies); +} +const sets = Object.entries(peers).map(([name, range]) => `devDependencies.${name}=${floor(range)}`); + +console.log('peer floors:', sets.join(' ')); +execFileSync('npm', ['pkg', 'set', ...sets], { stdio: 'inherit' }); +execFileSync('npm', ['install', '--no-package-lock', '--no-audit', '--no-fund'], { stdio: 'inherit' }); diff --git a/scripts/rule-table.js b/scripts/rule-table.js index ea73b6d..563237d 100644 --- a/scripts/rule-table.js +++ b/scripts/rule-table.js @@ -92,6 +92,7 @@ async function main() { renderTable('React JS (vs. Base JS)', ...table(reactJs, baseJs)), renderTable('React TypeScript (vs. Base TypeScript)', ...table(reactTsRules, baseTsRules)), ].join('\n'); + fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); } diff --git a/scripts/rule-table.test.js b/scripts/rule-table.test.js index 532d694..ce7e52f 100644 --- a/scripts/rule-table.test.js +++ b/scripts/rule-table.test.js @@ -11,21 +11,25 @@ test('buildRuleTable lists only rules where ours differs from the baseline', () [{ rules: { 'no-console': 'warn', eqeqeq: 'error', 'no-var': 'error' } }], sources, ); + assert.deepEqual(table, [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); }); test('buildRuleTable includes rules the baseline does not set, marked source "new" when unknown', () => { const table = buildRuleTable([{ rules: { 'prefer-const': ['error', { destructuring: 'all' }] } }], [{ rules: {} }], sources); + assert.deepEqual(table, [{ rule: 'prefer-const', ours: 'error {"destructuring":"all"}', recommended: '-', source: 'new' }]); }); test('renderTable renders a markdown table with a Decision column', () => { const md = renderTable('Base JS', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); + assert.match(md, /^## Base JS/m); assert.match(md, /\| `no-console` \| error \| warn \| eventured \| open \|/); }); test('renderTable fills the Decision column from the decisions map', () => { const md = renderTable('X', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }], { 'no-console': 'error, allow warn/error' }); + assert.match(md, /\| `no-console` \| error \| warn \| eventured \| error, allow warn\/error \|/); }); diff --git a/scripts/smoke-pack.js b/scripts/smoke-pack.js new file mode 100644 index 0000000..45bfb5c --- /dev/null +++ b/scripts/smoke-pack.js @@ -0,0 +1,57 @@ +// Packs both packages, installs the tarballs into a clean project and lints with one-line configs. +// Proves what gets published works: exports, `files`, peer dependencies, zero-option defaults. +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const root = path.resolve(import.meta.dirname, '..'); +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cospired-smoke-')); +const run = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'inherit'], encoding: 'utf8' }); + +const tarballs = ['eslint-config-base', 'eslint-config-react'].map((dir) => { + const [{ filename }] = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', tmp], path.join(root, 'packages', dir))); + + return path.join(tmp, filename); +}); + +const peers = {}; + +for (const dir of ['eslint-config-base', 'eslint-config-react']) { + Object.assign(peers, JSON.parse(fs.readFileSync(path.join(root, 'packages', dir, 'package.json'), 'utf8')).peerDependencies); +} +const project = path.join(tmp, 'project'); + +fs.mkdirSync(project); +fs.writeFileSync(path.join(project, 'package.json'), JSON.stringify({ name: 'smoke', private: true, type: 'module', dependencies: peers })); +run('npm', ['install', '--no-audit', '--no-fund', ...tarballs], project); + +const clean = 'export function add(a: number, b: number): number {\n return a + b;\n}\n'; +const cases = [ + ['js', '@cospired/eslint-config-base', 'src/index.js', 'export const answer = 42;\n'], + ['node-js', '@cospired/eslint-config-base/node-js', 'src/index.js', "import { cwd } from 'node:process';\n\nexport const here = cwd();\n"], + ['typescript', '@cospired/eslint-config-base/typescript', 'src/index.ts', clean], + ['node', '@cospired/eslint-config-base/node', 'src/index.ts', clean], + ['react', '@cospired/eslint-config-react', 'src/index.jsx', 'export const App = () =>
;\n'], + ['react-typescript', '@cospired/eslint-config-react/typescript', 'src/index.tsx', 'export const App = (): null => null;\n'], +]; +let failed = false; + +for (const [name, entry, file, code] of cases) { + const dir = path.join(project, name); + + fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'eslint.config.js'), `export { default } from '${entry}';\n`); + fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', jsx: 'react-jsx', strict: true, noEmit: true, skipLibCheck: true }, include: ['src'] })); + fs.writeFileSync(path.join(dir, file), code); + try { + run(path.join(project, 'node_modules/.bin/eslint'), ['.', '--max-warnings', '0'], dir); + console.log(`ok ${name}`); + } catch (error) { + failed = true; + console.log(`FAIL ${name}\n${error.stdout ?? ''}`); + } +} + +fs.rmSync(tmp, { recursive: true, force: true }); +process.exit(failed ? 1 : 0); diff --git a/test-utils/completeness.test.js b/test-utils/completeness.test.js index 2c2668f..d1cf2dd 100644 --- a/test-utils/completeness.test.js +++ b/test-utils/completeness.test.js @@ -7,6 +7,7 @@ const plugins = { '': { a: {}, b: {}, gone: { meta: { deprecated: true } } }, p: test('unconfiguredRules lists non-deprecated rules no config item sets (on or off)', () => { const config = [{ rules: { a: 'error', 'p/x': 'off' } }]; + assert.deepEqual(unconfiguredRules(config, ['', 'p'], plugins), ['b', 'p/y']); }); @@ -19,5 +20,6 @@ test('redundantRules flags a named config item that repeats the value already in { rules: { a: ['error', { x: 1 }], b: 'warn' } }, { name: 'cospired/test', rules: { a: [2, { x: 1 }], b: 'error', c: 'off' } }, ]; + assert.deepEqual(redundantRules(config), ['a']); }); diff --git a/test-utils/lint.js b/test-utils/lint.js index 3b4ca0a..a6635dd 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -9,14 +9,15 @@ const here = path.dirname(fileURLToPath(import.meta.url)); export const fixtureProject = path.join(here, 'fixtures', 'ts-project'); /** Lint `code` with a flat config array; returns the flattened messages. */ -export async function lintText(config, code, { filePath = 'src/sample.ts', cwd } = {}) { - filePath = path.resolve(fixtureProject, filePath); +export async function lintText(config, code, { filePath: file = 'src/sample.ts', cwd } = {}) { + const filePath = path.resolve(fixtureProject, file); const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: cwd ?? path.dirname(filePath), }); const [result] = await eslint.lintText(code, { filePath }); + return result.messages; } @@ -24,18 +25,21 @@ export const ruleIds = (messages) => [...new Set(messages.map((m) => m.ruleId).f const isOn = (entry) => { const level = Array.isArray(entry) ? entry[0] : entry; + return level !== 'off' && level !== 0; }; /** Every rule id in a config array whose severity is not off. */ export function loadedRules(config) { const rules = new Set(); + for (const item of config) { for (const [id, entry] of Object.entries(item.rules ?? {})) { if (isOn(entry)) rules.add(id); else rules.delete(id); } } + return rules; } @@ -46,6 +50,7 @@ export function missingRules(config) { return [...ids].filter((id) => { const slash = id.lastIndexOf('/'); + if (slash === -1) return !builtinRules.has(id); const plugin = plugins[id.slice(0, slash)]; @@ -54,8 +59,8 @@ export function missingRules(config) { } /** The severity (0|1|2) a rule has for `filePath` once all config objects are merged. */ -export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts', cwd) { - filePath = path.resolve(fixtureProject, filePath); +export async function ruleSeverity(config, ruleId, file, cwd) { + const filePath = path.resolve(fixtureProject, file ?? 'src/sample.ts'); const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: cwd ?? path.dirname(filePath) }); const entry = (await eslint.calculateConfigForFile(filePath)).rules?.[ruleId]; @@ -63,8 +68,8 @@ export async function ruleSeverity(config, ruleId, filePath = 'src/sample.ts', c } /** Rule ids that resolve to severity "warn" for `filePath`. The shipped configs must have none. */ -export async function warnRules(config, filePath = 'src/sample.ts', cwd) { - filePath = path.resolve(fixtureProject, filePath); +export async function warnRules(config, file, cwd) { + const filePath = path.resolve(fixtureProject, file ?? 'src/sample.ts'); const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: cwd ?? path.dirname(filePath) }); const { rules = {} } = await eslint.calculateConfigForFile(filePath); diff --git a/test-utils/lint.test.js b/test-utils/lint.test.js index e6dab98..9d33db7 100644 --- a/test-utils/lint.test.js +++ b/test-utils/lint.test.js @@ -9,6 +9,7 @@ test('lintText reports no-debugger for "debugger;"', async () => { 'debugger;', { filePath: 'x.js' }, ); + assert.deepEqual(ruleIds(messages), ['no-debugger']); }); @@ -17,6 +18,7 @@ test('loadedRules ignores off rules', () => { { rules: { 'no-debugger': 'error', 'no-var': 'off', eqeqeq: ['warn'] } }, { rules: { 'no-console': 0 } }, ]); + assert.deepEqual([...rules].sort(), ['eqeqeq', 'no-debugger']); }); @@ -24,5 +26,6 @@ test('missingRules reports ids whose plugin or rule does not exist', () => { const config = [ { plugins: { p: { rules: { real: {} } } }, rules: { 'p/real': 'error', 'p/fake': 'error', 'q/x': 'error', 'no-debugger': 'error', 'no-nope': 'error', 'gone/off': 'off', 'gone/zero': [0] } }, ]; + assert.deepEqual(missingRules(config).sort(), ['no-nope', 'p/fake', 'q/x']); }); From c8146a1a59aa81adfbcc08978041d5f7451ba11c Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:28:46 +0800 Subject: [PATCH 20/33] docs, CI matrix + peer floors + smoke, release provenance, rule table completeness Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 39 ++- .github/workflows/release.yml | 16 +- MIGRATION.md | 6 +- README.md | 8 +- docs/rules.md | 306 +++++++++++++++++++--- package.json | 5 +- packages/eslint-config-base/README.md | 56 ++-- packages/eslint-config-base/package.json | 2 +- packages/eslint-config-react/README.md | 19 +- packages/eslint-config-react/package.json | 2 +- scripts/rule-table.js | 42 ++- scripts/rule-table.test.js | 17 +- 12 files changed, 425 insertions(+), 93 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24d9f28..1380ce2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,52 @@ name: CI + on: push: - branches: [main] pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: test: + name: test (node ${{ matrix.node }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [20.19.0, 22.13.0, 24] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: ${{ matrix.node }} cache: npm - run: npm ci - run: npm test - run: npm run lint + + peer-floors: + name: peer dependency floors + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: node scripts/peer-floors.js + - run: npm test + + smoke: + name: packed tarballs work in a clean project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run smoke diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d9acf0..74928da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,18 @@ name: Release + on: push: branches: [main] + +concurrency: + group: release + cancel-in-progress: false + permissions: contents: write pull-requests: write - id-token: write + id-token: write # npm provenance + jobs: release: runs-on: ubuntu-latest @@ -17,10 +24,15 @@ jobs: cache: npm registry-url: https://registry.npmjs.org - run: npm ci + - run: npm test + - run: npm run lint - uses: changesets/action@v1 with: + version: npm run version publish: npm run release + title: 'chore: version packages' + commit: 'chore: version packages' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: 'true' diff --git a/MIGRATION.md b/MIGRATION.md index a79302a..dbc628d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -5,9 +5,11 @@ - **Flat config only, ESLint 10.** `.eslintrc` is gone; use `eslint.config.js` (see the package READMEs). - **No airbnb.** `eslint-config-airbnb(-base)` and `airbnb-typescript` are no longer used or needed. The airbnb rules we kept are listed in [docs/rules.md](docs/rules.md); the rest are dropped. New rule set: ESLint and typescript-eslint recommended plus the rules in that table. - **No hapi plugin.** `@hapi/scope-start` (a blank line at the start of function bodies) cannot coexist with Prettier and is dropped. `@hapi/no-arrowception` is dropped. Blank lines before `return` and after declarations are enforced via `@stylistic/padding-line-between-statements`. -- **TypeScript is a first-class entry:** `@cospired/eslint-config-base/typescript` (and `/node`, `@cospired/eslint-config-react/typescript`). The unpublished `@cospired/eslint-config-typescript-base` package is gone. +- **TypeScript is a first-class entry:** `@cospired/eslint-config-base/typescript` (and `/node`, `@cospired/eslint-config-react/typescript`). +- **Every entry works with no options:** `export { default } from '@cospired/eslint-config-base/node';` is a complete `eslint.config.js`. Node.js JavaScript projects use `@cospired/eslint-config-base/node-js`. - **No `warn`.** Every rule is `error` or off. - **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. - **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. - **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket `#123`; imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template`, `no-await-in-loop` and others are errors; TypeScript files require explicit return types outside tests. -- **Type-aware linting needs tsconfigs:** the TypeScript configs (including `react/typescript`) lint with type information, so every linted file, tests included, must belong to a tsconfig. Pass them all via `tsconfigPath` (an array is fine), for example a test-only `tsconfig.test.json`. Files outside every tsconfig report a parsing error. +- **Type-aware linting:** the TypeScript configs (including `react/typescript`) lint source files with type information through typescript-eslint's project service, so those files must belong to a tsconfig. Tests, scripts and TypeScript files at the package root (`vitest.config.ts`, ...) are linted without type information because build tsconfigs usually exclude them; set `typedTests: true` to type-check those too (they must then be in a tsconfig). Files outside every tsconfig report a parsing error. +- **Every ESLint, typescript-eslint, import-x, @stylistic, eslint-comments, react-hooks and formatjs rule is configured** (on, or off with a reason in `docs/rules.md`). Rules we left off that you want can be turned on in your own config. diff --git a/README.md b/README.md index 82725ed..924ba02 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Slightly opinionated ESLint flat configs (ESLint 10) for JavaScript and TypeScri | Package | Entry | For | |---|---|---| | [`@cospired/eslint-config-base`](packages/eslint-config-base) | `.` | JavaScript | +| | `./node-js` | Node.js JavaScript | | | `./typescript` | TypeScript (type-aware) | -| | `./node` | Node/backend TypeScript | +| | `./node` | Node.js TypeScript | | [`@cospired/eslint-config-react`](packages/eslint-config-react) | `.` | React, JavaScript | | | `./typescript` | React, TypeScript | @@ -17,9 +18,12 @@ Every rule that differs from ESLint/typescript-eslint recommended, with its sour ```sh npm install npm test # node:test over every package, the test helpers and the rule-table script -npm run lint +npm run lint # the repo lints itself with its own config +npm run smoke # pack both packages, install them in a clean project, lint with one-line configs npm run rules # regenerate docs/rules.md npx changeset # describe a change for the next release ``` Releases are cut by the changesets GitHub Action on `main`. + +CI runs the tests on Node 20.19, 22.13 and 24, again with every peer dependency at its minimum version, and the pack smoke test. diff --git a/docs/rules.md b/docs/rules.md index 81b60d6..857cd18 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -12,41 +12,119 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | | `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | | `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | +| `@stylistic/jsx-self-closing-comp` | error {"component":true,"html":true} | - | new | added for completeness (unreviewed call) | +| `@stylistic/lines-between-class-members` | error "always" {"exceptAfterOverload":true,"exceptAfterSingleLine":true} | - | new | added for completeness (unreviewed call) | | `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | +| `@stylistic/spaced-comment` | error "always" {"markers":["/"]} | - | new | added for completeness (unreviewed call) | +| `accessor-pairs` | error {"enforceForTSTypes":false,"enforceForClassMembers":true,"getWithoutSet":false,"setWithoutGet":true} | - | new | added for completeness (unreviewed call) | +| `array-callback-return` | error {"allowImplicit":false,"checkForEach":false,"allowVoid":false} | - | new | added for completeness (unreviewed call) | +| `arrow-body-style` | error "as-needed" | - | new | added for completeness (unreviewed call) | +| `camelcase` | error {"allow":[],"ignoreDestructuring":false,"ignoreGlobals":false,"ignoreImports":false,"properties":"never"} | - | new | added for completeness (unreviewed call) | | `consistent-return` | error {"treatUndefinedAsUnspecified":false} | - | new | adopted, JS only | | `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | +| `default-case-last` | error | - | new | added for completeness (unreviewed call) | | `default-param-last` | error | - | new | adopted | +| `dot-notation` | error {"allowKeywords":true,"allowPattern":""} | - | new | added for completeness (unreviewed call) | | `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | -| `import-x/default` | error | - | new | plugin preset, unchanged | -| `import-x/export` | error | - | new | plugin preset, unchanged | -| `import-x/named` | error | - | new | plugin preset, unchanged | -| `import-x/namespace` | error | - | new | plugin preset, unchanged | +| `func-name-matching` | error "always" | - | new | added for completeness (unreviewed call) | +| `grouped-accessor-pairs` | error "getBeforeSet" {"enforceForTSTypes":false} | - | new | added for completeness (unreviewed call) | +| `guard-for-in` | error | - | new | added for completeness (unreviewed call) | +| `import-x/default` | error | - | new | added for completeness (unreviewed call) | +| `import-x/export` | error | - | new | added for completeness (unreviewed call) | +| `import-x/first` | error | - | new | added for completeness (unreviewed call) | +| `import-x/named` | error | - | new | added for completeness (unreviewed call) | +| `import-x/namespace` | error | - | new | added for completeness (unreviewed call) | | `import-x/newline-after-import` | error | - | cospired | kept | +| `import-x/no-absolute-path` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-amd` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-cycle` | error {"ignoreExternal":true,"allowUnsafeDynamicCyclicDependency":false} | - | new | added for completeness (unreviewed call) | +| `import-x/no-deprecated` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-duplicates` | error | - | new | kept | +| `import-x/no-dynamic-require` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-empty-named-blocks` | error | - | cospired | kept | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","tests/**","{apps,packages}/*/tests/**","**/__tests__/**","{apps,packages}/*/**/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | | `import-x/no-import-module-exports` | error | - | cospired | kept | +| `import-x/no-mutable-exports` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-named-default` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-relative-packages` | error | - | cospired | kept | -| `import-x/no-unresolved` | error | - | new | plugin preset, unchanged | +| `import-x/no-self-import` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-unresolved` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-useless-path-segments` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-webpack-loader-syntax` | error | - | new | added for completeness (unreviewed call) | | `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | | `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | +| `new-cap` | error {"capIsNew":false,"capIsNewExceptions":[],"newIsCap":true,"newIsCapExceptions":[],"properties":true} | - | new | added for completeness (unreviewed call) | +| `no-alert` | error | - | new | added for completeness (unreviewed call) | +| `no-array-constructor` | error | - | new | added for completeness (unreviewed call) | | `no-await-in-loop` | error | - | new | adopted | +| `no-caller` | error | - | new | added for completeness (unreviewed call) | | `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | +| `no-constructor-return` | error | - | new | added for completeness (unreviewed call) | | `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | +| `no-empty-function` | error {"allow":["arrowFunctions","functions","methods"]} | - | new | added for completeness (unreviewed call) | +| `no-eval` | error {"allowIndirect":false} | - | new | added for completeness (unreviewed call) | +| `no-extend-native` | error {"exceptions":[]} | - | new | added for completeness (unreviewed call) | +| `no-extra-bind` | error | - | new | added for completeness (unreviewed call) | +| `no-extra-label` | error | - | new | added for completeness (unreviewed call) | +| `no-implied-eval` | error | - | new | added for completeness (unreviewed call) | +| `no-invalid-this` | error {"capIsConstructor":true} | - | new | added for completeness (unreviewed call) | +| `no-iterator` | error | - | new | added for completeness (unreviewed call) | +| `no-label-var` | error | - | new | added for completeness (unreviewed call) | +| `no-labels` | error {"allowLoop":false,"allowSwitch":false} | - | new | added for completeness (unreviewed call) | +| `no-lone-blocks` | error | - | new | added for completeness (unreviewed call) | +| `no-lonely-if` | error | - | new | added for completeness (unreviewed call) | +| `no-loop-func` | error | - | new | added for completeness (unreviewed call) | +| `no-multi-assign` | error {"ignoreNonDeclaration":false} | - | new | added for completeness (unreviewed call) | +| `no-multi-str` | error | - | new | added for completeness (unreviewed call) | | `no-nested-ternary` | error | - | new | adopted | +| `no-new-func` | error | - | new | added for completeness (unreviewed call) | +| `no-new-wrappers` | error | - | new | added for completeness (unreviewed call) | +| `no-object-constructor` | error | - | new | added for completeness (unreviewed call) | +| `no-octal-escape` | error | - | new | added for completeness (unreviewed call) | | `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | +| `no-promise-executor-return` | error {"allowVoid":false} | - | new | added for completeness (unreviewed call) | +| `no-proto` | error | - | new | added for completeness (unreviewed call) | +| `no-return-assign` | error "except-parens" | - | new | added for completeness (unreviewed call) | +| `no-script-url` | error | - | new | added for completeness (unreviewed call) | +| `no-self-compare` | error | - | new | added for completeness (unreviewed call) | +| `no-sequences` | error {"allowInParentheses":true} | - | new | added for completeness (unreviewed call) | | `no-shadow` | error {"allow":[],"builtinGlobals":false,"hoist":"functions","ignoreOnInitialization":false,"ignoreTypeValueShadow":true,"ignoreFunctionTypeParameterNameValueShadow":true} | - | new | adopted (TS: typescript-eslint version) | +| `no-template-curly-in-string` | error | - | new | added for completeness (unreviewed call) | +| `no-throw-literal` | error | - | new | added for completeness (unreviewed call) | +| `no-undef-init` | error | - | new | added for completeness (unreviewed call) | | `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | +| `no-unmodified-loop-condition` | error {"checkConditionalExpressions":false} | - | new | added for completeness (unreviewed call) | +| `no-unneeded-ternary` | error {"defaultAssignment":true} | - | new | added for completeness (unreviewed call) | +| `no-unreachable-loop` | error {"ignore":[]} | - | new | added for completeness (unreviewed call) | | `no-unused-expressions` | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | - | eventured | strict, no React relaxations | | `no-unused-vars` | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | error, `_` prefix ignored | | `no-use-before-define` | error {"classes":true,"functions":true,"variables":true,"allowNamedExports":false,"enums":true,"typedefs":true,"ignoreTypeReferences":true} | - | new | adopted, strict | +| `no-useless-call` | error | - | new | added for completeness (unreviewed call) | +| `no-useless-computed-key` | error {"enforceForClassMembers":true} | - | new | added for completeness (unreviewed call) | +| `no-useless-concat` | error | - | new | added for completeness (unreviewed call) | +| `no-useless-constructor` | error | - | new | added for completeness (unreviewed call) | +| `no-useless-rename` | error {"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false} | - | new | added for completeness (unreviewed call) | +| `no-useless-return` | error | - | new | added for completeness (unreviewed call) | | `no-var` | error | - | eventured | kept | | `object-shorthand` | error "always" | - | eventured | kept | +| `one-var` | error "never" | - | new | added for completeness (unreviewed call) | +| `operator-assignment` | error "always" | - | new | added for completeness (unreviewed call) | | `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | | `prefer-const` | error {"destructuring":"any","ignoreReadBeforeAssign":false} | - | eventured | kept | +| `prefer-exponentiation-operator` | error | - | new | added for completeness (unreviewed call) | +| `prefer-numeric-literals` | error | - | new | added for completeness (unreviewed call) | | `prefer-object-has-own` | error | - | cospired | kept | +| `prefer-object-spread` | error | - | new | added for completeness (unreviewed call) | +| `prefer-promise-reject-errors` | error {"allowEmptyReject":false} | - | new | added for completeness (unreviewed call) | +| `prefer-regex-literals` | error {"disallowRedundantWrapping":false} | - | new | added for completeness (unreviewed call) | +| `prefer-rest-params` | error | - | new | added for completeness (unreviewed call) | +| `prefer-spread` | error | - | new | added for completeness (unreviewed call) | | `prefer-template` | error | - | new | adopted | +| `radix` | error | - | new | added for completeness (unreviewed call) | +| `require-await` | error | - | new | added for completeness (unreviewed call) | +| `symbol-description` | error | - | new | added for completeness (unreviewed call) | | `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | +| `yoda` | error "never" {"exceptRange":false,"onlyEquality":false} | - | new | added for completeness (unreviewed call) | ## Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked) @@ -58,62 +136,171 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | | `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | | `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | +| `@stylistic/jsx-self-closing-comp` | error {"component":true,"html":true} | - | new | added for completeness (unreviewed call) | +| `@stylistic/lines-between-class-members` | error "always" {"exceptAfterOverload":true,"exceptAfterSingleLine":true} | - | new | added for completeness (unreviewed call) | | `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | +| `@stylistic/spaced-comment` | error "always" {"markers":["/"]} | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/await-thenable` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/consistent-type-imports` | error {"fixStyle":"inline-type-imports"} | - | new | added for completeness (unreviewed call) | | `@typescript-eslint/default-param-last` | error | - | new | adopted | +| `@typescript-eslint/dot-notation` | off {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | error {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | new | plugin preset, unchanged | | `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | default on for non-test files, allowExpressions | -| `@typescript-eslint/no-generated-empty-object-type` | error | - | eventured | default on | -| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | default on, flagUnions strict | +| `@typescript-eslint/no-array-delete` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-base-to-string` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-duplicate-type-constituents` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-dynamic-delete` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-floating-promises` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-for-in-array` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-implied-eval` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-import-type-side-effects` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-invalid-this` | error {"capIsConstructor":true} | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-invalid-void-type` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-misused-promises` | off {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | default on, flagUnions strict | +| `@typescript-eslint/no-non-null-asserted-nullish-coalescing` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-non-null-assertion` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-redundant-type-constituents` | off | error | new | plugin preset, unchanged | | `@typescript-eslint/no-shadow` | error | - | new | adopted | +| `@typescript-eslint/no-unnecessary-parameter-property-assignment` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-unnecessary-type-assertion` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-argument` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-assignment` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-call` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-enum-comparison` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-member-access` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-return` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-unary-minus` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/no-unused-private-class-members` | error | - | new | added for completeness (unreviewed call) | | `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error | eventured | error, `_` prefix ignored | | `@typescript-eslint/no-use-before-define` | error | - | new | adopted, strict | +| `@typescript-eslint/no-useless-constructor` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/no-useless-empty-export` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/non-nullable-type-assertion-style` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/only-throw-error` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-find` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-includes` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-literal-enum-member` | error | - | new | added for completeness (unreviewed call) | +| `@typescript-eslint/prefer-nullish-coalescing` | off | error | eventured | plugin preset, unchanged | +| `@typescript-eslint/prefer-optional-chain` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-promise-reject-errors` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-regexp-exec` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-string-starts-ends-with` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/require-await` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/restrict-plus-operands` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/restrict-template-expressions` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/unbound-method` | off | error | new | plugin preset, unchanged | +| `@typescript-eslint/unified-signatures` | error | - | new | added for completeness (unreviewed call) | +| `accessor-pairs` | error {"enforceForTSTypes":false,"enforceForClassMembers":true,"getWithoutSet":false,"setWithoutGet":true} | - | new | added for completeness (unreviewed call) | +| `array-callback-return` | error {"allowImplicit":false,"checkForEach":false,"allowVoid":false} | - | new | added for completeness (unreviewed call) | +| `arrow-body-style` | error "as-needed" | - | new | added for completeness (unreviewed call) | +| `camelcase` | error {"allow":[],"ignoreDestructuring":false,"ignoreGlobals":false,"ignoreImports":false,"properties":"never"} | - | new | added for completeness (unreviewed call) | | `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | +| `default-case-last` | error | - | new | added for completeness (unreviewed call) | | `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | +| `func-name-matching` | error "always" | - | new | added for completeness (unreviewed call) | +| `grouped-accessor-pairs` | error "getBeforeSet" {"enforceForTSTypes":false} | - | new | added for completeness (unreviewed call) | +| `guard-for-in` | error | - | new | added for completeness (unreviewed call) | | `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | kept: prefer-inline | -| `import-x/default` | error | - | new | plugin preset, unchanged | -| `import-x/export` | error | - | new | plugin preset, unchanged | -| `import-x/namespace` | error | - | new | plugin preset, unchanged | +| `import-x/default` | error | - | new | added for completeness (unreviewed call) | +| `import-x/export` | error | - | new | added for completeness (unreviewed call) | +| `import-x/first` | error | - | new | added for completeness (unreviewed call) | +| `import-x/namespace` | error | - | new | added for completeness (unreviewed call) | | `import-x/newline-after-import` | error | - | cospired | kept | +| `import-x/no-absolute-path` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-amd` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-cycle` | error {"ignoreExternal":true,"allowUnsafeDynamicCyclicDependency":false} | - | new | added for completeness (unreviewed call) | +| `import-x/no-deprecated` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-duplicates` | error | - | new | kept | +| `import-x/no-dynamic-require` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-empty-named-blocks` | error | - | cospired | kept | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","src/test/**","{apps,packages}/*/src/test/**","src/__tests__/**","{apps,packages}/*/src/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | +| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","tests/**","{apps,packages}/*/tests/**","**/__tests__/**","{apps,packages}/*/**/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | | `import-x/no-import-module-exports` | error | - | cospired | kept | +| `import-x/no-mutable-exports` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-named-default` | error | - | new | added for completeness (unreviewed call) | | `import-x/no-relative-packages` | error | - | cospired | kept | -| `import-x/no-unresolved` | error | - | new | plugin preset, unchanged | +| `import-x/no-self-import` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-unresolved` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-useless-path-segments` | error | - | new | added for completeness (unreviewed call) | +| `import-x/no-webpack-loader-syntax` | error | - | new | added for completeness (unreviewed call) | | `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | | `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | +| `new-cap` | error {"capIsNew":false,"capIsNewExceptions":[],"newIsCap":true,"newIsCapExceptions":[],"properties":true} | - | new | added for completeness (unreviewed call) | +| `no-alert` | error | - | new | added for completeness (unreviewed call) | | `no-await-in-loop` | error | - | new | adopted | +| `no-caller` | error | - | new | added for completeness (unreviewed call) | | `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | +| `no-constructor-return` | error | - | new | added for completeness (unreviewed call) | | `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | +| `no-eval` | error {"allowIndirect":false} | - | new | added for completeness (unreviewed call) | +| `no-extend-native` | error {"exceptions":[]} | - | new | added for completeness (unreviewed call) | +| `no-extra-bind` | error | - | new | added for completeness (unreviewed call) | +| `no-extra-label` | error | - | new | added for completeness (unreviewed call) | +| `no-iterator` | error | - | new | added for completeness (unreviewed call) | +| `no-label-var` | error | - | new | added for completeness (unreviewed call) | +| `no-labels` | error {"allowLoop":false,"allowSwitch":false} | - | new | added for completeness (unreviewed call) | +| `no-lone-blocks` | error | - | new | added for completeness (unreviewed call) | +| `no-lonely-if` | error | - | new | added for completeness (unreviewed call) | +| `no-loop-func` | error | - | new | added for completeness (unreviewed call) | +| `no-multi-assign` | error {"ignoreNonDeclaration":false} | - | new | added for completeness (unreviewed call) | +| `no-multi-str` | error | - | new | added for completeness (unreviewed call) | | `no-nested-ternary` | error | - | new | adopted | +| `no-new-func` | error | - | new | added for completeness (unreviewed call) | +| `no-new-wrappers` | error | - | new | added for completeness (unreviewed call) | +| `no-object-constructor` | error | - | new | added for completeness (unreviewed call) | +| `no-octal-escape` | error | - | new | added for completeness (unreviewed call) | | `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | +| `no-promise-executor-return` | error {"allowVoid":false} | - | new | added for completeness (unreviewed call) | +| `no-proto` | error | - | new | added for completeness (unreviewed call) | +| `no-return-assign` | error "except-parens" | - | new | added for completeness (unreviewed call) | +| `no-script-url` | error | - | new | added for completeness (unreviewed call) | +| `no-self-compare` | error | - | new | added for completeness (unreviewed call) | +| `no-sequences` | error {"allowInParentheses":true} | - | new | added for completeness (unreviewed call) | +| `no-template-curly-in-string` | error | - | new | added for completeness (unreviewed call) | +| `no-undef-init` | error | - | new | added for completeness (unreviewed call) | | `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | -| `no-unused-vars` | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | off {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | error, `_` prefix ignored | +| `no-unmodified-loop-condition` | error {"checkConditionalExpressions":false} | - | new | added for completeness (unreviewed call) | +| `no-unneeded-ternary` | error {"defaultAssignment":true} | - | new | added for completeness (unreviewed call) | +| `no-unreachable-loop` | error {"ignore":[]} | - | new | added for completeness (unreviewed call) | +| `no-unused-private-class-members` | off | error | new | added for completeness (unreviewed call) | +| `no-useless-call` | error | - | new | added for completeness (unreviewed call) | +| `no-useless-computed-key` | error {"enforceForClassMembers":true} | - | new | added for completeness (unreviewed call) | +| `no-useless-concat` | error | - | new | added for completeness (unreviewed call) | +| `no-useless-rename` | error {"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false} | - | new | added for completeness (unreviewed call) | +| `no-useless-return` | error | - | new | added for completeness (unreviewed call) | | `object-shorthand` | error "always" | - | eventured | kept | +| `one-var` | error "never" | - | new | added for completeness (unreviewed call) | +| `operator-assignment` | error "always" | - | new | added for completeness (unreviewed call) | | `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | +| `prefer-exponentiation-operator` | error | - | new | added for completeness (unreviewed call) | +| `prefer-numeric-literals` | error | - | new | added for completeness (unreviewed call) | | `prefer-object-has-own` | error | - | cospired | kept | +| `prefer-object-spread` | error | - | new | added for completeness (unreviewed call) | +| `prefer-regex-literals` | error {"disallowRedundantWrapping":false} | - | new | added for completeness (unreviewed call) | | `prefer-template` | error | - | new | adopted | +| `radix` | error | - | new | added for completeness (unreviewed call) | +| `symbol-description` | error | - | new | added for completeness (unreviewed call) | | `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | +| `yoda` | error "never" {"exceptRange":false,"onlyEquality":false} | - | new | added for completeness (unreviewed call) | ## React JS (vs. Base JS) | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | plugin preset, unchanged | -| `formatjs/enforce-default-message` | error "literal" | - | new | plugin preset, unchanged | -| `formatjs/enforce-placeholders` | error | - | new | plugin preset, unchanged | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | plugin preset, unchanged | +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-default-message` | error "literal" | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-placeholders` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | added for completeness (unreviewed call) | | `formatjs/no-camel-case` | error | - | eventured | error (eventured) | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | plugin preset, unchanged | -| `formatjs/no-emoji` | error | - | new | plugin preset, unchanged | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | added for completeness (unreviewed call) | +| `formatjs/no-emoji` | error | - | new | added for completeness (unreviewed call) | | `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | | `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | plugin preset, unchanged | -| `formatjs/no-multiple-plurals` | error | - | new | plugin preset, unchanged | -| `formatjs/no-multiple-whitespaces` | error | - | new | plugin preset, unchanged | -| `formatjs/no-offset` | error | - | new | plugin preset, unchanged | -| `formatjs/no-useless-message` | error | - | new | plugin preset, unchanged | -| `formatjs/prefer-full-sentence` | error | - | new | plugin preset, unchanged | -| `formatjs/prefer-pound-in-plural` | error | - | new | plugin preset, unchanged | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-multiple-plurals` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-multiple-whitespaces` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-offset` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-useless-message` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/prefer-full-sentence` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/prefer-pound-in-plural` | error | - | new | added for completeness (unreviewed call) | | `react-hooks/config` | error | - | new | plugin preset, unchanged | | `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | | `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | @@ -136,22 +323,54 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | Rule | Ours | Recommended | Source | Decision | |---|---|---|---|---| -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | plugin preset, unchanged | -| `formatjs/enforce-default-message` | error "literal" | - | new | plugin preset, unchanged | -| `formatjs/enforce-placeholders` | error | - | new | plugin preset, unchanged | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | plugin preset, unchanged | +| `@typescript-eslint/await-thenable` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/dot-notation` | error {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | off {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | new | plugin preset, unchanged | +| `@typescript-eslint/no-array-delete` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-base-to-string` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-duplicate-type-constituents` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-floating-promises` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-for-in-array` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-generated-empty-object-type` | error | off | eventured | default on | +| `@typescript-eslint/no-implied-eval` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | off {"checksConditionals":{"flagUnions":"strict"}} | eventured | default on, flagUnions strict | +| `@typescript-eslint/no-redundant-type-constituents` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unnecessary-type-assertion` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-argument` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-assignment` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-call` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-enum-comparison` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-member-access` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-return` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/no-unsafe-unary-minus` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/non-nullable-type-assertion-style` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/only-throw-error` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-find` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-includes` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-nullish-coalescing` | error | off | eventured | plugin preset, unchanged | +| `@typescript-eslint/prefer-optional-chain` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-promise-reject-errors` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-regexp-exec` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/prefer-string-starts-ends-with` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/require-await` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/restrict-plus-operands` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/restrict-template-expressions` | error | off | new | plugin preset, unchanged | +| `@typescript-eslint/unbound-method` | error | off | new | plugin preset, unchanged | +| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-default-message` | error "literal" | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-placeholders` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | added for completeness (unreviewed call) | | `formatjs/no-camel-case` | error | - | eventured | error (eventured) | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | plugin preset, unchanged | -| `formatjs/no-emoji` | error | - | new | plugin preset, unchanged | +| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | added for completeness (unreviewed call) | +| `formatjs/no-emoji` | error | - | new | added for completeness (unreviewed call) | | `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | | `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | plugin preset, unchanged | -| `formatjs/no-multiple-plurals` | error | - | new | plugin preset, unchanged | -| `formatjs/no-multiple-whitespaces` | error | - | new | plugin preset, unchanged | -| `formatjs/no-offset` | error | - | new | plugin preset, unchanged | -| `formatjs/no-useless-message` | error | - | new | plugin preset, unchanged | -| `formatjs/prefer-full-sentence` | error | - | new | plugin preset, unchanged | -| `formatjs/prefer-pound-in-plural` | error | - | new | plugin preset, unchanged | +| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-multiple-plurals` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-multiple-whitespaces` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-offset` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/no-useless-message` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/prefer-full-sentence` | error | - | new | added for completeness (unreviewed call) | +| `formatjs/prefer-pound-in-plural` | error | - | new | added for completeness (unreviewed call) | | `react-hooks/config` | error | - | new | plugin preset, unchanged | | `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | | `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | @@ -169,3 +388,10 @@ Generated by `npm run rules`. Rows are rules where our setting differs from the | `react-hooks/unsupported-syntax` | error | - | new | error (no warn policy) | | `react-hooks/use-memo` | error | - | new | plugin preset, unchanged | | `react-refresh/only-export-components` | error {"allowConstantExport":true} | - | eventured | error (no warn policy) | + +## Explicitly off (considered, not enforced) + +Rules no preset sets, switched off on purpose: opinion, thresholds, project-specific, or covered by Prettier/TypeScript. + +- ESLint core, import-x, typescript-eslint, @stylistic, eslint-comments: `@eslint-community/eslint-comments/no-restricted-disable`, `@eslint-community/eslint-comments/no-use`, `@stylistic/curly-newline`, `@stylistic/exp-jsx-props-style`, `@stylistic/exp-list-style`, `@stylistic/jsx-curly-brace-presence`, `@stylistic/jsx-function-call-newline`, `@stylistic/jsx-pascal-case`, `@stylistic/line-comment-position`, `@stylistic/multiline-comment-style`, `@typescript-eslint/class-methods-use-this`, `@typescript-eslint/explicit-member-accessibility`, `@typescript-eslint/explicit-module-boundary-types`, `@typescript-eslint/init-declarations`, `@typescript-eslint/max-params`, `@typescript-eslint/member-ordering`, `@typescript-eslint/method-signature-style`, `@typescript-eslint/no-dupe-class-members`, `@typescript-eslint/no-extraneous-class`, `@typescript-eslint/no-magic-numbers`, `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-restricted-types`, `@typescript-eslint/parameter-properties`, `@typescript-eslint/prefer-enum-initializers`, `block-scoped-var`, `capitalized-comments`, `class-methods-use-this`, `complexity`, `consistent-return`, `consistent-this`, `default-case`, `default-param-last`, `formatjs/enforce-description`, `formatjs/enforce-id`, `formatjs/enforce-message-types`, `formatjs/no-id`, `formatjs/no-literal-string-in-object`, `formatjs/prefer-formatted-message`, `func-names`, `func-style`, `id-denylist`, `id-length`, `id-match`, `import-x/avoid-barrel-files`, `import-x/avoid-importing-barrel-files`, `import-x/avoid-namespace-import`, `import-x/avoid-re-export-all`, `import-x/consistent-type-specifier-style`, `import-x/dynamic-import-chunkname`, `import-x/exports-last`, `import-x/extensions`, `import-x/first`, `import-x/group-exports`, `import-x/max-dependencies`, `import-x/no-anonymous-default-export`, `import-x/no-commonjs`, `import-x/no-default-export`, `import-x/no-internal-modules`, `import-x/no-named-as-default`, `import-x/no-named-as-default-member`, `import-x/no-named-export`, `import-x/no-namespace`, `import-x/no-nodejs-modules`, `import-x/no-relative-parent-imports`, `import-x/no-rename-default`, `import-x/no-restricted-paths`, `import-x/no-unassigned-import`, `import-x/no-unused-modules`, `import-x/prefer-default-export`, `import-x/prefer-namespace-import`, `import-x/unambiguous`, `init-declarations`, `max-classes-per-file`, `max-depth`, `max-lines`, `max-lines-per-function`, `max-nested-callbacks`, `max-params`, `max-statements`, `no-bitwise`, `no-continue`, `no-div-regex`, `no-duplicate-imports`, `no-eq-null`, `no-implicit-coercion`, `no-implicit-globals`, `no-inline-comments`, `no-inner-declarations`, `no-invalid-this`, `no-magic-numbers`, `no-negated-condition`, `no-new`, `no-plusplus`, `no-restricted-exports`, `no-restricted-globals`, `no-restricted-imports`, `no-restricted-properties`, `no-restricted-syntax`, `no-shadow`, `no-ternary`, `no-undefined`, `no-underscore-dangle`, `no-unused-private-class-members`, `no-use-before-define`, `no-useless-constructor`, `no-void`, `no-warning-comments`, `prefer-destructuring`, `prefer-named-capture-group`, `react-hooks/capitalized-calls`, `react-hooks/exhaustive-effect-dependencies`, `react-hooks/fbt`, `react-hooks/hooks`, `react-hooks/invariant`, `react-hooks/memo-dependencies`, `react-hooks/memoized-effect-dependencies`, `react-hooks/no-deriving-state-in-effects`, `react-hooks/rule-suppression`, `react-hooks/syntax`, `react-hooks/todo`, `react-hooks/void-use-memo`, `require-atomic-updates`, `require-unicode-regexp`, `sort-imports`, `sort-keys`, `sort-vars`, `strict`, `unicode-bom`, `vars-on-top` +- React plugins: `formatjs/enforce-description`, `formatjs/enforce-id`, `formatjs/enforce-message-types`, `formatjs/no-id`, `formatjs/no-literal-string-in-object`, `formatjs/prefer-formatted-message`, `react-hooks/capitalized-calls`, `react-hooks/exhaustive-effect-dependencies`, `react-hooks/fbt`, `react-hooks/hooks`, `react-hooks/invariant`, `react-hooks/memo-dependencies`, `react-hooks/memoized-effect-dependencies`, `react-hooks/no-deriving-state-in-effects`, `react-hooks/rule-suppression`, `react-hooks/syntax`, `react-hooks/todo`, `react-hooks/void-use-memo` diff --git a/package.json b/package.json index 2f85015..3b5094a 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "packages/*" ], "scripts": { - "test": "npm test --workspaces --if-present && node --test test-utils scripts", + "test": "npm test --workspaces --if-present && node --test test-utils/*.test.js scripts/*.test.js", "lint": "eslint .", "changeset": "changeset", "release": "changeset publish", "rules": "node scripts/rule-table.js", - "smoke": "node scripts/smoke-pack.js" + "smoke": "node scripts/smoke-pack.js", + "version": "changeset version && npm install --package-lock-only --ignore-scripts" }, "repository": { "type": "git", diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index 7f85548..141534d 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -2,6 +2,13 @@ Slightly opinionated ESLint flat config for JavaScript and TypeScript. Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. +| Entry | For | +|---|---| +| `@cospired/eslint-config-base` | JavaScript (environment neutral) | +| `@cospired/eslint-config-base/node-js` | Node.js JavaScript (Node globals, CommonJS or ESM from your `package.json` `type`) | +| `@cospired/eslint-config-base/typescript` | TypeScript, type-aware | +| `@cospired/eslint-config-base/node` | Node.js TypeScript (Node globals) | + ## Install ```sh @@ -12,41 +19,52 @@ npm i -D @cospired/eslint-config-base eslint @eslint/js typescript-eslint typesc ## Usage -JavaScript: +No options needed. `eslint.config.js`: ```js -import config, { globalIgnores } from '@cospired/eslint-config-base'; - -export default [globalIgnores, ...config]; +export { default } from '@cospired/eslint-config-base/node'; // or /typescript, /node-js, or the package root ``` -TypeScript (type-aware): +The defaults ignore build output (`dist`, `build`, `coverage`, ...), use typescript-eslint's project service to find your tsconfigs, and lint tests, scripts and package-root TypeScript files (`vitest.config.ts`, ...) without type information, since build tsconfigs usually exclude them. + +Add your own rules after the config: ```js -import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; +import config from '@cospired/eslint-config-base/node'; export default [ - globalIgnores, - ...createTypescriptConfig({ - tsconfigRootDir: import.meta.dirname, - tsconfigPath: ['tsconfig.json', 'test/tsconfig.json'], // optional; every linted file must belong to one - importOrder: { pathGroups: [{ pattern: '#shared/**', group: 'internal', position: 'before' }] }, // optional - }), + ...config, + { + files: ['src/domain/**/*.ts'], + rules: { + // project-specific layering rules belong in the project, not in the shared config + 'no-restricted-imports': ['error', { paths: [{ name: 'fastify', message: 'domain/ must not import fastify.' }] }], + }, + }, ]; ``` -Node backends: `createNodeConfig` from `@cospired/eslint-config-base/node` (same options, plus Node globals). +### Options -Options: `tsconfigPath` (string or array), `tsconfigRootDir`, `files`, `devDependencyGlobs` (files allowed to import devDependencies; defaults to tests, `scripts/`, `*.config.*`), `importOrder.pathGroups`. +`createTypescriptConfig` and `createNodeConfig` (from `.../typescript` and `.../node`) take: + +| Option | Default | | +|---|---|---| +| `tsconfigPath` | project service | tsconfig(s), string or array; every linted file must belong to one | +| `tsconfigRootDir` | cwd | pass `import.meta.dirname` when one ESLint run loads several workspace configs | +| `typedTests` | `false` | lint tests, scripts and root config files type-aware too (they must then be in a tsconfig) | +| `files` | all TS files | files that get type-aware rules | +| `devDependencyGlobs` | tests, `scripts/`, `*.config.*` | files that may import devDependencies | +| `importOrder.pathGroups` | none | extra `import-x/order` groups | + +`createNodeJsConfig({ cwd })` from `.../node-js` takes the directory whose `package.json` decides CommonJS vs ESM. ## What it enforces -Highlights; the complete table is in [docs/rules.md](../../docs/rules.md). +Every rule is organised per plugin in `src/rules/` and listed with its decision in [docs/rules.md](../../docs/rules.md). A test fails when a plugin ships a rule that is neither in a recommended preset nor configured here, so new plugin releases cannot add gaps silently. - `eslint-disable` comments need a reason (`-- why`); TODOs need a ticket (`TODO(#123): ...`). -- Alphabetized imports in groups separated by a blank line; devDependency imports only in test/config/script files. +- Alphabetized imports in groups separated by a blank line; devDependency imports only in test, config and script files. - Blank line before `return` and after declarations. - No `warn` severities: rules are `error` or off. Downgrade locally if you need to. -- TypeScript: type-checked recommended + stylistic, explicit return types in non-test files, `no-misused-promises`. - -Project-specific rules (for example `no-restricted-imports` for layer boundaries) belong in your own `eslint.config.js`. +- TypeScript: type-checked recommended + stylistic, explicit return types outside tests, `no-misused-promises`. diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index f4d2676..4c21bdf 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -31,7 +31,7 @@ "./package.json": "./package.json" }, "scripts": { - "test": "node --test 'test/*.test.js'" + "test": "node --test test/*.test.js" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", diff --git a/packages/eslint-config-react/README.md b/packages/eslint-config-react/README.md index 3e06a2e..01c5865 100644 --- a/packages/eslint-config-react/README.md +++ b/packages/eslint-config-react/README.md @@ -13,21 +13,12 @@ The peer dependencies of `@cospired/eslint-config-base` are required as well (se ## Usage -```js -// JavaScript -import config, { globalIgnores } from '@cospired/eslint-config-react'; - -export default [globalIgnores, ...config]; -``` +No options needed. `eslint.config.js`: ```js -// TypeScript: same options as createTypescriptConfig in the base package -import { createReactTypescriptConfig, globalIgnores } from '@cospired/eslint-config-react/typescript'; - -export default [ - globalIgnores, - ...createReactTypescriptConfig({ tsconfigRootDir: import.meta.dirname, tsconfigPath: ['tsconfig.app.json'] }), -]; +export { default } from '@cospired/eslint-config-react/typescript'; // or '@cospired/eslint-config-react' for JavaScript ``` -formatjs rules apply to all files except `*.test`/`*.spec`. jsx-a11y is not included yet: `eslint-plugin-jsx-a11y` does not support ESLint 10. +`createReactTypescriptConfig` takes the same options as `createTypescriptConfig` in the base package (`tsconfigPath`, `tsconfigRootDir`, `typedTests`, ...). Add project-specific rules after the config, as described in the base README. + +formatjs rules apply to all files except tests. jsx-a11y is not included yet: `eslint-plugin-jsx-a11y` does not support ESLint 10. diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index 7dc91c5..7b3f9ff 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -29,7 +29,7 @@ "./package.json": "./package.json" }, "scripts": { - "test": "node --test 'test/*.test.js'" + "test": "node --test test/*.test.js" }, "dependencies": { "@cospired/eslint-config-base": "^7.0.2" diff --git a/scripts/rule-table.js b/scripts/rule-table.js index 563237d..916aba0 100644 --- a/scripts/rule-table.js +++ b/scripts/rule-table.js @@ -33,11 +33,29 @@ export function buildRuleTable(config, baseline, sources = {}) { return Object.keys(ours) .sort() - .filter((rule) => level(ours[rule]) !== 'off' || base[rule] !== undefined) + .filter((rule) => level(ours[rule]) !== 'off' || (base[rule] !== undefined && level(base[rule]) !== 'off')) + .filter((rule) => !(level(ours[rule]) === 'off' && level(base[rule]) === 'off')) .filter((rule) => format(ours[rule]) !== format(base[rule])) .map((rule) => ({ rule, ours: format(ours[rule]), recommended: format(base[rule]), source: sources[rule] ?? 'new' })); } +/** Rules that config items named `cospired/*` switch off (completeness: considered and deliberately not enforced). */ +export function explicitOffRules(config) { + const off = new Set(); + + for (const item of config) { + if (!item.name?.startsWith('cospired/')) continue; + for (const [id, entry] of Object.entries(item.rules ?? {})) { + if (level(entry) === 'off') off.add(id); + } + } + + return [...off].sort(); +} + +/** Rules set by config items named `cospired/*`: our own decisions, as opposed to a plugin preset. */ +const ownRules = (config) => new Set(config.flatMap((item) => (item.name?.startsWith('cospired/') ? Object.keys(item.rules ?? {}) : []))); + export function renderTable(title, rows, decisions = {}) { const lines = rows.map((r) => `| \`${r.rule}\` | ${r.ours} | ${r.recommended} | ${r.source} | ${decisions[r.rule] ?? 'open'} |`); @@ -66,6 +84,7 @@ async function main() { const recommended = [js.configs.recommended]; const tsRecommended = [...recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked]; const tsConfig = baseTsModule.createTypescriptConfig(); + const reactJsOnly = react.default; const reactTsConfig = reactTs.createReactTypescriptConfig(); const [baseJs, recJs, baseTsRules, recTs, reactJs, reactTsRules] = await Promise.all([ @@ -77,20 +96,29 @@ async function main() { resolved(reactTsConfig, 'src/a.tsx'), ]); // Rules nobody listed in rule-decisions.json come straight from a plugin's own preset. - const table = (config, baseline) => { + const table = (config, baseline, raw) => { const rows = buildRuleTable(config, baseline, sources); + const own = ownRules(raw); + const fallback = (rule) => (own.has(rule) ? 'added for completeness (unreviewed call)' : 'plugin preset, unchanged'); - return [rows, Object.fromEntries(rows.map((r) => [r.rule, decisions[r.rule] ?? 'plugin preset, unchanged']))]; + return [rows, Object.fromEntries(rows.map((r) => [r.rule, decisions[r.rule] ?? fallback(r.rule)]))]; }; const out = [ '# Rule comparison', '', 'Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review.', '', - renderTable('Base JS (vs. @eslint/js recommended)', ...table(baseJs, recJs)), - renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', ...table(baseTsRules, recTs)), - renderTable('React JS (vs. Base JS)', ...table(reactJs, baseJs)), - renderTable('React TypeScript (vs. Base TypeScript)', ...table(reactTsRules, baseTsRules)), + renderTable('Base JS (vs. @eslint/js recommended)', ...table(baseJs, recJs, base.default)), + renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', ...table(baseTsRules, recTs, tsConfig)), + renderTable('React JS (vs. Base JS)', ...table(reactJs, baseJs, react.default)), + renderTable('React TypeScript (vs. Base TypeScript)', ...table(reactTsRules, baseTsRules, reactTsConfig)), + '## Explicitly off (considered, not enforced)', + '', + 'Rules no preset sets, switched off on purpose: opinion, thresholds, project-specific, or covered by Prettier/TypeScript.', + '', + `- ESLint core, import-x, typescript-eslint, @stylistic, eslint-comments: ${explicitOffRules(reactTsConfig).map((r) => `\`${r}\``).join(', ')}`, + `- React plugins: ${explicitOffRules(reactJsOnly).filter((r) => /^(react|formatjs)/.test(r)).map((r) => `\`${r}\``).join(', ')}`, + '', ].join('\n'); fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); diff --git a/scripts/rule-table.test.js b/scripts/rule-table.test.js index ce7e52f..29bd4c3 100644 --- a/scripts/rule-table.test.js +++ b/scripts/rule-table.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { buildRuleTable, renderTable } from './rule-table.js'; +import { buildRuleTable, explicitOffRules, renderTable } from './rule-table.js'; const sources = { 'no-console': 'eventured' }; @@ -33,3 +33,18 @@ test('renderTable fills the Decision column from the decisions map', () => { assert.match(md, /\| `no-console` \| error \| warn \| eventured \| error, allow warn\/error \|/); }); + +test('buildRuleTable skips rows where both sides are off, even if only options differ', () => { + const table = buildRuleTable([{ rules: { 'no-unused-vars': ['off', { a: 1 }] } }], [{ rules: { 'no-unused-vars': 'off' } }], {}); + + assert.deepEqual(table, []); +}); + +test('explicitOffRules lists rules our named config items switch off', () => { + const config = [ + { rules: { 'no-var': 'off' } }, + { name: 'cospired/x', rules: { 'no-plusplus': 'off', eqeqeq: 'error', 'no-void': 0 } }, + ]; + + assert.deepEqual(explicitOffRules(config), ['no-plusplus', 'no-void']); +}); From 8de338053be7154cb4da4f7d01e54cb268ee3582 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:30:59 +0800 Subject: [PATCH 21/33] docs: spec amendments --- .../specs/2026-11-10-eslint-config-2026-design.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md index 208f836..a7c8581 100644 --- a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md +++ b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md @@ -70,3 +70,10 @@ What the three consumer configs add on top of the shared package, and what that ## Left to the builder File layout, test harness details, fixture format, changesets config, CI (existing is Travis; choose a current setup), README/migration docs, exact plugin versions. + +## Amendments (after approval, from Dirk) + +- **Completeness:** every non-deprecated rule of every plugin used (ESLint core, typescript-eslint, import-x, eslint-comments, todo-plz, @stylistic, react-hooks, react-refresh, formatjs) is configured, either by a recommended preset or explicitly on/off by us. Our files only list rules a preset does not set, or sets differently. `test/completeness.test.js` in each package fails otherwise (replaces `eslint-find-rules --unused`) and also fails on a named `cospired/*` override that repeats the preset's value. The ~200 on/off calls for rules nobody had decided were made by the implementer by clean-code principles (bug-preventing on, opinion/thresholds/formatting off) and are marked "added for completeness (unreviewed call)" in `docs/rules.md`. +- **Layout:** rules are split per plugin in `src/rules/` (`eslint.js`, `typescript-eslint.js`, `import-x.js`, `eslint-comments.js`, `todo-plz.js`, `stylistic.js`; react: `react-hooks.js`, `react-refresh.js`, `formatjs.js`, `react.js`), as in the old packages. +- **Goal:** configs usable with no overrides in eventured, danceregistry and our other projects: every entry has a ready-made default export (`export { default } from '@cospired/eslint-config-base/node'`). Entries: base JS, node-js, typescript, node, react, react/typescript. Tests, scripts and package-root TS files are linted without type information unless `typedTests: true`. +- **Publish readiness:** GitHub CI (Node 20.19/22.13/24, peer-dependency floors, packed-tarball smoke test), changesets release workflow with npm provenance, 8.0.0 for both packages. From e9186b09b4755af5ea346d7b0659dfb67f163dc5 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:45:04 +0800 Subject: [PATCH 22/33] test: disable typescript-estree single-run inference (CI=true cached the first source text per path) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- test-utils/lint.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-utils/lint.js b/test-utils/lint.js index a6635dd..80e23cf 100644 --- a/test-utils/lint.js +++ b/test-utils/lint.js @@ -1,9 +1,13 @@ +// typescript-eslint infers "single run" mode from CI=true and then caches the first source text it sees per file path; +// these helpers lint different code against the same fixture path, so that inference must be off. import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ESLint } from 'eslint'; import { builtinRules } from 'eslint/use-at-your-own-risk'; +process.env.TSESTREE_SINGLE_RUN ??= 'false'; + const here = path.dirname(fileURLToPath(import.meta.url)); export const fixtureProject = path.join(here, 'fixtures', 'ts-project'); From ec23fc004725d3cd7b925140dbdba3396f5b9067 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 16:49:44 +0800 Subject: [PATCH 23/33] ci: dry-run publish with pending changesets; docs: RELEASING.md Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ RELEASING.md | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 RELEASING.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1380ce2..6ab8ebd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,3 +50,23 @@ jobs: cache: npm - run: npm ci - run: npm run smoke + + publish-dry-run: + name: publish dry run (pending changesets applied) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org + - run: npm ci + - name: Apply pending changesets and dry-run publish + run: | + if [ -z "$(find .changeset -name '*.md' ! -name README.md)" ]; then + echo "No pending changesets: nothing to publish." + exit 0 + fi + npx changeset version + npm publish --dry-run --workspaces --access public diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..d25c565 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,18 @@ +# Releasing + +Releases are cut by the `Release` workflow (`.github/workflows/release.yml`) with changesets. + +## One-time setup + +1. Give the repo an npm credential that can publish to the `@cospired` scope, either: + - a repo secret `NPM_TOKEN` (npm automation token), or + - npm trusted publishing: on npmjs.com add this repo and the `release.yml` workflow as a trusted publisher for both packages (then no token is needed). +2. Allow GitHub Actions to create pull requests (Settings, Actions, General, Workflow permissions). + +## Each release + +1. Describe the change with `npx changeset` (CI's "publish dry run" job applies pending changesets and dry-runs the publish on every push). +2. Merge to `main`. The workflow opens a "chore: version packages" PR that bumps versions and changelogs. +3. Merge that PR. The workflow publishes to npm with provenance and tags the release. + +`npm run smoke` packs both packages and lints a clean project with every entry; CI runs it too. From d00bc9906d88277fbc531cb197a4aa4f10ef1048 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:03:00 +0800 Subject: [PATCH 24/33] refactor: four packages (base, typescript, react, react-typescript) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .changeset/config.json | 10 ++- .changeset/flat-config-rewrite.md | 4 +- MIGRATION.md | 6 +- README.md | 14 ++-- eslint.config.js | 2 +- package-lock.json | 79 +++++++++++++++++-- packages/eslint-config-base/README.md | 36 +++------ packages/eslint-config-base/package.json | 10 +-- packages/eslint-config-base/src/globs.js | 15 +--- packages/eslint-config-base/src/index.js | 2 +- packages/eslint-config-base/src/node-js.js | 48 ----------- packages/eslint-config-base/src/node.js | 50 +++++++----- .../eslint-config-base/src/rules/import-x.js | 19 +---- .../test/completeness.test.js | 10 +-- .../eslint-config-base/test/decisions.test.js | 44 ++++------- .../eslint-config-base/test/defaults.test.js | 41 ++-------- .../test/fixtures/esm-project/package.json | 1 + .../eslint-config-base/test/node-js.test.js | 34 -------- packages/eslint-config-base/test/node.test.js | 50 ++++-------- .../eslint-config-react-typescript/LICENSE | 12 +++ .../eslint-config-react-typescript/README.md | 19 +++++ .../package.json | 62 +++++++++++++++ .../src/index.js | 18 +++++ .../test/completeness.test.js | 17 ++++ .../test/decisions.test.js | 11 +++ .../test/fixtures.test.js | 22 ++++++ .../test/fixtures/ts-project/package.json | 1 + .../test/fixtures/ts-project/src/a.test.tsx | 0 .../test/fixtures/ts-project/src/a.tsx | 1 + .../test/fixtures/ts-project/tsconfig.json | 1 + .../test/helpers.js | 18 +++++ .../test/load.test.js | 10 +++ .../test/rules-exist.test.js | 16 ++++ packages/eslint-config-react/README.md | 13 ++- packages/eslint-config-react/package.json | 13 ++- packages/eslint-config-react/src/index.js | 9 +-- packages/eslint-config-react/src/parts.js | 7 ++ .../eslint-config-react/src/typescript.js | 22 ------ .../test/completeness.test.js | 22 ++---- .../test/decisions.test.js | 5 +- .../eslint-config-react/test/fixtures.test.js | 20 +---- packages/eslint-config-react/test/helpers.js | 2 - .../eslint-config-react/test/load.test.js | 8 +- .../test/rules-exist.test.js | 13 ++- packages/eslint-config-typescript/LICENSE | 12 +++ packages/eslint-config-typescript/README.md | 47 +++++++++++ .../eslint-config-typescript/package.json | 58 ++++++++++++++ .../eslint-config-typescript/src/globs.js | 13 +++ .../src/index.js} | 6 +- packages/eslint-config-typescript/src/node.js | 36 +++++++++ .../src/rules/import-x-typescript.js | 20 +++++ .../src/rules/typescript-eslint.js | 0 .../test/completeness.test.js | 23 ++++++ .../test/decisions.test.js | 47 +++++++++++ .../test/defaults.test.js | 64 +++++++++++++++ .../test/fixtures/ts-project/other/c.test.ts | 0 .../test/fixtures/ts-project/package.json | 0 .../test/fixtures/ts-project/src/a.ts | 0 .../test/fixtures/ts-project/src/b.ts | 0 .../test/fixtures/ts-project/src/shared/x.ts | 0 .../test/fixtures/ts-project/test/a.test.ts | 0 .../test/fixtures/ts-project/tsconfig.json | 0 .../fixtures/ts-project/tsconfig.test.json | 0 .../test/fixtures/ts-project/vitest.config.ts | 0 .../test/fixtures/ts-project/vitest.shared.ts | 0 .../test/node.test.js | 50 ++++++++++++ .../test/typescript.test.js | 4 +- scripts/rule-table.js | 4 +- scripts/smoke-pack.js | 13 +-- 69 files changed, 814 insertions(+), 400 deletions(-) delete mode 100644 packages/eslint-config-base/src/node-js.js create mode 100644 packages/eslint-config-base/test/fixtures/esm-project/package.json delete mode 100644 packages/eslint-config-base/test/node-js.test.js create mode 100644 packages/eslint-config-react-typescript/LICENSE create mode 100644 packages/eslint-config-react-typescript/README.md create mode 100644 packages/eslint-config-react-typescript/package.json create mode 100644 packages/eslint-config-react-typescript/src/index.js create mode 100644 packages/eslint-config-react-typescript/test/completeness.test.js create mode 100644 packages/eslint-config-react-typescript/test/decisions.test.js create mode 100644 packages/eslint-config-react-typescript/test/fixtures.test.js create mode 100644 packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json rename packages/{eslint-config-react => eslint-config-react-typescript}/test/fixtures/ts-project/src/a.test.tsx (100%) create mode 100644 packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx create mode 100644 packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json create mode 100644 packages/eslint-config-react-typescript/test/helpers.js create mode 100644 packages/eslint-config-react-typescript/test/load.test.js create mode 100644 packages/eslint-config-react-typescript/test/rules-exist.test.js create mode 100644 packages/eslint-config-react/src/parts.js delete mode 100644 packages/eslint-config-react/src/typescript.js create mode 100644 packages/eslint-config-typescript/LICENSE create mode 100644 packages/eslint-config-typescript/README.md create mode 100644 packages/eslint-config-typescript/package.json create mode 100644 packages/eslint-config-typescript/src/globs.js rename packages/{eslint-config-base/src/typescript.js => eslint-config-typescript/src/index.js} (94%) create mode 100644 packages/eslint-config-typescript/src/node.js create mode 100644 packages/eslint-config-typescript/src/rules/import-x-typescript.js rename packages/{eslint-config-base => eslint-config-typescript}/src/rules/typescript-eslint.js (100%) create mode 100644 packages/eslint-config-typescript/test/completeness.test.js create mode 100644 packages/eslint-config-typescript/test/decisions.test.js create mode 100644 packages/eslint-config-typescript/test/defaults.test.js rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/other/c.test.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/package.json (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/src/a.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/src/b.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/src/shared/x.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/test/a.test.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/tsconfig.json (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/tsconfig.test.json (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/vitest.config.ts (100%) rename packages/{eslint-config-base => eslint-config-typescript}/test/fixtures/ts-project/vitest.shared.ts (100%) create mode 100644 packages/eslint-config-typescript/test/node.test.js rename packages/{eslint-config-base => eslint-config-typescript}/test/typescript.test.js (96%) diff --git a/.changeset/config.json b/.changeset/config.json index 221bd13..b8e45ac 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,5 +4,13 @@ "commit": false, "access": "public", "baseBranch": "main", - "updateInternalDependencies": "patch" + "updateInternalDependencies": "patch", + "fixed": [ + [ + "@cospired/eslint-config-base", + "@cospired/eslint-config-typescript", + "@cospired/eslint-config-react", + "@cospired/eslint-config-react-typescript" + ] + ] } diff --git a/.changeset/flat-config-rewrite.md b/.changeset/flat-config-rewrite.md index d8ca9f2..16be0ae 100644 --- a/.changeset/flat-config-rewrite.md +++ b/.changeset/flat-config-rewrite.md @@ -1,6 +1,8 @@ --- '@cospired/eslint-config-base': major +'@cospired/eslint-config-typescript': major '@cospired/eslint-config-react': major +'@cospired/eslint-config-react-typescript': major --- -Rewrite for ESLint 10 flat config with JavaScript and TypeScript entries, replacing airbnb and the hapi plugin. See MIGRATION.md. +Rewrite for ESLint 10 flat config, replacing airbnb and the hapi plugin. Four packages: base (JavaScript, plus `/node`), typescript (plus `/node`), react, react-typescript. See MIGRATION.md. diff --git a/MIGRATION.md b/MIGRATION.md index dbc628d..f6254a7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,12 +1,12 @@ # Migrating from 7.x to 8.0 -8.0 is a rewrite. Both packages keep their names. +8.0 is a rewrite. `base` and `react` keep their names; `typescript` and `react-typescript` are new packages, so JavaScript-only projects don't need the TypeScript peers. - **Flat config only, ESLint 10.** `.eslintrc` is gone; use `eslint.config.js` (see the package READMEs). - **No airbnb.** `eslint-config-airbnb(-base)` and `airbnb-typescript` are no longer used or needed. The airbnb rules we kept are listed in [docs/rules.md](docs/rules.md); the rest are dropped. New rule set: ESLint and typescript-eslint recommended plus the rules in that table. - **No hapi plugin.** `@hapi/scope-start` (a blank line at the start of function bodies) cannot coexist with Prettier and is dropped. `@hapi/no-arrowception` is dropped. Blank lines before `return` and after declarations are enforced via `@stylistic/padding-line-between-statements`. -- **TypeScript is a first-class entry:** `@cospired/eslint-config-base/typescript` (and `/node`, `@cospired/eslint-config-react/typescript`). -- **Every entry works with no options:** `export { default } from '@cospired/eslint-config-base/node';` is a complete `eslint.config.js`. Node.js JavaScript projects use `@cospired/eslint-config-base/node-js`. +- **TypeScript has its own packages:** `@cospired/eslint-config-typescript` (and `/node`) and `@cospired/eslint-config-react-typescript`. +- **Every entry works with no options:** `export { default } from '@cospired/eslint-config-typescript/node';` is a complete `eslint.config.js`. Node.js JavaScript projects use `@cospired/eslint-config-base/node`. - **No `warn`.** Every rule is `error` or off. - **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. - **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. diff --git a/README.md b/README.md index 924ba02..41bd780 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ Slightly opinionated ESLint flat configs (ESLint 10) for JavaScript and TypeScript. -| Package | Entry | For | +| Package | Entries | For | |---|---|---| -| [`@cospired/eslint-config-base`](packages/eslint-config-base) | `.` | JavaScript | -| | `./node-js` | Node.js JavaScript | -| | `./typescript` | TypeScript (type-aware) | -| | `./node` | Node.js TypeScript | -| [`@cospired/eslint-config-react`](packages/eslint-config-react) | `.` | React, JavaScript | -| | `./typescript` | React, TypeScript | +| [`@cospired/eslint-config-base`](packages/eslint-config-base) | `.`, `./node` | JavaScript, Node.js JavaScript | +| [`@cospired/eslint-config-typescript`](packages/eslint-config-typescript) | `.`, `./node` | TypeScript (type-aware), Node.js TypeScript | +| [`@cospired/eslint-config-react`](packages/eslint-config-react) | `.` | React JavaScript | +| [`@cospired/eslint-config-react-typescript`](packages/eslint-config-react-typescript) | `.` | React TypeScript | + +Every entry works with no options: `export { default } from '@cospired/eslint-config-typescript/node';` is a complete `eslint.config.js`. Every rule that differs from ESLint/typescript-eslint recommended, with its source and the decision taken, is listed in [docs/rules.md](docs/rules.md). Upgrading from 7.x: [MIGRATION.md](MIGRATION.md). diff --git a/eslint.config.js b/eslint.config.js index 9b89e71..802d3df 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,4 @@ -import nodeJs from './packages/eslint-config-base/src/node-js.js'; +import nodeJs from './packages/eslint-config-base/src/node.js'; // The repo lints itself with the config it ships. export default [ diff --git a/package-lock.json b/package-lock.json index 95a282a..0795e5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -506,6 +506,14 @@ "resolved": "packages/eslint-config-react", "link": true }, + "node_modules/@cospired/eslint-config-react-typescript": { + "resolved": "packages/eslint-config-react-typescript", + "link": true + }, + "node_modules/@cospired/eslint-config-typescript": { + "resolved": "packages/eslint-config-typescript", + "link": true + }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { "version": "4.8.1", "license": "MIT", @@ -2877,12 +2885,9 @@ "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.18.1", "eslint-plugin-todo-plz": "^1.4.1", - "globals": "^17.0.0", - "typescript": ">=5.4 <6.1", - "typescript-eslint": "^8.57.0" + "globals": "^17.14.0" } }, "packages/eslint-config-react": { @@ -2890,18 +2895,76 @@ "version": "7.0.0", "license": "BSD-3-Clause-Clear", "dependencies": { - "@cospired/eslint-config-base": "*" + "@cospired/eslint-config-base": "^7.0.2" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-formatjs": "^8.1.4", - "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-react-refresh": "^0.5.0", - "globals": "^17.0.0" + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0" + } + }, + "packages/eslint-config-react-typescript": { + "name": "@cospired/eslint-config-react-typescript", + "version": "7.0.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2", + "@cospired/eslint-config-react": "^7.0.0", + "@cospired/eslint-config-typescript": "^7.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "typescript": ">=5.4.2 <6.1", + "typescript-eslint": "^8.72.2" + } + }, + "packages/eslint-config-typescript": { + "name": "@cospired/eslint-config-typescript", + "version": "7.0.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "typescript": ">=5.4.2 <6.1", + "typescript-eslint": "^8.72.2" } } } diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index 141534d..88e3ad1 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -1,20 +1,18 @@ # @cospired/eslint-config-base -Slightly opinionated ESLint flat config for JavaScript and TypeScript. Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. +Slightly opinionated ESLint flat config for JavaScript. Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. | Entry | For | |---|---| | `@cospired/eslint-config-base` | JavaScript (environment neutral) | -| `@cospired/eslint-config-base/node-js` | Node.js JavaScript (Node globals, CommonJS or ESM from your `package.json` `type`) | -| `@cospired/eslint-config-base/typescript` | TypeScript, type-aware | -| `@cospired/eslint-config-base/node` | Node.js TypeScript (Node globals) | +| `@cospired/eslint-config-base/node` | Node.js JavaScript (Node globals, CommonJS or ESM from your `package.json` `type`) | + +TypeScript: [`@cospired/eslint-config-typescript`](../eslint-config-typescript). React: [`@cospired/eslint-config-react`](../eslint-config-react) and [`-react-typescript`](../eslint-config-react-typescript). ## Install ```sh -npm i -D @cospired/eslint-config-base eslint @eslint/js typescript-eslint typescript globals \ - @eslint-community/eslint-plugin-eslint-comments @stylistic/eslint-plugin eslint-config-prettier \ - eslint-import-resolver-typescript eslint-plugin-import-x eslint-plugin-todo-plz +npm i -D @cospired/eslint-config-base @eslint-community/eslint-plugin-eslint-comments @eslint/js @stylistic/eslint-plugin eslint eslint-config-prettier eslint-plugin-import-x eslint-plugin-todo-plz globals ``` ## Usage @@ -22,10 +20,10 @@ npm i -D @cospired/eslint-config-base eslint @eslint/js typescript-eslint typesc No options needed. `eslint.config.js`: ```js -export { default } from '@cospired/eslint-config-base/node'; // or /typescript, /node-js, or the package root +export { default } from '@cospired/eslint-config-base/node'; // or the package root for environment-neutral JavaScript ``` -The defaults ignore build output (`dist`, `build`, `coverage`, ...), use typescript-eslint's project service to find your tsconfigs, and lint tests, scripts and package-root TypeScript files (`vitest.config.ts`, ...) without type information, since build tsconfigs usually exclude them. +The defaults ignore build output (`dist`, `build`, `coverage`, ...). `createNodeJsConfig({ cwd })` (from `/node`) takes the directory whose `package.json` decides CommonJS vs ESM. Add your own rules after the config: @@ -35,7 +33,7 @@ import config from '@cospired/eslint-config-base/node'; export default [ ...config, { - files: ['src/domain/**/*.ts'], + files: ['src/domain/**/*.js'], rules: { // project-specific layering rules belong in the project, not in the shared config 'no-restricted-imports': ['error', { paths: [{ name: 'fastify', message: 'domain/ must not import fastify.' }] }], @@ -44,27 +42,11 @@ export default [ ]; ``` -### Options - -`createTypescriptConfig` and `createNodeConfig` (from `.../typescript` and `.../node`) take: - -| Option | Default | | -|---|---|---| -| `tsconfigPath` | project service | tsconfig(s), string or array; every linted file must belong to one | -| `tsconfigRootDir` | cwd | pass `import.meta.dirname` when one ESLint run loads several workspace configs | -| `typedTests` | `false` | lint tests, scripts and root config files type-aware too (they must then be in a tsconfig) | -| `files` | all TS files | files that get type-aware rules | -| `devDependencyGlobs` | tests, `scripts/`, `*.config.*` | files that may import devDependencies | -| `importOrder.pathGroups` | none | extra `import-x/order` groups | - -`createNodeJsConfig({ cwd })` from `.../node-js` takes the directory whose `package.json` decides CommonJS vs ESM. - ## What it enforces -Every rule is organised per plugin in `src/rules/` and listed with its decision in [docs/rules.md](../../docs/rules.md). A test fails when a plugin ships a rule that is neither in a recommended preset nor configured here, so new plugin releases cannot add gaps silently. +Every rule is organised per plugin in `src/rules/`, with a comment block (description, reasoning, link) per rule, and listed with its decision in [docs/rules.md](../../docs/rules.md). A test fails when a plugin ships a rule that is neither in a recommended preset nor configured here, so new plugin releases cannot add gaps silently. - `eslint-disable` comments need a reason (`-- why`); TODOs need a ticket (`TODO(#123): ...`). - Alphabetized imports in groups separated by a blank line; devDependency imports only in test, config and script files. - Blank line before `return` and after declarations. - No `warn` severities: rules are `error` or off. Downgrade locally if you need to. -- TypeScript: type-checked recommended + stylistic, explicit return types outside tests, `no-misused-promises`. diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index 4c21bdf..fd609dd 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -1,7 +1,7 @@ { "name": "@cospired/eslint-config-base", "version": "7.0.2", - "description": "Slightly opinionated ESLint flat config for JavaScript and TypeScript", + "description": "Slightly opinionated ESLint flat config for JavaScript", "type": "module", "files": [ "src" @@ -17,7 +17,6 @@ "eslint", "eslintconfig", "flat-config", - "typescript", "cospired" ], "engines": { @@ -25,9 +24,7 @@ }, "exports": { ".": "./src/index.js", - "./typescript": "./src/typescript.js", "./node": "./src/node.js", - "./node-js": "./src/node-js.js", "./package.json": "./package.json" }, "scripts": { @@ -39,12 +36,9 @@ "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.18.1", "eslint-plugin-todo-plz": "^1.4.1", - "globals": "^17.14.0", - "typescript": ">=5.4.2 <6.1", - "typescript-eslint": "^8.72.2" + "globals": "^17.14.0" }, "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-base#readme", "bugs": { diff --git a/packages/eslint-config-base/src/globs.js b/packages/eslint-config-base/src/globs.js index 504cece..cd33753 100644 --- a/packages/eslint-config-base/src/globs.js +++ b/packages/eslint-config-base/src/globs.js @@ -1,16 +1,5 @@ -/** File globs shared by the configs. */ -export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; /** Patterns are relative to the eslint config's directory; the `{apps,packages}/*` twins cover one root config linting a monorepo. */ export const withMonorepoTwins = (globs) => globs.flatMap((glob) => [glob, `{apps,packages}/*/${glob}`]); -export const testGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']); -/** Any language: the same test files for rules that also apply to JS (formatjs, devDependency imports). */ -export const anyTestGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', 'test/**', 'tests/**', '**/__tests__/**']); -/** JS files can be matched by a consumer's `files`; they are never type-checked. */ -export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; -/** - * Files linted without type information unless `typedTests: true`: tests, TypeScript files at the package root - * (vitest.config.ts, drizzle.config.ts, ...) and scripts. They are often not part of any tsconfig (build configs - * exclude them), and type-aware linting reports a parsing error for such files. - */ -export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; +/** Test files in any language: used for rules that also apply to JS (formatjs, import order of `vi.mock()`). */ +export const anyTestGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', 'test/**', 'tests/**', '**/__tests__/**']); diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index 48d74e8..7d16769 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -9,7 +9,7 @@ import { createImportsConfig } from './rules/import-x.js'; import stylistic from './rules/stylistic.js'; import todoPlz from './rules/todo-plz.js'; -export { anyTestGlobs, testGlobs, withMonorepoTwins } from './globs.js'; +export { anyTestGlobs, withMonorepoTwins } from './globs.js'; export { globalIgnores } from './ignores.js'; export { createImportsConfig, defaultDevDependencyGlobs } from './rules/import-x.js'; diff --git a/packages/eslint-config-base/src/node-js.js b/packages/eslint-config-base/src/node-js.js deleted file mode 100644 index 82ae327..0000000 --- a/packages/eslint-config-base/src/node-js.js +++ /dev/null @@ -1,48 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import globals from 'globals'; - -import baseConfig, { globalIgnores } from './index.js'; - -export { globalIgnores }; - -/** `module` when the nearest package.json at or above `cwd` says `"type": "module"`, otherwise `commonjs`. */ -function detectSourceType(cwd) { - for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) { - const file = path.join(dir, 'package.json'); - - if (fs.existsSync(file)) { - try { - return JSON.parse(fs.readFileSync(file, 'utf8')).type === 'module' ? 'module' : 'commonjs'; - } catch { - return 'commonjs'; - } - } - if (path.dirname(dir) === dir) return 'commonjs'; - } -} - -/** - * Node.js JavaScript config: base JS rules, Node globals, and `.js` files parsed as CommonJS or ESM depending on - * the package's `"type"`. `.cjs`/`.mjs` keep ESLint's own defaults. - * - * @param {{ cwd?: string }} [options] Directory whose package.json decides the module type; defaults to the working directory. - * @returns {import('eslint').Linter.Config[]} - */ -export function createNodeJsConfig({ cwd = process.cwd() } = {}) { - const sourceType = detectSourceType(cwd); - - return [ - ...baseConfig, - { - name: 'cospired/node-js', - // CommonJS wrappers (require, module, __dirname) are only defined in CommonJS packages. - languageOptions: { globals: sourceType === 'module' ? globals.nodeBuiltin : globals.node }, - }, - { name: 'cospired/node-js-source-type', files: ['**/*.js'], languageOptions: { sourceType } }, - ]; -} - -/** Node JavaScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node-js'`. */ -export default createNodeJsConfig(); diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 9f749bd..82ae327 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,34 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + import globals from 'globals'; -import { jsFiles, untypedGlobs } from './globs.js'; -import { createTypescriptConfig } from './typescript.js'; +import baseConfig, { globalIgnores } from './index.js'; + +export { globalIgnores }; -export { globalIgnores } from './ignores.js'; +/** `module` when the nearest package.json at or above `cwd` says `"type": "module"`, otherwise `commonjs`. */ +function detectSourceType(cwd) { + for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) { + const file = path.join(dir, 'package.json'); + if (fs.existsSync(file)) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')).type === 'module' ? 'module' : 'commonjs'; + } catch { + return 'commonjs'; + } + } + if (path.dirname(dir) === dir) return 'commonjs'; + } +} /** - * Node.js/backend TypeScript config: `createTypescriptConfig` plus node globals. - * Takes the same options. + * Node.js JavaScript config: base JS rules, Node globals, and `.js` files parsed as CommonJS or ESM depending on + * the package's `"type"`. `.cjs`/`.mjs` keep ESLint's own defaults. * - * @param {Parameters[0]} [options] + * @param {{ cwd?: string }} [options] Directory whose package.json decides the module type; defaults to the working directory. * @returns {import('eslint').Linter.Config[]} */ -export function createNodeConfig(options = {}) { - const { files = ['**/*.{ts,tsx,mts,cts}'], typedTests = false } = options; +export function createNodeJsConfig({ cwd = process.cwd() } = {}) { + const sourceType = detectSourceType(cwd); return [ - ...createTypescriptConfig(options), - { languageOptions: { sourceType: 'module', globals: globals.node } }, + ...baseConfig, { - files, - ignores: typedTests ? jsFiles : [...jsFiles, ...untypedGlobs], - rules: { - // String `||` is deliberate empty-string fallthrough (env vars, trimmed input) that `??` would silently drop. - '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], - }, + name: 'cospired/node-js', + // CommonJS wrappers (require, module, __dirname) are only defined in CommonJS packages. + languageOptions: { globals: sourceType === 'module' ? globals.nodeBuiltin : globals.node }, }, + { name: 'cospired/node-js-source-type', files: ['**/*.js'], languageOptions: { sourceType } }, ]; } -/** Node TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node'`. */ -export default createNodeConfig(); +/** Node JavaScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node-js'`. */ +export default createNodeJsConfig(); diff --git a/packages/eslint-config-base/src/rules/import-x.js b/packages/eslint-config-base/src/rules/import-x.js index eea9836..f512fae 100644 --- a/packages/eslint-config-base/src/rules/import-x.js +++ b/packages/eslint-config-base/src/rules/import-x.js @@ -1,7 +1,6 @@ -import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; import importX, { flatConfigs } from 'eslint-plugin-import-x'; -import { tsExtensions, withMonorepoTwins } from '../globs.js'; +import { withMonorepoTwins } from '../globs.js'; /** * Where a devDependency import is allowed. Patterns are relative to the directory of the eslint config file; @@ -88,19 +87,3 @@ export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyG }, }; } - -/** TypeScript-only import-x config: the typescript preset and a type-import style that matches `consistent-type-imports`. */ -export const importXTypescript = flatConfigs.typescript; -export const importXTypescriptRules = { 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'] }; - -/** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ -export function importSettings(project) { - return { - 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs', ...tsExtensions], - 'import-x/parsers': { '@typescript-eslint/parser': tsExtensions }, - 'import-x/resolver-next': [ - // Several projects is the intended shape (app + test-only tsconfigs); the suggested `references` fix needs `composite`. - createTypeScriptImportResolver({ alwaysTryTypes: true, noWarnOnMultipleProjects: true, ...(project && { project }) }), - ], - }; -} diff --git a/packages/eslint-config-base/test/completeness.test.js b/packages/eslint-config-base/test/completeness.test.js index 3ce1536..3e6ae9a 100644 --- a/packages/eslint-config-base/test/completeness.test.js +++ b/packages/eslint-config-base/test/completeness.test.js @@ -2,18 +2,14 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; -import { baseNamespaces, pluginRules, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import { baseNamespaces, pluginRules } from '../../../test-utils/plugins.js'; import jsConfig from '../src/index.js'; -import nodeJs from '../src/node-js.js'; -import { createNodeConfig } from '../src/node.js'; -import { createTypescriptConfig } from '../src/typescript.js'; +import nodeJs from '../src/node.js'; // A rule counts as configured when a recommended preset or one of our files sets it (on or off). const cases = [ ['base JS', jsConfig, baseNamespaces], - ['base TypeScript', createTypescriptConfig(), typescriptNamespaces], - ['node', createNodeConfig(), typescriptNamespaces], - ['node JS', nodeJs, baseNamespaces], + ['node', nodeJs, baseNamespaces], ]; for (const [name, config, namespaces] of cases) { diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index bc8b846..08a43ca 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -3,14 +3,12 @@ import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; -import { loadedRules, lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; +import { loadedRules, lintText, ruleIds, warnRules } from '../../../test-utils/lint.js'; +import { anyTestGlobs } from '../src/globs.js'; import config from '../src/index.js'; -import { createNodeConfig } from '../src/node.js'; -import { createTypescriptConfig } from '../src/typescript.js'; +import nodeConfig from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); -const tsOptions = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; -const tsConfig = createTypescriptConfig(tsOptions); +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'esm-project'); const lint = async (code, file = 'src/a.js', cfg = config) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); // [rule, code that must be flagged, code that must pass] @@ -57,39 +55,27 @@ test('import-x named-as-default rules are off', () => { assert.ok(!rules.has('import-x/no-named-as-default') && !rules.has('import-x/no-named-as-default-member')); }); -test('no rule resolves to "warn" in any base config', async () => { +test('no rule resolves to "warn" in the JS configs', async () => { assert.deepEqual(await warnRules(config, path.join(root, 'src/a.js'), root), []); - assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.ts'), root), []); - assert.deepEqual(await warnRules(createNodeConfig(tsOptions), path.join(root, 'src/a.ts'), root), []); + assert.deepEqual(await warnRules(nodeConfig, path.join(root, 'src/a.js'), root), []); }); -test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { - const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts'), root); +test('scripts/ files get node globals', async () => { + const found = await lint('process.exit(1);\n', 'scripts/run.mjs'); - for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); - for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); + assert.ok(!found.includes('no-undef'), 'process should be defined in scripts/'); + assert.ok((await lint('process.exit(1);\n', 'src/run.mjs')).includes('no-undef')); }); -test('scripts/ files get node globals in every base config', async () => { - for (const [name, cfg] of [['js', config], ['ts', tsConfig]]) { - const found = await lint('process.exit(1);\n', 'scripts/run.mjs', cfg); - - assert.ok(!found.includes('no-undef'), `${name}: process should be defined in scripts/`); - } - assert.ok((await lint('process.exit(1);\n', 'src/run.mjs', tsConfig)).includes('no-undef')); -}); - -test('test-file globs also match from a monorepo root config', async () => { - const { testGlobs } = await import('../src/globs.js'); - - for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts}']) { - assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); +test('test-file globs also match from a monorepo root config', () => { + for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']) { + assert.ok(anyTestGlobs.includes(glob) && anyTestGlobs.includes(`{apps,packages}/*/${glob}`), glob); } }); test('console is allowed in scripts of every extension', async () => { - for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { - const found = await lint('console.log(1);\n', `scripts/x.${ext}`, createTypescriptConfig(tsOptions)); + for (const ext of ['js', 'mjs', 'cjs']) { + const found = await lint('console.log(1);\n', `scripts/x.${ext}`); assert.ok(!found.includes('no-console'), ext); } diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index bbeecb4..6a0b6a1 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -6,51 +6,20 @@ import { fileURLToPath } from 'node:url'; import { lintText } from '../../../test-utils/lint.js'; import jsDefault from '../src/index.js'; import nodeDefault from '../src/node.js'; -import tsDefault, { createTypescriptConfig } from '../src/typescript.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'esm-project'); const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; test('default exports are ready-made config arrays that include the global ignores', () => { - for (const cfg of [jsDefault, tsDefault, nodeDefault]) { + for (const cfg of [jsDefault, nodeDefault]) { assert.ok(Array.isArray(cfg)); assert.ok(cfg.some((item) => item.ignores?.includes('**/dist/**'))); } }); -test('zero options: TS files lint via the project service, no parse errors', async () => { - for (const cfg of [tsDefault, nodeDefault]) { - for (const file of ['src/a.ts', 'src/b.ts']) { - const messages = await lint(cfg, 'export const v = 1;\n', file); - - assert.deepEqual(messages.filter((m) => m.fatal), [], file); - } - } -}); - -test('zero options: test and config files outside every tsconfig lint without type information', async () => { - for (const file of ['other/c.test.ts', 'vitest.config.ts', 'vitest.shared.ts']) { - for (const cfg of [tsDefault, nodeDefault]) { - const messages = await lint(cfg, 'export const v = 1;\n', file); - - assert.deepEqual(messages.filter((m) => m.fatal), [], file); - } - } -}); - -test('typedTests: true lints tests with type information (they must be in a tsconfig)', async () => { - const typed = createTypescriptConfig({ typedTests: true, tsconfigRootDir: root }); - const outside = await lint(typed, 'export const v = 1;\n', 'other/c.test.ts'); - - assert.ok(outside.some((m) => m.fatal), 'a test outside every tsconfig reports a parsing error'); - const inside = await lint(typed, 'export const v = 1;\n', 'src/a.ts'); - - assert.deepEqual(inside.filter((m) => m.fatal), []); -}); - test('zero options: dist output is ignored', async () => { - const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); + const messages = await lint(jsDefault, 'var x = 1;\n', 'dist/a.js'); assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); }); @@ -58,6 +27,6 @@ test('zero options: dist output is ignored', async () => { test('import-x/first is off in tests (vi.mock hoisting) and on in source', async () => { const code = "vi.mock('./a.js');\n\nimport { a } from './a.js';\n\nexport const v = a;\n"; - assert.ok(!(await ruleIdsFor(tsDefault, code, 'other/c.test.ts')).includes('import-x/first')); - assert.ok((await ruleIdsFor(tsDefault, code, 'src/b.ts')).includes('import-x/first')); + assert.ok(!(await ruleIdsFor(jsDefault, code, 'src/b.test.js')).includes('import-x/first')); + assert.ok((await ruleIdsFor(jsDefault, code, 'src/b.js')).includes('import-x/first')); }); diff --git a/packages/eslint-config-base/test/fixtures/esm-project/package.json b/packages/eslint-config-base/test/fixtures/esm-project/package.json new file mode 100644 index 0000000..a398f8f --- /dev/null +++ b/packages/eslint-config-base/test/fixtures/esm-project/package.json @@ -0,0 +1 @@ +{ "name": "esm-fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-base/test/node-js.test.js b/packages/eslint-config-base/test/node-js.test.js deleted file mode 100644 index 665126b..0000000 --- a/packages/eslint-config-base/test/node-js.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import test from 'node:test'; -import { fileURLToPath } from 'node:url'; - -import { lintText, ruleIds } from '../../../test-utils/lint.js'; -import nodeJsDefault, { createNodeJsConfig } from '../src/node-js.js'; - -const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures'); -const cjs = path.join(fixtures, 'cjs-project'); -const esm = path.join(fixtures, 'ts-project'); -const cjsCode = "const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"; -const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(createNodeJsConfig({ cwd }), code, { filePath: path.join(cwd, file), cwd })); - -test('CommonJS package (no "type"): require/module/process are defined', async () => { - const found = await ids(cjs, cjsCode); - - assert.ok(!found.includes('no-undef'), found.join()); -}); - -test('ESM package ("type": "module"): CommonJS globals are not defined', async () => { - assert.ok((await ids(esm, cjsCode)).includes('no-undef')); -}); - -test('node globals exist either way', async () => { - const code = 'export const c = process.cwd();\n'; - - assert.ok(!(await ids(esm, code)).includes('no-undef')); -}); - -test('default export is a ready-made config array with the global ignores', () => { - assert.ok(Array.isArray(nodeJsDefault)); - assert.ok(nodeJsDefault.some((item) => item.ignores?.includes('**/dist/**'))); -}); diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js index dd284dd..ad6825e 100644 --- a/packages/eslint-config-base/test/node.test.js +++ b/packages/eslint-config-base/test/node.test.js @@ -4,47 +4,31 @@ import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { lintText, ruleIds } from '../../../test-utils/lint.js'; -import { createNodeConfig } from '../src/node.js'; -import { createTypescriptConfig } from '../src/typescript.js'; +import nodeJsDefault, { createNodeJsConfig } from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); -const options = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; -const node = createNodeConfig(options); -const ids = async (cfg, code, file) => { - const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); +const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures'); +const cjs = path.join(fixtures, 'cjs-project'); +const esm = path.join(fixtures, 'esm-project'); +const cjsCode = "const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"; +const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(createNodeJsConfig({ cwd }), code, { filePath: path.join(cwd, file), cwd })); - assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); +test('CommonJS package (no "type"): require/module/process are defined', async () => { + const found = await ids(cjs, cjsCode); - return ruleIds(messages); -}; - -test('node globals are defined (process is not no-undef)', async () => { - const code = 'process.exit(1);\n'; - - assert.ok((await ids(createTypescriptConfig(options), code, 'src/x.mjs')).includes('no-undef')); - assert.ok(!(await ids(node, code, 'src/x.mjs')).includes('no-undef')); + assert.ok(!found.includes('no-undef'), found.join()); }); -test('string || fallback is allowed, number || fallback is flagged', async () => { - const rule = '@typescript-eslint/prefer-nullish-coalescing'; - const str = "export function f(s: string | undefined): string {\n return s || 'x';\n}\n"; - const num = 'export function f(n: number | undefined): number {\n return n || 1;\n}\n'; - - assert.ok(!(await ids(node, str, 'src/a.ts')).includes(rule)); - assert.ok((await ids(node, num, 'src/a.ts')).includes(rule)); +test('ESM package ("type": "module"): CommonJS globals are not defined', async () => { + assert.ok((await ids(esm, cjsCode)).includes('no-undef')); }); -test('no-console is allowed in scripts/ but not in src/', async () => { - const code = 'console.log(1);\n'; +test('node globals exist either way', async () => { + const code = 'export const c = process.cwd();\n'; - assert.ok(!(await ids(node, code, 'scripts/run.mjs')).includes('no-console')); - assert.ok((await ids(node, code, 'src/run.mjs')).includes('no-console')); + assert.ok(!(await ids(esm, code)).includes('no-undef')); }); -test('JS files matched by `files` are linted without type-aware rules (no crash)', async () => { - const cfg = createNodeConfig({ ...options, files: ['src/**/*.{ts,js}', '*.{ts,js}'] }); - const found = await ids(cfg, 'export const a = (x) => x || 1;\n', 'src/c.js'); - - assert.ok(!found.includes('@typescript-eslint/prefer-nullish-coalescing')); - assert.ok(!found.includes('@typescript-eslint/explicit-function-return-type')); +test('default export is a ready-made config array with the global ignores', () => { + assert.ok(Array.isArray(nodeJsDefault)); + assert.ok(nodeJsDefault.some((item) => item.ignores?.includes('**/dist/**'))); }); diff --git a/packages/eslint-config-react-typescript/LICENSE b/packages/eslint-config-react-typescript/LICENSE new file mode 100644 index 0000000..11b5315 --- /dev/null +++ b/packages/eslint-config-react-typescript/LICENSE @@ -0,0 +1,12 @@ +The Clear BSD License + +Copyright (c) 2018-2019 cospired GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of cospired GmbH nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/eslint-config-react-typescript/README.md b/packages/eslint-config-react-typescript/README.md new file mode 100644 index 0000000..a00ed74 --- /dev/null +++ b/packages/eslint-config-react-typescript/README.md @@ -0,0 +1,19 @@ +# @cospired/eslint-config-react-typescript + +React flat config for TypeScript: [`@cospired/eslint-config-typescript`](../eslint-config-typescript) plus the React rules of [`@cospired/eslint-config-react`](../eslint-config-react). Requires ESLint 10. + +## Install + +```sh +npm i -D @cospired/eslint-config-react-typescript @eslint-community/eslint-plugin-eslint-comments @eslint/js @stylistic/eslint-plugin eslint eslint-config-prettier eslint-plugin-import-x eslint-plugin-todo-plz globals eslint-import-resolver-typescript typescript typescript-eslint eslint-plugin-formatjs eslint-plugin-react-hooks eslint-plugin-react-refresh +``` + +## Usage + +No options needed. `eslint.config.js`: + +```js +export { default } from '@cospired/eslint-config-react-typescript'; +``` + +`createReactTypescriptConfig` takes the same options as `createTypescriptConfig` in the TypeScript package (`tsconfigPath`, `tsconfigRootDir`, `typedTests`, ...). Add project-specific rules after the config. diff --git a/packages/eslint-config-react-typescript/package.json b/packages/eslint-config-react-typescript/package.json new file mode 100644 index 0000000..d3e4849 --- /dev/null +++ b/packages/eslint-config-react-typescript/package.json @@ -0,0 +1,62 @@ +{ + "name": "@cospired/eslint-config-react-typescript", + "version": "7.0.0", + "description": "Slightly opinionated ESLint flat config for React with TypeScript", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "author": "Dirk Haage ", + "keywords": [ + "eslint", + "eslintconfig", + "flat-config", + "cospired" + ], + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "scripts": { + "test": "node --test test/*.test.js" + }, + "exports": { + ".": "./src/index.js", + "./package.json": "./package.json" + }, + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2", + "@cospired/eslint-config-react": "^7.0.0", + "@cospired/eslint-config-typescript": "^7.0.0" + }, + "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "eslint-import-resolver-typescript": "^4.4.5", + "typescript": ">=5.4.2 <6.1", + "typescript-eslint": "^8.72.2", + "eslint-plugin-formatjs": "^8.1.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cospired/javascript.git", + "directory": "packages/eslint-config-react-typescript" + }, + "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-react-typescript#readme", + "bugs": { + "url": "https://github.com/cospired/javascript/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "sideEffects": false +} diff --git a/packages/eslint-config-react-typescript/src/index.js b/packages/eslint-config-react-typescript/src/index.js new file mode 100644 index 0000000..d8eee3e --- /dev/null +++ b/packages/eslint-config-react-typescript/src/index.js @@ -0,0 +1,18 @@ +import { prettierLast } from '@cospired/eslint-config-base'; +import { reactParts } from '@cospired/eslint-config-react/parts'; +import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-typescript'; + +export { globalIgnores }; + +/** + * React TypeScript config: TypeScript rules + React rules. Takes the same options as `createTypescriptConfig`. + * + * @param {Parameters[0]} [options] + * @returns {import('eslint').Linter.Config[]} + */ +export function createReactTypescriptConfig(options = {}) { + return [...createTypescriptConfig(options), ...reactParts, ...prettierLast]; +} + +/** React TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-react-typescript'`. */ +export default createReactTypescriptConfig(); diff --git a/packages/eslint-config-react-typescript/test/completeness.test.js b/packages/eslint-config-react-typescript/test/completeness.test.js new file mode 100644 index 0000000..3193445 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/completeness.test.js @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; +import { pluginRules, reactNamespaces, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import { createReactTypescriptConfig } from '../src/index.js'; + +const namespaces = [...typescriptNamespaces, ...reactNamespaces]; +const config = createReactTypescriptConfig(); + +test(`every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(config, namespaces, pluginRules), []); +}); + +test('our overrides only repeat nothing the presets already set', () => { + assert.deepEqual(redundantRules(config), []); +}); diff --git a/packages/eslint-config-react-typescript/test/decisions.test.js b/packages/eslint-config-react-typescript/test/decisions.test.js new file mode 100644 index 0000000..62f2351 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/decisions.test.js @@ -0,0 +1,11 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; + +import { warnRules } from '../../../test-utils/lint.js'; + +import { root, tsConfig } from './helpers.js'; + +test('no rule resolves to "warn" in the React TypeScript config', async () => { + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.tsx'), root), []); +}); diff --git a/packages/eslint-config-react-typescript/test/fixtures.test.js b/packages/eslint-config-react-typescript/test/fixtures.test.js new file mode 100644 index 0000000..740d502 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/fixtures.test.js @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createReactTypescriptConfig } from '../src/index.js'; + +import { ids, tsConfig } from './helpers.js'; + +const message = 'declare const intl: { formatMessage: (d: { id: string }) => string };\n\nexport const m = intl.formatMessage({ id: "a" });\n'; + +test('formatjs flags a missing defaultMessage in src, not in tests', async () => { + assert.ok((await ids(tsConfig, message, 'src/a.tsx')).includes('formatjs/enforce-default-message')); + assert.ok(!(await ids(tsConfig, message, 'src/a.test.tsx')).includes('formatjs/enforce-default-message')); +}); + +test('two calls to createReactTypescriptConfig return independent arrays', () => { + const a = createReactTypescriptConfig({ tsconfigRootDir: '/a' }); + const b = createReactTypescriptConfig({ tsconfigRootDir: '/b' }); + + assert.notEqual(a, b); + a.length = 0; + assert.ok(b.length > 0); +}); diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json b/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json new file mode 100644 index 0000000..6a2659b --- /dev/null +++ b/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json @@ -0,0 +1 @@ +{ "name": "fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx b/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.test.tsx similarity index 100% rename from packages/eslint-config-react/test/fixtures/ts-project/src/a.test.tsx rename to packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.test.tsx diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx b/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx @@ -0,0 +1 @@ +export const a = 1; diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json new file mode 100644 index 0000000..eb46ae9 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-react-typescript/test/helpers.js b/packages/eslint-config-react-typescript/test/helpers.js new file mode 100644 index 0000000..39e581c --- /dev/null +++ b/packages/eslint-config-react-typescript/test/helpers.js @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { createReactTypescriptConfig } from '../src/index.js'; + +export const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +export const tsConfig = createReactTypescriptConfig({ tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }); + +/** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ +export async function ids(cfg, code, file) { + const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); + + return ruleIds(messages); +} diff --git a/packages/eslint-config-react-typescript/test/load.test.js b/packages/eslint-config-react-typescript/test/load.test.js new file mode 100644 index 0000000..b996dc5 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/load.test.js @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ids, tsConfig } from './helpers.js'; + +test('JSX parses in .tsx under the TS config', async () => { + const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/a.tsx'); + + assert.ok(Array.isArray(found)); +}); diff --git a/packages/eslint-config-react-typescript/test/rules-exist.test.js b/packages/eslint-config-react-typescript/test/rules-exist.test.js new file mode 100644 index 0000000..97c95be --- /dev/null +++ b/packages/eslint-config-react-typescript/test/rules-exist.test.js @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, missingRules } from '../../../test-utils/lint.js'; + +import { tsConfig } from './helpers.js'; + +test('every configured rule exists', () => { + assert.deepEqual(missingRules(tsConfig), []); +}); + +test('React TypeScript config enables hooks, refresh, formatjs and typescript-eslint rules', () => { + const rules = loadedRules(tsConfig); + + for (const id of ['react-hooks/rules-of-hooks', 'react-refresh/only-export-components', 'formatjs/enforce-default-message', '@typescript-eslint/no-non-null-assertion']) assert.ok(rules.has(id), id); +}); diff --git a/packages/eslint-config-react/README.md b/packages/eslint-config-react/README.md index 01c5865..88900d7 100644 --- a/packages/eslint-config-react/README.md +++ b/packages/eslint-config-react/README.md @@ -1,24 +1,21 @@ # @cospired/eslint-config-react -React flat config on top of [`@cospired/eslint-config-base`](../eslint-config-base): react-hooks, react-refresh (Vite), formatjs (i18n) and browser globals. Requires ESLint 10. +React flat config (JavaScript) on top of [`@cospired/eslint-config-base`](../eslint-config-base): react-hooks, react-refresh (Vite), formatjs (i18n) and browser globals. Requires ESLint 10. For TypeScript use [`@cospired/eslint-config-react-typescript`](../eslint-config-react-typescript). ## Install ```sh -npm i -D @cospired/eslint-config-react eslint eslint-config-prettier globals \ - eslint-plugin-react-hooks eslint-plugin-react-refresh eslint-plugin-formatjs +npm i -D @cospired/eslint-config-react @eslint-community/eslint-plugin-eslint-comments @eslint/js @stylistic/eslint-plugin eslint eslint-config-prettier eslint-plugin-import-x eslint-plugin-todo-plz globals eslint-plugin-formatjs eslint-plugin-react-hooks eslint-plugin-react-refresh ``` -The peer dependencies of `@cospired/eslint-config-base` are required as well (see its README). - ## Usage No options needed. `eslint.config.js`: ```js -export { default } from '@cospired/eslint-config-react/typescript'; // or '@cospired/eslint-config-react' for JavaScript +export { default } from '@cospired/eslint-config-react'; ``` -`createReactTypescriptConfig` takes the same options as `createTypescriptConfig` in the base package (`tsconfigPath`, `tsconfigRootDir`, `typedTests`, ...). Add project-specific rules after the config, as described in the base README. +Add project-specific rules after the config, as described in the base README. formatjs rules apply to all files except tests. jsx-a11y is not included yet: `eslint-plugin-jsx-a11y` does not support ESLint 10. -formatjs rules apply to all files except tests. jsx-a11y is not included yet: `eslint-plugin-jsx-a11y` does not support ESLint 10. +The `/parts` entry exports the React-specific config objects (`reactParts`) for the TypeScript package. diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index 7b3f9ff..37c4b5c 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -1,7 +1,7 @@ { "name": "@cospired/eslint-config-react", "version": "7.0.0", - "description": "Slightly opinionated ESLint flat config for React (JavaScript and TypeScript)", + "description": "Slightly opinionated ESLint flat config for React (JavaScript)", "type": "module", "files": [ "src" @@ -25,7 +25,7 @@ }, "exports": { ".": "./src/index.js", - "./typescript": "./src/typescript.js", + "./parts": "./src/parts.js", "./package.json": "./package.json" }, "scripts": { @@ -35,12 +35,17 @@ "@cospired/eslint-config-base": "^7.0.2" }, "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", "eslint-plugin-formatjs": "^8.1.4", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.7", - "globals": "^17.14.0" + "eslint-plugin-react-refresh": "^0.5.7" }, "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-react#readme", "bugs": { diff --git a/packages/eslint-config-react/src/index.js b/packages/eslint-config-react/src/index.js index c8f86e6..5867170 100644 --- a/packages/eslint-config-react/src/index.js +++ b/packages/eslint-config-react/src/index.js @@ -1,11 +1,8 @@ import baseConfig, { globalIgnores, prettierLast } from '@cospired/eslint-config-base'; -import formatjs from './rules/formatjs.js'; -import reactHooks from './rules/react-hooks.js'; -import reactRefresh from './rules/react-refresh.js'; -import react from './rules/react.js'; +import { reactParts } from './parts.js'; export { globalIgnores }; -/** React JavaScript config: base JS + React rules. */ -export default [...baseConfig, ...react, ...reactHooks, reactRefresh, formatjs, ...prettierLast]; +/** React JavaScript config: base JS + React rules, ready to use as `export { default } from '@cospired/eslint-config-react'`. */ +export default [...baseConfig, ...reactParts, ...prettierLast]; diff --git a/packages/eslint-config-react/src/parts.js b/packages/eslint-config-react/src/parts.js new file mode 100644 index 0000000..424af6e --- /dev/null +++ b/packages/eslint-config-react/src/parts.js @@ -0,0 +1,7 @@ +import formatjs from './rules/formatjs.js'; +import reactHooks from './rules/react-hooks.js'; +import reactRefresh from './rules/react-refresh.js'; +import react from './rules/react.js'; + +/** The React-specific config objects, shared by the JavaScript and TypeScript React configs. Prettier must follow them. */ +export const reactParts = [...react, ...reactHooks, reactRefresh, formatjs]; diff --git a/packages/eslint-config-react/src/typescript.js b/packages/eslint-config-react/src/typescript.js deleted file mode 100644 index 39cef64..0000000 --- a/packages/eslint-config-react/src/typescript.js +++ /dev/null @@ -1,22 +0,0 @@ -import { prettierLast } from '@cospired/eslint-config-base'; -import { createTypescriptConfig, globalIgnores } from '@cospired/eslint-config-base/typescript'; - -import formatjs from './rules/formatjs.js'; -import reactHooks from './rules/react-hooks.js'; -import reactRefresh from './rules/react-refresh.js'; -import react from './rules/react.js'; - -export { globalIgnores }; - -/** - * React TypeScript config: base TypeScript + React rules. Takes the same options as `createTypescriptConfig`. - * - * @param {Parameters[0]} [options] - * @returns {import('eslint').Linter.Config[]} - */ -export function createReactTypescriptConfig(options = {}) { - return [...createTypescriptConfig(options), ...react, ...reactHooks, reactRefresh, formatjs, ...prettierLast]; -} - -/** React TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-react/typescript'`. */ -export default createReactTypescriptConfig(); diff --git a/packages/eslint-config-react/test/completeness.test.js b/packages/eslint-config-react/test/completeness.test.js index 06e30ce..91b5633 100644 --- a/packages/eslint-config-react/test/completeness.test.js +++ b/packages/eslint-config-react/test/completeness.test.js @@ -2,21 +2,15 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; -import { baseNamespaces, pluginRules, reactNamespaces, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import { baseNamespaces, pluginRules, reactNamespaces } from '../../../test-utils/plugins.js'; import jsConfig from '../src/index.js'; -import { createReactTypescriptConfig } from '../src/typescript.js'; -const cases = [ - ['react JS', jsConfig, [...baseNamespaces, ...reactNamespaces]], - ['react TypeScript', createReactTypescriptConfig(), [...typescriptNamespaces, ...reactNamespaces]], -]; +const namespaces = [...baseNamespaces, ...reactNamespaces]; -for (const [name, config, namespaces] of cases) { - test(`${name}: every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { - assert.deepEqual(unconfiguredRules(config, namespaces, pluginRules), []); - }); +test(`every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(jsConfig, namespaces, pluginRules), []); +}); - test(`${name}: our overrides only repeat nothing the presets already set`, () => { - assert.deepEqual(redundantRules(config), []); - }); -} +test('our overrides only repeat nothing the presets already set', () => { + assert.deepEqual(redundantRules(jsConfig), []); +}); diff --git a/packages/eslint-config-react/test/decisions.test.js b/packages/eslint-config-react/test/decisions.test.js index 38ae37a..7d04027 100644 --- a/packages/eslint-config-react/test/decisions.test.js +++ b/packages/eslint-config-react/test/decisions.test.js @@ -4,11 +4,10 @@ import test from 'node:test'; import { ruleSeverity, warnRules } from '../../../test-utils/lint.js'; -import { ids, jsConfig, root, tsConfig } from './helpers.js'; +import { ids, jsConfig, root } from './helpers.js'; -test('no rule resolves to "warn" in the React configs', async () => { +test('no rule resolves to "warn" in the React config', async () => { assert.deepEqual(await warnRules(jsConfig, path.join(root, 'src/a.jsx'), root), []); - assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.tsx'), root), []); }); test('hooks exhaustive-deps, refresh and formatjs literal-string rules are errors', async () => { diff --git a/packages/eslint-config-react/test/fixtures.test.js b/packages/eslint-config-react/test/fixtures.test.js index d219158..ba62e05 100644 --- a/packages/eslint-config-react/test/fixtures.test.js +++ b/packages/eslint-config-react/test/fixtures.test.js @@ -1,22 +1,14 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { ids, jsConfig, tsConfig } from './helpers.js'; +import { ids, jsConfig } from './helpers.js'; const hook = 'function useThing() {\n return 1;\n}\n\nexport function A({ c }) {\n if (c) {\n useThing();\n }\n\n return null;\n}\n'; -const message = 'export const m = intl.formatMessage({ id: "a" });\n'; test('hooks rule flags a conditional hook call', async () => { assert.ok((await ids(jsConfig, hook, 'src/a.jsx')).includes('react-hooks/rules-of-hooks')); }); -test('formatjs flags a missing defaultMessage in src, not in tests', async () => { - const typed = `declare const intl: { formatMessage: (d: { id: string }) => string };\n\n${ message}`; - - assert.ok((await ids(tsConfig, typed, 'src/a.tsx')).includes('formatjs/enforce-default-message')); - assert.ok(!(await ids(tsConfig, typed, 'src/a.test.tsx')).includes('formatjs/enforce-default-message')); -}); - test('browser globals are defined', async () => { assert.ok(!(await ids(jsConfig, 'export const w = window.innerWidth;\n', 'src/a.js')).includes('no-undef')); }); @@ -26,13 +18,3 @@ test('react-refresh flags a non-component export next to a component', async () assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('react-refresh/only-export-components')); }); - -test('two calls to createReactTypescriptConfig return independent arrays', async () => { - const { createReactTypescriptConfig } = await import('../src/typescript.js'); - const a = createReactTypescriptConfig({ tsconfigRootDir: '/a' }); - const b = createReactTypescriptConfig({ tsconfigRootDir: '/b' }); - - assert.notEqual(a, b); - a.length = 0; - assert.ok(b.length > 0); -}); diff --git a/packages/eslint-config-react/test/helpers.js b/packages/eslint-config-react/test/helpers.js index 3a4fdf7..3ecc428 100644 --- a/packages/eslint-config-react/test/helpers.js +++ b/packages/eslint-config-react/test/helpers.js @@ -4,10 +4,8 @@ import { fileURLToPath } from 'node:url'; import { lintText, ruleIds } from '../../../test-utils/lint.js'; import jsConfig from '../src/index.js'; -import { createReactTypescriptConfig } from '../src/typescript.js'; export const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); -export const tsConfig = createReactTypescriptConfig({ tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }); export { jsConfig }; /** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ diff --git a/packages/eslint-config-react/test/load.test.js b/packages/eslint-config-react/test/load.test.js index b617182..a1c2d07 100644 --- a/packages/eslint-config-react/test/load.test.js +++ b/packages/eslint-config-react/test/load.test.js @@ -1,14 +1,8 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { ids, jsConfig, tsConfig } from './helpers.js'; +import { ids, jsConfig } from './helpers.js'; test('JSX parses in .jsx under the JS config', async () => { assert.deepEqual(await ids(jsConfig, 'export const A = () =>
;\n', 'src/a.jsx'), []); }); - -test('JSX parses in .tsx under the TS config', async () => { - const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/a.tsx'); - - assert.ok(Array.isArray(found)); -}); diff --git a/packages/eslint-config-react/test/rules-exist.test.js b/packages/eslint-config-react/test/rules-exist.test.js index 0c7da21..1eb1ad4 100644 --- a/packages/eslint-config-react/test/rules-exist.test.js +++ b/packages/eslint-config-react/test/rules-exist.test.js @@ -3,21 +3,18 @@ import test from 'node:test'; import { loadedRules, missingRules } from '../../../test-utils/lint.js'; -import { jsConfig, tsConfig } from './helpers.js'; +import { jsConfig } from './helpers.js'; -test('every configured rule exists (JS and TS)', () => { +test('every configured rule exists', () => { assert.deepEqual(missingRules(jsConfig), []); - assert.deepEqual(missingRules(tsConfig), []); }); test('React JS config contains no @typescript-eslint rules', () => { assert.deepEqual([...loadedRules(jsConfig)].filter((id) => id.startsWith('@typescript-eslint/')), []); }); -test('React configs enable hooks, refresh and formatjs rules', () => { - for (const cfg of [jsConfig, tsConfig]) { - const rules = loadedRules(cfg); +test('React config enables hooks, refresh and formatjs rules', () => { + const rules = loadedRules(jsConfig); - for (const id of ['react-hooks/rules-of-hooks', 'react-refresh/only-export-components', 'formatjs/enforce-default-message']) assert.ok(rules.has(id), id); - } + for (const id of ['react-hooks/rules-of-hooks', 'react-refresh/only-export-components', 'formatjs/enforce-default-message']) assert.ok(rules.has(id), id); }); diff --git a/packages/eslint-config-typescript/LICENSE b/packages/eslint-config-typescript/LICENSE new file mode 100644 index 0000000..11b5315 --- /dev/null +++ b/packages/eslint-config-typescript/LICENSE @@ -0,0 +1,12 @@ +The Clear BSD License + +Copyright (c) 2018-2019 cospired GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of cospired GmbH nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/eslint-config-typescript/README.md b/packages/eslint-config-typescript/README.md new file mode 100644 index 0000000..902cb54 --- /dev/null +++ b/packages/eslint-config-typescript/README.md @@ -0,0 +1,47 @@ +# @cospired/eslint-config-typescript + +Slightly opinionated, type-aware ESLint flat config for TypeScript, on top of [`@cospired/eslint-config-base`](../eslint-config-base). Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. + +| Entry | For | +|---|---| +| `@cospired/eslint-config-typescript` | TypeScript | +| `@cospired/eslint-config-typescript/node` | Node.js TypeScript (Node globals) | + +## Install + +```sh +npm i -D @cospired/eslint-config-typescript @eslint-community/eslint-plugin-eslint-comments @eslint/js @stylistic/eslint-plugin eslint eslint-config-prettier eslint-plugin-import-x eslint-plugin-todo-plz globals eslint-import-resolver-typescript typescript typescript-eslint +``` + +## Usage + +No options needed. `eslint.config.js`: + +```js +export { default } from '@cospired/eslint-config-typescript/node'; // or the package root +``` + +It uses typescript-eslint's project service to find your tsconfigs, ignores build output, and lints tests, scripts and package-root TypeScript files (`vitest.config.ts`, ...) without type information, since build tsconfigs usually exclude them. + +### Options + +`createTypescriptConfig` (and `createNodeConfig` from `/node`) take: + +| Option | Default | | +|---|---|---| +| `tsconfigPath` | project service | tsconfig(s), string or array; every linted file must belong to one | +| `tsconfigRootDir` | cwd | pass `import.meta.dirname` when one ESLint run loads several workspace configs | +| `typedTests` | `false` | lint tests, scripts and root config files type-aware too (they must then be in a tsconfig) | +| `files` | all TS files | files that get type-aware rules | +| `devDependencyGlobs` | tests, `scripts/`, `*.config.*` | files that may import devDependencies | +| `importOrder.pathGroups` | none | extra `import-x/order` groups | + +```js +import { createNodeConfig } from '@cospired/eslint-config-typescript/node'; + +export default [...createNodeConfig({ tsconfigPath: ['tsconfig.json', 'test/tsconfig.json'], typedTests: true })]; +``` + +## What it adds to the base config + +typescript-eslint type-checked recommended + stylistic, explicit return types outside tests, `no-misused-promises`, `consistent-type-imports` (inline), and every typescript-eslint rule configured (see [docs/rules.md](../../docs/rules.md)). diff --git a/packages/eslint-config-typescript/package.json b/packages/eslint-config-typescript/package.json new file mode 100644 index 0000000..bd57b7c --- /dev/null +++ b/packages/eslint-config-typescript/package.json @@ -0,0 +1,58 @@ +{ + "name": "@cospired/eslint-config-typescript", + "version": "7.0.0", + "description": "Slightly opinionated ESLint flat config for TypeScript (type-aware)", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "author": "Dirk Haage ", + "keywords": [ + "eslint", + "eslintconfig", + "flat-config", + "cospired" + ], + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "scripts": { + "test": "node --test test/*.test.js" + }, + "exports": { + ".": "./src/index.js", + "./node": "./src/node.js", + "./package.json": "./package.json" + }, + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2" + }, + "peerDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0", + "eslint-import-resolver-typescript": "^4.4.5", + "typescript": ">=5.4.2 <6.1", + "typescript-eslint": "^8.72.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cospired/javascript.git", + "directory": "packages/eslint-config-typescript" + }, + "homepage": "https://github.com/cospired/javascript/tree/main/packages/eslint-config-typescript#readme", + "bugs": { + "url": "https://github.com/cospired/javascript/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "sideEffects": false +} diff --git a/packages/eslint-config-typescript/src/globs.js b/packages/eslint-config-typescript/src/globs.js new file mode 100644 index 0000000..79b4b52 --- /dev/null +++ b/packages/eslint-config-typescript/src/globs.js @@ -0,0 +1,13 @@ +import { withMonorepoTwins } from '@cospired/eslint-config-base'; + +export const tsExtensions = ['.ts', '.tsx', '.mts', '.cts']; +export const testGlobs = withMonorepoTwins(['**/*.{test,spec}.{ts,tsx,mts,cts}', 'test/**', 'tests/**', '**/__tests__/**']); +/** JS files can be matched by a consumer's `files`; they are never type-checked. */ +export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; + +/** + * Files linted without type information unless `typedTests: true`: tests, TypeScript files at the package root + * (vitest.config.ts, drizzle.config.ts, ...) and scripts. They are often not part of any tsconfig (build configs + * exclude them), and type-aware linting reports a parsing error for such files. + */ +export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; diff --git a/packages/eslint-config-base/src/typescript.js b/packages/eslint-config-typescript/src/index.js similarity index 94% rename from packages/eslint-config-base/src/typescript.js rename to packages/eslint-config-typescript/src/index.js index 6f7c6c1..bc26c05 100644 --- a/packages/eslint-config-base/src/typescript.js +++ b/packages/eslint-config-typescript/src/index.js @@ -1,13 +1,11 @@ +import { createBaseConfig, globalIgnores, prettierLast } from '@cospired/eslint-config-base'; import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import { jsFiles, testGlobs, untypedGlobs } from './globs.js'; -import { globalIgnores } from './ignores.js'; -import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x.js'; +import { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x-typescript.js'; import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; -import { createBaseConfig, prettierLast } from './index.js'; - export { globalIgnores }; /** diff --git a/packages/eslint-config-typescript/src/node.js b/packages/eslint-config-typescript/src/node.js new file mode 100644 index 0000000..9c7ac1b --- /dev/null +++ b/packages/eslint-config-typescript/src/node.js @@ -0,0 +1,36 @@ +import { globalIgnores } from '@cospired/eslint-config-base'; +import globals from 'globals'; + +import { jsFiles, untypedGlobs } from './globs.js'; + +import { createTypescriptConfig } from './index.js'; + +export { globalIgnores }; + + +/** + * Node.js/backend TypeScript config: `createTypescriptConfig` plus node globals. + * Takes the same options. + * + * @param {Parameters[0]} [options] + * @returns {import('eslint').Linter.Config[]} + */ +export function createNodeConfig(options = {}) { + const { files = ['**/*.{ts,tsx,mts,cts}'], typedTests = false } = options; + + return [ + ...createTypescriptConfig(options), + { languageOptions: { sourceType: 'module', globals: globals.node } }, + { + files, + ignores: typedTests ? jsFiles : [...jsFiles, ...untypedGlobs], + rules: { + // String `||` is deliberate empty-string fallthrough (env vars, trimmed input) that `??` would silently drop. + '@typescript-eslint/prefer-nullish-coalescing': ['error', { ignorePrimitives: { string: true } }], + }, + }, + ]; +} + +/** Node TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node'`. */ +export default createNodeConfig(); diff --git a/packages/eslint-config-typescript/src/rules/import-x-typescript.js b/packages/eslint-config-typescript/src/rules/import-x-typescript.js new file mode 100644 index 0000000..7315bfd --- /dev/null +++ b/packages/eslint-config-typescript/src/rules/import-x-typescript.js @@ -0,0 +1,20 @@ +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; +import { flatConfigs } from 'eslint-plugin-import-x'; + +import { tsExtensions } from '../globs.js'; + +/** TypeScript-only import-x config: the typescript preset and a type-import style that matches `consistent-type-imports`. */ +export const importXTypescript = flatConfigs.typescript; +export const importXTypescriptRules = { 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'] }; + +/** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ +export function importSettings(project) { + return { + 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs', ...tsExtensions], + 'import-x/parsers': { '@typescript-eslint/parser': tsExtensions }, + 'import-x/resolver-next': [ + // Several projects is the intended shape (app + test-only tsconfigs); the suggested `references` fix needs `composite`. + createTypeScriptImportResolver({ alwaysTryTypes: true, noWarnOnMultipleProjects: true, ...(project && { project }) }), + ], + }; +} diff --git a/packages/eslint-config-base/src/rules/typescript-eslint.js b/packages/eslint-config-typescript/src/rules/typescript-eslint.js similarity index 100% rename from packages/eslint-config-base/src/rules/typescript-eslint.js rename to packages/eslint-config-typescript/src/rules/typescript-eslint.js diff --git a/packages/eslint-config-typescript/test/completeness.test.js b/packages/eslint-config-typescript/test/completeness.test.js new file mode 100644 index 0000000..eb7a57a --- /dev/null +++ b/packages/eslint-config-typescript/test/completeness.test.js @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; +import { pluginRules, typescriptNamespaces } from '../../../test-utils/plugins.js'; +import { createTypescriptConfig } from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; + +// A rule counts as configured when a recommended preset or one of our files sets it (on or off). +const cases = [ + ['TypeScript', createTypescriptConfig()], + ['node', createNodeConfig()], +]; + +for (const [name, config] of cases) { + test(`${name}: every non-deprecated rule of ${typescriptNamespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(config, typescriptNamespaces, pluginRules), []); + }); + + test(`${name}: our overrides only repeat nothing the presets already set`, () => { + assert.deepEqual(redundantRules(config), []); + }); +} diff --git a/packages/eslint-config-typescript/test/decisions.test.js b/packages/eslint-config-typescript/test/decisions.test.js new file mode 100644 index 0000000..3f83e78 --- /dev/null +++ b/packages/eslint-config-typescript/test/decisions.test.js @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; +import { testGlobs } from '../src/globs.js'; +import { createTypescriptConfig } from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const tsOptions = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; +const tsConfig = createTypescriptConfig(tsOptions); +const lint = async (code, file, cfg = tsConfig) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); + +test('no rule resolves to "warn" in the TypeScript configs', async () => { + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.ts'), root), []); + assert.deepEqual(await warnRules(createNodeConfig(tsOptions), path.join(root, 'src/a.ts'), root), []); +}); + +test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { + const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts'), root); + + for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); + for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); +}); + +test('scripts/ files get node globals', async () => { + const found = await lint('process.exit(1);\n', 'scripts/run.mjs'); + + assert.ok(!found.includes('no-undef'), 'process should be defined in scripts/'); + assert.ok((await lint('process.exit(1);\n', 'src/run.mjs')).includes('no-undef')); +}); + +test('test-file globs also match from a monorepo root config', () => { + for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts}']) { + assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); + } +}); + +test('console is allowed in scripts of every extension', async () => { + for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { + const found = await lint('console.log(1);\n', `scripts/x.${ext}`); + + assert.ok(!found.includes('no-console'), ext); + } +}); diff --git a/packages/eslint-config-typescript/test/defaults.test.js b/packages/eslint-config-typescript/test/defaults.test.js new file mode 100644 index 0000000..000fd09 --- /dev/null +++ b/packages/eslint-config-typescript/test/defaults.test.js @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import jsDefault from '@cospired/eslint-config-base'; + +import { lintText } from '../../../test-utils/lint.js'; +import tsDefault, { createTypescriptConfig } from '../src/index.js'; +import nodeDefault from '../src/node.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); +const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; + +test('default exports are ready-made config arrays that include the global ignores', () => { + for (const cfg of [jsDefault, tsDefault, nodeDefault]) { + assert.ok(Array.isArray(cfg)); + assert.ok(cfg.some((item) => item.ignores?.includes('**/dist/**'))); + } +}); + +test('zero options: TS files lint via the project service, no parse errors', async () => { + for (const cfg of [tsDefault, nodeDefault]) { + for (const file of ['src/a.ts', 'src/b.ts']) { + const messages = await lint(cfg, 'export const v = 1;\n', file); + + assert.deepEqual(messages.filter((m) => m.fatal), [], file); + } + } +}); + +test('zero options: test and config files outside every tsconfig lint without type information', async () => { + for (const file of ['other/c.test.ts', 'vitest.config.ts', 'vitest.shared.ts']) { + for (const cfg of [tsDefault, nodeDefault]) { + const messages = await lint(cfg, 'export const v = 1;\n', file); + + assert.deepEqual(messages.filter((m) => m.fatal), [], file); + } + } +}); + +test('typedTests: true lints tests with type information (they must be in a tsconfig)', async () => { + const typed = createTypescriptConfig({ typedTests: true, tsconfigRootDir: root }); + const outside = await lint(typed, 'export const v = 1;\n', 'other/c.test.ts'); + + assert.ok(outside.some((m) => m.fatal), 'a test outside every tsconfig reports a parsing error'); + const inside = await lint(typed, 'export const v = 1;\n', 'src/a.ts'); + + assert.deepEqual(inside.filter((m) => m.fatal), []); +}); + +test('zero options: dist output is ignored', async () => { + const messages = await lint(tsDefault, 'var x = 1;\n', 'dist/a.js'); + + assert.ok(messages.every((m) => /ignored/i.test(m.message)), 'only the "file ignored" notice'); +}); + +test('import-x/first is off in tests (vi.mock hoisting) and on in source', async () => { + const code = "vi.mock('./a.js');\n\nimport { a } from './a.js';\n\nexport const v = a;\n"; + + assert.ok(!(await ruleIdsFor(tsDefault, code, 'other/c.test.ts')).includes('import-x/first')); + assert.ok((await ruleIdsFor(tsDefault, code, 'src/b.ts')).includes('import-x/first')); +}); diff --git a/packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/other/c.test.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/other/c.test.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/other/c.test.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/package.json b/packages/eslint-config-typescript/test/fixtures/ts-project/package.json similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/package.json rename to packages/eslint-config-typescript/test/fixtures/ts-project/package.json diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/a.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/src/a.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/src/a.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/src/a.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/b.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/src/b.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/src/b.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/src/b.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/src/shared/x.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/src/shared/x.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/src/shared/x.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/test/a.test.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/test/a.test.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/test/a.test.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/tsconfig.json rename to packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json diff --git a/packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json b/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.test.json similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/tsconfig.test.json rename to packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.test.json diff --git a/packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/vitest.config.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/vitest.config.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/vitest.config.ts diff --git a/packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/vitest.shared.ts similarity index 100% rename from packages/eslint-config-base/test/fixtures/ts-project/vitest.shared.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/vitest.shared.ts diff --git a/packages/eslint-config-typescript/test/node.test.js b/packages/eslint-config-typescript/test/node.test.js new file mode 100644 index 0000000..579ab91 --- /dev/null +++ b/packages/eslint-config-typescript/test/node.test.js @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { createTypescriptConfig } from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const options = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; +const node = createNodeConfig(options); +const ids = async (cfg, code, file) => { + const messages = await lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); + + assert.deepEqual(messages.filter((m) => m.fatal), [], 'fixture must parse'); + + return ruleIds(messages); +}; + +test('node globals are defined (process is not no-undef)', async () => { + const code = 'process.exit(1);\n'; + + assert.ok((await ids(createTypescriptConfig(options), code, 'src/x.mjs')).includes('no-undef')); + assert.ok(!(await ids(node, code, 'src/x.mjs')).includes('no-undef')); +}); + +test('string || fallback is allowed, number || fallback is flagged', async () => { + const rule = '@typescript-eslint/prefer-nullish-coalescing'; + const str = "export function f(s: string | undefined): string {\n return s || 'x';\n}\n"; + const num = 'export function f(n: number | undefined): number {\n return n || 1;\n}\n'; + + assert.ok(!(await ids(node, str, 'src/a.ts')).includes(rule)); + assert.ok((await ids(node, num, 'src/a.ts')).includes(rule)); +}); + +test('no-console is allowed in scripts/ but not in src/', async () => { + const code = 'console.log(1);\n'; + + assert.ok(!(await ids(node, code, 'scripts/run.mjs')).includes('no-console')); + assert.ok((await ids(node, code, 'src/run.mjs')).includes('no-console')); +}); + +test('JS files matched by `files` are linted without type-aware rules (no crash)', async () => { + const cfg = createNodeConfig({ ...options, files: ['src/**/*.{ts,js}', '*.{ts,js}'] }); + const found = await ids(cfg, 'export const a = (x) => x || 1;\n', 'src/c.js'); + + assert.ok(!found.includes('@typescript-eslint/prefer-nullish-coalescing')); + assert.ok(!found.includes('@typescript-eslint/explicit-function-return-type')); +}); diff --git a/packages/eslint-config-base/test/typescript.test.js b/packages/eslint-config-typescript/test/typescript.test.js similarity index 96% rename from packages/eslint-config-base/test/typescript.test.js rename to packages/eslint-config-typescript/test/typescript.test.js index bc9be4e..43003d7 100644 --- a/packages/eslint-config-base/test/typescript.test.js +++ b/packages/eslint-config-typescript/test/typescript.test.js @@ -4,7 +4,7 @@ import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { lintText, ruleIds, ruleSeverity } from '../../../test-utils/lint.js'; -import { createTypescriptConfig } from '../src/typescript.js'; +import { createTypescriptConfig } from '../src/index.js'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); const abs = (file) => path.join(root, file); @@ -89,7 +89,7 @@ test('every rule exists and JS configs hold no TypeScript rules', async () => { const { missingRules, loadedRules } = await import('../../../test-utils/lint.js'); assert.deepEqual(missingRules(config), []); - const { default: js } = await import('../src/index.js'); + const { default: js } = await import('@cospired/eslint-config-base'); assert.deepEqual([...loadedRules(js)].filter((id) => id.startsWith('@typescript-eslint/')), []); }); diff --git a/scripts/rule-table.js b/scripts/rule-table.js index 916aba0..c06184c 100644 --- a/scripts/rule-table.js +++ b/scripts/rule-table.js @@ -75,9 +75,9 @@ async function main() { import('@eslint/js'), import('typescript-eslint'), import('../packages/eslint-config-base/src/index.js'), - import('../packages/eslint-config-base/src/typescript.js'), + import('../packages/eslint-config-typescript/src/index.js'), import('../packages/eslint-config-react/src/index.js'), - import('../packages/eslint-config-react/src/typescript.js'), + import('../packages/eslint-config-react-typescript/src/index.js'), ]); const sources = JSON.parse(fs.readFileSync(path.join(here, 'rule-sources.json'), 'utf8')); const decisions = JSON.parse(fs.readFileSync(path.join(here, 'rule-decisions.json'), 'utf8')); diff --git a/scripts/smoke-pack.js b/scripts/smoke-pack.js index 45bfb5c..6a73160 100644 --- a/scripts/smoke-pack.js +++ b/scripts/smoke-pack.js @@ -9,7 +9,8 @@ const root = path.resolve(import.meta.dirname, '..'); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cospired-smoke-')); const run = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'inherit'], encoding: 'utf8' }); -const tarballs = ['eslint-config-base', 'eslint-config-react'].map((dir) => { +const packages = ['eslint-config-base', 'eslint-config-typescript', 'eslint-config-react', 'eslint-config-react-typescript']; +const tarballs = packages.map((dir) => { const [{ filename }] = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', tmp], path.join(root, 'packages', dir))); return path.join(tmp, filename); @@ -17,7 +18,7 @@ const tarballs = ['eslint-config-base', 'eslint-config-react'].map((dir) => { const peers = {}; -for (const dir of ['eslint-config-base', 'eslint-config-react']) { +for (const dir of packages) { Object.assign(peers, JSON.parse(fs.readFileSync(path.join(root, 'packages', dir, 'package.json'), 'utf8')).peerDependencies); } const project = path.join(tmp, 'project'); @@ -29,11 +30,11 @@ run('npm', ['install', '--no-audit', '--no-fund', ...tarballs], project); const clean = 'export function add(a: number, b: number): number {\n return a + b;\n}\n'; const cases = [ ['js', '@cospired/eslint-config-base', 'src/index.js', 'export const answer = 42;\n'], - ['node-js', '@cospired/eslint-config-base/node-js', 'src/index.js', "import { cwd } from 'node:process';\n\nexport const here = cwd();\n"], - ['typescript', '@cospired/eslint-config-base/typescript', 'src/index.ts', clean], - ['node', '@cospired/eslint-config-base/node', 'src/index.ts', clean], + ['node-js', '@cospired/eslint-config-base/node', 'src/index.js', "import { cwd } from 'node:process';\n\nexport const here = cwd();\n"], + ['typescript', '@cospired/eslint-config-typescript', 'src/index.ts', clean], + ['node', '@cospired/eslint-config-typescript/node', 'src/index.ts', clean], ['react', '@cospired/eslint-config-react', 'src/index.jsx', 'export const App = () =>
;\n'], - ['react-typescript', '@cospired/eslint-config-react/typescript', 'src/index.tsx', 'export const App = (): null => null;\n'], + ['react-typescript', '@cospired/eslint-config-react-typescript', 'src/index.tsx', 'export const App = (): null => null;\n'], ]; let failed = false; From 912d4798b35ff5afdaecbbf47ca8e9e74610dabe Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:07:24 +0800 Subject: [PATCH 25/33] docs: comment block (description, reasoning, URL) per rule; regenerate rule table Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .../2026-11-10-eslint-config-2026-design.md | 2 + .../src/rules/eslint-comments.js | 19 + .../eslint-config-base/src/rules/eslint.js | 824 +++++++++++++++++- .../eslint-config-base/src/rules/import-x.js | 278 +++++- .../eslint-config-base/src/rules/stylistic.js | 74 ++ .../eslint-config-base/src/rules/todo-plz.js | 11 +- .../eslint-config-react/src/rules/formatjs.js | 46 + .../src/rules/react-hooks.js | 89 ++ .../src/rules/react-refresh.js | 9 +- .../src/rules/import-x-typescript.js | 10 +- .../src/rules/typescript-eslint.js | 256 +++++- 11 files changed, 1604 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md index a7c8581..a4d8c44 100644 --- a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md +++ b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md @@ -77,3 +77,5 @@ File layout, test harness details, fixture format, changesets config, CI (existi - **Layout:** rules are split per plugin in `src/rules/` (`eslint.js`, `typescript-eslint.js`, `import-x.js`, `eslint-comments.js`, `todo-plz.js`, `stylistic.js`; react: `react-hooks.js`, `react-refresh.js`, `formatjs.js`, `react.js`), as in the old packages. - **Goal:** configs usable with no overrides in eventured, danceregistry and our other projects: every entry has a ready-made default export (`export { default } from '@cospired/eslint-config-base/node'`). Entries: base JS, node-js, typescript, node, react, react/typescript. Tests, scripts and package-root TS files are linted without type information unless `typedTests: true`. - **Publish readiness:** GitHub CI (Node 20.19/22.13/24, peer-dependency floors, packed-tarball smoke test), changesets release workflow with npm provenance, 8.0.0 for both packages. +- **Four packages (Dirk, after review):** `@cospired/eslint-config-base` (JS, `/node`), `-typescript` (TS, `/node`), `-react`, `-react-typescript`, all 8.0.0 via a changesets `fixed` group. JavaScript-only projects no longer need the TypeScript peers. Supersedes the earlier two-package layout. +- **Rule documentation (Dirk):** every rule in the rule files has a comment block with the rule's description, our reasoning and its documentation URL, and a blank line after it, as in the old packages. The blocks were generated from the installed plugins' metadata plus a reason per rule; new rules added later follow the same format. diff --git a/packages/eslint-config-base/src/rules/eslint-comments.js b/packages/eslint-config-base/src/rules/eslint-comments.js index 1283869..d4aec0a 100644 --- a/packages/eslint-config-base/src/rules/eslint-comments.js +++ b/packages/eslint-config-base/src/rules/eslint-comments.js @@ -7,8 +7,27 @@ export default { linterOptions: { reportUnusedDisableDirectives: 'error' }, rules: { ...recommended.rules, + /* + Require include descriptions in ESLint directive-comments. + Every disable comment explains itself (`-- reason`), so reviewers know why a rule was + bypassed. + https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/require-description.html + */ '@eslint-community/eslint-comments/require-description': 'error', + + /* + Disallow `eslint-disable` comments about specific rules. + Off: the shared config restricts no particular rules from being disabled. + https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-restricted-disable.html + */ '@eslint-community/eslint-comments/no-restricted-disable': 'off', + + /* + Disallow ESLint directive-comments. + Off: directive comments are allowed, as long as they carry a description. + https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-use.html + */ '@eslint-community/eslint-comments/no-use': 'off', + }, }; diff --git a/packages/eslint-config-base/src/rules/eslint.js b/packages/eslint-config-base/src/rules/eslint.js index 855addb..5d1ac45 100644 --- a/packages/eslint-config-base/src/rules/eslint.js +++ b/packages/eslint-config-base/src/rules/eslint.js @@ -9,148 +9,964 @@ export default [ { name: 'cospired/eslint', rules: { - // --- on: possible bugs + /* + Enforce `return` statements in callbacks of array methods. + Catches likely bugs. + https://eslint.org/docs/latest/rules/array-callback-return + */ 'array-callback-return': 'error', + + /* + Enforce `default` clauses in `switch` statements to be last. + Catches likely bugs. + https://eslint.org/docs/latest/rules/default-case-last + */ 'default-case-last': 'error', + + /* + Require `for-in` loops to include an `if` statement. + Catches likely bugs. + https://eslint.org/docs/latest/rules/guard-for-in + */ 'guard-for-in': 'error', + + /* + Disallow `Array` constructors. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-array-constructor + */ 'no-array-constructor': 'error', + + /* + Disallow `await` inside of loops. + Sequential awaits in a loop are usually accidental; an intentional one needs a disable + comment with a reason. + https://eslint.org/docs/latest/rules/no-await-in-loop + */ 'no-await-in-loop': 'error', + + /* + Disallow returning value from constructor. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-constructor-return + */ 'no-constructor-return': 'error', + + /* + Disallow function declarations that contain unsafe references inside loop statements. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-loop-func + */ 'no-loop-func': 'error', + + /* + Disallow `new` operators with the `Function` object. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-new-func + */ 'no-new-func': 'error', + + /* + Disallow `new` operators with the `String`, `Number`, and `Boolean` objects. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-new-wrappers + */ 'no-new-wrappers': 'error', + + /* + Disallow returning values from Promise executor functions. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-promise-executor-return + */ 'no-promise-executor-return': 'error', + + /* + Disallow comparisons where both sides are exactly the same. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-self-compare + */ 'no-self-compare': 'error', + + /* + Disallow template literal placeholder syntax in regular strings. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-template-curly-in-string + */ 'no-template-curly-in-string': 'error', + + /* + Disallow unmodified loop conditions. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-unmodified-loop-condition + */ 'no-unmodified-loop-condition': 'error', + + /* + Disallow loops with a body that allows only one iteration. + Catches likely bugs. + https://eslint.org/docs/latest/rules/no-unreachable-loop + */ 'no-unreachable-loop': 'error', + + /* + Disallow unused variables. + Unused code never lands; a leading underscore is the explicit opt-out (arguments, variables, + caught errors). + https://eslint.org/docs/latest/rules/no-unused-vars + */ 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + + /* + Disallow the use of variables before they are defined. + Strict: nothing is used before its definition, functions included, so files read top to + bottom. + https://eslint.org/docs/latest/rules/no-use-before-define + */ 'no-use-before-define': 'error', + + /* + Enforce the use of the radix argument when using `parseInt()`. + Catches likely bugs. + https://eslint.org/docs/latest/rules/radix + */ radix: 'error', + + /* + Require symbol descriptions. + Catches likely bugs. + https://eslint.org/docs/latest/rules/symbol-description + */ 'symbol-description': 'error', - // --- on: unsafe or discouraged features + /* + Disallow the use of `alert`, `confirm`, and `prompt`. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-alert + */ 'no-alert': 'error', + + /* + Disallow the use of `arguments.caller` or `arguments.callee`. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-caller + */ 'no-caller': 'error', + + /* + Disallow the use of `eval()`. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-eval + */ 'no-eval': 'error', + + /* + Disallow extending native types. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-extend-native + */ 'no-extend-native': 'error', + + /* + Disallow the use of `eval()`-like methods. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-implied-eval + */ 'no-implied-eval': 'error', + + /* + Disallow the use of the `__iterator__` property. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-iterator + */ 'no-iterator': 'error', + + /* + Disallow labels that share a name with a variable. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-label-var + */ 'no-label-var': 'error', + + /* + Disallow labeled statements. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-labels + */ 'no-labels': 'error', + + /* + Disallow use of chained assignment expressions. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-multi-assign + */ 'no-multi-assign': 'error', + + /* + Disallow multiline strings. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-multi-str + */ 'no-multi-str': 'error', + + /* + Disallow octal escape sequences in string literals. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-octal-escape + */ 'no-octal-escape': 'error', + + /* + Disallow reassigning function parameters. + Reassigning a parameter hides what the caller passed; mutating its properties stays allowed + (builders, reducers, DOM nodes). + https://eslint.org/docs/latest/rules/no-param-reassign + */ 'no-param-reassign': ['error', { props: false }], + + /* + Disallow the use of the `__proto__` property. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-proto + */ 'no-proto': 'error', + + /* + Disallow assignment operators in `return` statements. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-return-assign + */ 'no-return-assign': 'error', + + /* + Disallow `javascript:` URLs. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-script-url + */ 'no-script-url': 'error', + + /* + Disallow comma operators. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-sequences + */ 'no-sequences': 'error', + + /* + Disallow variable declarations from shadowing variables declared in the outer scope. + An inner variable that reuses an outer name invites mixing the two up. + https://eslint.org/docs/latest/rules/no-shadow + */ 'no-shadow': 'error', + + /* + Disallow initializing variables to `undefined`. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-undef-init + */ 'no-undef-init': 'error', + + /* + Disallow throwing literals as exceptions. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-throw-literal + */ 'no-throw-literal': 'error', + + /* + Require `let` or `const` instead of `var`. + Avoids unsafe or error-prone language features. + https://eslint.org/docs/latest/rules/no-var + */ 'no-var': 'error', - // --- on: clarity and consistency + /* + Enforce getter and setter pairs in objects and classes. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/accessor-pairs + */ 'accessor-pairs': 'error', + + /* + Require braces around arrow function bodies. + No braces and `return` where an expression is enough. + https://eslint.org/docs/latest/rules/arrow-body-style + */ 'arrow-body-style': ['error', 'as-needed'], + + /* + Enforce camelcase naming convention. + camelCase identifiers; property names are ignored because API payloads often use snake_case. + https://eslint.org/docs/latest/rules/camelcase + */ camelcase: ['error', { properties: 'never' }], + + /* + Require `return` statements to either always or never specify values. + A function either always returns a value or never does. Off for TypeScript, where the + compiler's noImplicitReturns covers it. + https://eslint.org/docs/latest/rules/consistent-return + */ 'consistent-return': 'error', + + /* + Enforce default parameters to be last. + A default before a required parameter cannot be omitted anyway. + https://eslint.org/docs/latest/rules/default-param-last + */ 'default-param-last': 'error', + + /* + Enforce dot notation whenever possible. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/dot-notation + */ 'dot-notation': 'error', + + /* + Require the use of `===` and `!==`. + Strict equality everywhere except `== null`, which checks null and undefined in one go. + https://eslint.org/docs/latest/rules/eqeqeq + */ eqeqeq: ['error', 'always', { null: 'ignore' }], + + /* + Require function names to match the name of the variable or property to which they are + assigned. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/func-name-matching + */ 'func-name-matching': 'error', + + /* + Require grouped accessor pairs in object literals and classes. + Getter and setter sit next to each other, getter first. + https://eslint.org/docs/latest/rules/grouped-accessor-pairs + */ 'grouped-accessor-pairs': ['error', 'getBeforeSet'], + + /* + Require or disallow logical assignment operator shorthand. + `a ||= b` instead of `a = a || b`, including the `if (!a) { a = b; }` form. + https://eslint.org/docs/latest/rules/logical-assignment-operators + */ 'logical-assignment-operators': ['error', 'always', { enforceForIfStatements: true }], + + /* + Require constructor names to begin with a capital letter. + A `new` call needs a capitalized constructor; capitalized calls without `new` stay allowed + (decorators, factories, `Number()`). + https://eslint.org/docs/latest/rules/new-cap + */ 'new-cap': ['error', { capIsNew: false }], + + /* + Disallow the use of `console`. + Only `warn` and `error` are runtime reporting; everything else goes through a logger. + scripts/ are exempt. + https://eslint.org/docs/latest/rules/no-console + */ 'no-console': ['error', { allow: ['warn', 'error'] }], + + /* + Disallow `else` blocks after `return` statements in `if` statements. + Early returns keep nesting flat; `else if` chains after a return are flagged too. + https://eslint.org/docs/latest/rules/no-else-return + */ 'no-else-return': ['error', { allowElseIf: false }], + + /* + Disallow empty functions. + Empty functions are usually unfinished; empty arrow functions, functions and methods stay + allowed for no-op callbacks. + https://eslint.org/docs/latest/rules/no-empty-function + */ 'no-empty-function': ['error', { allow: ['arrowFunctions', 'functions', 'methods'] }], + + /* + Disallow unnecessary calls to `.bind()`. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-extra-bind + */ 'no-extra-bind': 'error', + + /* + Disallow unnecessary labels. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-extra-label + */ 'no-extra-label': 'error', + + /* + Disallow use of `this` in contexts where the value of `this` is `undefined`. + `this` outside a method is almost always a mistake. + https://eslint.org/docs/latest/rules/no-invalid-this + */ 'no-invalid-this': 'error', + + /* + Disallow unnecessary nested blocks. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-lone-blocks + */ 'no-lone-blocks': 'error', + + /* + Disallow `if` statements as the only statement in `else` blocks. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-lonely-if + */ 'no-lonely-if': 'error', + + /* + Disallow nested ternary expressions. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-nested-ternary + */ 'no-nested-ternary': 'error', + + /* + Disallow calls to the `Object` constructor without an argument. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-object-constructor + */ 'no-object-constructor': 'error', + + /* + Disallow ternary operators when simpler alternatives exist. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-unneeded-ternary + */ 'no-unneeded-ternary': 'error', + + /* + Disallow unused expressions. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-unused-expressions + */ 'no-unused-expressions': 'error', + + /* + Disallow unnecessary calls to `.call()` and `.apply()`. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-useless-call + */ 'no-useless-call': 'error', + + /* + Disallow unnecessary computed property keys in objects and classes. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-useless-computed-key + */ 'no-useless-computed-key': 'error', + + /* + Disallow unnecessary concatenation of literals or template literals. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-useless-concat + */ 'no-useless-concat': 'error', + + /* + Disallow unnecessary constructors. + A constructor that only forwards to super adds nothing. + https://eslint.org/docs/latest/rules/no-useless-constructor + */ 'no-useless-constructor': 'error', + + /* + Disallow renaming import, export, and destructured assignments to the same name. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-useless-rename + */ 'no-useless-rename': 'error', + + /* + Disallow redundant return statements. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/no-useless-return + */ 'no-useless-return': 'error', + + /* + Require or disallow method and property shorthand syntax for object literals. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/object-shorthand + */ 'object-shorthand': 'error', + + /* + Enforce variables to be declared either together or separately in functions. + One declaration per variable. + https://eslint.org/docs/latest/rules/one-var + */ 'one-var': ['error', 'never'], + + /* + Require or disallow assignment operator shorthand where possible. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/operator-assignment + */ 'operator-assignment': 'error', + + /* + Require using arrow functions for callbacks. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-arrow-callback + */ 'prefer-arrow-callback': 'error', + + /* + Require `const` declarations for variables that are never reassigned after declared. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-const + */ 'prefer-const': 'error', + + /* + Disallow the use of `Math.pow` in favor of the `**` operator. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-exponentiation-operator + */ 'prefer-exponentiation-operator': 'error', + + /* + Disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal + literals. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-numeric-literals + */ 'prefer-numeric-literals': 'error', + + /* + Disallow use of `Object.prototype.hasOwnProperty.call()` and prefer use of + `Object.hasOwn()`. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-object-has-own + */ 'prefer-object-has-own': 'error', + + /* + Disallow using `Object.assign` with an object literal as the first argument and prefer the + use of object spread instead. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-object-spread + */ 'prefer-object-spread': 'error', + + /* + Require using Error objects as Promise rejection reasons. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-promise-reject-errors + */ 'prefer-promise-reject-errors': 'error', + + /* + Disallow use of the `RegExp` constructor in favor of regular expression literals. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-regex-literals + */ 'prefer-regex-literals': 'error', + + /* + Require rest parameters instead of `arguments`. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-rest-params + */ 'prefer-rest-params': 'error', + + /* + Require spread operators instead of `.apply()`. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-spread + */ 'prefer-spread': 'error', + + /* + Require template literals instead of string concatenation. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/prefer-template + */ 'prefer-template': 'error', + + /* + Disallow async functions which have no `await` expression. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/require-await + */ 'require-await': 'error', + + /* + Require or disallow "Yoda" conditions. + Keeps code explicit and consistent. + https://eslint.org/docs/latest/rules/yoda + */ yoda: 'error', - // --- off: opinion, thresholds, or covered elsewhere (import-x, TypeScript, Prettier, the ticket-ref plugin) + /* + Enforce the use of variables within the scope they are defined. + Off: `no-var` makes it redundant. + https://eslint.org/docs/latest/rules/block-scoped-var + */ 'block-scoped-var': 'off', + + /* + Enforce or disallow capitalization of the first letter of a comment. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/capitalized-comments + */ 'capitalized-comments': 'off', + + /* + Enforce that class methods utilize `this`. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/class-methods-use-this + */ 'class-methods-use-this': 'off', + + /* + Enforce a maximum cyclomatic complexity allowed in a program. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/complexity + */ complexity: 'off', + + /* + Enforce consistent naming when capturing the current execution context. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/consistent-this + */ 'consistent-this': 'off', + + /* + Require `default` cases in `switch` statements. + Off: TypeScript's exhaustiveness checks do this better. + https://eslint.org/docs/latest/rules/default-case + */ 'default-case': 'off', + + /* + Require or disallow named `function` expressions. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/func-names + */ 'func-names': 'off', + + /* + Enforce the consistent use of either `function` declarations or expressions assigned to + variables. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/func-style + */ 'func-style': 'off', + + /* + Disallow specified identifiers. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/id-denylist + */ 'id-denylist': 'off', + + /* + Enforce minimum and maximum identifier lengths. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/id-length + */ 'id-length': 'off', + + /* + Require identifiers to match a specified regular expression. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/id-match + */ 'id-match': 'off', + + /* + Require or disallow initialization in variable declarations. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/init-declarations + */ 'init-declarations': 'off', + + /* + Enforce a maximum number of classes per file. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-classes-per-file + */ 'max-classes-per-file': 'off', + + /* + Enforce a maximum depth that blocks can be nested. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-depth + */ 'max-depth': 'off', + + /* + Enforce a maximum number of lines per file. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-lines + */ 'max-lines': 'off', + + /* + Enforce a maximum number of lines of code in a function. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-lines-per-function + */ 'max-lines-per-function': 'off', + + /* + Enforce a maximum depth that callbacks can be nested. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-nested-callbacks + */ 'max-nested-callbacks': 'off', + + /* + Enforce a maximum number of parameters in function definitions. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-params + */ 'max-params': 'off', + + /* + Enforce a maximum number of statements allowed in function blocks. + Off: needs a project-specific threshold. + https://eslint.org/docs/latest/rules/max-statements + */ 'max-statements': 'off', + + /* + Disallow bitwise operators. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-bitwise + */ 'no-bitwise': 'off', + + /* + Disallow `continue` statements. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-continue + */ 'no-continue': 'off', + + /* + Disallow equal signs explicitly at the beginning of regular expressions. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-div-regex + */ 'no-div-regex': 'off', + + /* + Disallow duplicate module imports. + Off: import-x/no-duplicates covers it. + https://eslint.org/docs/latest/rules/no-duplicate-imports + */ 'no-duplicate-imports': 'off', + + /* + Disallow `null` comparisons without type-checking operators. + Off: eqeqeq allows `== null` on purpose. + https://eslint.org/docs/latest/rules/no-eq-null + */ 'no-eq-null': 'off', + + /* + Disallow shorthand type conversions. + Off: `!!x` and `+x` are common and clear. + https://eslint.org/docs/latest/rules/no-implicit-coercion + */ 'no-implicit-coercion': 'off', + + /* + Disallow declarations in the global scope. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-implicit-globals + */ 'no-implicit-globals': 'off', + + /* + Disallow inline comments after code. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-inline-comments + */ 'no-inline-comments': 'off', + + /* + Disallow variable or `function` declarations in nested blocks. + Off: block-scoped functions are valid since ES2015. + https://eslint.org/docs/latest/rules/no-inner-declarations + */ 'no-inner-declarations': 'off', + + /* + Disallow magic numbers. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-magic-numbers + */ 'no-magic-numbers': 'off', + + /* + Disallow negated conditions. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-negated-condition + */ 'no-negated-condition': 'off', + + /* + Disallow `new` operators outside of assignments or comparisons. + Off: `new Foo()` for its side effects is common. + https://eslint.org/docs/latest/rules/no-new + */ 'no-new': 'off', + + /* + Disallow the unary operators `++` and `--`. + Off: `i++` is idiomatic, and Prettier removes the old ASI risk. + https://eslint.org/docs/latest/rules/no-plusplus + */ 'no-plusplus': 'off', + + /* + Disallow specified names in exports. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-restricted-exports + */ 'no-restricted-exports': 'off', + + /* + Disallow specified global variables. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-restricted-globals + */ 'no-restricted-globals': 'off', + + /* + Disallow specified modules when loaded by `import`. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-restricted-imports + */ 'no-restricted-imports': 'off', + + /* + Disallow certain properties on certain objects. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-restricted-properties + */ 'no-restricted-properties': 'off', + + /* + Disallow specified syntax. + Off: configured per project; the shared config has nothing to restrict. + https://eslint.org/docs/latest/rules/no-restricted-syntax + */ 'no-restricted-syntax': 'off', + + /* + Disallow ternary operators. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-ternary + */ 'no-ternary': 'off', + + /* + Disallow the use of `undefined` as an identifier. + Off: `undefined` is a normal value in modern code. + https://eslint.org/docs/latest/rules/no-undefined + */ 'no-undefined': 'off', + + /* + Disallow dangling underscores in identifiers. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/no-underscore-dangle + */ 'no-underscore-dangle': 'off', + + /* + Disallow `void` operators. + Off: `void promise` marks a fire-and-forget call on purpose. + https://eslint.org/docs/latest/rules/no-void + */ 'no-void': 'off', + + /* + Disallow specified warning terms in comments. + Off: the ticket-ref plugin requires a ticket instead of banning to-dos. + https://eslint.org/docs/latest/rules/no-warning-comments + */ 'no-warning-comments': 'off', + + /* + Require destructuring from arrays and/or objects. + Off: noisy; destructuring is not always clearer. + https://eslint.org/docs/latest/rules/prefer-destructuring + */ 'prefer-destructuring': 'off', + + /* + Enforce using named capture group in regular expression. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/prefer-named-capture-group + */ 'prefer-named-capture-group': 'off', + + /* + Disallow assignments that can lead to race conditions due to usage of `await` or `yield`. + Off: known false positives. + https://eslint.org/docs/latest/rules/require-atomic-updates + */ 'require-atomic-updates': 'off', + + /* + Enforce the use of `u` or `v` flag on regular expressions. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/require-unicode-regexp + */ 'require-unicode-regexp': 'off', + + /* + Enforce sorted `import` declarations within modules. + Off: import-x/order covers it, with groups. + https://eslint.org/docs/latest/rules/sort-imports + */ 'sort-imports': 'off', + + /* + Require object keys to be sorted. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/sort-keys + */ 'sort-keys': 'off', + + /* + Require variables within the same declaration block to be sorted. + Off: opinion; left to review. + https://eslint.org/docs/latest/rules/sort-vars + */ 'sort-vars': 'off', + + /* + Require or disallow strict mode directives. + Off: ES modules are strict already. + https://eslint.org/docs/latest/rules/strict + */ strict: 'off', + + /* + Require or disallow Unicode byte order mark (BOM). + Off: editors and Prettier handle the byte order mark. + https://eslint.org/docs/latest/rules/unicode-bom + */ 'unicode-bom': 'off', + + /* + Require `var` declarations be placed at the top of their containing scope. + Off: `no-var` makes it redundant. + https://eslint.org/docs/latest/rules/vars-on-top + */ 'vars-on-top': 'off', + }, }, ]; diff --git a/packages/eslint-config-base/src/rules/import-x.js b/packages/eslint-config-base/src/rules/import-x.js index f512fae..934e7df 100644 --- a/packages/eslint-config-base/src/rules/import-x.js +++ b/packages/eslint-config-base/src/rules/import-x.js @@ -25,25 +25,135 @@ export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyG plugins: { 'import-x': importX }, rules: { ...flatConfigs.recommended.rules, - - // --- on + /* + Ensure all imports appear before other statements. + Imports come before other statements. Tests are exempt, because `vi.mock()`/`jest.mock()` + calls are hoisted above imports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/first.md + */ 'import-x/first': 'error', + + /* + Enforce a newline after import statements. + A blank line after the last import visually separates the import block from the code. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/newline-after-import.md + */ 'import-x/newline-after-import': 'error', + + /* + Forbid import of modules using absolute paths. + Absolute file system paths break on every other machine; import by package name or relative + path. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-absolute-path.md + */ 'import-x/no-absolute-path': 'error', + + /* + Forbid AMD `require` and `define` calls. + AMD `require`/`define` has no place in an ES module codebase. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-amd.md + */ 'import-x/no-amd': 'error', + + /* + Forbid a module from importing a module with a dependency path back to itself. + Import cycles make load order fragile; external packages are ignored. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-cycle.md + */ 'import-x/no-cycle': ['error', { ignoreExternal: true }], + + /* + Forbid imported names marked with `@deprecated` documentation tag. + Using symbols marked @deprecated is flagged. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-deprecated.md + */ 'import-x/no-deprecated': 'error', + + /* + Forbid repeated import of the same module in multiple places. + The same module imported twice in one file should be one import statement. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-duplicates.md + */ 'import-x/no-duplicates': 'error', + + /* + Forbid `require()` calls with expressions. + `require(variable)` cannot be analyzed by bundlers or by this plugin. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-dynamic-require.md + */ 'import-x/no-dynamic-require': 'error', + + /* + Forbid empty named import blocks. + `import {} from 'x'` is almost always a leftover after removing a name. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-empty-named-blocks.md + */ 'import-x/no-empty-named-blocks': 'error', + + /* + Forbid the use of extraneous packages. + Only declared dependencies may be imported; devDependencies and peer dependencies are + allowed where listed (tests, scripts, config files). + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-extraneous-dependencies.md + */ 'import-x/no-extraneous-dependencies': ['error', { devDependencies: devDependencyGlobs, peerDependencies: true }], + + /* + Forbid import statements with CommonJS module.exports. + A file mixing ES `import` with `module.exports` breaks bundlers and Node's module detection. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-import-module-exports.md + */ 'import-x/no-import-module-exports': 'error', + + /* + Forbid the use of mutable exports with `var` or `let`. + An exported `let` can change behind the importer's back; export constants. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-mutable-exports.md + */ 'import-x/no-mutable-exports': 'error', + + /* + Forbid named default exports. + `import { default as x }` is just `import x`. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-default.md + */ 'import-x/no-named-default': 'error', + + /* + Forbid importing packages through relative paths. + Import a workspace package by its name, not through `../../other-package/src`. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-relative-packages.md + */ 'import-x/no-relative-packages': 'error', + + /* + Forbid a module from importing itself. + A module importing itself is always a mistake. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-self-import.md + */ 'import-x/no-self-import': 'error', + + /* + Forbid unnecessary path segments in import and require statements. + `./a/../b` is just `./b`. `index.js` stays explicit because ESM has no directory imports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-useless-path-segments.md + */ 'import-x/no-useless-path-segments': 'error', + + /* + Forbid webpack loader syntax in imports. + `import 'style-loader!./a.css'` ties the source to one bundler; configure loaders in the + build. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-webpack-loader-syntax.md + */ 'import-x/no-webpack-loader-syntax': 'error', + + /* + Enforce a convention in module import order. + Groups (built-in, external, internal, parent, sibling, index) separated by a blank line and + sorted A-Z inside; `pathGroups` lets projects place `#alias` imports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/order.md + */ 'import-x/order': [ 'error', { @@ -54,36 +164,196 @@ export function createImportsConfig({ devDependencyGlobs = defaultDevDependencyG }, ], - // --- off: plugins default-export objects whose members also ship as named exports + /* + Forbid use of exported name as identifier of default export. + Off: plugins default-export an object whose members also ship as named exports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-as-default.md + */ 'import-x/no-named-as-default': 'off', + + /* + Forbid use of exported name as property of default export. + Off: plugins default-export an object whose members also ship as named exports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-as-default-member.md + */ 'import-x/no-named-as-default-member': 'off', - // --- off: opinion, project-specific, or too costly for a shared config + /* + Forbid authoring of barrel files. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/avoid-barrel-files.md + */ 'import-x/avoid-barrel-files': 'off', + + /* + Enforce or ban the use of inline type-only markers for named imports. + One style for type imports: inline specifiers, one import statement per module. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/consistent-type-specifier-style.md + */ 'import-x/consistent-type-specifier-style': 'off', + + /* + Forbid importing barrel files. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/avoid-importing-barrel-files.md + */ 'import-x/avoid-importing-barrel-files': 'off', + + /* + Forbid namespace imports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/avoid-namespace-import.md + */ 'import-x/avoid-namespace-import': 'off', + + /* + Forbid re-exporting * from a module. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/avoid-re-export-all.md + */ 'import-x/avoid-re-export-all': 'off', + + /* + Enforce a leading comment with the webpackChunkName for dynamic imports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/dynamic-import-chunkname.md + */ 'import-x/dynamic-import-chunkname': 'off', + + /* + Ensure all exports appear after other statements. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/exports-last.md + */ 'import-x/exports-last': 'off', + + /* + Ensure consistent use of file extension within the import path. + Off: file extension rules differ per project (Node ESM vs bundlers). + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/extensions.md + */ 'import-x/extensions': 'off', + + /* + Prefer named exports to be grouped together in a single export declaration. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/group-exports.md + */ 'import-x/group-exports': 'off', + + /* + Enforce the maximum number of dependencies a module can have. + Off: needs a project-specific threshold. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/max-dependencies.md + */ 'import-x/max-dependencies': 'off', + + /* + Forbid anonymous values as default exports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-anonymous-default-export.md + */ 'import-x/no-anonymous-default-export': 'off', + + /* + Forbid CommonJS `require` calls and `module.exports` or `exports.*`. + Off: config files and scripts may stay CommonJS; mixing both in one file is caught by no- + import-module-exports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-commonjs.md + */ 'import-x/no-commonjs': 'off', + + /* + Forbid default exports. + Off: config files require default exports. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-default-export.md + */ 'import-x/no-default-export': 'off', + + /* + Forbid importing the submodules of other modules. + Off: configured per project. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-internal-modules.md + */ 'import-x/no-internal-modules': 'off', + + /* + Forbid named exports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-export.md + */ 'import-x/no-named-export': 'off', + + /* + Forbid namespace (a.k.a. "wildcard" `*`) imports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-namespace.md + */ 'import-x/no-namespace': 'off', + + /* + Forbid Node.js builtin modules. + Off: this config targets Node.js as well. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-nodejs-modules.md + */ 'import-x/no-nodejs-modules': 'off', + + /* + Forbid importing modules from parent directories. + Off: parent imports are normal in most layouts. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-relative-parent-imports.md + */ 'import-x/no-relative-parent-imports': 'off', + + /* + Forbid importing a default export by a different name. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-rename-default.md + */ 'import-x/no-rename-default': 'off', + + /* + Enforce which files can be imported in a given folder. + Off: configured per project; the shared config has no zones to restrict. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-restricted-paths.md + */ 'import-x/no-restricted-paths': 'off', + + /* + Forbid unassigned imports. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-unassigned-import.md + */ 'import-x/no-unassigned-import': 'off', + + /* + Forbid modules without exports, or exports without matching import in another module. + Off: slow and noisy in monorepos. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-unused-modules.md + */ 'import-x/no-unused-modules': 'off', + + /* + Prefer a default export if module exports a single name or multiple names. + Off: named exports are easier to refactor and to find. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/prefer-default-export.md + */ 'import-x/prefer-default-export': 'off', + + /* + Enforce using namespace imports for specific modules, like `react`/`react-dom`, etc. + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/prefer-namespace-import.md + */ 'import-x/prefer-namespace-import': 'off', + + /* + Forbid potentially ambiguous parse goal (`script` vs. `module`). + Off: opinion, project-specific, or too costly for a shared config. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/unambiguous.md + */ 'import-x/unambiguous': 'off', + }, }; } diff --git a/packages/eslint-config-base/src/rules/stylistic.js b/packages/eslint-config-base/src/rules/stylistic.js index 15620fd..cbaf3d3 100644 --- a/packages/eslint-config-base/src/rules/stylistic.js +++ b/packages/eslint-config-base/src/rules/stylistic.js @@ -8,23 +8,97 @@ export default { name: 'cospired/stylistic', plugins: { '@stylistic': stylistic }, rules: { + /* + Disallow extra closing tags for components without children. + `
` instead of `
`. + https://eslint.style/rules/jsx-self-closing-comp + */ '@stylistic/jsx-self-closing-comp': 'error', + + /* + Require or disallow an empty line between class members. + A blank line between class members, except between single-line ones. + https://eslint.style/rules/lines-between-class-members + */ '@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + + /* + Require or disallow padding lines between statements. + A blank line before every `return` and after declaration blocks makes early returns and the + end of setup visible. Prettier leaves these lines alone. + https://eslint.style/rules/padding-line-between-statements + */ '@stylistic/padding-line-between-statements': [ 'error', { blankLine: 'always', prev: '*', next: 'return' }, { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, ], + + /* + Enforce consistent spacing after the `//` or `/*` in a comment. + `// comment` with a space; `///` stays allowed for TypeScript directives. + https://eslint.style/rules/spaced-comment + */ '@stylistic/spaced-comment': ['error', 'always', { markers: ['/'] }], + /* + Enforce consistent line breaks after opening and before closing braces. + Off: formatting is Prettier's job. + https://eslint.style/rules/curly-newline + */ '@stylistic/curly-newline': 'off', + + /* + Enforce consistent line break styles for JSX props. + Off: formatting is Prettier's job. + https://eslint.style/rules/jsx-props-style + */ '@stylistic/exp-jsx-props-style': 'off', + + /* + Enforce consistent spacing and line break styles inside brackets. + Off: formatting is Prettier's job. + https://eslint.style/rules/list-style + */ '@stylistic/exp-list-style': 'off', + + /* + Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX + expressions on literals in JSX children or attributes. + Off: formatting is Prettier's job. + https://eslint.style/rules/jsx-curly-brace-presence + */ '@stylistic/jsx-curly-brace-presence': 'off', + + /* + Enforce line breaks before and after JSX elements when they are used as arguments to a + function. + Off: formatting is Prettier's job. + https://eslint.style/rules/jsx-function-call-newline + */ '@stylistic/jsx-function-call-newline': 'off', + + /* + Enforce PascalCase for user-defined JSX components. + Off: formatting is Prettier's job. + https://eslint.style/rules/jsx-pascal-case + */ '@stylistic/jsx-pascal-case': 'off', + + /* + Enforce position of line comments. + Off: formatting is Prettier's job. + https://eslint.style/rules/line-comment-position + */ '@stylistic/line-comment-position': 'off', + + /* + Enforce a particular style for multiline comments. + Off: formatting is Prettier's job. + https://eslint.style/rules/multiline-comment-style + */ '@stylistic/multiline-comment-style': 'off', + }, }; diff --git a/packages/eslint-config-base/src/rules/todo-plz.js b/packages/eslint-config-base/src/rules/todo-plz.js index 60724f6..c852c27 100644 --- a/packages/eslint-config-base/src/rules/todo-plz.js +++ b/packages/eslint-config-base/src/rules/todo-plz.js @@ -4,5 +4,14 @@ import todoPlz from 'eslint-plugin-todo-plz'; export default { name: 'cospired/todo-plz', plugins: { 'todo-plz': todoPlz }, - rules: { 'todo-plz/ticket-ref': ['error', { pattern: '#\\d+' }] }, + rules: { + // eslint-disable-next-line todo-plz/ticket-ref -- documentation text and links contain the word "todo" + /* + Require a ticket reference in the to-do comment. + Every TODO references a ticket so it cannot rot: `TODO(#123): ...`. + https://github.com/sawyerh/eslint-plugin-todo-plz/blob/main/docs/rules/ticket-ref.md + */ + 'todo-plz/ticket-ref': ['error', { pattern: '#\\d+' }], + + }, }; diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js index f5d9a6e..742ffe2 100644 --- a/packages/eslint-config-react/src/rules/formatjs.js +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -10,13 +10,59 @@ export default { name: 'cospired/formatjs', rules: { ...errorOnly(formatjs.configs.recommended.rules), + /* + Enforce description in message descriptor. + Off: there is no translator workflow that needs a description on every message yet. + https://formatjs.github.io/docs/tooling/linter#enforce-description + */ 'formatjs/enforce-description': 'off', + + /* + Enforce (generated) ID in message descriptor. + Off: message ids are managed by the build tooling, not by hand. + https://formatjs.github.io/docs/tooling/linter#enforce-id + */ 'formatjs/enforce-id': 'off', + + /* + Check ICU placeholders and optionally generate TypeScript argument contracts. + Off: no project-wide restriction on message types. + */ 'formatjs/enforce-message-types': 'off', + + /* + Ban explicit ID from MessageDescriptor. + Off: message ids are managed by the build tooling, not by hand. + https://formatjs.github.io/docs/tooling/linter#no-id + */ 'formatjs/no-id': 'off', + + /* + Enforce translation of specific object properties. + Off: plain strings in objects are not translatable text in general. + https://formatjs.github.io/docs/tooling/linter#no-literal-string-in-object + */ 'formatjs/no-literal-string-in-object': 'off', + + /* + Prefer `FormattedMessage` component over `intl.formatMessage` if applicable. + Off: imperative `intl.formatMessage` is used deliberately. + https://formatjs.github.io/docs/tooling/linter#prefer-formatted-message + */ 'formatjs/prefer-formatted-message': 'off', + + /* + Disallow camel case placeholders in message. + snake_case placeholders keep translator tooling and translation memories happy. + https://formatjs.github.io/docs/tooling/linter#no-camel-case + */ 'formatjs/no-camel-case': 'error', + + /* + Make sure ICU messages are formatted correctly with no bad select statements, plurals, etc. + A syntax error in an ICU message only shows up at runtime otherwise. + */ 'formatjs/no-invalid-icu': 'error', + }, }; diff --git a/packages/eslint-config-react/src/rules/react-hooks.js b/packages/eslint-config-react/src/rules/react-hooks.js index 39087dd..6fc7c6b 100644 --- a/packages/eslint-config-react/src/rules/react-hooks.js +++ b/packages/eslint-config-react/src/rules/react-hooks.js @@ -10,18 +10,107 @@ export default [ { name: 'cospired/react-hooks', rules: { + /* + Validates against calling capitalized functions/methods instead of using JSX. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/capitalized-calls + */ 'react-hooks/capitalized-calls': 'off', + + /* + Validates that effect dependencies are exhaustive and without extraneous values. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/exhaustive-effect-dependencies + */ 'react-hooks/exhaustive-effect-dependencies': 'off', + + /* + Validates usage of fbt. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/fbt + */ 'react-hooks/fbt': 'off', + + /* + Validates the rules of hooks. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/hooks + */ 'react-hooks/hooks': 'off', + + /* + Internal invariants. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/invariant + */ 'react-hooks/invariant': 'off', + + /* + Validates that useMemo() and useCallback() specify comprehensive dependencies without + extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for + more information. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/memo-dependencies + */ 'react-hooks/memo-dependencies': 'off', + + /* + Validates that effect dependencies are memoized. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/memoized-effect-dependencies + */ 'react-hooks/memoized-effect-dependencies': 'off', + + /* + Validates against deriving values from state in an effect. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/no-deriving-state-in-effects + */ 'react-hooks/no-deriving-state-in-effects': 'off', + + /* + Validates against suppression of other rules. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/rule-suppression + */ 'react-hooks/rule-suppression': 'off', + + /* + Validates against invalid syntax. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/syntax + */ 'react-hooks/syntax': 'off', + + // eslint-disable-next-line todo-plz/ticket-ref -- documentation text and links contain the word "todo" + /* + Unimplemented features. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/todo + */ 'react-hooks/todo': 'off', + + /* + Validates that useMemos always return a value and that the result of the useMemo is used by + the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for + more information. + Off: experimental or internal React Compiler diagnostic, not part of the stable recommended + set. + https://react.dev/reference/eslint-plugin-react-hooks/lints/void-use-memo + */ 'react-hooks/void-use-memo': 'off', + }, }, ]; diff --git a/packages/eslint-config-react/src/rules/react-refresh.js b/packages/eslint-config-react/src/rules/react-refresh.js index 5d383ec..fd0bf8b 100644 --- a/packages/eslint-config-react/src/rules/react-refresh.js +++ b/packages/eslint-config-react/src/rules/react-refresh.js @@ -4,5 +4,12 @@ import reactRefresh from 'eslint-plugin-react-refresh'; export default { name: 'cospired/react-refresh', plugins: { 'react-refresh': reactRefresh }, - rules: { 'react-refresh/only-export-components': ['error', { allowConstantExport: true }] }, + rules: { + /* + Keeps components hot-reloadable (Vite Fast Refresh): a file exporting components exports + nothing else but constants. + */ + 'react-refresh/only-export-components': ['error', { allowConstantExport: true }], + + }, }; diff --git a/packages/eslint-config-typescript/src/rules/import-x-typescript.js b/packages/eslint-config-typescript/src/rules/import-x-typescript.js index 7315bfd..e276e67 100644 --- a/packages/eslint-config-typescript/src/rules/import-x-typescript.js +++ b/packages/eslint-config-typescript/src/rules/import-x-typescript.js @@ -5,7 +5,15 @@ import { tsExtensions } from '../globs.js'; /** TypeScript-only import-x config: the typescript preset and a type-import style that matches `consistent-type-imports`. */ export const importXTypescript = flatConfigs.typescript; -export const importXTypescriptRules = { 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'] }; +export const importXTypescriptRules = { + /* + Enforce or ban the use of inline type-only markers for named imports. + One style for type imports: inline specifiers, one import statement per module. + https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/consistent-type-specifier-style.md + */ + 'import-x/consistent-type-specifier-style': ['error', 'prefer-inline'], + +}; /** import-x settings so `#alias`, tsconfig `paths` and `.js`-to-`.ts` imports resolve. */ export function importSettings(project) { diff --git a/packages/eslint-config-typescript/src/rules/typescript-eslint.js b/packages/eslint-config-typescript/src/rules/typescript-eslint.js index 55f2c97..de4c3d8 100644 --- a/packages/eslint-config-typescript/src/rules/typescript-eslint.js +++ b/packages/eslint-config-typescript/src/rules/typescript-eslint.js @@ -3,54 +3,304 @@ * Every non-deprecated rule is configured: here or by a preset (see test/completeness). */ export const typescriptRules = { - // --- on + /* + Enforce consistent usage of type imports. + Type-only imports are written inline (`import { type Foo }`), matching import-x/consistent-type- + specifier-style, so they vanish at compile time. + https://typescript-eslint.io/rules/consistent-type-imports + */ '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], + + /* + Enforce default parameters to be last. + Same as the core rule; the core version is off for TypeScript. + https://typescript-eslint.io/rules/default-param-last + */ '@typescript-eslint/default-param-last': 'error', + + /* + Disallow using the `delete` operator on computed key expressions. + Deleting by a computed key defeats the type checker; a Map or an object spread says it better. + https://typescript-eslint.io/rules/no-dynamic-delete + */ '@typescript-eslint/no-dynamic-delete': 'error', + + /* + Disallow type operations that resolve to the "empty object" type. + An empty object type accepts everything except null and undefined, which is rarely what was + meant. + https://typescript-eslint.io/rules/no-generated-empty-object-type + */ '@typescript-eslint/no-generated-empty-object-type': 'error', + + /* + Enforce the use of top-level import type qualifier when an import only has specifiers with + inline type qualifiers. + An import whose specifiers are all `type` would still load the module at runtime; make it a top- + level `import type`. + https://typescript-eslint.io/rules/no-import-type-side-effects + */ '@typescript-eslint/no-import-type-side-effects': 'error', + + /* + Disallow `this` keywords outside of classes or class-like objects. + Same as the core rule; the core version is off for TypeScript. + https://typescript-eslint.io/rules/no-invalid-this + */ '@typescript-eslint/no-invalid-this': 'error', + + /* + Disallow `void` type outside of generic or return types. + `void` only makes sense as a return type; elsewhere it usually means `undefined`. + https://typescript-eslint.io/rules/no-invalid-void-type + */ '@typescript-eslint/no-invalid-void-type': 'error', + + /* + Disallow Promises in places not designed to handle them. + Promises in conditions or passed where a void return is expected are bugs; unions are checked + strictly. + https://typescript-eslint.io/rules/no-misused-promises + */ '@typescript-eslint/no-misused-promises': ['error', { checksConditionals: { flagUnions: 'strict' } }], + + /* + Disallow non-null assertions in the left operand of a nullish coalescing operator. + `x! ?? y` contradicts itself: if `x` can be null the `!` is wrong, if not the `??` is dead. + https://typescript-eslint.io/rules/no-non-null-asserted-nullish-coalescing + */ '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error', + + /* + Disallow non-null assertions using the `!` postfix operator. + `!` silences the compiler instead of handling null; narrow or validate instead. + https://typescript-eslint.io/rules/no-non-null-assertion + */ '@typescript-eslint/no-non-null-assertion': 'error', + + /* + Disallow variable declarations from shadowing variables declared in the outer scope. + Same as the core rule, but understands types and enums; the core version is off for TypeScript. + https://typescript-eslint.io/rules/no-shadow + */ '@typescript-eslint/no-shadow': 'error', + + /* + Disallow unnecessary assignment of constructor property parameter. + A parameter property already assigns the field; assigning it again in the constructor is noise. + https://typescript-eslint.io/rules/no-unnecessary-parameter-property-assignment + */ '@typescript-eslint/no-unnecessary-parameter-property-assignment': 'error', + + /* + Disallow unused private class members. + Same as the core rule; the core version is off for TypeScript. + https://typescript-eslint.io/rules/no-unused-private-class-members + */ '@typescript-eslint/no-unused-private-class-members': 'error', + + /* + Disallow unused variables. + Same as the core rule: unused code never lands; a leading underscore is the explicit opt-out. + https://typescript-eslint.io/rules/no-unused-vars + */ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + + /* + Disallow the use of variables before they are defined. + Same as the core rule, strict; the core version is off for TypeScript. + https://typescript-eslint.io/rules/no-use-before-define + */ '@typescript-eslint/no-use-before-define': 'error', + + /* + Disallow unnecessary constructors. + Same as the core rule, aware of parameter properties; the core version is off for TypeScript. + https://typescript-eslint.io/rules/no-useless-constructor + */ '@typescript-eslint/no-useless-constructor': 'error', + + /* + Disallow empty exports that don't change anything in a module file. + An empty `export {}` is only needed in files without other imports or exports. + https://typescript-eslint.io/rules/no-useless-empty-export + */ '@typescript-eslint/no-useless-empty-export': 'error', + + /* + Require all enum members to be literal values. + Enum members are literals, so their values are known without evaluating code. + https://typescript-eslint.io/rules/prefer-literal-enum-member + */ '@typescript-eslint/prefer-literal-enum-member': 'error', + + /* + Disallow two overloads that could be unified into one with a union or an optional/rest + parameter. + Overloads that differ by one parameter are clearer as one signature with a union or optional + parameter. + https://typescript-eslint.io/rules/unified-signatures + */ '@typescript-eslint/unified-signatures': 'error', - // --- core rules replaced by the typescript-eslint versions above (or covered by the compiler) + /* + Require `return` statements to either always or never specify values. + A function either always returns a value or never does. Off for TypeScript, where the compiler's + noImplicitReturns covers it. + https://eslint.org/docs/latest/rules/consistent-return + */ 'consistent-return': 'off', + + /* + Enforce default parameters to be last. + A default before a required parameter cannot be omitted anyway. + https://eslint.org/docs/latest/rules/default-param-last + */ 'default-param-last': 'off', + + /* + Disallow use of `this` in contexts where the value of `this` is `undefined`. + `this` outside a method is almost always a mistake. + https://eslint.org/docs/latest/rules/no-invalid-this + */ 'no-invalid-this': 'off', + + /* + Disallow variable declarations from shadowing variables declared in the outer scope. + An inner variable that reuses an outer name invites mixing the two up. + https://eslint.org/docs/latest/rules/no-shadow + */ 'no-shadow': 'off', + + /* + Disallow unused private class members. + Private members nobody uses are dead code. + https://eslint.org/docs/latest/rules/no-unused-private-class-members + */ 'no-unused-private-class-members': 'off', + + /* + Disallow the use of variables before they are defined. + Strict: nothing is used before its definition, functions included, so files read top to bottom. + https://eslint.org/docs/latest/rules/no-use-before-define + */ 'no-use-before-define': 'off', + + /* + Disallow unnecessary constructors. + A constructor that only forwards to super adds nothing. + https://eslint.org/docs/latest/rules/no-useless-constructor + */ 'no-useless-constructor': 'off', - // --- off: opinion, thresholds, or the compiler already reports it + /* + Enforce that class methods utilize `this`. + Off: opinion; left to review. + https://typescript-eslint.io/rules/class-methods-use-this + */ '@typescript-eslint/class-methods-use-this': 'off', + + /* + Require explicit accessibility modifiers on class properties and methods. + Off: opinion; left to review. + https://typescript-eslint.io/rules/explicit-member-accessibility + */ '@typescript-eslint/explicit-member-accessibility': 'off', + + /* + Require explicit return and argument types on exported functions' and classes' public class + methods. + Off: explicit-function-return-type covers it. + https://typescript-eslint.io/rules/explicit-module-boundary-types + */ '@typescript-eslint/explicit-module-boundary-types': 'off', + + /* + Require or disallow initialization in variable declarations. + Off: needs a project-specific threshold. + https://typescript-eslint.io/rules/init-declarations + */ '@typescript-eslint/init-declarations': 'off', + + /* + Enforce a maximum number of parameters in function definitions. + Off: needs a project-specific threshold. + https://typescript-eslint.io/rules/max-params + */ '@typescript-eslint/max-params': 'off', + + /* + Require a consistent member declaration order. + Off: opinion; left to review. + https://typescript-eslint.io/rules/member-ordering + */ '@typescript-eslint/member-ordering': 'off', + + /* + Enforce using a particular method signature syntax. + Off: opinion; left to review. + https://typescript-eslint.io/rules/method-signature-style + */ '@typescript-eslint/method-signature-style': 'off', + + /* + Disallow duplicate class members. + Off: the compiler already reports duplicate members. + https://typescript-eslint.io/rules/no-dupe-class-members + */ '@typescript-eslint/no-dupe-class-members': 'off', + + /* + Disallow classes used as namespaces. + Off: opinion; left to review. + https://typescript-eslint.io/rules/no-extraneous-class + */ '@typescript-eslint/no-extraneous-class': 'off', + + /* + Disallow magic numbers. + Off: configured per project; the shared config has nothing to restrict. + https://typescript-eslint.io/rules/no-magic-numbers + */ '@typescript-eslint/no-magic-numbers': 'off', + + /* + Disallow variable redeclaration. + Off: the compiler already reports redeclarations. + https://typescript-eslint.io/rules/no-redeclare + */ '@typescript-eslint/no-redeclare': 'off', + + /* + Disallow certain types. + Off: configured per project; the shared config has nothing to restrict. + https://typescript-eslint.io/rules/no-restricted-types + */ '@typescript-eslint/no-restricted-types': 'off', + + /* + Require or disallow parameter properties in class constructors. + Off: opinion; left to review. + https://typescript-eslint.io/rules/parameter-properties + */ '@typescript-eslint/parameter-properties': 'off', + + /* + Require each enum member value to be explicitly initialized. + Off: opinion; left to review. + https://typescript-eslint.io/rules/prefer-enum-initializers + */ '@typescript-eslint/prefer-enum-initializers': 'off', + }; /** Rules for source files only; tests are exempt. */ export const sourceOnlyRules = { + /* + Require explicit return types on functions and class methods. + Return types are written down in source files; callbacks (expressions) and tests are exempt. + https://typescript-eslint.io/rules/explicit-function-return-type + */ '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], + }; From 9d5d88b58888b6340752a9e715555bf213461a20 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:22:35 +0800 Subject: [PATCH 26/33] test: every rule has a comment block (description, reasoning, URL) and a blank line after it Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- package-lock.json | 51 ++++++++----- package.json | 1 + .../eslint-config-react/src/rules/formatjs.js | 4 + .../src/rules/react-refresh.js | 2 + test-utils/rule-docs.test.js | 75 +++++++++++++++++++ 5 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 test-utils/rule-docs.test.js diff --git a/package-lock.json b/package-lock.json index 0795e5c..8e099e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-todo-plz": "^1.4.1", + "espree": "^11.2.0", "globals": "^17.14.0", "prettier": "^3.9.10", "typescript": "~6.0.3", @@ -834,6 +835,23 @@ "eslint": "^9.0.0 || ^10.0.0" } }, + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "license": "MIT", @@ -1936,8 +1954,17 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/espree": { + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.16.0", @@ -1951,23 +1978,13 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" diff --git a/package.json b/package.json index 3b5094a..9f2de3a 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-todo-plz": "^1.4.1", + "espree": "^11.2.0", "globals": "^17.14.0", "prettier": "^3.9.10", "typescript": "~6.0.3", diff --git a/packages/eslint-config-react/src/rules/formatjs.js b/packages/eslint-config-react/src/rules/formatjs.js index 742ffe2..c2a482d 100644 --- a/packages/eslint-config-react/src/rules/formatjs.js +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -14,6 +14,8 @@ export default { Enforce description in message descriptor. Off: there is no translator workflow that needs a description on every message yet. https://formatjs.github.io/docs/tooling/linter#enforce-description + https://formatjs.github.io/docs/tooling/linter#enforce-message-types + https://formatjs.github.io/docs/tooling/linter#no-invalid-icu */ 'formatjs/enforce-description': 'off', @@ -27,6 +29,7 @@ export default { /* Check ICU placeholders and optionally generate TypeScript argument contracts. Off: no project-wide restriction on message types. + https://formatjs.github.io/docs/tooling/linter#enforce-message-types */ 'formatjs/enforce-message-types': 'off', @@ -61,6 +64,7 @@ export default { /* Make sure ICU messages are formatted correctly with no bad select statements, plurals, etc. A syntax error in an ICU message only shows up at runtime otherwise. + https://formatjs.github.io/docs/tooling/linter#no-invalid-icu */ 'formatjs/no-invalid-icu': 'error', diff --git a/packages/eslint-config-react/src/rules/react-refresh.js b/packages/eslint-config-react/src/rules/react-refresh.js index fd0bf8b..3f0ea8a 100644 --- a/packages/eslint-config-react/src/rules/react-refresh.js +++ b/packages/eslint-config-react/src/rules/react-refresh.js @@ -6,8 +6,10 @@ export default { plugins: { 'react-refresh': reactRefresh }, rules: { /* + Validate that components can safely be updated with Fast Refresh. Keeps components hot-reloadable (Vite Fast Refresh): a file exporting components exports nothing else but constants. + https://github.com/ArnaudBarre/eslint-plugin-react-refresh#only-export-components */ 'react-refresh/only-export-components': ['error', { allowConstantExport: true }], diff --git a/test-utils/rule-docs.test.js b/test-utils/rule-docs.test.js new file mode 100644 index 0000000..3f8975f --- /dev/null +++ b/test-utils/rule-docs.test.js @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import * as espree from 'espree'; + +const packages = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'packages'); +const files = fs + .readdirSync(packages) + .flatMap((dir) => { + const rulesDir = path.join(packages, dir, 'src', 'rules'); + + return fs.existsSync(rulesDir) ? fs.readdirSync(rulesDir).map((file) => path.join(rulesDir, file)) : []; + }) + .filter((file) => file.endsWith('.js')); + +const keyName = (property) => property.key.value ?? property.key.name; + +/** Objects holding rule settings: a `rules: {...}` property or a `const xxxRules = {...}` declaration. */ +function ruleObjects(node, found = []) { + if (node && typeof node === 'object') { + if (node.type === 'Property' && keyName(node) === 'rules' && node.value.type === 'ObjectExpression') found.push(node.value); + if (node.type === 'VariableDeclarator' && /Rules$/.test(node.id.name ?? '') && node.init?.type === 'ObjectExpression') found.push(node.init); + for (const child of Object.values(node)) { + if (child && typeof child === 'object') ruleObjects(child, found); + } + } + + return found; +} + +/** Problems for one rules file: a rule without a documentation comment block (description, reasoning, URL) or without a blank line after it. */ +export function undocumentedRules(source) { + const ast = espree.parse(source, { ecmaVersion: 'latest', sourceType: 'module', comment: true, range: true }); + const problems = []; + + for (const object of ruleObjects(ast)) { + for (const property of object.properties) { + if (property.type !== 'Property') continue; + const name = keyName(property); + const comment = ast.comments.filter((c) => c.type === 'Block' && c.range[1] <= property.range[0]).pop(); + const between = comment ? source.slice(comment.range[1], property.range[0]) : 'x'; + const lines = comment?.value.split('\n').map((line) => line.trim()).filter(Boolean) ?? []; + + if (!comment || between.trim() !== '' || !lines.some((line) => /^https?:\/\//.test(line)) || lines.length < 3) { + problems.push(`${name}: needs a comment block (description, reasoning, URL) directly above it`); + } else if (!/^,?[ \t]*\r?\n[ \t]*\r?\n/.test(source.slice(property.range[1]))) { + problems.push(`${name}: needs an empty line after it`); + } + } + } + + return problems; +} + +test('the rule files exist', () => { + assert.ok(files.length >= 8, `found ${files.length} rule files`); +}); + +test('undocumentedRules flags a rule without a comment block or trailing empty line', () => { + const good = "export const fooRules = {\n /*\n Does a thing.\n Because.\n https://example.com/a\n */\n a: 'error',\n\n};\n"; + + assert.deepEqual(undocumentedRules(good), []); + assert.equal(undocumentedRules(good.replace(/ {2}\/\*[\s\S]*\*\/\n/, '')).length, 1); + assert.equal(undocumentedRules(good.replace('\n\n}', '\n}')).length, 1); + assert.equal(undocumentedRules(good.replace(' https://example.com/a\n', '')).length, 1); +}); + +for (const file of files) { + test(`${path.relative(packages, file)}: every rule has a comment block (description, reasoning, URL) and an empty line after it`, () => { + assert.deepEqual(undocumentedRules(fs.readFileSync(file, 'utf8')), []); + }); +} From 410fe70416aafccdcb8c27e7856ca064be1007f7 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:25:15 +0800 Subject: [PATCH 27/33] chore: drop Node 20 (engines ^22.13 || >=24, CI on 22.13, 24, 26) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 2 +- README.md | 2 +- .../specs/2026-11-10-eslint-config-2026-design.md | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- packages/eslint-config-base/README.md | 2 +- packages/eslint-config-base/package.json | 2 +- packages/eslint-config-react-typescript/package.json | 2 +- packages/eslint-config-react/package.json | 2 +- packages/eslint-config-typescript/README.md | 2 +- packages/eslint-config-typescript/package.json | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ab8ebd..5e8d84a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node: [20.19.0, 22.13.0, 24] + node: [22.13.0, 24, 26] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/README.md b/README.md index 41bd780..8695933 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ npx changeset # describe a change for the next release Releases are cut by the changesets GitHub Action on `main`. -CI runs the tests on Node 20.19, 22.13 and 24, again with every peer dependency at its minimum version, and the pack smoke test. +CI runs the tests on Node 22.13, 24 and 26, again with every peer dependency at its minimum version, and the pack smoke test. diff --git a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md index a4d8c44..0a42e2e 100644 --- a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md +++ b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md @@ -76,6 +76,6 @@ File layout, test harness details, fixture format, changesets config, CI (existi - **Completeness:** every non-deprecated rule of every plugin used (ESLint core, typescript-eslint, import-x, eslint-comments, todo-plz, @stylistic, react-hooks, react-refresh, formatjs) is configured, either by a recommended preset or explicitly on/off by us. Our files only list rules a preset does not set, or sets differently. `test/completeness.test.js` in each package fails otherwise (replaces `eslint-find-rules --unused`) and also fails on a named `cospired/*` override that repeats the preset's value. The ~200 on/off calls for rules nobody had decided were made by the implementer by clean-code principles (bug-preventing on, opinion/thresholds/formatting off) and are marked "added for completeness (unreviewed call)" in `docs/rules.md`. - **Layout:** rules are split per plugin in `src/rules/` (`eslint.js`, `typescript-eslint.js`, `import-x.js`, `eslint-comments.js`, `todo-plz.js`, `stylistic.js`; react: `react-hooks.js`, `react-refresh.js`, `formatjs.js`, `react.js`), as in the old packages. - **Goal:** configs usable with no overrides in eventured, danceregistry and our other projects: every entry has a ready-made default export (`export { default } from '@cospired/eslint-config-base/node'`). Entries: base JS, node-js, typescript, node, react, react/typescript. Tests, scripts and package-root TS files are linted without type information unless `typedTests: true`. -- **Publish readiness:** GitHub CI (Node 20.19/22.13/24, peer-dependency floors, packed-tarball smoke test), changesets release workflow with npm provenance, 8.0.0 for both packages. +- **Publish readiness:** GitHub CI (Node 22.13/24/26, peer-dependency floors, packed-tarball smoke test), changesets release workflow with npm provenance, 8.0.0 for both packages. - **Four packages (Dirk, after review):** `@cospired/eslint-config-base` (JS, `/node`), `-typescript` (TS, `/node`), `-react`, `-react-typescript`, all 8.0.0 via a changesets `fixed` group. JavaScript-only projects no longer need the TypeScript peers. Supersedes the earlier two-package layout. - **Rule documentation (Dirk):** every rule in the rule files has a comment block with the rule's description, our reasoning and its documentation URL, and a blank line after it, as in the old packages. The blocks were generated from the installed plugins' metadata plus a reason per rule; new rules added later follow the same format. diff --git a/package-lock.json b/package-lock.json index 8e099e9..093f7d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "typescript-eslint": "^8.72.2" }, "engines": { - "node": ">=22.13" + "node": "^22.13.0 || >=24" } }, "node_modules/@babel/code-frame": { @@ -2894,7 +2894,7 @@ "version": "7.0.2", "license": "BSD-3-Clause-Clear", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", @@ -2915,7 +2915,7 @@ "@cospired/eslint-config-base": "^7.0.2" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", @@ -2941,7 +2941,7 @@ "@cospired/eslint-config-typescript": "^7.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", @@ -2968,7 +2968,7 @@ "@cospired/eslint-config-base": "^7.0.2" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", diff --git a/package.json b/package.json index 9f2de3a..0fab1a4 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "homepage": "https://github.com/cospired/javascript#readme", "engines": { - "node": ">=22.13" + "node": "^22.13.0 || >=24" }, "devDependencies": { "@changesets/cli": "^3.0.3", diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index 88e3ad1..a49b148 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -1,6 +1,6 @@ # @cospired/eslint-config-base -Slightly opinionated ESLint flat config for JavaScript. Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. +Slightly opinionated ESLint flat config for JavaScript. Requires ESLint 10 and Node `^22.13 || >=24`. | Entry | For | |---|---| diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index fd609dd..922bdc4 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -20,7 +20,7 @@ "cospired" ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "exports": { ".": "./src/index.js", diff --git a/packages/eslint-config-react-typescript/package.json b/packages/eslint-config-react-typescript/package.json index d3e4849..25baf9f 100644 --- a/packages/eslint-config-react-typescript/package.json +++ b/packages/eslint-config-react-typescript/package.json @@ -15,7 +15,7 @@ "cospired" ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "scripts": { "test": "node --test test/*.test.js" diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index 37c4b5c..0dc2f3a 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -21,7 +21,7 @@ "cospired" ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "exports": { ".": "./src/index.js", diff --git a/packages/eslint-config-typescript/README.md b/packages/eslint-config-typescript/README.md index 902cb54..a468b81 100644 --- a/packages/eslint-config-typescript/README.md +++ b/packages/eslint-config-typescript/README.md @@ -1,6 +1,6 @@ # @cospired/eslint-config-typescript -Slightly opinionated, type-aware ESLint flat config for TypeScript, on top of [`@cospired/eslint-config-base`](../eslint-config-base). Requires ESLint 10 and Node `^20.19 || ^22.13 || >=24`. +Slightly opinionated, type-aware ESLint flat config for TypeScript, on top of [`@cospired/eslint-config-base`](../eslint-config-base). Requires ESLint 10 and Node `^22.13 || >=24`. | Entry | For | |---|---| diff --git a/packages/eslint-config-typescript/package.json b/packages/eslint-config-typescript/package.json index bd57b7c..2170be7 100644 --- a/packages/eslint-config-typescript/package.json +++ b/packages/eslint-config-typescript/package.json @@ -15,7 +15,7 @@ "cospired" ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^22.13.0 || >=24" }, "scripts": { "test": "node --test test/*.test.js" From 73d4144181ad72c8e17678167564d050e0ceaf5e Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:30:16 +0800 Subject: [PATCH 28/33] test: generic names instead of vitest examples in fixtures and docs Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- MIGRATION.md | 2 +- packages/eslint-config-typescript/README.md | 2 +- packages/eslint-config-typescript/src/globs.js | 2 +- packages/eslint-config-typescript/test/defaults.test.js | 2 +- .../fixtures/ts-project/{vitest.config.ts => tool.config.ts} | 0 .../fixtures/ts-project/{vitest.shared.ts => tool.shared.ts} | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename packages/eslint-config-typescript/test/fixtures/ts-project/{vitest.config.ts => tool.config.ts} (100%) rename packages/eslint-config-typescript/test/fixtures/ts-project/{vitest.shared.ts => tool.shared.ts} (100%) diff --git a/MIGRATION.md b/MIGRATION.md index f6254a7..06d715a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -11,5 +11,5 @@ - **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. - **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. - **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket `#123`; imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template`, `no-await-in-loop` and others are errors; TypeScript files require explicit return types outside tests. -- **Type-aware linting:** the TypeScript configs (including `react/typescript`) lint source files with type information through typescript-eslint's project service, so those files must belong to a tsconfig. Tests, scripts and TypeScript files at the package root (`vitest.config.ts`, ...) are linted without type information because build tsconfigs usually exclude them; set `typedTests: true` to type-check those too (they must then be in a tsconfig). Files outside every tsconfig report a parsing error. +- **Type-aware linting:** the TypeScript configs (including `react/typescript`) lint source files with type information through typescript-eslint's project service, so those files must belong to a tsconfig. Tests, scripts and TypeScript files at the package root (bundler, test runner or ORM configs) are linted without type information because build tsconfigs usually exclude them; set `typedTests: true` to type-check those too (they must then be in a tsconfig). Files outside every tsconfig report a parsing error. - **Every ESLint, typescript-eslint, import-x, @stylistic, eslint-comments, react-hooks and formatjs rule is configured** (on, or off with a reason in `docs/rules.md`). Rules we left off that you want can be turned on in your own config. diff --git a/packages/eslint-config-typescript/README.md b/packages/eslint-config-typescript/README.md index a468b81..5f1bb00 100644 --- a/packages/eslint-config-typescript/README.md +++ b/packages/eslint-config-typescript/README.md @@ -21,7 +21,7 @@ No options needed. `eslint.config.js`: export { default } from '@cospired/eslint-config-typescript/node'; // or the package root ``` -It uses typescript-eslint's project service to find your tsconfigs, ignores build output, and lints tests, scripts and package-root TypeScript files (`vitest.config.ts`, ...) without type information, since build tsconfigs usually exclude them. +It uses typescript-eslint's project service to find your tsconfigs, ignores build output, and lints tests, scripts and package-root TypeScript files (bundler, test runner or ORM configs) without type information, since build tsconfigs usually exclude them. ### Options diff --git a/packages/eslint-config-typescript/src/globs.js b/packages/eslint-config-typescript/src/globs.js index 79b4b52..a657e15 100644 --- a/packages/eslint-config-typescript/src/globs.js +++ b/packages/eslint-config-typescript/src/globs.js @@ -7,7 +7,7 @@ export const jsFiles = ['**/*.{js,jsx,mjs,cjs}']; /** * Files linted without type information unless `typedTests: true`: tests, TypeScript files at the package root - * (vitest.config.ts, drizzle.config.ts, ...) and scripts. They are often not part of any tsconfig (build configs + * (bundler, test runner or ORM configs such as `tool.config.ts`) and scripts. They are often not part of any tsconfig (build configs * exclude them), and type-aware linting reports a parsing error for such files. */ export const untypedGlobs = [...testGlobs, ...withMonorepoTwins(['*.{ts,mts,cts}', 'scripts/**/*.{ts,mts,cts}'])]; diff --git a/packages/eslint-config-typescript/test/defaults.test.js b/packages/eslint-config-typescript/test/defaults.test.js index 000fd09..9b6ffc9 100644 --- a/packages/eslint-config-typescript/test/defaults.test.js +++ b/packages/eslint-config-typescript/test/defaults.test.js @@ -31,7 +31,7 @@ test('zero options: TS files lint via the project service, no parse errors', asy }); test('zero options: test and config files outside every tsconfig lint without type information', async () => { - for (const file of ['other/c.test.ts', 'vitest.config.ts', 'vitest.shared.ts']) { + for (const file of ['other/c.test.ts', 'tool.config.ts', 'tool.shared.ts']) { for (const cfg of [tsDefault, nodeDefault]) { const messages = await lint(cfg, 'export const v = 1;\n', file); diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/vitest.config.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/tool.config.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/vitest.config.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/tool.config.ts diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/vitest.shared.ts b/packages/eslint-config-typescript/test/fixtures/ts-project/tool.shared.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/vitest.shared.ts rename to packages/eslint-config-typescript/test/fixtures/ts-project/tool.shared.ts From ffc5aa40c518bdc463054d61ffd7a1c4b27f094a Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:34:38 +0800 Subject: [PATCH 29/33] chore: address PR review (drop plans/specs, MIGRATION, rule table; one shared fixture project) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .changeset/flat-config-rewrite.md | 15 +- MIGRATION.md | 15 - README.md | 7 +- docs/rules.md | 397 ------------------ .../plans/2026-11-10-eslint-config-2026.md | 160 ------- .../2026-11-10-eslint-config-2026-design.md | 81 ---- package.json | 3 +- packages/eslint-config-base/README.md | 2 +- .../eslint-config-base/test/decisions.test.js | 5 +- .../eslint-config-base/test/defaults.test.js | 5 +- .../test/fixtures/cjs-project/src/a.js | 1 - .../test/fixtures/esm-project/package.json | 1 - packages/eslint-config-base/test/node.test.js | 8 +- .../test/decisions.test.js | 2 +- .../test/fixtures.test.js | 2 +- .../test/fixtures/ts-project/package.json | 1 - .../test/fixtures/ts-project/tsconfig.json | 1 - .../test/helpers.js | 5 +- .../test/load.test.js | 2 +- .../test/fixtures/ts-project/package.json | 1 - .../test/fixtures/ts-project/src/a.tsx | 1 - .../test/fixtures/ts-project/tsconfig.json | 1 - packages/eslint-config-react/test/helpers.js | 5 +- packages/eslint-config-typescript/README.md | 2 +- .../test/decisions.test.js | 5 +- .../test/defaults.test.js | 5 +- .../test/fixtures/ts-project/package.json | 1 - .../test/fixtures/ts-project/tsconfig.json | 1 - .../test/node.test.js | 5 +- .../test/typescript.test.js | 5 +- scripts/rule-decisions.json | 54 --- scripts/rule-sources.json | 31 -- scripts/rule-table.js | 127 ------ scripts/rule-table.test.js | 50 --- test-utils/fixtures/README.md | 16 + .../fixtures/cjs-project/package.json | 0 .../fixtures/ts-project/other/c.test.ts | 0 test-utils/fixtures/ts-project/package.json | 1 + .../fixtures/ts-project/src/a.test.tsx | 0 .../fixtures/ts-project/src/a.ts | 0 .../fixtures/ts-project/src/b.ts | 0 .../fixtures/ts-project/src/component.tsx | 0 test-utils/fixtures/ts-project/src/ok.ts | 1 - .../fixtures/ts-project/src/shared/x.ts | 0 .../fixtures/ts-project/test/a.test.ts | 0 .../fixtures/ts-project/tool.config.ts | 0 .../fixtures/ts-project/tool.shared.ts | 0 test-utils/fixtures/ts-project/tsconfig.json | 7 +- .../fixtures/ts-project/tsconfig.test.json | 0 49 files changed, 64 insertions(+), 968 deletions(-) delete mode 100644 MIGRATION.md delete mode 100644 docs/rules.md delete mode 100644 docs/superpowers/plans/2026-11-10-eslint-config-2026.md delete mode 100644 docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md delete mode 100644 packages/eslint-config-base/test/fixtures/cjs-project/src/a.js delete mode 100644 packages/eslint-config-base/test/fixtures/esm-project/package.json delete mode 100644 packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json delete mode 100644 packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json delete mode 100644 packages/eslint-config-react/test/fixtures/ts-project/package.json delete mode 100644 packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx delete mode 100644 packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json delete mode 100644 packages/eslint-config-typescript/test/fixtures/ts-project/package.json delete mode 100644 packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json delete mode 100644 scripts/rule-decisions.json delete mode 100644 scripts/rule-sources.json delete mode 100644 scripts/rule-table.js delete mode 100644 scripts/rule-table.test.js create mode 100644 test-utils/fixtures/README.md rename {packages/eslint-config-base/test => test-utils}/fixtures/cjs-project/package.json (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/other/c.test.ts (100%) create mode 100644 test-utils/fixtures/ts-project/package.json rename {packages/eslint-config-react-typescript/test => test-utils}/fixtures/ts-project/src/a.test.tsx (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/src/a.ts (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/src/b.ts (100%) rename packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx => test-utils/fixtures/ts-project/src/component.tsx (100%) delete mode 100644 test-utils/fixtures/ts-project/src/ok.ts rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/src/shared/x.ts (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/test/a.test.ts (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/tool.config.ts (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/tool.shared.ts (100%) rename {packages/eslint-config-typescript/test => test-utils}/fixtures/ts-project/tsconfig.test.json (100%) diff --git a/.changeset/flat-config-rewrite.md b/.changeset/flat-config-rewrite.md index 16be0ae..eea9667 100644 --- a/.changeset/flat-config-rewrite.md +++ b/.changeset/flat-config-rewrite.md @@ -5,4 +5,17 @@ '@cospired/eslint-config-react-typescript': major --- -Rewrite for ESLint 10 flat config, replacing airbnb and the hapi plugin. Four packages: base (JavaScript, plus `/node`), typescript (plus `/node`), react, react-typescript. See MIGRATION.md. +Rewrite for ESLint 10 flat config, replacing airbnb and the hapi plugin. + +Four packages: `base` (JavaScript, plus `/node`), `typescript` (plus `/node`), `react` and `react-typescript`. `base` and `react` keep their names; `typescript` and `react-typescript` are new, so JavaScript-only projects no longer need the TypeScript peers. Every entry works with no options: `export { default } from '@cospired/eslint-config-typescript/node';` is a complete `eslint.config.js`. + +Migrating from 7.x: + +- **Flat config only, ESLint 10, Node `^22.13 || >=24`.** `.eslintrc` is gone; use `eslint.config.js`. +- **No airbnb, no hapi.** The airbnb rules we kept are documented in the rule files (`src/rules/`); the rest are dropped. hapi `scope-start` cannot coexist with Prettier and `no-arrowception` is dropped. Blank lines before `return` and after declarations come from `@stylistic/padding-line-between-statements`. +- **Every rule is configured** (on, or off with a reason) for ESLint, typescript-eslint, import-x, @stylistic, eslint-comments, todo-plz, react-hooks, react-refresh and formatjs. Each rule has a comment block with description, reasoning and link. Rules left off that you want can be turned on in your own config. +- **No `warn` severities:** every rule is `error` or off. +- **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react` and `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. +- **React:** the `/hooks` export is gone (hooks rules are part of the default). jsx-a11y returns when the plugin supports ESLint 10. +- **Type-aware linting:** source files are linted with type information through typescript-eslint's project service and must belong to a tsconfig. Tests, scripts and TypeScript files at the package root (bundler, test runner or ORM configs) are linted without type information because build tsconfigs usually exclude them; `typedTests: true` type-checks those too (they must then be in a tsconfig). +- **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket (`#123`); imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template` and `no-await-in-loop` are errors; TypeScript source files need explicit return types. diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 06d715a..0000000 --- a/MIGRATION.md +++ /dev/null @@ -1,15 +0,0 @@ -# Migrating from 7.x to 8.0 - -8.0 is a rewrite. `base` and `react` keep their names; `typescript` and `react-typescript` are new packages, so JavaScript-only projects don't need the TypeScript peers. - -- **Flat config only, ESLint 10.** `.eslintrc` is gone; use `eslint.config.js` (see the package READMEs). -- **No airbnb.** `eslint-config-airbnb(-base)` and `airbnb-typescript` are no longer used or needed. The airbnb rules we kept are listed in [docs/rules.md](docs/rules.md); the rest are dropped. New rule set: ESLint and typescript-eslint recommended plus the rules in that table. -- **No hapi plugin.** `@hapi/scope-start` (a blank line at the start of function bodies) cannot coexist with Prettier and is dropped. `@hapi/no-arrowception` is dropped. Blank lines before `return` and after declarations are enforced via `@stylistic/padding-line-between-statements`. -- **TypeScript has its own packages:** `@cospired/eslint-config-typescript` (and `/node`) and `@cospired/eslint-config-react-typescript`. -- **Every entry works with no options:** `export { default } from '@cospired/eslint-config-typescript/node';` is a complete `eslint.config.js`. Node.js JavaScript projects use `@cospired/eslint-config-base/node`. -- **No `warn`.** Every rule is `error` or off. -- **Peers changed:** `@hapi/eslint-plugin`, `eslint-plugin-import`, `eslint-plugin-eslint-comments`, `eslint-plugin-react`, `eslint-plugin-jsx-a11y` are no longer used. New: `eslint-plugin-import-x`, `@eslint-community/eslint-plugin-eslint-comments`, `eslint-plugin-todo-plz`, `@stylistic/eslint-plugin`, `typescript-eslint`, `eslint-import-resolver-typescript`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, `eslint-plugin-formatjs`. -- **React:** the `/hooks` convenience export is gone (hooks rules are part of the default). `eslint-plugin-react` rules and jsx-a11y are not included; jsx-a11y returns when the plugin supports ESLint 10. -- **Rules you will notice:** disable comments need `-- reason`; TODOs need a ticket `#123`; imports are alphabetized; `no-console` allows only `warn`/`error`; `eqeqeq`, `no-shadow`, `no-nested-ternary`, `prefer-template`, `no-await-in-loop` and others are errors; TypeScript files require explicit return types outside tests. -- **Type-aware linting:** the TypeScript configs (including `react/typescript`) lint source files with type information through typescript-eslint's project service, so those files must belong to a tsconfig. Tests, scripts and TypeScript files at the package root (bundler, test runner or ORM configs) are linted without type information because build tsconfigs usually exclude them; set `typedTests: true` to type-check those too (they must then be in a tsconfig). Files outside every tsconfig report a parsing error. -- **Every ESLint, typescript-eslint, import-x, @stylistic, eslint-comments, react-hooks and formatjs rule is configured** (on, or off with a reason in `docs/rules.md`). Rules we left off that you want can be turned on in your own config. diff --git a/README.md b/README.md index 8695933..0789eda 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,15 @@ Slightly opinionated ESLint flat configs (ESLint 10) for JavaScript and TypeScri Every entry works with no options: `export { default } from '@cospired/eslint-config-typescript/node';` is a complete `eslint.config.js`. -Every rule that differs from ESLint/typescript-eslint recommended, with its source and the decision taken, is listed in [docs/rules.md](docs/rules.md). Upgrading from 7.x: [MIGRATION.md](MIGRATION.md). +Every rule is documented in the rule files under `packages/*/src/rules/`: a comment block with the description, the reasoning and a link. ## Development ```sh npm install -npm test # node:test over every package, the test helpers and the rule-table script +npm test # node:test over every package and the test helpers npm run lint # the repo lints itself with its own config -npm run smoke # pack both packages, install them in a clean project, lint with one-line configs -npm run rules # regenerate docs/rules.md +npm run smoke # pack the packages, install them in a clean project, lint with one-line configs npx changeset # describe a change for the next release ``` diff --git a/docs/rules.md b/docs/rules.md deleted file mode 100644 index 857cd18..0000000 --- a/docs/rules.md +++ /dev/null @@ -1,397 +0,0 @@ -# Rule comparison - -Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review. - -## Base JS (vs. @eslint/js recommended) - -| Rule | Ours | Recommended | Source | Decision | -|---|---|---|---|---| -| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | -| `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | -| `@stylistic/jsx-self-closing-comp` | error {"component":true,"html":true} | - | new | added for completeness (unreviewed call) | -| `@stylistic/lines-between-class-members` | error "always" {"exceptAfterOverload":true,"exceptAfterSingleLine":true} | - | new | added for completeness (unreviewed call) | -| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | -| `@stylistic/spaced-comment` | error "always" {"markers":["/"]} | - | new | added for completeness (unreviewed call) | -| `accessor-pairs` | error {"enforceForTSTypes":false,"enforceForClassMembers":true,"getWithoutSet":false,"setWithoutGet":true} | - | new | added for completeness (unreviewed call) | -| `array-callback-return` | error {"allowImplicit":false,"checkForEach":false,"allowVoid":false} | - | new | added for completeness (unreviewed call) | -| `arrow-body-style` | error "as-needed" | - | new | added for completeness (unreviewed call) | -| `camelcase` | error {"allow":[],"ignoreDestructuring":false,"ignoreGlobals":false,"ignoreImports":false,"properties":"never"} | - | new | added for completeness (unreviewed call) | -| `consistent-return` | error {"treatUndefinedAsUnspecified":false} | - | new | adopted, JS only | -| `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | -| `default-case-last` | error | - | new | added for completeness (unreviewed call) | -| `default-param-last` | error | - | new | adopted | -| `dot-notation` | error {"allowKeywords":true,"allowPattern":""} | - | new | added for completeness (unreviewed call) | -| `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | -| `func-name-matching` | error "always" | - | new | added for completeness (unreviewed call) | -| `grouped-accessor-pairs` | error "getBeforeSet" {"enforceForTSTypes":false} | - | new | added for completeness (unreviewed call) | -| `guard-for-in` | error | - | new | added for completeness (unreviewed call) | -| `import-x/default` | error | - | new | added for completeness (unreviewed call) | -| `import-x/export` | error | - | new | added for completeness (unreviewed call) | -| `import-x/first` | error | - | new | added for completeness (unreviewed call) | -| `import-x/named` | error | - | new | added for completeness (unreviewed call) | -| `import-x/namespace` | error | - | new | added for completeness (unreviewed call) | -| `import-x/newline-after-import` | error | - | cospired | kept | -| `import-x/no-absolute-path` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-amd` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-cycle` | error {"ignoreExternal":true,"allowUnsafeDynamicCyclicDependency":false} | - | new | added for completeness (unreviewed call) | -| `import-x/no-deprecated` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-duplicates` | error | - | new | kept | -| `import-x/no-dynamic-require` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-empty-named-blocks` | error | - | cospired | kept | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","tests/**","{apps,packages}/*/tests/**","**/__tests__/**","{apps,packages}/*/**/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | -| `import-x/no-import-module-exports` | error | - | cospired | kept | -| `import-x/no-mutable-exports` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-named-default` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-relative-packages` | error | - | cospired | kept | -| `import-x/no-self-import` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-unresolved` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-useless-path-segments` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-webpack-loader-syntax` | error | - | new | added for completeness (unreviewed call) | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | -| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | -| `new-cap` | error {"capIsNew":false,"capIsNewExceptions":[],"newIsCap":true,"newIsCapExceptions":[],"properties":true} | - | new | added for completeness (unreviewed call) | -| `no-alert` | error | - | new | added for completeness (unreviewed call) | -| `no-array-constructor` | error | - | new | added for completeness (unreviewed call) | -| `no-await-in-loop` | error | - | new | adopted | -| `no-caller` | error | - | new | added for completeness (unreviewed call) | -| `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | -| `no-constructor-return` | error | - | new | added for completeness (unreviewed call) | -| `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | -| `no-empty-function` | error {"allow":["arrowFunctions","functions","methods"]} | - | new | added for completeness (unreviewed call) | -| `no-eval` | error {"allowIndirect":false} | - | new | added for completeness (unreviewed call) | -| `no-extend-native` | error {"exceptions":[]} | - | new | added for completeness (unreviewed call) | -| `no-extra-bind` | error | - | new | added for completeness (unreviewed call) | -| `no-extra-label` | error | - | new | added for completeness (unreviewed call) | -| `no-implied-eval` | error | - | new | added for completeness (unreviewed call) | -| `no-invalid-this` | error {"capIsConstructor":true} | - | new | added for completeness (unreviewed call) | -| `no-iterator` | error | - | new | added for completeness (unreviewed call) | -| `no-label-var` | error | - | new | added for completeness (unreviewed call) | -| `no-labels` | error {"allowLoop":false,"allowSwitch":false} | - | new | added for completeness (unreviewed call) | -| `no-lone-blocks` | error | - | new | added for completeness (unreviewed call) | -| `no-lonely-if` | error | - | new | added for completeness (unreviewed call) | -| `no-loop-func` | error | - | new | added for completeness (unreviewed call) | -| `no-multi-assign` | error {"ignoreNonDeclaration":false} | - | new | added for completeness (unreviewed call) | -| `no-multi-str` | error | - | new | added for completeness (unreviewed call) | -| `no-nested-ternary` | error | - | new | adopted | -| `no-new-func` | error | - | new | added for completeness (unreviewed call) | -| `no-new-wrappers` | error | - | new | added for completeness (unreviewed call) | -| `no-object-constructor` | error | - | new | added for completeness (unreviewed call) | -| `no-octal-escape` | error | - | new | added for completeness (unreviewed call) | -| `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | -| `no-promise-executor-return` | error {"allowVoid":false} | - | new | added for completeness (unreviewed call) | -| `no-proto` | error | - | new | added for completeness (unreviewed call) | -| `no-return-assign` | error "except-parens" | - | new | added for completeness (unreviewed call) | -| `no-script-url` | error | - | new | added for completeness (unreviewed call) | -| `no-self-compare` | error | - | new | added for completeness (unreviewed call) | -| `no-sequences` | error {"allowInParentheses":true} | - | new | added for completeness (unreviewed call) | -| `no-shadow` | error {"allow":[],"builtinGlobals":false,"hoist":"functions","ignoreOnInitialization":false,"ignoreTypeValueShadow":true,"ignoreFunctionTypeParameterNameValueShadow":true} | - | new | adopted (TS: typescript-eslint version) | -| `no-template-curly-in-string` | error | - | new | added for completeness (unreviewed call) | -| `no-throw-literal` | error | - | new | added for completeness (unreviewed call) | -| `no-undef-init` | error | - | new | added for completeness (unreviewed call) | -| `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | -| `no-unmodified-loop-condition` | error {"checkConditionalExpressions":false} | - | new | added for completeness (unreviewed call) | -| `no-unneeded-ternary` | error {"defaultAssignment":true} | - | new | added for completeness (unreviewed call) | -| `no-unreachable-loop` | error {"ignore":[]} | - | new | added for completeness (unreviewed call) | -| `no-unused-expressions` | error {"allowShortCircuit":false,"allowTernary":false,"allowTaggedTemplates":false,"enforceForJSX":false,"ignoreDirectives":false} | - | eventured | strict, no React relaxations | -| `no-unused-vars` | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false,"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error {"vars":"all","args":"after-used","ignoreRestSiblings":false,"caughtErrors":"all","ignoreClassWithStaticInitBlock":false,"ignoreUsingDeclarations":false,"reportUsedIgnorePattern":false} | cospired | error, `_` prefix ignored | -| `no-use-before-define` | error {"classes":true,"functions":true,"variables":true,"allowNamedExports":false,"enums":true,"typedefs":true,"ignoreTypeReferences":true} | - | new | adopted, strict | -| `no-useless-call` | error | - | new | added for completeness (unreviewed call) | -| `no-useless-computed-key` | error {"enforceForClassMembers":true} | - | new | added for completeness (unreviewed call) | -| `no-useless-concat` | error | - | new | added for completeness (unreviewed call) | -| `no-useless-constructor` | error | - | new | added for completeness (unreviewed call) | -| `no-useless-rename` | error {"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false} | - | new | added for completeness (unreviewed call) | -| `no-useless-return` | error | - | new | added for completeness (unreviewed call) | -| `no-var` | error | - | eventured | kept | -| `object-shorthand` | error "always" | - | eventured | kept | -| `one-var` | error "never" | - | new | added for completeness (unreviewed call) | -| `operator-assignment` | error "always" | - | new | added for completeness (unreviewed call) | -| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | -| `prefer-const` | error {"destructuring":"any","ignoreReadBeforeAssign":false} | - | eventured | kept | -| `prefer-exponentiation-operator` | error | - | new | added for completeness (unreviewed call) | -| `prefer-numeric-literals` | error | - | new | added for completeness (unreviewed call) | -| `prefer-object-has-own` | error | - | cospired | kept | -| `prefer-object-spread` | error | - | new | added for completeness (unreviewed call) | -| `prefer-promise-reject-errors` | error {"allowEmptyReject":false} | - | new | added for completeness (unreviewed call) | -| `prefer-regex-literals` | error {"disallowRedundantWrapping":false} | - | new | added for completeness (unreviewed call) | -| `prefer-rest-params` | error | - | new | added for completeness (unreviewed call) | -| `prefer-spread` | error | - | new | added for completeness (unreviewed call) | -| `prefer-template` | error | - | new | adopted | -| `radix` | error | - | new | added for completeness (unreviewed call) | -| `require-await` | error | - | new | added for completeness (unreviewed call) | -| `symbol-description` | error | - | new | added for completeness (unreviewed call) | -| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | -| `yoda` | error "never" {"exceptRange":false,"onlyEquality":false} | - | new | added for completeness (unreviewed call) | - -## Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked) - -| Rule | Ours | Recommended | Source | Decision | -|---|---|---|---|---| -| `@eslint-community/eslint-comments/disable-enable-pair` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-aggregating-enable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-duplicate-disable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-unlimited-disable` | error | - | new | kept | -| `@eslint-community/eslint-comments/no-unused-enable` | error | - | new | kept | -| `@eslint-community/eslint-comments/require-description` | error | - | eventured | kept | -| `@stylistic/jsx-self-closing-comp` | error {"component":true,"html":true} | - | new | added for completeness (unreviewed call) | -| `@stylistic/lines-between-class-members` | error "always" {"exceptAfterOverload":true,"exceptAfterSingleLine":true} | - | new | added for completeness (unreviewed call) | -| `@stylistic/padding-line-between-statements` | error {"blankLine":"always","prev":"*","next":"return"} {"blankLine":"always","prev":["const","let","var"],"next":"*"} {"blankLine":"any","prev":["const","let","var"],"next":["const","let","var"]} | - | hapi | kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines) | -| `@stylistic/spaced-comment` | error "always" {"markers":["/"]} | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/await-thenable` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/consistent-type-imports` | error {"fixStyle":"inline-type-imports"} | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/default-param-last` | error | - | new | adopted | -| `@typescript-eslint/dot-notation` | off {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | error {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | new | plugin preset, unchanged | -| `@typescript-eslint/explicit-function-return-type` | error {"allowExpressions":true} | - | eventured | default on for non-test files, allowExpressions | -| `@typescript-eslint/no-array-delete` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-base-to-string` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-duplicate-type-constituents` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-dynamic-delete` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-floating-promises` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-for-in-array` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-implied-eval` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-import-type-side-effects` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-invalid-this` | error {"capIsConstructor":true} | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-invalid-void-type` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-misused-promises` | off {"checksConditionals":{"flagUnions":"strict"}} | error | eventured | default on, flagUnions strict | -| `@typescript-eslint/no-non-null-asserted-nullish-coalescing` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-non-null-assertion` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-redundant-type-constituents` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-shadow` | error | - | new | adopted | -| `@typescript-eslint/no-unnecessary-parameter-property-assignment` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-unnecessary-type-assertion` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-argument` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-assignment` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-call` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-enum-comparison` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-member-access` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-return` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-unary-minus` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/no-unused-private-class-members` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-unused-vars` | error {"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrorsIgnorePattern":"^_"} | error | eventured | error, `_` prefix ignored | -| `@typescript-eslint/no-use-before-define` | error | - | new | adopted, strict | -| `@typescript-eslint/no-useless-constructor` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/no-useless-empty-export` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/non-nullable-type-assertion-style` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/only-throw-error` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-find` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-includes` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-literal-enum-member` | error | - | new | added for completeness (unreviewed call) | -| `@typescript-eslint/prefer-nullish-coalescing` | off | error | eventured | plugin preset, unchanged | -| `@typescript-eslint/prefer-optional-chain` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-promise-reject-errors` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-regexp-exec` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-string-starts-ends-with` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/require-await` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/restrict-plus-operands` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/restrict-template-expressions` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/unbound-method` | off | error | new | plugin preset, unchanged | -| `@typescript-eslint/unified-signatures` | error | - | new | added for completeness (unreviewed call) | -| `accessor-pairs` | error {"enforceForTSTypes":false,"enforceForClassMembers":true,"getWithoutSet":false,"setWithoutGet":true} | - | new | added for completeness (unreviewed call) | -| `array-callback-return` | error {"allowImplicit":false,"checkForEach":false,"allowVoid":false} | - | new | added for completeness (unreviewed call) | -| `arrow-body-style` | error "as-needed" | - | new | added for completeness (unreviewed call) | -| `camelcase` | error {"allow":[],"ignoreDestructuring":false,"ignoreGlobals":false,"ignoreImports":false,"properties":"never"} | - | new | added for completeness (unreviewed call) | -| `curly` | error "multi-line" | - | new | adopted, multi-line (re-enabled after Prettier) | -| `default-case-last` | error | - | new | added for completeness (unreviewed call) | -| `eqeqeq` | error "always" {"null":"ignore"} | - | new | adopted, `== null` allowed | -| `func-name-matching` | error "always" | - | new | added for completeness (unreviewed call) | -| `grouped-accessor-pairs` | error "getBeforeSet" {"enforceForTSTypes":false} | - | new | added for completeness (unreviewed call) | -| `guard-for-in` | error | - | new | added for completeness (unreviewed call) | -| `import-x/consistent-type-specifier-style` | error "prefer-inline" | - | cospired | kept: prefer-inline | -| `import-x/default` | error | - | new | added for completeness (unreviewed call) | -| `import-x/export` | error | - | new | added for completeness (unreviewed call) | -| `import-x/first` | error | - | new | added for completeness (unreviewed call) | -| `import-x/namespace` | error | - | new | added for completeness (unreviewed call) | -| `import-x/newline-after-import` | error | - | cospired | kept | -| `import-x/no-absolute-path` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-amd` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-cycle` | error {"ignoreExternal":true,"allowUnsafeDynamicCyclicDependency":false} | - | new | added for completeness (unreviewed call) | -| `import-x/no-deprecated` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-duplicates` | error | - | new | kept | -| `import-x/no-dynamic-require` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-empty-named-blocks` | error | - | cospired | kept | -| `import-x/no-extraneous-dependencies` | error {"devDependencies":["**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","{apps,packages}/*/**/*.{test,spec}.{ts,tsx,js,jsx,mjs,cjs}","test/**","{apps,packages}/*/test/**","tests/**","{apps,packages}/*/tests/**","**/__tests__/**","{apps,packages}/*/**/__tests__/**","scripts/**","{apps,packages}/*/scripts/**","*.config.{ts,js,mjs,cjs}","{apps,packages}/*/*.config.{ts,js,mjs,cjs}"],"peerDependencies":true} | - | eventured | kept (eventured globs, overridable) | -| `import-x/no-import-module-exports` | error | - | cospired | kept | -| `import-x/no-mutable-exports` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-named-default` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-relative-packages` | error | - | cospired | kept | -| `import-x/no-self-import` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-unresolved` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-useless-path-segments` | error | - | new | added for completeness (unreviewed call) | -| `import-x/no-webpack-loader-syntax` | error | - | new | added for completeness (unreviewed call) | -| `import-x/order` | error {"groups":["builtin","external","internal","parent","sibling","index"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true,"orderImportKind":"ignore"},"distinctGroup":true,"sortTypesGroup":false,"named":false,"warnOnUnassignedImports":false} | - | eventured | always + alphabetize | -| `logical-assignment-operators` | error "always" {"enforceForIfStatements":true} | - | cospired | kept (incl. if statements) | -| `new-cap` | error {"capIsNew":false,"capIsNewExceptions":[],"newIsCap":true,"newIsCapExceptions":[],"properties":true} | - | new | added for completeness (unreviewed call) | -| `no-alert` | error | - | new | added for completeness (unreviewed call) | -| `no-await-in-loop` | error | - | new | adopted | -| `no-caller` | error | - | new | added for completeness (unreviewed call) | -| `no-console` | error {"allow":["warn","error"]} | - | eventured | error, allow warn/error; scripts/** exempt | -| `no-constructor-return` | error | - | new | added for completeness (unreviewed call) | -| `no-else-return` | error {"allowElseIf":false} | - | new | adopted, allowElseIf: false | -| `no-eval` | error {"allowIndirect":false} | - | new | added for completeness (unreviewed call) | -| `no-extend-native` | error {"exceptions":[]} | - | new | added for completeness (unreviewed call) | -| `no-extra-bind` | error | - | new | added for completeness (unreviewed call) | -| `no-extra-label` | error | - | new | added for completeness (unreviewed call) | -| `no-iterator` | error | - | new | added for completeness (unreviewed call) | -| `no-label-var` | error | - | new | added for completeness (unreviewed call) | -| `no-labels` | error {"allowLoop":false,"allowSwitch":false} | - | new | added for completeness (unreviewed call) | -| `no-lone-blocks` | error | - | new | added for completeness (unreviewed call) | -| `no-lonely-if` | error | - | new | added for completeness (unreviewed call) | -| `no-loop-func` | error | - | new | added for completeness (unreviewed call) | -| `no-multi-assign` | error {"ignoreNonDeclaration":false} | - | new | added for completeness (unreviewed call) | -| `no-multi-str` | error | - | new | added for completeness (unreviewed call) | -| `no-nested-ternary` | error | - | new | adopted | -| `no-new-func` | error | - | new | added for completeness (unreviewed call) | -| `no-new-wrappers` | error | - | new | added for completeness (unreviewed call) | -| `no-object-constructor` | error | - | new | added for completeness (unreviewed call) | -| `no-octal-escape` | error | - | new | added for completeness (unreviewed call) | -| `no-param-reassign` | error {"props":false} | - | new | adopted, props: false | -| `no-promise-executor-return` | error {"allowVoid":false} | - | new | added for completeness (unreviewed call) | -| `no-proto` | error | - | new | added for completeness (unreviewed call) | -| `no-return-assign` | error "except-parens" | - | new | added for completeness (unreviewed call) | -| `no-script-url` | error | - | new | added for completeness (unreviewed call) | -| `no-self-compare` | error | - | new | added for completeness (unreviewed call) | -| `no-sequences` | error {"allowInParentheses":true} | - | new | added for completeness (unreviewed call) | -| `no-template-curly-in-string` | error | - | new | added for completeness (unreviewed call) | -| `no-undef-init` | error | - | new | added for completeness (unreviewed call) | -| `no-unexpected-multiline` | off | error | new | plugin preset, unchanged | -| `no-unmodified-loop-condition` | error {"checkConditionalExpressions":false} | - | new | added for completeness (unreviewed call) | -| `no-unneeded-ternary` | error {"defaultAssignment":true} | - | new | added for completeness (unreviewed call) | -| `no-unreachable-loop` | error {"ignore":[]} | - | new | added for completeness (unreviewed call) | -| `no-unused-private-class-members` | off | error | new | added for completeness (unreviewed call) | -| `no-useless-call` | error | - | new | added for completeness (unreviewed call) | -| `no-useless-computed-key` | error {"enforceForClassMembers":true} | - | new | added for completeness (unreviewed call) | -| `no-useless-concat` | error | - | new | added for completeness (unreviewed call) | -| `no-useless-rename` | error {"ignoreDestructuring":false,"ignoreImport":false,"ignoreExport":false} | - | new | added for completeness (unreviewed call) | -| `no-useless-return` | error | - | new | added for completeness (unreviewed call) | -| `object-shorthand` | error "always" | - | eventured | kept | -| `one-var` | error "never" | - | new | added for completeness (unreviewed call) | -| `operator-assignment` | error "always" | - | new | added for completeness (unreviewed call) | -| `prefer-arrow-callback` | error {"allowNamedFunctions":false,"allowUnboundThis":true} | - | eventured | kept | -| `prefer-exponentiation-operator` | error | - | new | added for completeness (unreviewed call) | -| `prefer-numeric-literals` | error | - | new | added for completeness (unreviewed call) | -| `prefer-object-has-own` | error | - | cospired | kept | -| `prefer-object-spread` | error | - | new | added for completeness (unreviewed call) | -| `prefer-regex-literals` | error {"disallowRedundantWrapping":false} | - | new | added for completeness (unreviewed call) | -| `prefer-template` | error | - | new | adopted | -| `radix` | error | - | new | added for completeness (unreviewed call) | -| `symbol-description` | error | - | new | added for completeness (unreviewed call) | -| `todo-plz/ticket-ref` | error {"pattern":"#\\d+"} | - | eventured | kept | -| `yoda` | error "never" {"exceptRange":false,"onlyEquality":false} | - | new | added for completeness (unreviewed call) | - -## React JS (vs. Base JS) - -| Rule | Ours | Recommended | Source | Decision | -|---|---|---|---|---| -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-default-message` | error "literal" | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-placeholders` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | added for completeness (unreviewed call) | -| `formatjs/no-camel-case` | error | - | eventured | error (eventured) | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | added for completeness (unreviewed call) | -| `formatjs/no-emoji` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | -| `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-multiple-plurals` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-multiple-whitespaces` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-offset` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-useless-message` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/prefer-full-sentence` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/prefer-pound-in-plural` | error | - | new | added for completeness (unreviewed call) | -| `react-hooks/config` | error | - | new | plugin preset, unchanged | -| `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | -| `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | -| `react-hooks/gating` | error | - | new | plugin preset, unchanged | -| `react-hooks/globals` | error | - | new | plugin preset, unchanged | -| `react-hooks/immutability` | error | - | new | plugin preset, unchanged | -| `react-hooks/incompatible-library` | error | - | new | error (no warn policy) | -| `react-hooks/preserve-manual-memoization` | error | - | new | plugin preset, unchanged | -| `react-hooks/purity` | error | - | new | plugin preset, unchanged | -| `react-hooks/refs` | error | - | new | plugin preset, unchanged | -| `react-hooks/rules-of-hooks` | error | - | new | plugin preset, unchanged | -| `react-hooks/set-state-in-effect` | error | - | new | plugin preset, unchanged | -| `react-hooks/set-state-in-render` | error | - | new | plugin preset, unchanged | -| `react-hooks/static-components` | error | - | new | plugin preset, unchanged | -| `react-hooks/unsupported-syntax` | error | - | new | error (no warn policy) | -| `react-hooks/use-memo` | error | - | new | plugin preset, unchanged | -| `react-refresh/only-export-components` | error {"allowConstantExport":true} | - | eventured | error (no warn policy) | - -## React TypeScript (vs. Base TypeScript) - -| Rule | Ours | Recommended | Source | Decision | -|---|---|---|---|---| -| `@typescript-eslint/await-thenable` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/dot-notation` | error {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | off {"allowIndexSignaturePropertyAccess":false,"allowKeywords":true,"allowPattern":"","allowPrivateClassPropertyAccess":false,"allowProtectedClassPropertyAccess":false} | new | plugin preset, unchanged | -| `@typescript-eslint/no-array-delete` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-base-to-string` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-duplicate-type-constituents` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-floating-promises` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-for-in-array` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-generated-empty-object-type` | error | off | eventured | default on | -| `@typescript-eslint/no-implied-eval` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-misused-promises` | error {"checksConditionals":{"flagUnions":"strict"}} | off {"checksConditionals":{"flagUnions":"strict"}} | eventured | default on, flagUnions strict | -| `@typescript-eslint/no-redundant-type-constituents` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unnecessary-type-assertion` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-argument` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-assignment` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-call` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-enum-comparison` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-member-access` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-return` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/no-unsafe-unary-minus` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/non-nullable-type-assertion-style` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/only-throw-error` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-find` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-includes` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-nullish-coalescing` | error | off | eventured | plugin preset, unchanged | -| `@typescript-eslint/prefer-optional-chain` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-promise-reject-errors` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-regexp-exec` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/prefer-string-starts-ends-with` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/require-await` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/restrict-plus-operands` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/restrict-template-expressions` | error | off | new | plugin preset, unchanged | -| `@typescript-eslint/unbound-method` | error | off | new | plugin preset, unchanged | -| `formatjs/blocklist-elements` | error ["selectordinal",{"type":"select","allow":{"variable":"gender","options":["male","female","other"]}}] | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-default-message` | error "literal" | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-placeholders` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/enforce-plural-rules` | error {"one":true,"other":true} | - | new | added for completeness (unreviewed call) | -| `formatjs/no-camel-case` | error | - | eventured | error (eventured) | -| `formatjs/no-complex-selectors` | error {"limit":20} | - | new | added for completeness (unreviewed call) | -| `formatjs/no-emoji` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-invalid-icu` | error | - | eventured | error (eventured) | -| `formatjs/no-literal-string-in-jsx` | error {"props":{"include":[["*","{label,placeholder,title}"]]}} | - | new | error (no warn policy) | -| `formatjs/no-missing-icu-plural-one-placeholders` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-multiple-plurals` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-multiple-whitespaces` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-offset` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/no-useless-message` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/prefer-full-sentence` | error | - | new | added for completeness (unreviewed call) | -| `formatjs/prefer-pound-in-plural` | error | - | new | added for completeness (unreviewed call) | -| `react-hooks/config` | error | - | new | plugin preset, unchanged | -| `react-hooks/error-boundaries` | error | - | new | plugin preset, unchanged | -| `react-hooks/exhaustive-deps` | error | - | new | error (no warn policy) | -| `react-hooks/gating` | error | - | new | plugin preset, unchanged | -| `react-hooks/globals` | error | - | new | plugin preset, unchanged | -| `react-hooks/immutability` | error | - | new | plugin preset, unchanged | -| `react-hooks/incompatible-library` | error | - | new | error (no warn policy) | -| `react-hooks/preserve-manual-memoization` | error | - | new | plugin preset, unchanged | -| `react-hooks/purity` | error | - | new | plugin preset, unchanged | -| `react-hooks/refs` | error | - | new | plugin preset, unchanged | -| `react-hooks/rules-of-hooks` | error | - | new | plugin preset, unchanged | -| `react-hooks/set-state-in-effect` | error | - | new | plugin preset, unchanged | -| `react-hooks/set-state-in-render` | error | - | new | plugin preset, unchanged | -| `react-hooks/static-components` | error | - | new | plugin preset, unchanged | -| `react-hooks/unsupported-syntax` | error | - | new | error (no warn policy) | -| `react-hooks/use-memo` | error | - | new | plugin preset, unchanged | -| `react-refresh/only-export-components` | error {"allowConstantExport":true} | - | eventured | error (no warn policy) | - -## Explicitly off (considered, not enforced) - -Rules no preset sets, switched off on purpose: opinion, thresholds, project-specific, or covered by Prettier/TypeScript. - -- ESLint core, import-x, typescript-eslint, @stylistic, eslint-comments: `@eslint-community/eslint-comments/no-restricted-disable`, `@eslint-community/eslint-comments/no-use`, `@stylistic/curly-newline`, `@stylistic/exp-jsx-props-style`, `@stylistic/exp-list-style`, `@stylistic/jsx-curly-brace-presence`, `@stylistic/jsx-function-call-newline`, `@stylistic/jsx-pascal-case`, `@stylistic/line-comment-position`, `@stylistic/multiline-comment-style`, `@typescript-eslint/class-methods-use-this`, `@typescript-eslint/explicit-member-accessibility`, `@typescript-eslint/explicit-module-boundary-types`, `@typescript-eslint/init-declarations`, `@typescript-eslint/max-params`, `@typescript-eslint/member-ordering`, `@typescript-eslint/method-signature-style`, `@typescript-eslint/no-dupe-class-members`, `@typescript-eslint/no-extraneous-class`, `@typescript-eslint/no-magic-numbers`, `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-restricted-types`, `@typescript-eslint/parameter-properties`, `@typescript-eslint/prefer-enum-initializers`, `block-scoped-var`, `capitalized-comments`, `class-methods-use-this`, `complexity`, `consistent-return`, `consistent-this`, `default-case`, `default-param-last`, `formatjs/enforce-description`, `formatjs/enforce-id`, `formatjs/enforce-message-types`, `formatjs/no-id`, `formatjs/no-literal-string-in-object`, `formatjs/prefer-formatted-message`, `func-names`, `func-style`, `id-denylist`, `id-length`, `id-match`, `import-x/avoid-barrel-files`, `import-x/avoid-importing-barrel-files`, `import-x/avoid-namespace-import`, `import-x/avoid-re-export-all`, `import-x/consistent-type-specifier-style`, `import-x/dynamic-import-chunkname`, `import-x/exports-last`, `import-x/extensions`, `import-x/first`, `import-x/group-exports`, `import-x/max-dependencies`, `import-x/no-anonymous-default-export`, `import-x/no-commonjs`, `import-x/no-default-export`, `import-x/no-internal-modules`, `import-x/no-named-as-default`, `import-x/no-named-as-default-member`, `import-x/no-named-export`, `import-x/no-namespace`, `import-x/no-nodejs-modules`, `import-x/no-relative-parent-imports`, `import-x/no-rename-default`, `import-x/no-restricted-paths`, `import-x/no-unassigned-import`, `import-x/no-unused-modules`, `import-x/prefer-default-export`, `import-x/prefer-namespace-import`, `import-x/unambiguous`, `init-declarations`, `max-classes-per-file`, `max-depth`, `max-lines`, `max-lines-per-function`, `max-nested-callbacks`, `max-params`, `max-statements`, `no-bitwise`, `no-continue`, `no-div-regex`, `no-duplicate-imports`, `no-eq-null`, `no-implicit-coercion`, `no-implicit-globals`, `no-inline-comments`, `no-inner-declarations`, `no-invalid-this`, `no-magic-numbers`, `no-negated-condition`, `no-new`, `no-plusplus`, `no-restricted-exports`, `no-restricted-globals`, `no-restricted-imports`, `no-restricted-properties`, `no-restricted-syntax`, `no-shadow`, `no-ternary`, `no-undefined`, `no-underscore-dangle`, `no-unused-private-class-members`, `no-use-before-define`, `no-useless-constructor`, `no-void`, `no-warning-comments`, `prefer-destructuring`, `prefer-named-capture-group`, `react-hooks/capitalized-calls`, `react-hooks/exhaustive-effect-dependencies`, `react-hooks/fbt`, `react-hooks/hooks`, `react-hooks/invariant`, `react-hooks/memo-dependencies`, `react-hooks/memoized-effect-dependencies`, `react-hooks/no-deriving-state-in-effects`, `react-hooks/rule-suppression`, `react-hooks/syntax`, `react-hooks/todo`, `react-hooks/void-use-memo`, `require-atomic-updates`, `require-unicode-regexp`, `sort-imports`, `sort-keys`, `sort-vars`, `strict`, `unicode-bom`, `vars-on-top` -- React plugins: `formatjs/enforce-description`, `formatjs/enforce-id`, `formatjs/enforce-message-types`, `formatjs/no-id`, `formatjs/no-literal-string-in-object`, `formatjs/prefer-formatted-message`, `react-hooks/capitalized-calls`, `react-hooks/exhaustive-effect-dependencies`, `react-hooks/fbt`, `react-hooks/hooks`, `react-hooks/invariant`, `react-hooks/memo-dependencies`, `react-hooks/memoized-effect-dependencies`, `react-hooks/no-deriving-state-in-effects`, `react-hooks/rule-suppression`, `react-hooks/syntax`, `react-hooks/todo`, `react-hooks/void-use-memo` diff --git a/docs/superpowers/plans/2026-11-10-eslint-config-2026.md b/docs/superpowers/plans/2026-11-10-eslint-config-2026.md deleted file mode 100644 index edfd001..0000000 --- a/docs/superpowers/plans/2026-11-10-eslint-config-2026.md +++ /dev/null @@ -1,160 +0,0 @@ -# @cospired ESLint configs 2026 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the legacy airbnb/eslintrc `@cospired/eslint-config-*` packages with flat-config ESLint 10 packages (`base`, `react`, each with JS and TypeScript variants, plus a node entry), tested for completeness, released as 8.0.0. - -**Architecture:** npm workspace with two ESM packages. Each package exports config(s) built from small rule modules in `src/rules/`. A shared test helper lints fixture code through the ESLint Node API. Rule decisions that conflict are placeholders until the interactive rule review (Task 8). - -**Tech Stack:** ESLint 10, `@eslint/js`, typescript-eslint 8, eslint-plugin-import-x, `@eslint-community/eslint-plugin-eslint-comments`, eslint-plugin-todo-plz, `@stylistic/eslint-plugin`, eslint-config-prettier, eslint-plugin-react-hooks, -react-refresh, -jsx-a11y, -formatjs, `node:test`, changesets, GitHub Actions. - -**Spec:** `docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md` (read it first, including "Findings from eventured consumers"). - -## Global Constraints - -- Flat config, ESLint 10, typescript-eslint 8+, ESM only. No airbnb, no hapi plugin, no Babel/tape/`eslint-find-rules`/`in-publish`/`safe-publish-latest`/Travis. -- Packages: `@cospired/eslint-config-base` (`.` JS, `./typescript`, `./node`) and `@cospired/eslint-config-react` (`.` JS, `./typescript`). Both released as `8.0.0`. Delete `packages/eslint-config-typescript`. -- `react` depends on `base` (regular dependency); `react/typescript` extends `base/typescript`. `base` has no React rules and no React peers. -- Peer dependencies are ranges, not exact pins; they must include ESLint 10, typescript-eslint 8.72, TypeScript 6, Prettier 3.9. -- Carry over: `eslint-disable` needs `-- reason`; TODOs need `#\d+` ticket (`todo-plz/ticket-ref`); alphabetized `import-x/order`; devDependency imports only in test/config/script files (eventured's glob list, overridable via option). -- Prettier owns formatting; `eslint-config-prettier` goes last. -- Warnings are failures in consumers (`--max-warnings 0`): prefer `error`/`off`. -- Defaults moved into the shared TS configs (Dirk-approved): `@typescript-eslint/explicit-function-return-type` (`{ allowExpressions: true }`, non-test files only), `@typescript-eslint/no-misused-promises` (`{ checksConditionals: { flagUnions: 'strict' } }`), `@typescript-eslint/no-generated-empty-object-type: 'error'`. -- `tsconfigPath` accepts a string or array; `tsconfigRootDir` stays an option; `import-x/resolver` typescript settings with `noWarnOnMultipleProjects` follow eventured's `node.js`. -- Rule conflicts are decided one by one with Dirk (Task 8); until then keep eventured's setting and list the rule as open. -- Eventured source: `/Users/dh/projects/eventured/eventured/packages/eslint-config/src/{base,node,react}.js`. Old rules: `packages/eslint-config-{base,react}/rules/*.js` (read via `git show main:` once the packages are rewritten). - -## Review Focus - -- A TS file not covered by any given tsconfig: lint reports a clear parse error for that file, no crash or hang (Task 4). -- `.mjs`/`.cjs`/`.js` config and script files under the TS configs: linted without type-aware errors, node globals where intended (Task 4). -- `#alias` subpath imports and relative workspace imports: no `import-x/no-unresolved` false positives, and `importOrder.pathGroups` option groups them (Task 4). -- Test files: exempt from `explicit-function-return-type` and allowed devDependency imports; non-test files are not (Task 4). -- Calling a `create*Config` function twice with different options returns independent arrays (no shared mutated state), and JSX parses in `.jsx`/`.tsx` under the react configs (Tasks 4, 6). - ---- - -### Task 1: Workspace and tooling scaffold - -**Files:** -- Modify: `package.json` (root: `private`, `type: module`, `workspaces: ["packages/*"]`, scripts `test`, `lint`, `changeset`, `release`; drop `preinstall` and `travis*`) -- Delete: `packages/eslint-config-typescript/`, per-package `.babelrc`, `.eslintrc`, `.npmrc`, `test/requires.js`, `test/base.test.js`, `rules/airbnb.js`, `rules/hapi.js` (rules/import.js and eslint-comment.js stay until Tasks 3/6 port them) -- Create: `.changeset/config.json`, `.github/workflows/ci.yml`, `.github/workflows/release.yml`, `eslint.config.js` (lints the repo itself), `.gitignore` entries -- Modify: `packages/eslint-config-{base,react}/package.json` (name kept, `version` stays 7.x until changeset, `type: module`, `exports` per Global Constraints, `files: ["src"]`, `scripts.test: "node --test"`, `engines.node` matching ESLint 10's) - -**Interfaces:** -- Produces: `npm test` at the root runs `node --test` in each workspace; workspaces `@cospired/eslint-config-base` and `@cospired/eslint-config-react` installable. - -- [ ] **Step 1:** Write `packages/eslint-config-base/test/smoke.test.js` asserting `import('../src/index.js')` resolves and its default export is an array (test name `base JS entry exports a flat config array`). -- [ ] **Step 2:** Run `npm test -w @cospired/eslint-config-base`; expected FAIL (module missing). -- [ ] **Step 3:** Apply the file changes above; create `src/index.js` exporting `export default []`. Install peers/devDeps with current versions (`npm install -D eslint@10 @eslint/js typescript-eslint …`); look up the real latest versions on npm instead of copying eventured's pins unverified. -- [ ] **Step 4:** Run `npm test`; expected PASS. Run `npx eslint .` at root; expected no errors. -- [ ] **Step 5:** CI workflow runs `npm ci && npm test && npm run lint` on push/PR; release workflow uses `changesets/action`. Commit: `chore: flat-config workspace, node:test, changesets, GitHub Actions`. - -### Task 2: Lint test helper - -**Files:** -- Create: `test-utils/lint.js`, `test-utils/lint.test.js`, `test-utils/fixtures/ts-project/{tsconfig.json,src/ok.ts}` - -**Interfaces:** -- Produces: `lintText(config: Linter.Config[], code: string, opts?: { filePath?: string }): Promise>` — creates `new ESLint({ overrideConfigFile: true, overrideConfig: config })`, lints with `filePath` default `fixtures/ts-project/src/sample.ts` (absolute, inside the fixture project), returns the flattened messages. -- Produces: `ruleIds(messages): string[]` (sorted unique non-null ruleIds). -- Produces: `loadedRules(config): Set` — every rule id in the config array with severity not `off`. - -- [ ] **Step 1:** Test `lintText reports no-debugger for "debugger;"` with config `[{ rules: { 'no-debugger': 'error' } }]` and `filePath` `x.js`; asserts `ruleIds` equals `['no-debugger']`. Test `loadedRules ignores off rules`. -- [ ] **Step 2:** Run `node --test test-utils`; expected FAIL. -- [ ] **Step 3:** Implement the three functions in `test-utils/lint.js`. -- [ ] **Step 4:** Run; expected PASS. Commit: `test: lint helper for config fixtures`. - -### Task 3: Base JS config - -**Files:** -- Create: `packages/eslint-config-base/src/{index.js,ignores.js}`, `src/rules/{comments.js,imports.js,style.js}`, `test/{load.test.js,rules-exist.test.js,fixtures.test.js}` -- Delete: `rules/eslint-comment.js`, `rules/import.js` after porting - -**Interfaces:** -- Consumes: `lintText`, `ruleIds`, `loadedRules` (Task 2). -- Produces: `default export: Linter.Config[]` from `src/index.js` (recommended + comments + imports + style + prettier last); `export const globalIgnores: Linter.Config` from `src/ignores.js` (eventured's ignore list); each `src/rules/*.js` default-exports `Linter.Config` (plugin registrations plus `rules`). -- Produces: `devDependencyGlobs(): string[]` and `createImportsConfig(options?: { devDependencyGlobs?: string[]; pathGroups?: object[] }): Linter.Config` in `src/rules/imports.js` (eventured's glob list, both anchored spellings). - -- [ ] **Step 1:** Write tests, each with the spec's exact values: - - `load.test.js`: `base JS config lints a trivial file without errors` (`const a = 1;\nexport default a;\n`, expects `[]`). - - `rules-exist.test.js`: for every rule id in the config, assert the plugin's `rules` object (or ESLint's `builtinRules`) contains it — name `every configured rule exists`. Also assert no rule id starts with `react/`. - - `fixtures.test.js`: `requires a description on disable comments` (`// eslint-disable-next-line no-console` → `@eslint-community/eslint-comments/require-description`); `requires a ticket on TODO` (`// TODO: fix` → `todo-plz/ticket-ref`; `// TODO(#12): fix` passes); `alphabetizes imports` (`import b from 'b';import a from 'a';` → `import-x/order`); `rejects devDependency import in src, allows in *.test.js`; `no-var`, `prefer-const` still flagged. -- [ ] **Step 2:** Run `npm test -w @cospired/eslint-config-base`; expected FAIL. -- [ ] **Step 3:** Port eventured's `createBaseConfig` JS parts into the rule modules. Before writing `style.js`, spike: install `@stylistic/eslint-plugin`, confirm `padding-line-between-statements` (blank line before `return`, after imports) works with `eslint-config-prettier` last and that Prettier does not rewrite the result (`npx prettier --check` on a fixture). If not, apply the spec's fallback and tell Dirk. Express `newline-before-return` and `max-statements-per-line` intent with the same rule; hapi `scope-start`/`no-arrowception` are not ported yet (Task 8). -- [ ] **Step 4:** Run; expected PASS. Commit: `feat(base): flat-config JS ruleset`. - -### Task 4: Base TypeScript config - -**Files:** -- Create: `packages/eslint-config-base/src/typescript.js`, `src/rules/typescript.js`, `test/typescript.test.js` -- Create: `packages/eslint-config-base/test/fixtures/ts-project/{tsconfig.json,tsconfig.test.json,src/a.ts,test/a.test.ts}` - -**Interfaces:** -- Consumes: Task 3 exports. -- Produces: `createTypescriptConfig(options?: { tsconfigPath?: string | string[]; tsconfigRootDir?: string; files?: string[]; devDependencyGlobs?: string[]; importOrder?: { pathGroups?: object[] } }): Linter.Config[]` default-exported helpers re-exporting `globalIgnores`. Includes recommendedTypeChecked + stylisticTypeChecked + the three default rules from Global Constraints + import-x typescript resolver settings (`noWarnOnMultipleProjects: true`). - -- [ ] **Step 1:** Tests in `typescript.test.js`, using the fixture project and `tsconfigRootDir`: - - `flags missing return type in src, not in *.test.ts` (uses `allowExpressions`: `const f = () => 1` inside call expression passes, `export function f() { return 1; }` fails). - - `no-misused-promises flags async callback in condition` and `no-generated-empty-object-type`. - - `accepts an array of tsconfigs` (src/ in `tsconfig.json`, test/ in `tsconfig.test.json`, both lint without parse errors). - - `file outside every tsconfig reports a parsing error, does not throw` (Review Focus 1). - - `.mjs config file is linted without type-aware errors` (Review Focus 2). - - `#shared/x import: no no-unresolved error with a package.json imports map; pathGroups option places it before relative imports` (Review Focus 3). - - `two calls return independent arrays` (Review Focus 5). -- [ ] **Step 2:** Run; expected FAIL. -- [ ] **Step 3:** Implement `createTypescriptConfig`; use `parserOptions.project` when `tsconfigPath` is given, else `projectService: true`. Apply type-aware rules only to `files` and non-JS files get `tseslint.configs.disableTypeChecked`. -- [ ] **Step 4:** Run; expected PASS. Commit: `feat(base): TypeScript config`. - -### Task 5: Base node entry - -**Files:** -- Create: `packages/eslint-config-base/src/node.js`, `test/node.test.js` - -**Interfaces:** -- Produces: `createNodeConfig(options?: same options as createTypescriptConfig): Linter.Config[]` — typescript config plus node/es2022 globals, `ecmaVersion: 2022`, `sourceType: 'module'`, `@typescript-eslint/prefer-nullish-coalescing` with `ignorePrimitives: { string: true }` (eventured's reasoning comment kept), `scripts/**/*.{mjs,js}` with node globals and `no-console` off. - -- [ ] **Step 1:** Tests: `process is a known global` (`process.exit(1)` no `no-undef`), `string || fallback is allowed, number || fallback is flagged`, `no-console allowed in scripts/ but not src/`. -- [ ] **Step 2:** Run; expected FAIL. **Step 3:** Implement. **Step 4:** Run; expected PASS. Commit: `feat(base): node config`. - -### Task 6: React package (JS and TypeScript) - -**Files:** -- Create: `packages/eslint-config-react/src/{index.js,typescript.js}`, `src/rules/{react.js,hooks.js,a11y.js,formatjs.js}`, `test/{load.test.js,rules-exist.test.js,fixtures.test.js}` -- Delete: old `rules/*.js`, `index.js`, `hooks.js` after porting (hooks convenience export dropped, mention in migration notes) - -**Interfaces:** -- Consumes: base default config and `createTypescriptConfig`. -- Produces: `default export: Linter.Config[]` (base + react + react-hooks + react-refresh + jsx-a11y + formatjs + browser globals) from `src/index.js`; `createReactTypescriptConfig(options?: same as createTypescriptConfig): Linter.Config[]` from `src/typescript.js`. formatjs excludes `**/*.{test,spec}.{ts,tsx,js,jsx}`. jsx-a11y inclusion follows Task 8's decision; ship enabled until then. - -- [ ] **Step 1:** Tests: `JSX parses in .jsx and .tsx`; `hooks rule flags conditional hook call`; `formatjs/enforce-default-message flags intl.formatMessage without defaultMessage in src, not in tests`; `no react rules in base JS or TS configs`; `jsx-a11y rule flags without alt`; rules-exist over all react rule ids (including `formatjs/*` against the installed plugin); `React JS config contains no @typescript-eslint rules`. -- [ ] **Step 2:** Run; expected FAIL. **Step 3:** Port eventured `react.js` and formatjs/a11y rules, reconciling per spec (placeholder: eventured's frontend formatjs settings). **Step 4:** Run; expected PASS. Commit: `feat(react): React JS and TypeScript configs`. - -### Task 7: Rule comparison table - -**Files:** -- Create: `scripts/rule-table.js`, `scripts/rule-table.test.js`, generated `docs/rules.md` - -**Interfaces:** -- Produces: `buildRuleTable(config: Linter.Config[], baseline: Linter.Config[]): Array<{ rule: string; ours: unknown; recommended: unknown; source: 'cospired' | 'eventured' | 'airbnb-override' | 'hapi' | 'new' }>`, listing only rules where ours differs from baseline (`@eslint/js` + typescript-eslint recommended/stylistic). `source` comes from a hand-written `scripts/rule-sources.json` seeded from the old and eventured files. - -- [ ] **Step 1:** Test: given a config that sets `no-console: 'error'` against baseline `warn`, the table has one row with both values. **Step 2:** FAIL. **Step 3:** Implement and generate `docs/rules.md` (one table per config). **Step 4:** PASS. Commit: `docs: rule comparison table`. - -### Task 8: Interactive rule review with Dirk (gate) - -**Files:** modify `src/rules/*.js` and the fixture tests per decision; update `docs/rules.md` with a "Decision" column. - -- [ ] **Step 1:** Present `docs/rules.md` rows to Dirk grouped by theme, one row/conflict at a time, in this order: hapi `scope-start` and `no-arrowception` replacements; `no-unused-vars` severity; `import/order` newlines (`always` vs `always-and-inside-groups`); `no-console`; React `no-unused-expressions` relaxations; severity policy for `warn`; `#` alias default; jsx-a11y keep/opt-in/drop; formatjs preset values; then every remaining row. Do not decide for Dirk. -- [ ] **Step 2:** For each decision, update the rule, add or adjust a fixture test asserting the new behavior, rerun `npm test`, record the decision in `docs/rules.md`. -- [ ] **Step 3:** When no open rows remain, commit: `feat: apply rule review decisions`. - -### Task 9: Docs, changeset, eventured acceptance - -**Files:** -- Modify: `README.md` (root), `packages/*/README.md`; create `.changeset/.md` (major for both packages), `MIGRATION.md` - -- [ ] **Step 1:** READMEs show usage for each entry (JS, TS, node, react JS/TS), options, the `no-restricted-imports` local-pattern example, and the peer install commands. `MIGRATION.md` lists removed airbnb/hapi rules, dropped `hooks` export, new peers. -- [ ] **Step 2:** Add the major changeset. Run `npx changeset version --snapshot` in a scratch branch to confirm both packages become 8.0.0; do not publish. -- [ ] **Step 3:** Acceptance: in a scratch copy of eventured (not the real repo), rewrite its three `eslint.config.js` files to use the new packages with only the local additions, run `eslint .`, and report to Dirk the list of rule-level differences versus the current config (lint errors themselves are not a concern). -- [ ] **Step 4:** Commit: `docs: usage, migration, 8.0.0 changeset`. Publishing is Dirk's call and is not part of this plan. diff --git a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md b/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md deleted file mode 100644 index 0a42e2e..0000000 --- a/docs/superpowers/specs/2026-11-10-eslint-config-2026-design.md +++ /dev/null @@ -1,81 +0,0 @@ -# @cospired ESLint configs for 2026/2027 — design - -## Intent - -Cospired projects currently copy a hand-built TypeScript ESLint config between repos (the one in `eventured/packages/eslint-config`) because the published `@cospired/eslint-config-*` packages are stuck on ESLint 8, the legacy `.eslintrc` format and airbnb, and have no working TypeScript support. Goal: one published package family that every project installs, replacing both. Released as a new major version. - -## Goals - -- Flat config, ESLint 10, typescript-eslint 8+, ESM. No airbnb (`eslint-config-airbnb-base`/`-typescript` are unmaintained and eslintrc-only). -- Four configs, in two packages (**my call**, keeps existing package names): - - `@cospired/eslint-config-base` — `.` (JS), `./typescript` (TS) - - `@cospired/eslint-config-react` — `.` (JS), `./typescript` (TS); react-hooks, jsx-a11y, formatjs. Replaces the old `eslint-config-typescript` package branch work. -- Rule set built on `@eslint/js` / typescript-eslint recommended (+ stylistic, type-checked for the TS variant). Every custom rule is compared against what recommended already sets and kept only on clean-code merit, decided with Dirk one by one (see "Rule review"). -- Carry over from eventured: `eslint-disable` needs a `-- reason`, TODOs need a ticket ref (`todo-plz`), alphabetized `import-x/order`, devDependency imports only in test/config/script files. -- Carry over from the old packages: eslint-comments rules, formatjs (i18n) rules, jsx-a11y extras, the useful import rules, `no-unused-vars` with `^_` ignore. -- Replace the hapi plugin. Keep its readability intent (blank line after function open / before return) via a non-hapi rule — candidate: `padding-line-between-statements` from `@stylistic/eslint-plugin` (to verify it coexists with Prettier; core version is deprecated). `no-arrowception` replacement: a core rule such as `no-confusing-arrow` / `no-return-assign` family, or drop. Decide during review. -- Tests that prove completeness: every config loads in ESLint 10; every configured rule exists in the installed plugin versions (replaces `eslint-find-rules`); fixture files assert expected errors/non-errors (including blank-line, disable-reason, TODO-ticket, import-order, and a11y/formatjs cases); JS configs contain no TS-only rules; base contains no React rules. -- Tooling modernized: drop Babel, tape, `eslint-find-rules`, `in-publish`, `safe-publish-latest`, Travis scripts. Use `node:test` + the ESLint Node API, npm workspaces, changesets for versioning/publish. Turbo only if it earns its place for two packages (**my call: no**). - -## Non-goals - -- No formatting rules that Prettier owns (`eslint-config-prettier` stays last). -- No support for ESLint < 10 or legacy `.eslintrc`. -- No new rule categories beyond what is listed (no security, unicorn, sonarjs, etc.) in this pass. -- No migration of consuming projects (eventured etc.); only a migration note in the README. - -## Anti-goals - -- A config that loads but silently misses rules (typos, renamed/removed rules, wrong plugin namespace). -- Rules kept only because airbnb or hapi had them. -- Forcing React/formatjs/a11y peer dependencies onto non-React users. - -## Constraints - -- Node/ESLint/typescript-eslint floors: ESLint 10, current Node LTS at release (builder: confirm against ESLint 10's own `engines`). -- Peer dependencies are ranges, not the exact pins eventured uses. -- Type-aware linting options (`tsconfigRootDir`, project paths, multiple tsconfigs, import-x resolver settings) from eventured's `base.js`/`node.js` must survive in the TS variant. -- New major; old packages' last 7.x line stays on npm unchanged. - -## Rule review (process, decided with Dirk) - -Builder produces a table of every custom rule: source (airbnb-override / cospired / eventured / hapi), our setting, recommended's setting, difference. Conflicts and divergences are presented to Dirk and decided one at a time on clean-code grounds; the decisions are recorded in the repo (rule comments or `docs/rules.md`). Hapi `scope-start`/`no-arrowception` and the eventured-vs-cospired conflicts (e.g. `no-unused-vars` severity, `import/order` newlines, `no-console`, React `no-unused-expressions` relaxations) are the first items. - -## Delivery phases and settled details - -- **Two phases.** (1) Infrastructure: workspace, tooling, loader/existence tests, a first port of eventured's rules, rule-comparison table. (2) Interactive rule review with Dirk, one rule/conflict at a time; fixture tests are written alongside each decision, not before. The builder never ships rule decisions by default; until reviewed, conflicts keep eventured's setting as a placeholder and are listed as open. -- **Sources.** Eventured config: `/Users/dh/projects/eventured/eventured/packages/eslint-config/src/{base,node,react}.js` (local sibling checkout, readable). Old rules: `packages/eslint-config-{base,react}/rules/*.js` on `main`. -- **Versioning (my call).** Both packages release as `8.0.0`. The unpublished `eslint-config-typescript` (0.1.0) package is deleted; its work is superseded by the `./typescript` exports. -- **Layering (my call).** `react` depends on `base` as a regular dependency and composes it; `react/typescript` extends `base/typescript`. `base` contains no React rules and no React peer dependencies; React/formatjs/a11y plugins are peers of `react` only. -- **devDependency globs (my call).** Default to eventured's glob list (test/spec files, `test/`, `__tests__/`, `scripts/`, `*.config.*`), overridable via an option. -- **Prettier fallback (my call).** If `@stylistic` blank-line rules conflict with Prettier, fall back to core `padding-line-between-statements` if still present in ESLint 10; otherwise drop the blank-line rule and tell Dirk. -- **CI/publish (my call).** Repo is on GitHub (`cospired/javascript`): GitHub Actions running tests, plus the changesets release action. Dirk supplies the npm token / trusted-publishing setup. - -## Findings from eventured consumers (apps/api, apps/frontend, packages/api-contracts) - -What the three consumer configs add on top of the shared package, and what that means for the new package: - -- **Node config is needed.** `apps/api` uses `createNodeConfig` (type-aware, node globals, `prefer-nullish-coalescing` with string `||` allowed, import-x resolver with `noWarnOnMultipleProjects`). The two-package layout had no node variant. **My call:** `@cospired/eslint-config-base/node` (TS only), built on base/typescript. Dirk to confirm. -- **Multiple tsconfigs + absolute paths + `tsconfigRootDir`.** api lints with three tsconfigs (`tsconfig.json`, `tsconfig.lint.json`, `test/tsconfig.json`) from both package root (Turbo) and repo root (lint-staged). `tsconfigPath` must accept an array; `tsconfigRootDir` stays an option. Test this scenario. -- **Import order needs extensible alias groups.** api overrides `import-x/order` to place `#shared/#data/#domain/#services/#entrypoints` aliases as an `internal` group before relative imports. The config should take `importOrder.pathGroups` (or treat Node `#` subpath imports as internal by default — decide in rule review) so consumers stop copy-pasting the whole rule. -- **`explicit-function-return-type`.** Base turns it off; api and api-contracts both turn it back on (`allowExpressions: true`, not for tests), and frontend uses disables for it (4×). Candidate to become a default (non-test files). Rule review item. -- **Type-aware extras both consumers add:** `no-misused-promises` (`checksConditionals: {flagUnions: 'strict'}`), `no-generated-empty-object-type`. Candidates for defaults. -- **formatjs is configured locally in frontend** from `formatjs.configs.recommended` plus: `no-invalid-icu` error, `no-camel-case` error, `enforce-description` off, test files excluded, `no-literal-string-in-jsx` left at warn. Old cospired `format.js` differs (enforce-description on, camel-case off, many extras). The new react config should own one reconciled formatjs preset, test files excluded by default. Rule review item. -- **No jsx-a11y in eventured frontend** (UI is react-aria based). Old cospired react config enables jsx-a11y. Open question for rule review: keep jsx-a11y by default, make it opt-in, or drop. react-hooks and react-refresh are used in eventured (React 19, Vite) but absent from the old react package; add both. -- **`no-restricted-imports` stays local.** All uses (icon subpath imports, layered-architecture bans, `#` alias rules) are project-specific; the package does not ship them, but the README should show the pattern. -- **Warnings are failures.** All lint scripts run `--report-unused-disable-directives --max-warnings 0`, so `warn` severity == `error` in practice. Rule review should settle a policy (prefer `error`/`off`; `warn` only for rules needing a transition period). -- **Misc:** CommonJS override for `*.config.{js,mjs}` (api-contracts), local `ignores`, `**/*.d.ts` globally ignored, `scripts/**` gets node globals with `no-console` off. `no-console` is disabled inline 10×, so the default stays `warn`/`error` with scripts exempted. Consumers pin exact versions via pnpm and use ESLint 10, typescript-eslint 8.72, TypeScript 6, Prettier 3.9; the package peers should be ranges that include these. -- **Acceptance check:** after the new package exists, the three eventured configs should be reproducible from it with only the local additions above, lint output on the eventured repo should match the current one except for deliberate rule decisions (diff list produced for Dirk). - -## Left to the builder - -File layout, test harness details, fixture format, changesets config, CI (existing is Travis; choose a current setup), README/migration docs, exact plugin versions. - -## Amendments (after approval, from Dirk) - -- **Completeness:** every non-deprecated rule of every plugin used (ESLint core, typescript-eslint, import-x, eslint-comments, todo-plz, @stylistic, react-hooks, react-refresh, formatjs) is configured, either by a recommended preset or explicitly on/off by us. Our files only list rules a preset does not set, or sets differently. `test/completeness.test.js` in each package fails otherwise (replaces `eslint-find-rules --unused`) and also fails on a named `cospired/*` override that repeats the preset's value. The ~200 on/off calls for rules nobody had decided were made by the implementer by clean-code principles (bug-preventing on, opinion/thresholds/formatting off) and are marked "added for completeness (unreviewed call)" in `docs/rules.md`. -- **Layout:** rules are split per plugin in `src/rules/` (`eslint.js`, `typescript-eslint.js`, `import-x.js`, `eslint-comments.js`, `todo-plz.js`, `stylistic.js`; react: `react-hooks.js`, `react-refresh.js`, `formatjs.js`, `react.js`), as in the old packages. -- **Goal:** configs usable with no overrides in eventured, danceregistry and our other projects: every entry has a ready-made default export (`export { default } from '@cospired/eslint-config-base/node'`). Entries: base JS, node-js, typescript, node, react, react/typescript. Tests, scripts and package-root TS files are linted without type information unless `typedTests: true`. -- **Publish readiness:** GitHub CI (Node 22.13/24/26, peer-dependency floors, packed-tarball smoke test), changesets release workflow with npm provenance, 8.0.0 for both packages. -- **Four packages (Dirk, after review):** `@cospired/eslint-config-base` (JS, `/node`), `-typescript` (TS, `/node`), `-react`, `-react-typescript`, all 8.0.0 via a changesets `fixed` group. JavaScript-only projects no longer need the TypeScript peers. Supersedes the earlier two-package layout. -- **Rule documentation (Dirk):** every rule in the rule files has a comment block with the rule's description, our reasoning and its documentation URL, and a blank line after it, as in the old packages. The blocks were generated from the installed plugins' metadata plus a reason per rule; new rules added later follow the same format. diff --git a/package.json b/package.json index 0fab1a4..fe0bba5 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,10 @@ "packages/*" ], "scripts": { - "test": "npm test --workspaces --if-present && node --test test-utils/*.test.js scripts/*.test.js", + "test": "npm test --workspaces --if-present && node --test test-utils/*.test.js", "lint": "eslint .", "changeset": "changeset", "release": "changeset publish", - "rules": "node scripts/rule-table.js", "smoke": "node scripts/smoke-pack.js", "version": "changeset version && npm install --package-lock-only --ignore-scripts" }, diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index a49b148..c4b68ea 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -44,7 +44,7 @@ export default [ ## What it enforces -Every rule is organised per plugin in `src/rules/`, with a comment block (description, reasoning, link) per rule, and listed with its decision in [docs/rules.md](../../docs/rules.md). A test fails when a plugin ships a rule that is neither in a recommended preset nor configured here, so new plugin releases cannot add gaps silently. +Every rule is organised per plugin in `src/rules/`, with a comment block (description, reasoning, link) per rule. A test fails when a plugin ships a rule that is neither in a recommended preset nor configured here, so new plugin releases cannot add gaps silently. - `eslint-disable` comments need a reason (`-- why`); TODOs need a ticket (`TODO(#123): ...`). - Alphabetized imports in groups separated by a blank line; devDependency imports only in test, config and script files. diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js index 08a43ca..d2ad13f 100644 --- a/packages/eslint-config-base/test/decisions.test.js +++ b/packages/eslint-config-base/test/decisions.test.js @@ -1,14 +1,13 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { loadedRules, lintText, ruleIds, warnRules } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, loadedRules, ruleIds, warnRules } from '../../../test-utils/lint.js'; import { anyTestGlobs } from '../src/globs.js'; import config from '../src/index.js'; import nodeConfig from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'esm-project'); +const root = fixtureProject; const lint = async (code, file = 'src/a.js', cfg = config) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); // [rule, code that must be flagged, code that must pass] diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index 6a0b6a1..6defdcf 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { lintText } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText } from '../../../test-utils/lint.js'; import jsDefault from '../src/index.js'; import nodeDefault from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'esm-project'); +const root = fixtureProject; const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; diff --git a/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js b/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js deleted file mode 100644 index f053ebf..0000000 --- a/packages/eslint-config-base/test/fixtures/cjs-project/src/a.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/packages/eslint-config-base/test/fixtures/esm-project/package.json b/packages/eslint-config-base/test/fixtures/esm-project/package.json deleted file mode 100644 index a398f8f..0000000 --- a/packages/eslint-config-base/test/fixtures/esm-project/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "esm-fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js index ad6825e..4869002 100644 --- a/packages/eslint-config-base/test/node.test.js +++ b/packages/eslint-config-base/test/node.test.js @@ -1,14 +1,12 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; import nodeJsDefault, { createNodeJsConfig } from '../src/node.js'; -const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures'); -const cjs = path.join(fixtures, 'cjs-project'); -const esm = path.join(fixtures, 'esm-project'); +const cjs = path.join(path.dirname(fixtureProject), 'cjs-project'); +const esm = fixtureProject; const cjsCode = "const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"; const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(createNodeJsConfig({ cwd }), code, { filePath: path.join(cwd, file), cwd })); diff --git a/packages/eslint-config-react-typescript/test/decisions.test.js b/packages/eslint-config-react-typescript/test/decisions.test.js index 62f2351..b3910b2 100644 --- a/packages/eslint-config-react-typescript/test/decisions.test.js +++ b/packages/eslint-config-react-typescript/test/decisions.test.js @@ -7,5 +7,5 @@ import { warnRules } from '../../../test-utils/lint.js'; import { root, tsConfig } from './helpers.js'; test('no rule resolves to "warn" in the React TypeScript config', async () => { - assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.tsx'), root), []); + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/component.tsx'), root), []); }); diff --git a/packages/eslint-config-react-typescript/test/fixtures.test.js b/packages/eslint-config-react-typescript/test/fixtures.test.js index 740d502..eaadc8c 100644 --- a/packages/eslint-config-react-typescript/test/fixtures.test.js +++ b/packages/eslint-config-react-typescript/test/fixtures.test.js @@ -8,7 +8,7 @@ import { ids, tsConfig } from './helpers.js'; const message = 'declare const intl: { formatMessage: (d: { id: string }) => string };\n\nexport const m = intl.formatMessage({ id: "a" });\n'; test('formatjs flags a missing defaultMessage in src, not in tests', async () => { - assert.ok((await ids(tsConfig, message, 'src/a.tsx')).includes('formatjs/enforce-default-message')); + assert.ok((await ids(tsConfig, message, 'src/component.tsx')).includes('formatjs/enforce-default-message')); assert.ok(!(await ids(tsConfig, message, 'src/a.test.tsx')).includes('formatjs/enforce-default-message')); }); diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json b/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json deleted file mode 100644 index 6a2659b..0000000 --- a/packages/eslint-config-react-typescript/test/fixtures/ts-project/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json deleted file mode 100644 index eb46ae9..0000000 --- a/packages/eslint-config-react-typescript/test/fixtures/ts-project/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-react-typescript/test/helpers.js b/packages/eslint-config-react-typescript/test/helpers.js index 39e581c..b504814 100644 --- a/packages/eslint-config-react-typescript/test/helpers.js +++ b/packages/eslint-config-react-typescript/test/helpers.js @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; import { createReactTypescriptConfig } from '../src/index.js'; -export const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +export const root = fixtureProject; export const tsConfig = createReactTypescriptConfig({ tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }); /** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ diff --git a/packages/eslint-config-react-typescript/test/load.test.js b/packages/eslint-config-react-typescript/test/load.test.js index b996dc5..44ab72e 100644 --- a/packages/eslint-config-react-typescript/test/load.test.js +++ b/packages/eslint-config-react-typescript/test/load.test.js @@ -4,7 +4,7 @@ import test from 'node:test'; import { ids, tsConfig } from './helpers.js'; test('JSX parses in .tsx under the TS config', async () => { - const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/a.tsx'); + const found = await ids(tsConfig, 'export const A = (): unknown =>
;\n', 'src/component.tsx'); assert.ok(Array.isArray(found)); }); diff --git a/packages/eslint-config-react/test/fixtures/ts-project/package.json b/packages/eslint-config-react/test/fixtures/ts-project/package.json deleted file mode 100644 index 6a2659b..0000000 --- a/packages/eslint-config-react/test/fixtures/ts-project/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "fixture", "private": true, "type": "module" } diff --git a/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx b/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx deleted file mode 100644 index cc798ff..0000000 --- a/packages/eslint-config-react/test/fixtures/ts-project/src/a.tsx +++ /dev/null @@ -1 +0,0 @@ -export const a = 1; diff --git a/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json deleted file mode 100644 index eb46ae9..0000000 --- a/packages/eslint-config-react/test/fixtures/ts-project/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-react/test/helpers.js b/packages/eslint-config-react/test/helpers.js index 3ecc428..820cee4 100644 --- a/packages/eslint-config-react/test/helpers.js +++ b/packages/eslint-config-react/test/helpers.js @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; import jsConfig from '../src/index.js'; -export const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +export const root = fixtureProject; export { jsConfig }; /** Rule ids for `code` at `file` (relative to the fixture project); asserts the file parses. */ diff --git a/packages/eslint-config-typescript/README.md b/packages/eslint-config-typescript/README.md index 5f1bb00..e320fe5 100644 --- a/packages/eslint-config-typescript/README.md +++ b/packages/eslint-config-typescript/README.md @@ -44,4 +44,4 @@ export default [...createNodeConfig({ tsconfigPath: ['tsconfig.json', 'test/tsco ## What it adds to the base config -typescript-eslint type-checked recommended + stylistic, explicit return types outside tests, `no-misused-promises`, `consistent-type-imports` (inline), and every typescript-eslint rule configured (see [docs/rules.md](../../docs/rules.md)). +typescript-eslint type-checked recommended + stylistic, explicit return types outside tests, `no-misused-promises`, `consistent-type-imports` (inline), and every typescript-eslint rule configured. diff --git a/packages/eslint-config-typescript/test/decisions.test.js b/packages/eslint-config-typescript/test/decisions.test.js index 3f83e78..97999d0 100644 --- a/packages/eslint-config-typescript/test/decisions.test.js +++ b/packages/eslint-config-typescript/test/decisions.test.js @@ -1,14 +1,13 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; import { testGlobs } from '../src/globs.js'; import { createTypescriptConfig } from '../src/index.js'; import { createNodeConfig } from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const root = fixtureProject; const tsOptions = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; const tsConfig = createTypescriptConfig(tsOptions); const lint = async (code, file, cfg = tsConfig) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); diff --git a/packages/eslint-config-typescript/test/defaults.test.js b/packages/eslint-config-typescript/test/defaults.test.js index 9b6ffc9..4ebb61b 100644 --- a/packages/eslint-config-typescript/test/defaults.test.js +++ b/packages/eslint-config-typescript/test/defaults.test.js @@ -1,15 +1,14 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; import jsDefault from '@cospired/eslint-config-base'; -import { lintText } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText } from '../../../test-utils/lint.js'; import tsDefault, { createTypescriptConfig } from '../src/index.js'; import nodeDefault from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const root = fixtureProject; const lint = (cfg, code, file) => lintText(cfg, code, { filePath: path.join(root, file), cwd: root }); const ruleIdsFor = async (cfg, code, file) => [...new Set((await lint(cfg, code, file)).map((m) => m.ruleId))]; diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/package.json b/packages/eslint-config-typescript/test/fixtures/ts-project/package.json deleted file mode 100644 index 8f75309..0000000 --- a/packages/eslint-config-typescript/test/fixtures/ts-project/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "fixture", "private": true, "type": "module", "imports": { "#shared/*": "./src/shared/*.ts" } } diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json b/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json deleted file mode 100644 index 7dba039..0000000 --- a/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"] } diff --git a/packages/eslint-config-typescript/test/node.test.js b/packages/eslint-config-typescript/test/node.test.js index 579ab91..457e088 100644 --- a/packages/eslint-config-typescript/test/node.test.js +++ b/packages/eslint-config-typescript/test/node.test.js @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; import { createTypescriptConfig } from '../src/index.js'; import { createNodeConfig } from '../src/node.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const root = fixtureProject; const options = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; const node = createNodeConfig(options); const ids = async (cfg, code, file) => { diff --git a/packages/eslint-config-typescript/test/typescript.test.js b/packages/eslint-config-typescript/test/typescript.test.js index 43003d7..0753ef8 100644 --- a/packages/eslint-config-typescript/test/typescript.test.js +++ b/packages/eslint-config-typescript/test/typescript.test.js @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { lintText, ruleIds, ruleSeverity } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, ruleIds, ruleSeverity } from '../../../test-utils/lint.js'; import { createTypescriptConfig } from '../src/index.js'; -const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'ts-project'); +const root = fixtureProject; const abs = (file) => path.join(root, file); const options = { tsconfigPath: [abs('tsconfig.json'), abs('tsconfig.test.json')], tsconfigRootDir: root }; const config = createTypescriptConfig(options); diff --git a/scripts/rule-decisions.json b/scripts/rule-decisions.json deleted file mode 100644 index 119dc06..0000000 --- a/scripts/rule-decisions.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "@stylistic/padding-line-between-statements": "kept: blank line before return and after declarations; hapi scope-start dropped (Prettier strips block-leading blank lines)", - "no-unused-vars": "error, `_` prefix ignored", - "@typescript-eslint/no-unused-vars": "error, `_` prefix ignored", - "import-x/order": "always + alphabetize", - "no-console": "error, allow warn/error; scripts/** exempt", - "no-unused-expressions": "strict, no React relaxations", - "object-shorthand": "kept", - "prefer-arrow-callback": "kept", - "prefer-const": "kept", - "no-var": "kept", - "prefer-object-has-own": "kept", - "logical-assignment-operators": "kept (incl. if statements)", - "import-x/no-duplicates": "kept", - "import-x/no-relative-packages": "kept", - "import-x/no-import-module-exports": "kept", - "import-x/no-empty-named-blocks": "kept", - "import-x/newline-after-import": "kept", - "import-x/consistent-type-specifier-style": "kept: prefer-inline", - "@eslint-community/eslint-comments/disable-enable-pair": "kept", - "@eslint-community/eslint-comments/no-aggregating-enable": "kept", - "@eslint-community/eslint-comments/no-duplicate-disable": "kept", - "@eslint-community/eslint-comments/no-unlimited-disable": "kept", - "@eslint-community/eslint-comments/no-unused-enable": "kept", - "@eslint-community/eslint-comments/require-description": "kept", - "import-x/no-extraneous-dependencies": "kept (eventured globs, overridable)", - "todo-plz/ticket-ref": "kept", - "eqeqeq": "adopted, `== null` allowed", - "no-param-reassign": "adopted, props: false", - "consistent-return": "adopted, JS only", - "no-shadow": "adopted (TS: typescript-eslint version)", - "@typescript-eslint/no-shadow": "adopted", - "prefer-template": "adopted", - "no-nested-ternary": "adopted", - "no-else-return": "adopted, allowElseIf: false", - "curly": "adopted, multi-line (re-enabled after Prettier)", - "no-use-before-define": "adopted, strict", - "@typescript-eslint/no-use-before-define": "adopted, strict", - "default-param-last": "adopted", - "@typescript-eslint/default-param-last": "adopted", - "no-await-in-loop": "adopted", - "@typescript-eslint/explicit-function-return-type": "default on for non-test files, allowExpressions", - "@typescript-eslint/no-misused-promises": "default on, flagUnions strict", - "@typescript-eslint/no-generated-empty-object-type": "default on", - "@typescript-eslint/no-explicit-any": "error (no warn policy)", - "react-refresh/only-export-components": "error (no warn policy)", - "formatjs/enforce-description": "off (eventured)", - "formatjs/no-camel-case": "error (eventured)", - "formatjs/no-invalid-icu": "error (eventured)", - "formatjs/no-literal-string-in-jsx": "error (no warn policy)", - "react-hooks/exhaustive-deps": "error (no warn policy)", - "react-hooks/incompatible-library": "error (no warn policy)", - "react-hooks/unsupported-syntax": "error (no warn policy)" -} diff --git a/scripts/rule-sources.json b/scripts/rule-sources.json deleted file mode 100644 index e578dc4..0000000 --- a/scripts/rule-sources.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "logical-assignment-operators": "cospired", - "prefer-object-has-own": "cospired", - "@stylistic/padding-line-between-statements": "hapi", - "import-x/consistent-type-specifier-style": "cospired", - "import-x/no-empty-named-blocks": "cospired", - "import-x/no-import-module-exports": "cospired", - "import-x/no-relative-packages": "cospired", - "import-x/newline-after-import": "cospired", - "import-x/no-extraneous-dependencies": "eventured", - "import-x/order": "eventured", - "no-console": "eventured", - "no-unused-expressions": "eventured", - "no-unused-vars": "cospired", - "no-var": "eventured", - "object-shorthand": "eventured", - "prefer-arrow-callback": "eventured", - "prefer-const": "eventured", - "@eslint-community/eslint-comments/require-description": "eventured", - "todo-plz/ticket-ref": "eventured", - "@typescript-eslint/explicit-function-return-type": "eventured", - "@typescript-eslint/no-misused-promises": "eventured", - "@typescript-eslint/no-generated-empty-object-type": "eventured", - "@typescript-eslint/no-explicit-any": "eventured", - "@typescript-eslint/no-unused-vars": "eventured", - "@typescript-eslint/prefer-nullish-coalescing": "eventured", - "react-refresh/only-export-components": "eventured", - "formatjs/enforce-description": "eventured", - "formatjs/no-camel-case": "eventured", - "formatjs/no-invalid-icu": "eventured" -} diff --git a/scripts/rule-table.js b/scripts/rule-table.js deleted file mode 100644 index c06184c..0000000 --- a/scripts/rule-table.js +++ /dev/null @@ -1,127 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { ESLint } from 'eslint'; - -const here = path.dirname(fileURLToPath(import.meta.url)); -const LEVELS = { 0: 'off', 1: 'warn', 2: 'error' }; - -const level = (entry) => { - const raw = Array.isArray(entry) ? entry[0] : entry; - - return LEVELS[raw] ?? raw; -}; - -const format = (entry) => { - if (entry === undefined) return '-'; - const options = Array.isArray(entry) ? entry.slice(1) : []; - - return [level(entry), ...options.map((o) => JSON.stringify(o))].join(' '); -}; - -/** Last-wins merge of every `rules` object in a config array (ignores `files` scoping). */ -const effective = (config) => Object.assign({}, ...config.map((item) => item.rules ?? {})); - -/** - * Rows for every rule where `config` is on and differs from `baseline`. - * @returns {Array<{ rule: string, ours: string, recommended: string, source: string }>} - */ -export function buildRuleTable(config, baseline, sources = {}) { - const ours = effective(config); - const base = effective(baseline); - - return Object.keys(ours) - .sort() - .filter((rule) => level(ours[rule]) !== 'off' || (base[rule] !== undefined && level(base[rule]) !== 'off')) - .filter((rule) => !(level(ours[rule]) === 'off' && level(base[rule]) === 'off')) - .filter((rule) => format(ours[rule]) !== format(base[rule])) - .map((rule) => ({ rule, ours: format(ours[rule]), recommended: format(base[rule]), source: sources[rule] ?? 'new' })); -} - -/** Rules that config items named `cospired/*` switch off (completeness: considered and deliberately not enforced). */ -export function explicitOffRules(config) { - const off = new Set(); - - for (const item of config) { - if (!item.name?.startsWith('cospired/')) continue; - for (const [id, entry] of Object.entries(item.rules ?? {})) { - if (level(entry) === 'off') off.add(id); - } - } - - return [...off].sort(); -} - -/** Rules set by config items named `cospired/*`: our own decisions, as opposed to a plugin preset. */ -const ownRules = (config) => new Set(config.flatMap((item) => (item.name?.startsWith('cospired/') ? Object.keys(item.rules ?? {}) : []))); - -export function renderTable(title, rows, decisions = {}) { - const lines = rows.map((r) => `| \`${r.rule}\` | ${r.ours} | ${r.recommended} | ${r.source} | ${decisions[r.rule] ?? 'open'} |`); - - return [`## ${title}`, '', '| Rule | Ours | Recommended | Source | Decision |', '|---|---|---|---|---|', ...lines, ''].join('\n'); -} - -/** Rules as ESLint resolves them for one file (honours `files`/`ignores` scoping), as a one-item config array. */ -async function resolved(config, file) { - const filePath = path.join(here, '..', file); - const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: config, cwd: path.dirname(filePath) }); - - return [{ rules: (await eslint.calculateConfigForFile(filePath)).rules }]; -} - -async function main() { - const [{ default: js }, { default: tseslint }, base, baseTsModule, react, reactTs] = await Promise.all([ - import('@eslint/js'), - import('typescript-eslint'), - import('../packages/eslint-config-base/src/index.js'), - import('../packages/eslint-config-typescript/src/index.js'), - import('../packages/eslint-config-react/src/index.js'), - import('../packages/eslint-config-react-typescript/src/index.js'), - ]); - const sources = JSON.parse(fs.readFileSync(path.join(here, 'rule-sources.json'), 'utf8')); - const decisions = JSON.parse(fs.readFileSync(path.join(here, 'rule-decisions.json'), 'utf8')); - const recommended = [js.configs.recommended]; - const tsRecommended = [...recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked]; - const tsConfig = baseTsModule.createTypescriptConfig(); - const reactJsOnly = react.default; - const reactTsConfig = reactTs.createReactTypescriptConfig(); - - const [baseJs, recJs, baseTsRules, recTs, reactJs, reactTsRules] = await Promise.all([ - resolved(base.default, 'src/a.js'), - resolved(recommended, 'src/a.js'), - resolved(tsConfig, 'src/a.ts'), - resolved(tsRecommended, 'src/a.ts'), - resolved(react.default, 'src/a.jsx'), - resolved(reactTsConfig, 'src/a.tsx'), - ]); - // Rules nobody listed in rule-decisions.json come straight from a plugin's own preset. - const table = (config, baseline, raw) => { - const rows = buildRuleTable(config, baseline, sources); - const own = ownRules(raw); - const fallback = (rule) => (own.has(rule) ? 'added for completeness (unreviewed call)' : 'plugin preset, unchanged'); - - return [rows, Object.fromEntries(rows.map((r) => [r.rule, decisions[r.rule] ?? fallback(r.rule)]))]; - }; - const out = [ - '# Rule comparison', - '', - 'Generated by `npm run rules`. Rows are rules where our setting differs from the baseline; the Decision column is filled during the rule review.', - '', - renderTable('Base JS (vs. @eslint/js recommended)', ...table(baseJs, recJs, base.default)), - renderTable('Base TypeScript (vs. @eslint/js + typescript-eslint recommended/stylistic type-checked)', ...table(baseTsRules, recTs, tsConfig)), - renderTable('React JS (vs. Base JS)', ...table(reactJs, baseJs, react.default)), - renderTable('React TypeScript (vs. Base TypeScript)', ...table(reactTsRules, baseTsRules, reactTsConfig)), - '## Explicitly off (considered, not enforced)', - '', - 'Rules no preset sets, switched off on purpose: opinion, thresholds, project-specific, or covered by Prettier/TypeScript.', - '', - `- ESLint core, import-x, typescript-eslint, @stylistic, eslint-comments: ${explicitOffRules(reactTsConfig).map((r) => `\`${r}\``).join(', ')}`, - `- React plugins: ${explicitOffRules(reactJsOnly).filter((r) => /^(react|formatjs)/.test(r)).map((r) => `\`${r}\``).join(', ')}`, - '', - ].join('\n'); - - fs.writeFileSync(path.join(here, '..', 'docs', 'rules.md'), out); -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) await main(); diff --git a/scripts/rule-table.test.js b/scripts/rule-table.test.js deleted file mode 100644 index 29bd4c3..0000000 --- a/scripts/rule-table.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { buildRuleTable, explicitOffRules, renderTable } from './rule-table.js'; - -const sources = { 'no-console': 'eventured' }; - -test('buildRuleTable lists only rules where ours differs from the baseline', () => { - const table = buildRuleTable( - [{ rules: { 'no-console': 'error', eqeqeq: 'error', 'no-var': ['error'], 'no-debugger': 'off' } }], - [{ rules: { 'no-console': 'warn', eqeqeq: 'error', 'no-var': 'error' } }], - sources, - ); - - assert.deepEqual(table, [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); -}); - -test('buildRuleTable includes rules the baseline does not set, marked source "new" when unknown', () => { - const table = buildRuleTable([{ rules: { 'prefer-const': ['error', { destructuring: 'all' }] } }], [{ rules: {} }], sources); - - assert.deepEqual(table, [{ rule: 'prefer-const', ours: 'error {"destructuring":"all"}', recommended: '-', source: 'new' }]); -}); - -test('renderTable renders a markdown table with a Decision column', () => { - const md = renderTable('Base JS', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }]); - - assert.match(md, /^## Base JS/m); - assert.match(md, /\| `no-console` \| error \| warn \| eventured \| open \|/); -}); - -test('renderTable fills the Decision column from the decisions map', () => { - const md = renderTable('X', [{ rule: 'no-console', ours: 'error', recommended: 'warn', source: 'eventured' }], { 'no-console': 'error, allow warn/error' }); - - assert.match(md, /\| `no-console` \| error \| warn \| eventured \| error, allow warn\/error \|/); -}); - -test('buildRuleTable skips rows where both sides are off, even if only options differ', () => { - const table = buildRuleTable([{ rules: { 'no-unused-vars': ['off', { a: 1 }] } }], [{ rules: { 'no-unused-vars': 'off' } }], {}); - - assert.deepEqual(table, []); -}); - -test('explicitOffRules lists rules our named config items switch off', () => { - const config = [ - { rules: { 'no-var': 'off' } }, - { name: 'cospired/x', rules: { 'no-plusplus': 'off', eqeqeq: 'error', 'no-void': 0 } }, - ]; - - assert.deepEqual(explicitOffRules(config), ['no-plusplus', 'no-void']); -}); diff --git a/test-utils/fixtures/README.md b/test-utils/fixtures/README.md new file mode 100644 index 0000000..9cac4ee --- /dev/null +++ b/test-utils/fixtures/README.md @@ -0,0 +1,16 @@ +# Fixture projects + +The config tests lint code with the real ESLint and typescript-eslint, so they need a real project on disk. The files are tiny on purpose; their content does not matter, only that they exist and where they sit. + +`ts-project`, shared by every package's tests: + +| File | Why it exists | +|---|---| +| `tsconfig.json` (includes `src`), `tsconfig.test.json` (includes `test`) | Type-aware linting needs every linted file to belong to a tsconfig; two projects test the `tsconfigPath` array. | +| `src/a.ts`, `src/b.ts`, `src/component.tsx` | Source files inside the tsconfig: TypeScript and JSX parse with type information. | +| `src/shared/x.ts` and the `imports` map in `package.json` | `#shared/*` subpath imports must resolve. | +| `src/a.test.tsx`, `test/a.test.ts` | Test files, inside a tsconfig (`src`, `test`). | +| `other/c.test.ts` | A test file outside every tsconfig: must lint without a parsing error. | +| `tool.config.ts`, `tool.shared.ts` | TypeScript files at the project root outside every tsconfig (bundler, test runner or ORM configs): same. | + +`cjs-project` is a `package.json` without `"type"`, to test CommonJS detection. diff --git a/packages/eslint-config-base/test/fixtures/cjs-project/package.json b/test-utils/fixtures/cjs-project/package.json similarity index 100% rename from packages/eslint-config-base/test/fixtures/cjs-project/package.json rename to test-utils/fixtures/cjs-project/package.json diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/other/c.test.ts b/test-utils/fixtures/ts-project/other/c.test.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/other/c.test.ts rename to test-utils/fixtures/ts-project/other/c.test.ts diff --git a/test-utils/fixtures/ts-project/package.json b/test-utils/fixtures/ts-project/package.json new file mode 100644 index 0000000..28bb7fa --- /dev/null +++ b/test-utils/fixtures/ts-project/package.json @@ -0,0 +1 @@ +{ "name": "fixture", "private": true, "type": "module", "imports": { "#shared/*": "./src/shared/*.ts" }, "devDependencies": { "eslint": "*" } } diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.test.tsx b/test-utils/fixtures/ts-project/src/a.test.tsx similarity index 100% rename from packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.test.tsx rename to test-utils/fixtures/ts-project/src/a.test.tsx diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/src/a.ts b/test-utils/fixtures/ts-project/src/a.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/src/a.ts rename to test-utils/fixtures/ts-project/src/a.ts diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/src/b.ts b/test-utils/fixtures/ts-project/src/b.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/src/b.ts rename to test-utils/fixtures/ts-project/src/b.ts diff --git a/packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx b/test-utils/fixtures/ts-project/src/component.tsx similarity index 100% rename from packages/eslint-config-react-typescript/test/fixtures/ts-project/src/a.tsx rename to test-utils/fixtures/ts-project/src/component.tsx diff --git a/test-utils/fixtures/ts-project/src/ok.ts b/test-utils/fixtures/ts-project/src/ok.ts deleted file mode 100644 index d6ca325..0000000 --- a/test-utils/fixtures/ts-project/src/ok.ts +++ /dev/null @@ -1 +0,0 @@ -export const ok = 1; diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/src/shared/x.ts b/test-utils/fixtures/ts-project/src/shared/x.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/src/shared/x.ts rename to test-utils/fixtures/ts-project/src/shared/x.ts diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/test/a.test.ts b/test-utils/fixtures/ts-project/test/a.test.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/test/a.test.ts rename to test-utils/fixtures/ts-project/test/a.test.ts diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/tool.config.ts b/test-utils/fixtures/ts-project/tool.config.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/tool.config.ts rename to test-utils/fixtures/ts-project/tool.config.ts diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/tool.shared.ts b/test-utils/fixtures/ts-project/tool.shared.ts similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/tool.shared.ts rename to test-utils/fixtures/ts-project/tool.shared.ts diff --git a/test-utils/fixtures/ts-project/tsconfig.json b/test-utils/fixtures/ts-project/tsconfig.json index f0c7975..8e274d5 100644 --- a/test-utils/fixtures/ts-project/tsconfig.json +++ b/test-utils/fixtures/ts-project/tsconfig.json @@ -5,7 +5,10 @@ "moduleResolution": "NodeNext", "strict": true, "noEmit": true, - "skipLibCheck": true + "skipLibCheck": true, + "jsx": "react-jsx" }, - "include": ["src"] + "include": [ + "src" + ] } diff --git a/packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.test.json b/test-utils/fixtures/ts-project/tsconfig.test.json similarity index 100% rename from packages/eslint-config-typescript/test/fixtures/ts-project/tsconfig.test.json rename to test-utils/fixtures/ts-project/tsconfig.test.json From 9d0b29fb6b7d4f75abd1c6ed3b651cffc1fffe8c Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:34:59 +0800 Subject: [PATCH 30/33] chore: unpublished packages start at 0.0.0; changesets (fixed group) takes all four to 8.0.0 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- packages/eslint-config-react-typescript/package.json | 12 ++++++------ packages/eslint-config-typescript/package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/eslint-config-react-typescript/package.json b/packages/eslint-config-react-typescript/package.json index 25baf9f..2368415 100644 --- a/packages/eslint-config-react-typescript/package.json +++ b/packages/eslint-config-react-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@cospired/eslint-config-react-typescript", - "version": "7.0.0", + "version": "0.0.0", "description": "Slightly opinionated ESLint flat config for React with TypeScript", "type": "module", "files": [ @@ -35,15 +35,15 @@ "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-formatjs": "^8.1.4", "eslint-plugin-import-x": "^4.18.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-todo-plz": "^1.4.1", "globals": "^17.14.0", - "eslint-import-resolver-typescript": "^4.4.5", "typescript": ">=5.4.2 <6.1", - "typescript-eslint": "^8.72.2", - "eslint-plugin-formatjs": "^8.1.4", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.7" + "typescript-eslint": "^8.72.2" }, "repository": { "type": "git", diff --git a/packages/eslint-config-typescript/package.json b/packages/eslint-config-typescript/package.json index 2170be7..5ac7cde 100644 --- a/packages/eslint-config-typescript/package.json +++ b/packages/eslint-config-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@cospired/eslint-config-typescript", - "version": "7.0.0", + "version": "0.0.0", "description": "Slightly opinionated ESLint flat config for TypeScript (type-aware)", "type": "module", "files": [ @@ -34,10 +34,10 @@ "@stylistic/eslint-plugin": "^5.10.0", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.18.1", "eslint-plugin-todo-plz": "^1.4.1", "globals": "^17.14.0", - "eslint-import-resolver-typescript": "^4.4.5", "typescript": ">=5.4.2 <6.1", "typescript-eslint": "^8.72.2" }, From d4f22e5d01a7806bbc6b90a93888411f24296ffe Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:36:55 +0800 Subject: [PATCH 31/33] fix: react-typescript depends on typescript ^0.0.0 until changesets bumps all four Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- package-lock.json | 6 +++--- packages/eslint-config-react-typescript/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 093f7d1..e3a4e18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2933,12 +2933,12 @@ }, "packages/eslint-config-react-typescript": { "name": "@cospired/eslint-config-react-typescript", - "version": "7.0.0", + "version": "0.0.0", "license": "BSD-3-Clause-Clear", "dependencies": { "@cospired/eslint-config-base": "^7.0.2", "@cospired/eslint-config-react": "^7.0.0", - "@cospired/eslint-config-typescript": "^7.0.0" + "@cospired/eslint-config-typescript": "^0.0.0" }, "engines": { "node": "^22.13.0 || >=24" @@ -2962,7 +2962,7 @@ }, "packages/eslint-config-typescript": { "name": "@cospired/eslint-config-typescript", - "version": "7.0.0", + "version": "0.0.0", "license": "BSD-3-Clause-Clear", "dependencies": { "@cospired/eslint-config-base": "^7.0.2" diff --git a/packages/eslint-config-react-typescript/package.json b/packages/eslint-config-react-typescript/package.json index 2368415..6218ac8 100644 --- a/packages/eslint-config-react-typescript/package.json +++ b/packages/eslint-config-react-typescript/package.json @@ -27,7 +27,7 @@ "dependencies": { "@cospired/eslint-config-base": "^7.0.2", "@cospired/eslint-config-react": "^7.0.0", - "@cospired/eslint-config-typescript": "^7.0.0" + "@cospired/eslint-config-typescript": "^0.0.0" }, "peerDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", From dbc496d78ca4b282ca812de3915311540a2cbaa1 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:44:56 +0800 Subject: [PATCH 32/33] chore: address PR review (drop decisions tests, keep bug guards; CI on latest node) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/workflows/ci.yml | 2 +- README.md | 2 +- .../eslint-config-base/test/decisions.test.js | 87 ------------------- .../eslint-config-base/test/defaults.test.js | 26 +++++- .../test/decisions.test.js | 11 --- .../test/fixtures.test.js | 8 +- .../test/decisions.test.js | 35 -------- .../eslint-config-react/test/fixtures.test.js | 16 +++- .../test/decisions.test.js | 46 ---------- .../test/defaults.test.js | 20 ++++- 10 files changed, 68 insertions(+), 185 deletions(-) delete mode 100644 packages/eslint-config-base/test/decisions.test.js delete mode 100644 packages/eslint-config-react-typescript/test/decisions.test.js delete mode 100644 packages/eslint-config-react/test/decisions.test.js delete mode 100644 packages/eslint-config-typescript/test/decisions.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e8d84a..b1167b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node: [22.13.0, 24, 26] + node: [22.13.0, 24, 26, latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/README.md b/README.md index 0789eda..aff1e2f 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,4 @@ npx changeset # describe a change for the next release Releases are cut by the changesets GitHub Action on `main`. -CI runs the tests on Node 22.13, 24 and 26, again with every peer dependency at its minimum version, and the pack smoke test. +CI runs the tests on Node 22.13, 24, 26 and the latest release, again with every peer dependency at its minimum version, and the pack smoke test. diff --git a/packages/eslint-config-base/test/decisions.test.js b/packages/eslint-config-base/test/decisions.test.js deleted file mode 100644 index d2ad13f..0000000 --- a/packages/eslint-config-base/test/decisions.test.js +++ /dev/null @@ -1,87 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import test from 'node:test'; - -import { fixtureProject, lintText, loadedRules, ruleIds, warnRules } from '../../../test-utils/lint.js'; -import { anyTestGlobs } from '../src/globs.js'; -import config from '../src/index.js'; -import nodeConfig from '../src/node.js'; - -const root = fixtureProject; -const lint = async (code, file = 'src/a.js', cfg = config) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); - -// [rule, code that must be flagged, code that must pass] -/* eslint-disable no-template-curly-in-string -- fixture code is written as plain strings */ -const cases = [ - ['eqeqeq', 'export const a = (x, y) => x == y;\n', 'export const a = (x) => x == null;\n'], - ['no-param-reassign', 'export function f(a) {\n a = 1;\n\n return a;\n}\n', 'export function f(a) {\n a.x = 1;\n}\n'], - ['consistent-return', 'export function f(a) {\n if (a) {\n return 1;\n }\n}\n', 'export function f(a) {\n if (a) {\n return 1;\n }\n\n return 2;\n}\n'], - ['no-shadow', 'const a = 1;\n\nexport function f() {\n const a = 2;\n\n return a;\n}\n\nexport { a };\n', 'const a = 1;\n\nexport function f() {\n const b = 2;\n\n return b;\n}\n\nexport { a };\n'], - ['prefer-template', "export const s = (b) => 'a' + b;\n", 'export const s = (b) => `a${b}`;\n'], - ['no-nested-ternary', 'export const s = (a, b) => (a ? 1 : b ? 2 : 3);\n', 'export const s = (a) => (a ? 1 : 2);\n'], - ['no-else-return', 'export function f(a) {\n if (a) {\n return 1;\n } else {\n return 2;\n }\n}\n', 'export function f(a) {\n if (a) {\n return 1;\n }\n\n return 2;\n}\n'], - ['curly', 'export function f(a) {\n if (a)\n return 1;\n\n return 2;\n}\n', 'export function f(a) {\n if (a) return 1;\n\n return 2;\n}\n'], - ['no-use-before-define', 'export const r = f();\n\nfunction f() {\n return 1;\n}\n', 'function f() {\n return 1;\n}\n\nexport const r = f();\n'], - ['default-param-last', 'export function f(a = 1, b) {\n return [a, b];\n}\n', 'export function f(b, a = 1) {\n return [a, b];\n}\n'], - ['no-await-in-loop', 'export async function f(xs) {\n for (const x of xs) {\n await x;\n }\n}\n', 'export async function f(xs) {\n await Promise.all(xs);\n}\n'], - ['no-unused-vars', 'export function f(a) {\n return 1;\n}\n', 'export function f(_a) {\n return 1;\n}\n'], - ['no-unused-vars', 'export function f() {\n const x = 1;\n}\n', 'export function f() {\n const _x = 1;\n}\n'], - ['no-console', 'console.log(1);\n', 'console.warn(1);\nconsole.error(2);\n'], -]; -/* eslint-enable no-template-curly-in-string -- end of fixture table */ - -for (const [rule, bad, good] of cases) { - test(`${rule}: flagged / accepted`, async () => { - assert.ok((await lint(bad)).includes(rule), `${rule} should flag`); - assert.ok(!(await lint(good)).includes(rule), `${rule} should accept`); - }); -} - -test('no-plusplus is not enabled; hapi plugin is gone', () => { - const rules = loadedRules(config); - - assert.ok(!rules.has('no-plusplus')); - assert.deepEqual([...rules].filter((id) => id.startsWith('@hapi/')), []); -}); - -test('functions need no blank line at the start of the body (Prettier-safe)', async () => { - assert.ok(!(await lint('export function f() {\n return 1;\n}\n')).includes('@stylistic/padding-line-between-statements')); -}); - -test('import-x named-as-default rules are off', () => { - const rules = loadedRules(config); - - assert.ok(!rules.has('import-x/no-named-as-default') && !rules.has('import-x/no-named-as-default-member')); -}); - -test('no rule resolves to "warn" in the JS configs', async () => { - assert.deepEqual(await warnRules(config, path.join(root, 'src/a.js'), root), []); - assert.deepEqual(await warnRules(nodeConfig, path.join(root, 'src/a.js'), root), []); -}); - -test('scripts/ files get node globals', async () => { - const found = await lint('process.exit(1);\n', 'scripts/run.mjs'); - - assert.ok(!found.includes('no-undef'), 'process should be defined in scripts/'); - assert.ok((await lint('process.exit(1);\n', 'src/run.mjs')).includes('no-undef')); -}); - -test('test-file globs also match from a monorepo root config', () => { - for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']) { - assert.ok(anyTestGlobs.includes(glob) && anyTestGlobs.includes(`{apps,packages}/*/${glob}`), glob); - } -}); - -test('console is allowed in scripts of every extension', async () => { - for (const ext of ['js', 'mjs', 'cjs']) { - const found = await lint('console.log(1);\n', `scripts/x.${ext}`); - - assert.ok(!found.includes('no-console'), ext); - } -}); - -test('explicit index.js imports are not "useless" (ESM has no directory imports)', async () => { - const found = await lint("import { a } from './index.js';\n\nexport const b = a;\n", 'src/b.js'); - - assert.ok(!found.includes('import-x/no-useless-path-segments')); -}); diff --git a/packages/eslint-config-base/test/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js index 6defdcf..288d68a 100644 --- a/packages/eslint-config-base/test/defaults.test.js +++ b/packages/eslint-config-base/test/defaults.test.js @@ -2,7 +2,8 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fixtureProject, lintText } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, warnRules } from '../../../test-utils/lint.js'; +import { anyTestGlobs } from '../src/globs.js'; import jsDefault from '../src/index.js'; import nodeDefault from '../src/node.js'; @@ -29,3 +30,26 @@ test('import-x/first is off in tests (vi.mock hoisting) and on in source', async assert.ok(!(await ruleIdsFor(jsDefault, code, 'src/b.test.js')).includes('import-x/first')); assert.ok((await ruleIdsFor(jsDefault, code, 'src/b.js')).includes('import-x/first')); }); + +test('no rule resolves to "warn": consumers lint with --max-warnings 0', async () => { + assert.deepEqual(await warnRules(jsDefault, path.join(root, 'src/a.js'), root), []); + assert.deepEqual(await warnRules(nodeDefault, path.join(root, 'src/a.js'), root), []); +}); + +test('scripts/ get node globals and may log, in every extension', async () => { + for (const ext of ['js', 'mjs', 'cjs']) { + const found = await ruleIdsFor(jsDefault, 'process.exit(1);\nconsole.log(1);\n', `scripts/x.${ext}`); + + assert.ok(!found.includes('no-undef') && !found.includes('no-console'), ext); + } +}); + +test('explicit index.js imports are not "useless" (ESM has no directory imports)', async () => { + const found = await ruleIdsFor(jsDefault, "import { a } from './index.js';\n\nexport const b = a;\n", 'src/b.js'); + + assert.ok(!found.includes('import-x/no-useless-path-segments')); +}); + +test('test-file globs also match from a monorepo root config', () => { + for (const glob of ['test/**', '**/__tests__/**']) assert.ok(anyTestGlobs.includes(glob) && anyTestGlobs.includes(`{apps,packages}/*/${glob}`), glob); +}); diff --git a/packages/eslint-config-react-typescript/test/decisions.test.js b/packages/eslint-config-react-typescript/test/decisions.test.js deleted file mode 100644 index b3910b2..0000000 --- a/packages/eslint-config-react-typescript/test/decisions.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import test from 'node:test'; - -import { warnRules } from '../../../test-utils/lint.js'; - -import { root, tsConfig } from './helpers.js'; - -test('no rule resolves to "warn" in the React TypeScript config', async () => { - assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/component.tsx'), root), []); -}); diff --git a/packages/eslint-config-react-typescript/test/fixtures.test.js b/packages/eslint-config-react-typescript/test/fixtures.test.js index eaadc8c..ffa9b9d 100644 --- a/packages/eslint-config-react-typescript/test/fixtures.test.js +++ b/packages/eslint-config-react-typescript/test/fixtures.test.js @@ -1,9 +1,11 @@ import assert from 'node:assert/strict'; +import path from 'node:path'; import test from 'node:test'; +import { warnRules } from '../../../test-utils/lint.js'; import { createReactTypescriptConfig } from '../src/index.js'; -import { ids, tsConfig } from './helpers.js'; +import { ids, root, tsConfig } from './helpers.js'; const message = 'declare const intl: { formatMessage: (d: { id: string }) => string };\n\nexport const m = intl.formatMessage({ id: "a" });\n'; @@ -20,3 +22,7 @@ test('two calls to createReactTypescriptConfig return independent arrays', () => a.length = 0; assert.ok(b.length > 0); }); + +test('no rule resolves to "warn": consumers lint with --max-warnings 0', async () => { + assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/component.tsx'), root), []); +}); diff --git a/packages/eslint-config-react/test/decisions.test.js b/packages/eslint-config-react/test/decisions.test.js deleted file mode 100644 index 7d04027..0000000 --- a/packages/eslint-config-react/test/decisions.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import test from 'node:test'; - -import { ruleSeverity, warnRules } from '../../../test-utils/lint.js'; - -import { ids, jsConfig, root } from './helpers.js'; - -test('no rule resolves to "warn" in the React config', async () => { - assert.deepEqual(await warnRules(jsConfig, path.join(root, 'src/a.jsx'), root), []); -}); - -test('hooks exhaustive-deps, refresh and formatjs literal-string rules are errors', async () => { - for (const id of ['react-hooks/exhaustive-deps', 'react-refresh/only-export-components', 'formatjs/no-literal-string-in-jsx']) { - assert.equal(await ruleSeverity(jsConfig, id, path.join(root, 'src/a.jsx')), 2, id); - } -}); - -test('no-unused-expressions is strict in React code (no short-circuit statements)', async () => { - const code = 'export function f(a, g) {\n a && g();\n}\n'; - - assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('no-unused-expressions')); -}); - -test('formatjs: enforce-description is off, no-camel-case is an error', async () => { - assert.equal(await ruleSeverity(jsConfig, 'formatjs/enforce-description', path.join(root, 'src/a.jsx')), 0); - assert.equal(await ruleSeverity(jsConfig, 'formatjs/no-camel-case', path.join(root, 'src/a.jsx')), 2); -}); - -test('formatjs exempts the same test files as the base config', async () => { - const sev = (file) => ruleSeverity(jsConfig, 'formatjs/enforce-default-message', path.join(root, file), root); - - assert.equal(await sev('src/a.jsx'), 2); - for (const file of ['src/a.test.jsx', 'test/helpers.jsx', 'src/__tests__/a.jsx']) assert.notEqual(await sev(file), 2, file); -}); diff --git a/packages/eslint-config-react/test/fixtures.test.js b/packages/eslint-config-react/test/fixtures.test.js index ba62e05..810fa23 100644 --- a/packages/eslint-config-react/test/fixtures.test.js +++ b/packages/eslint-config-react/test/fixtures.test.js @@ -1,7 +1,10 @@ import assert from 'node:assert/strict'; +import path from 'node:path'; import test from 'node:test'; -import { ids, jsConfig } from './helpers.js'; +import { ruleSeverity, warnRules } from '../../../test-utils/lint.js'; + +import { ids, jsConfig, root } from './helpers.js'; const hook = 'function useThing() {\n return 1;\n}\n\nexport function A({ c }) {\n if (c) {\n useThing();\n }\n\n return null;\n}\n'; @@ -18,3 +21,14 @@ test('react-refresh flags a non-component export next to a component', async () assert.ok((await ids(jsConfig, code, 'src/a.jsx')).includes('react-refresh/only-export-components')); }); + +test('no rule resolves to "warn": consumers lint with --max-warnings 0', async () => { + assert.deepEqual(await warnRules(jsConfig, path.join(root, 'src/a.jsx'), root), []); +}); + +test('formatjs exempts test files', async () => { + const severity = (file) => ruleSeverity(jsConfig, 'formatjs/enforce-default-message', path.join(root, file), root); + + assert.equal(await severity('src/a.jsx'), 2); + for (const file of ['src/a.test.jsx', 'test/helpers.jsx', 'src/__tests__/a.jsx']) assert.notEqual(await severity(file), 2, file); +}); diff --git a/packages/eslint-config-typescript/test/decisions.test.js b/packages/eslint-config-typescript/test/decisions.test.js deleted file mode 100644 index 97999d0..0000000 --- a/packages/eslint-config-typescript/test/decisions.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import test from 'node:test'; - -import { fixtureProject, lintText, ruleIds, ruleSeverity, warnRules } from '../../../test-utils/lint.js'; -import { testGlobs } from '../src/globs.js'; -import { createTypescriptConfig } from '../src/index.js'; -import { createNodeConfig } from '../src/node.js'; - -const root = fixtureProject; -const tsOptions = { tsconfigPath: [path.join(root, 'tsconfig.json')], tsconfigRootDir: root }; -const tsConfig = createTypescriptConfig(tsOptions); -const lint = async (code, file, cfg = tsConfig) => ruleIds(await lintText(cfg, code, { filePath: path.join(root, file), cwd: root })); - -test('no rule resolves to "warn" in the TypeScript configs', async () => { - assert.deepEqual(await warnRules(tsConfig, path.join(root, 'src/a.ts'), root), []); - assert.deepEqual(await warnRules(createNodeConfig(tsOptions), path.join(root, 'src/a.ts'), root), []); -}); - -test('TS config uses the typescript-eslint variants and drops consistent-return', async () => { - const sev = (id) => ruleSeverity(tsConfig, id, path.join(root, 'src/a.ts'), root); - - for (const id of ['@typescript-eslint/no-shadow', '@typescript-eslint/no-use-before-define', '@typescript-eslint/default-param-last', '@typescript-eslint/no-explicit-any']) assert.equal(await sev(id), 2, id); - for (const id of ['no-shadow', 'no-use-before-define', 'default-param-last', 'consistent-return']) assert.equal(await sev(id), 0, id); -}); - -test('scripts/ files get node globals', async () => { - const found = await lint('process.exit(1);\n', 'scripts/run.mjs'); - - assert.ok(!found.includes('no-undef'), 'process should be defined in scripts/'); - assert.ok((await lint('process.exit(1);\n', 'src/run.mjs')).includes('no-undef')); -}); - -test('test-file globs also match from a monorepo root config', () => { - for (const glob of ['test/**', '**/__tests__/**', '**/*.{test,spec}.{ts,tsx,mts,cts}']) { - assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); - } -}); - -test('console is allowed in scripts of every extension', async () => { - for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { - const found = await lint('console.log(1);\n', `scripts/x.${ext}`); - - assert.ok(!found.includes('no-console'), ext); - } -}); diff --git a/packages/eslint-config-typescript/test/defaults.test.js b/packages/eslint-config-typescript/test/defaults.test.js index 4ebb61b..4775d3b 100644 --- a/packages/eslint-config-typescript/test/defaults.test.js +++ b/packages/eslint-config-typescript/test/defaults.test.js @@ -4,7 +4,8 @@ import test from 'node:test'; import jsDefault from '@cospired/eslint-config-base'; -import { fixtureProject, lintText } from '../../../test-utils/lint.js'; +import { fixtureProject, lintText, warnRules } from '../../../test-utils/lint.js'; +import { testGlobs } from '../src/globs.js'; import tsDefault, { createTypescriptConfig } from '../src/index.js'; import nodeDefault from '../src/node.js'; @@ -61,3 +62,20 @@ test('import-x/first is off in tests (vi.mock hoisting) and on in source', async assert.ok(!(await ruleIdsFor(tsDefault, code, 'other/c.test.ts')).includes('import-x/first')); assert.ok((await ruleIdsFor(tsDefault, code, 'src/b.ts')).includes('import-x/first')); }); + +test('no rule resolves to "warn": consumers lint with --max-warnings 0', async () => { + assert.deepEqual(await warnRules(tsDefault, path.join(root, 'src/a.ts'), root), []); + assert.deepEqual(await warnRules(nodeDefault, path.join(root, 'src/a.ts'), root), []); +}); + +test('scripts/ get node globals and may log, in every extension', async () => { + for (const ext of ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts']) { + const found = await ruleIdsFor(nodeDefault, 'process.exit(1);\nconsole.log(1);\n', `scripts/x.${ext}`); + + assert.ok(!found.includes('no-undef') && !found.includes('no-console'), ext); + } +}); + +test('test-file globs also match from a monorepo root config', () => { + for (const glob of ['test/**', '**/__tests__/**']) assert.ok(testGlobs.includes(glob) && testGlobs.includes(`{apps,packages}/*/${glob}`), glob); +}); From 877e88c94654f11b48fbd78670de2467c2d3b6c8 Mon Sep 17 00:00:00 2001 From: Dirk Haage Date: Tue, 10 Nov 2026 18:53:46 +0800 Subject: [PATCH 33/33] fix: node entry parses ESM and CJS (no module-type detection), pin actions by SHA, lint own config, drop unused exports Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HQAi8TFF159S4oBK6WA9Th --- .github/dependabot.yml | 7 +++ .github/workflows/ci.yml | 16 +++---- .github/workflows/release.yml | 6 +-- eslint.config.js | 2 +- package-lock.json | 1 + package.json | 1 + packages/eslint-config-base/README.md | 4 +- packages/eslint-config-base/src/ignores.js | 2 - packages/eslint-config-base/src/index.js | 1 - packages/eslint-config-base/src/node.js | 43 ++----------------- packages/eslint-config-base/test/node.test.js | 25 ++++------- .../eslint-config-typescript/src/index.js | 2 +- packages/eslint-config-typescript/src/node.js | 2 +- test-utils/fixtures/README.md | 2 - test-utils/fixtures/cjs-project/package.json | 1 - 15 files changed, 37 insertions(+), 78 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 test-utils/fixtures/cjs-project/package.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5c9fc88 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Keeps the SHA-pinned actions in .github/workflows up to date. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1167b4..f101a95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: matrix: node: [22.13.0, 24, 26, latest] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ matrix.node }} cache: npm @@ -30,8 +30,8 @@ jobs: name: peer dependency floors runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm @@ -43,8 +43,8 @@ jobs: name: packed tarballs work in a clean project runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm @@ -55,8 +55,8 @@ jobs: name: publish dry run (pending changesets applied) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74928da..a83fd0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,8 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm @@ -26,7 +26,7 @@ jobs: - run: npm ci - run: npm test - run: npm run lint - - uses: changesets/action@v1 + - uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 with: version: npm run version publish: npm run release diff --git a/eslint.config.js b/eslint.config.js index 802d3df..441864c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,4 @@ -import nodeJs from './packages/eslint-config-base/src/node.js'; +import nodeJs from '@cospired/eslint-config-base/node'; // The repo lints itself with the config it ships. export default [ diff --git a/package-lock.json b/package-lock.json index e3a4e18..8ff4f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ ], "devDependencies": { "@changesets/cli": "^3.0.3", + "@cospired/eslint-config-base": "*", "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.10.0", diff --git a/package.json b/package.json index fe0bba5..c72b6b2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@changesets/cli": "^3.0.3", + "@cospired/eslint-config-base": "*", "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1", "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.10.0", diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index c4b68ea..a951744 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -5,7 +5,7 @@ Slightly opinionated ESLint flat config for JavaScript. Requires ESLint 10 and N | Entry | For | |---|---| | `@cospired/eslint-config-base` | JavaScript (environment neutral) | -| `@cospired/eslint-config-base/node` | Node.js JavaScript (Node globals, CommonJS or ESM from your `package.json` `type`) | +| `@cospired/eslint-config-base/node` | Node.js JavaScript (Node globals; CommonJS and ESM) | TypeScript: [`@cospired/eslint-config-typescript`](../eslint-config-typescript). React: [`@cospired/eslint-config-react`](../eslint-config-react) and [`-react-typescript`](../eslint-config-react-typescript). @@ -23,7 +23,7 @@ No options needed. `eslint.config.js`: export { default } from '@cospired/eslint-config-base/node'; // or the package root for environment-neutral JavaScript ``` -The defaults ignore build output (`dist`, `build`, `coverage`, ...). `createNodeJsConfig({ cwd })` (from `/node`) takes the directory whose `package.json` decides CommonJS vs ESM. +The defaults ignore build output (`dist`, `build`, `coverage`, ...). Add your own rules after the config: diff --git a/packages/eslint-config-base/src/ignores.js b/packages/eslint-config-base/src/ignores.js index 2153462..b826e94 100644 --- a/packages/eslint-config-base/src/ignores.js +++ b/packages/eslint-config-base/src/ignores.js @@ -12,7 +12,5 @@ export const globalIgnores = { '**/*.log', '**/.cache/**', '**/.turbo/**', - 'eslint.config.js', - 'eslint-*.config.js', ], }; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js index 7d16769..eca124d 100644 --- a/packages/eslint-config-base/src/index.js +++ b/packages/eslint-config-base/src/index.js @@ -11,7 +11,6 @@ import todoPlz from './rules/todo-plz.js'; export { anyTestGlobs, withMonorepoTwins } from './globs.js'; export { globalIgnores } from './ignores.js'; -export { createImportsConfig, defaultDevDependencyGlobs } from './rules/import-x.js'; /** * eslint-config-prettier disables `curly`, so it must be re-enabled after it. Every config ends with these. diff --git a/packages/eslint-config-base/src/node.js b/packages/eslint-config-base/src/node.js index 82ae327..b0aefeb 100644 --- a/packages/eslint-config-base/src/node.js +++ b/packages/eslint-config-base/src/node.js @@ -1,48 +1,11 @@ -import fs from 'node:fs'; -import path from 'node:path'; - import globals from 'globals'; import baseConfig, { globalIgnores } from './index.js'; export { globalIgnores }; -/** `module` when the nearest package.json at or above `cwd` says `"type": "module"`, otherwise `commonjs`. */ -function detectSourceType(cwd) { - for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) { - const file = path.join(dir, 'package.json'); - - if (fs.existsSync(file)) { - try { - return JSON.parse(fs.readFileSync(file, 'utf8')).type === 'module' ? 'module' : 'commonjs'; - } catch { - return 'commonjs'; - } - } - if (path.dirname(dir) === dir) return 'commonjs'; - } -} - /** - * Node.js JavaScript config: base JS rules, Node globals, and `.js` files parsed as CommonJS or ESM depending on - * the package's `"type"`. `.cjs`/`.mjs` keep ESLint's own defaults. - * - * @param {{ cwd?: string }} [options] Directory whose package.json decides the module type; defaults to the working directory. - * @returns {import('eslint').Linter.Config[]} + * Node.js JavaScript config: base JS rules plus Node globals, for CommonJS and ESM alike. `.js` files are parsed as + * ESM (ESLint's default), which also accepts CommonJS code; `require`/`module` stay defined through the globals. */ -export function createNodeJsConfig({ cwd = process.cwd() } = {}) { - const sourceType = detectSourceType(cwd); - - return [ - ...baseConfig, - { - name: 'cospired/node-js', - // CommonJS wrappers (require, module, __dirname) are only defined in CommonJS packages. - languageOptions: { globals: sourceType === 'module' ? globals.nodeBuiltin : globals.node }, - }, - { name: 'cospired/node-js-source-type', files: ['**/*.js'], languageOptions: { sourceType } }, - ]; -} - -/** Node JavaScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node-js'`. */ -export default createNodeJsConfig(); +export default [...baseConfig, { name: 'cospired/node-js', languageOptions: { globals: globals.node } }]; diff --git a/packages/eslint-config-base/test/node.test.js b/packages/eslint-config-base/test/node.test.js index 4869002..292e7da 100644 --- a/packages/eslint-config-base/test/node.test.js +++ b/packages/eslint-config-base/test/node.test.js @@ -2,28 +2,21 @@ import assert from 'node:assert/strict'; import path from 'node:path'; import test from 'node:test'; -import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; -import nodeJsDefault, { createNodeJsConfig } from '../src/node.js'; +import { fixtureProject, lintText } from '../../../test-utils/lint.js'; +import nodeJsDefault from '../src/node.js'; -const cjs = path.join(path.dirname(fixtureProject), 'cjs-project'); -const esm = fixtureProject; -const cjsCode = "const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"; -const ids = async (cwd, code, file = 'src/a.js') => ruleIds(await lintText(createNodeJsConfig({ cwd }), code, { filePath: path.join(cwd, file), cwd })); +const lint = (code, file = 'src/a.js') => lintText(nodeJsDefault, code, { filePath: path.join(fixtureProject, file), cwd: fixtureProject }); -test('CommonJS package (no "type"): require/module/process are defined', async () => { - const found = await ids(cjs, cjsCode); +test('CommonJS code parses, and require/module/process are defined', async () => { + const messages = await lint("const fs = require('node:fs');\n\nmodule.exports = { fs, cwd: process.cwd() };\n"); - assert.ok(!found.includes('no-undef'), found.join()); + assert.deepEqual(messages.filter((m) => m.fatal || m.ruleId === 'no-undef'), []); }); -test('ESM package ("type": "module"): CommonJS globals are not defined', async () => { - assert.ok((await ids(esm, cjsCode)).includes('no-undef')); -}); - -test('node globals exist either way', async () => { - const code = 'export const c = process.cwd();\n'; +test('ESM code parses in .js, whatever the surrounding package.json says (monorepo root without "type")', async () => { + const messages = await lint("import { cwd } from 'node:process';\n\nexport const here = cwd();\n"); - assert.ok(!(await ids(esm, code)).includes('no-undef')); + assert.deepEqual(messages.filter((m) => m.fatal), []); }); test('default export is a ready-made config array with the global ignores', () => { diff --git a/packages/eslint-config-typescript/src/index.js b/packages/eslint-config-typescript/src/index.js index bc26c05..7b08281 100644 --- a/packages/eslint-config-typescript/src/index.js +++ b/packages/eslint-config-typescript/src/index.js @@ -50,5 +50,5 @@ export function createTypescriptConfig({ ]; } -/** TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/typescript'`. */ +/** TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-typescript'`. */ export default createTypescriptConfig(); diff --git a/packages/eslint-config-typescript/src/node.js b/packages/eslint-config-typescript/src/node.js index 9c7ac1b..cddc9bf 100644 --- a/packages/eslint-config-typescript/src/node.js +++ b/packages/eslint-config-typescript/src/node.js @@ -32,5 +32,5 @@ export function createNodeConfig(options = {}) { ]; } -/** Node TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-base/node'`. */ +/** Node TypeScript config with defaults, ready to use as `export { default } from '@cospired/eslint-config-typescript/node'`. */ export default createNodeConfig(); diff --git a/test-utils/fixtures/README.md b/test-utils/fixtures/README.md index 9cac4ee..5136946 100644 --- a/test-utils/fixtures/README.md +++ b/test-utils/fixtures/README.md @@ -12,5 +12,3 @@ The config tests lint code with the real ESLint and typescript-eslint, so they n | `src/a.test.tsx`, `test/a.test.ts` | Test files, inside a tsconfig (`src`, `test`). | | `other/c.test.ts` | A test file outside every tsconfig: must lint without a parsing error. | | `tool.config.ts`, `tool.shared.ts` | TypeScript files at the project root outside every tsconfig (bundler, test runner or ORM configs): same. | - -`cjs-project` is a `package.json` without `"type"`, to test CommonJS detection. diff --git a/test-utils/fixtures/cjs-project/package.json b/test-utils/fixtures/cjs-project/package.json deleted file mode 100644 index bd135cf..0000000 --- a/test-utils/fixtures/cjs-project/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "cjs-fixture", "private": true }