diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..b8e45ac --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "access": "public", + "baseBranch": "main", + "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 new file mode 100644 index 0000000..eea9667 --- /dev/null +++ b/.changeset/flat-config-rewrite.md @@ -0,0 +1,21 @@ +--- +'@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, 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/.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 new file mode 100644 index 0000000..f101a95 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + 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: [22.13.0, 24, 26, latest] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + 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@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + 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@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a83fd0b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + id-token: write # npm provenance + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org + - run: npm ci + - run: npm test + - run: npm run lint + - uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + version: npm run version + publish: npm run release + title: 'chore: version packages' + commit: 'chore: version packages' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: 'true' 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/.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/.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/README.md b/README.md index 17f8a92..aff1e2f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,28 @@ -[![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 | Entries | For | +|---|---|---| +| [`@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 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 and the test helpers +npm run lint # the repo lints itself with its own config +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 +``` + +Releases are cut by the changesets GitHub Action on `main`. + +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/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. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..441864c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import nodeJs from '@cospired/eslint-config-base/node'; + +// The repo lints itself with the config it ships. +export default [ + ...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-lock.json b/package-lock.json new file mode 100644 index 0000000..8ff4f9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2989 @@ +{ + "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", + "@cospired/eslint-config-base": "*", + "@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", + "espree": "^11.2.0", + "globals": "^17.14.0", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.72.2" + }, + "engines": { + "node": "^22.13.0 || >=24" + } + }, + "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/@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", + "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/@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", + "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/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", + "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/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": "^20.19.0 || ^22.13.0 || >=24" + }, + "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": "^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-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0" + } + }, + "packages/eslint-config-react": { + "name": "@cospired/eslint-config-react", + "version": "7.0.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2" + }, + "engines": { + "node": "^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-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": "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": "^0.0.0" + }, + "engines": { + "node": "^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": "0.0.0", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2" + }, + "engines": { + "node": "^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/package.json b/package.json index 56cc60e..c72b6b2 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,24 @@ { "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 && node --test test-utils/*.test.js", + "lint": "eslint .", + "changeset": "changeset", + "release": "changeset publish", + "smoke": "node scripts/smoke-pack.js", + "version": "changeset version && npm install --package-lock-only --ignore-scripts" }, "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 +36,29 @@ "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.0 || >=24" + }, + "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", + "@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", + "espree": "^11.2.0", + "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/README.md b/packages/eslint-config-base/README.md index ef16f54..a951744 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. Requires ESLint 10 and Node `^22.13 || >=24`. -1. Install the correct versions of each package, which are listed by the command: +| Entry | For | +|---|---| +| `@cospired/eslint-config-base` | JavaScript (environment neutral) | +| `@cospired/eslint-config-base/node` | Node.js JavaScript (Node globals; CommonJS and ESM) | - ```sh - npm info "@cospired/eslint-config-base@latest" peerDependencies - ``` +TypeScript: [`@cospired/eslint-config-typescript`](../eslint-config-typescript). React: [`@cospired/eslint-config-react`](../eslint-config-react) and [`-react-typescript`](../eslint-config-react-typescript). - If using **npm 5+**, use this shortcut +## Install - ```sh - npx install-peerdeps --dev @cospired/eslint-config-base - ``` +```sh +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 +``` - 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@^#.#.# - ``` - - 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-base - ``` - - The cli will produce and run a command like: +## Usage - ```sh - npm install --save-dev @cospired/eslint-config-base eslint@^#.#.# eslint-plugin-import@^#.#.# - ``` +No options needed. `eslint.config.js`: -2. Add `"extends": "@cospired/eslint-config-base"` to your .eslintrc. +```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`, ...). -## Improving this config +Add your own rules after the config: -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 +import config from '@cospired/eslint-config-base/node'; -You can run tests with `npm test`. +export default [ + ...config, + { + 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.' }] }], + }, + }, +]; +``` -You can make sure this module lints with itself using `npm run lint`. +## What it enforces -# Acknowledgment +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. -This package is based on the [airbnb](https://github.com/airbnb/javascript) javascript styleguide and eslint packages. +- `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. 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..922bdc4 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -1,71 +1,52 @@ { "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", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "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" }, - "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", + "cospired" ], - "license": "BSD-3-Clause-Clear", - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.0" + "engines": { + "node": "^22.13.0 || >=24" + }, + "exports": { + ".": "./src/index.js", + "./node": "./src/node.js", + "./package.json": "./package.json" }, - "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" + "scripts": { + "test": "node --test test/*.test.js" }, "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-plugin-import-x": "^4.18.1", + "eslint-plugin-todo-plz": "^1.4.1", + "globals": "^17.14.0" + }, + "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/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/globs.js b/packages/eslint-config-base/src/globs.js new file mode 100644 index 0000000..cd33753 --- /dev/null +++ b/packages/eslint-config-base/src/globs.js @@ -0,0 +1,5 @@ +/** 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}`]); + +/** 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/ignores.js b/packages/eslint-config-base/src/ignores.js new file mode 100644 index 0000000..b826e94 --- /dev/null +++ b/packages/eslint-config-base/src/ignores.js @@ -0,0 +1,16 @@ +/** 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/**', + ], +}; diff --git a/packages/eslint-config-base/src/index.js b/packages/eslint-config-base/src/index.js new file mode 100644 index 0000000..eca124d --- /dev/null +++ b/packages/eslint-config-base/src/index.js @@ -0,0 +1,40 @@ +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +import { anyTestGlobs, withMonorepoTwins } from './globs.js'; +import { globalIgnores } from './ignores.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'; + +export { anyTestGlobs, withMonorepoTwins } from './globs.js'; +export { globalIgnores } from './ignores.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 [ + ...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. + { files: withMonorepoTwins(['scripts/**/*.{js,mjs,cjs,ts,mts,cts}']), languageOptions: { globals: globals.node }, rules: { 'no-console': 'off' } }, + ...prettierLast, + ]; +} + +/** 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 new file mode 100644 index 0000000..b0aefeb --- /dev/null +++ b/packages/eslint-config-base/src/node.js @@ -0,0 +1,11 @@ +import globals from 'globals'; + +import baseConfig, { globalIgnores } from './index.js'; + +export { globalIgnores }; + +/** + * 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 default [...baseConfig, { name: 'cospired/node-js', languageOptions: { globals: globals.node } }]; diff --git a/packages/eslint-config-base/src/rules/eslint-comments.js b/packages/eslint-config-base/src/rules/eslint-comments.js new file mode 100644 index 0000000..d4aec0a --- /dev/null +++ b/packages/eslint-config-base/src/rules/eslint-comments.js @@ -0,0 +1,33 @@ +import { recommended } from '@eslint-community/eslint-plugin-eslint-comments/configs'; + +/** Directive comments stay narrow, named and explained. */ +export default { + name: 'cospired/eslint-comments', + plugins: recommended.plugins, + 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 new file mode 100644 index 0000000..5d1ac45 --- /dev/null +++ b/packages/eslint-config-base/src/rules/eslint.js @@ -0,0 +1,972 @@ +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: { + /* + 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', + + /* + 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', + + /* + 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', + + /* + 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 new file mode 100644 index 0000000..934e7df --- /dev/null +++ b/packages/eslint-config-base/src/rules/import-x.js @@ -0,0 +1,359 @@ +import importX, { flatConfigs } from 'eslint-plugin-import-x'; + +import { 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, + /* + 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', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + ...(pathGroups && { pathGroups, pathGroupsExcludedImportTypes: ['builtin'] }), + }, + ], + + /* + 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', + + /* + 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 new file mode 100644 index 0000000..cbaf3d3 --- /dev/null +++ b/packages/eslint-config-base/src/rules/stylistic.js @@ -0,0 +1,104 @@ +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: { + /* + 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 new file mode 100644 index 0000000..c852c27 --- /dev/null +++ b/packages/eslint-config-base/src/rules/todo-plz.js @@ -0,0 +1,17 @@ +import todoPlz from 'eslint-plugin-todo-plz'; + +/** TODOs need a ticket: `TODO(#123): ...`. */ +export default { + name: 'cospired/todo-plz', + plugins: { 'todo-plz': todoPlz }, + 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-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/completeness.test.js b/packages/eslint-config-base/test/completeness.test.js new file mode 100644 index 0000000..3e6ae9a --- /dev/null +++ b/packages/eslint-config-base/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 { baseNamespaces, pluginRules } from '../../../test-utils/plugins.js'; +import jsConfig from '../src/index.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], + ['node', nodeJs, baseNamespaces], +]; + +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/defaults.test.js b/packages/eslint-config-base/test/defaults.test.js new file mode 100644 index 0000000..288d68a --- /dev/null +++ b/packages/eslint-config-base/test/defaults.test.js @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; + +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'; + +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))]; + +test('default exports are ready-made config arrays that include the global ignores', () => { + for (const cfg of [jsDefault, nodeDefault]) { + assert.ok(Array.isArray(cfg)); + assert.ok(cfg.some((item) => item.ignores?.includes('**/dist/**'))); + } +}); + +test('zero options: dist output is ignored', async () => { + 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'); +}); + +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(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-base/test/fixtures.test.js b/packages/eslint-config-base/test/fixtures.test.js new file mode 100644 index 0000000..2191f6c --- /dev/null +++ b/packages/eslint-config-base/test/fixtures.test.js @@ -0,0 +1,41 @@ +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..d50eef1 --- /dev/null +++ b/packages/eslint-config-base/test/load.test.js @@ -0,0 +1,11 @@ +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/node.test.js b/packages/eslint-config-base/test/node.test.js new file mode 100644 index 0000000..292e7da --- /dev/null +++ b/packages/eslint-config-base/test/node.test.js @@ -0,0 +1,25 @@ +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 nodeJsDefault from '../src/node.js'; + +const lint = (code, file = 'src/a.js') => lintText(nodeJsDefault, code, { filePath: path.join(fixtureProject, file), cwd: fixtureProject }); + +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.deepEqual(messages.filter((m) => m.fatal || m.ruleId === 'no-undef'), []); +}); + +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.deepEqual(messages.filter((m) => m.fatal), []); +}); + +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/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/rules-exist.test.js b/packages/eslint-config-base/test/rules-exist.test.js new file mode 100644 index 0000000..4fdc671 --- /dev/null +++ b/packages/eslint-config-base/test/rules-exist.test.js @@ -0,0 +1,15 @@ +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-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..6218ac8 --- /dev/null +++ b/packages/eslint-config-react-typescript/package.json @@ -0,0 +1,62 @@ +{ + "name": "@cospired/eslint-config-react-typescript", + "version": "0.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": "^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": "^0.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-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" + }, + "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/fixtures.test.js b/packages/eslint-config-react-typescript/test/fixtures.test.js new file mode 100644 index 0000000..ffa9b9d --- /dev/null +++ b/packages/eslint-config-react-typescript/test/fixtures.test.js @@ -0,0 +1,28 @@ +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, root, 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/component.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); +}); + +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-typescript/test/helpers.js b/packages/eslint-config-react-typescript/test/helpers.js new file mode 100644 index 0000000..b504814 --- /dev/null +++ b/packages/eslint-config-react-typescript/test/helpers.js @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; +import { createReactTypescriptConfig } from '../src/index.js'; + +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. */ +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..44ab72e --- /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/component.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/.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/README.md b/packages/eslint-config-react/README.md index 5dddc50..88900d7 100644 --- a/packages/eslint-config-react/README.md +++ b/packages/eslint-config-react/README.md @@ -1,70 +1,21 @@ -[![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 - - ```sh - npx install-peerdeps --dev @cospired/eslint-config-react - ``` +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). - If using **npm < 5**, Linux/OSX users can run +## Install - ```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" - ) - ``` +```sh +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 +``` - 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 - -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? +## Usage -You can run tests with `npm test`. +No options needed. `eslint.config.js`: -You can make sure this module lints with itself using `npm run lint`. +```js +export { default } from '@cospired/eslint-config-react'; +``` -# Acknowledgment +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. -This package is based on the [airbnb](https://github.com/airbnb/javascript) javascript styleguide and eslint packages. +The `/parts` entry exports the React-specific config objects (`reactParts`) for the TypeScript package. 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..0dc2f3a 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -1,82 +1,59 @@ { "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)", + "type": "module", + "files": [ + "src" + ], + "license": "BSD-3-Clause-Clear", + "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" }, - "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": "^22.13.0 || >=24" + }, + "exports": { + ".": "./src/index.js", + "./parts": "./src/parts.js", + "./package.json": "./package.json" + }, + "scripts": { + "test": "node --test test/*.test.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" + "dependencies": { + "@cospired/eslint-config-base": "^7.0.2" }, "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-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" + }, + "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/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/src/index.js b/packages/eslint-config-react/src/index.js new file mode 100644 index 0000000..5867170 --- /dev/null +++ b/packages/eslint-config-react/src/index.js @@ -0,0 +1,8 @@ +import baseConfig, { globalIgnores, prettierLast } from '@cospired/eslint-config-base'; + +import { reactParts } from './parts.js'; + +export { globalIgnores }; + +/** 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/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 new file mode 100644 index 0000000..c2a482d --- /dev/null +++ b/packages/eslint-config-react/src/rules/formatjs.js @@ -0,0 +1,72 @@ +import { anyTestGlobs } from '@cospired/eslint-config-base'; +import formatjs from 'eslint-plugin-formatjs'; + +import { errorOnly } from './errors.js'; + +/** i18n: formatjs `recommended` plus extras. Test files are exempt. Values are placeholders until the rule review. */ +export default { + plugins: formatjs.configs.recommended.plugins, + ignores: anyTestGlobs, + 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 + https://formatjs.github.io/docs/tooling/linter#enforce-message-types + https://formatjs.github.io/docs/tooling/linter#no-invalid-icu + */ + '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. + https://formatjs.github.io/docs/tooling/linter#enforce-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. + 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-hooks.js b/packages/eslint-config-react/src/rules/react-hooks.js new file mode 100644 index 0000000..6fc7c6b --- /dev/null +++ b/packages/eslint-config-react/src/rules/react-hooks.js @@ -0,0 +1,116 @@ +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: { + /* + 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 new file mode 100644 index 0000000..3f0ea8a --- /dev/null +++ b/packages/eslint-config-react/src/rules/react-refresh.js @@ -0,0 +1,17 @@ +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: { + /* + 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/packages/eslint-config-react/src/rules/react.js b/packages/eslint-config-react/src/rules/react.js new file mode 100644 index 0000000..cce1692 --- /dev/null +++ b/packages/eslint-config-react/src/rules/react.js @@ -0,0 +1,11 @@ +import globals from 'globals'; + +/** Browser globals, and JSX in plain JS files. */ +export default [ + // ESLint lints only .js/.mjs/.cjs by default; .jsx must be opted in. + { name: 'cospired/react-files', files: ['**/*.jsx'] }, + { + name: 'cospired/react', + languageOptions: { globals: globals.browser, parserOptions: { ecmaFeatures: { jsx: true } } }, + }, +]; 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/completeness.test.js b/packages/eslint-config-react/test/completeness.test.js new file mode 100644 index 0000000..91b5633 --- /dev/null +++ b/packages/eslint-config-react/test/completeness.test.js @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { redundantRules, unconfiguredRules } from '../../../test-utils/lint.js'; +import { baseNamespaces, pluginRules, reactNamespaces } from '../../../test-utils/plugins.js'; +import jsConfig from '../src/index.js'; + +const namespaces = [...baseNamespaces, ...reactNamespaces]; + +test(`every non-deprecated rule of ${namespaces.map((n) => n || 'eslint').join(', ')} is configured`, () => { + assert.deepEqual(unconfiguredRules(jsConfig, namespaces, pluginRules), []); +}); + +test('our overrides only repeat nothing the presets already set', () => { + assert.deepEqual(redundantRules(jsConfig), []); +}); 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..810fa23 --- /dev/null +++ b/packages/eslint-config-react/test/fixtures.test.js @@ -0,0 +1,34 @@ +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'; + +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'; + +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('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('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-react/test/helpers.js b/packages/eslint-config-react/test/helpers.js new file mode 100644 index 0000000..820cee4 --- /dev/null +++ b/packages/eslint-config-react/test/helpers.js @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { fixtureProject, lintText, ruleIds } from '../../../test-utils/lint.js'; +import jsConfig from '../src/index.js'; + +export const root = fixtureProject; +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..a1c2d07 --- /dev/null +++ b/packages/eslint-config-react/test/load.test.js @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +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'), []); +}); 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(); -}); 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..1eb1ad4 --- /dev/null +++ b/packages/eslint-config-react/test/rules-exist.test.js @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, missingRules } from '../../../test-utils/lint.js'; + +import { jsConfig } from './helpers.js'; + +test('every configured rule exists', () => { + assert.deepEqual(missingRules(jsConfig), []); +}); + +test('React JS config contains no @typescript-eslint rules', () => { + assert.deepEqual([...loadedRules(jsConfig)].filter((id) => id.startsWith('@typescript-eslint/')), []); +}); + +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); +}); 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..e320fe5 --- /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 `^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 (bundler, test runner or ORM configs) 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. diff --git a/packages/eslint-config-typescript/package.json b/packages/eslint-config-typescript/package.json new file mode 100644 index 0000000..5ac7cde --- /dev/null +++ b/packages/eslint-config-typescript/package.json @@ -0,0 +1,58 @@ +{ + "name": "@cospired/eslint-config-typescript", + "version": "0.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": "^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-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" + }, + "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..a657e15 --- /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 + * (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/src/index.js b/packages/eslint-config-typescript/src/index.js new file mode 100644 index 0000000..7b08281 --- /dev/null +++ b/packages/eslint-config-typescript/src/index.js @@ -0,0 +1,54 @@ +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 { importSettings, importXTypescript, importXTypescriptRules } from './rules/import-x-typescript.js'; +import { sourceOnlyRules, typescriptRules } from './rules/typescript-eslint.js'; + +export { globalIgnores }; + +/** + * 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 {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[]} + */ +export function createTypescriptConfig({ + tsconfigPath, + tsconfigRootDir, + files = ['**/*.{ts,tsx,mts,cts}'], + devDependencyGlobs, + importOrder = {}, + typedTests = false, +} = {}) { + const parserOptions = tsconfigPath + ? { project: tsconfigPath, tsconfigRootDir } + : { projectService: true, tsconfigRootDir }; + + return [ + globalIgnores, + ...createBaseConfig({ devDependencyGlobs, pathGroups: importOrder.pathGroups }), + ...defineConfig({ + files, + extends: [tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, importXTypescript], + languageOptions: { parserOptions }, + settings: importSettings(tsconfigPath), + }), + { name: 'cospired/typescript', files, rules: { ...typescriptRules, ...importXTypescriptRules } }, + { name: 'cospired/typescript-eslint', files, ignores: [...testGlobs, ...jsFiles], rules: sourceOnlyRules }, + { name: 'cospired/untyped-js', files: jsFiles, ...tseslint.configs.disableTypeChecked }, + ...(typedTests ? [] : [{ name: 'cospired/untyped', files: untypedGlobs, ...tseslint.configs.disableTypeChecked }]), + ...prettierLast, + ]; +} + +/** 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 new file mode 100644 index 0000000..cddc9bf --- /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-typescript/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..e276e67 --- /dev/null +++ b/packages/eslint-config-typescript/src/rules/import-x-typescript.js @@ -0,0 +1,28 @@ +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 = { + /* + 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) { + 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-typescript/src/rules/typescript-eslint.js b/packages/eslint-config-typescript/src/rules/typescript-eslint.js new file mode 100644 index 0000000..de4c3d8 --- /dev/null +++ b/packages/eslint-config-typescript/src/rules/typescript-eslint.js @@ -0,0 +1,306 @@ +/** + * 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 = { + /* + 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', + + /* + 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', + + /* + 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 }], + +}; 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/defaults.test.js b/packages/eslint-config-typescript/test/defaults.test.js new file mode 100644 index 0000000..4775d3b --- /dev/null +++ b/packages/eslint-config-typescript/test/defaults.test.js @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; + +import jsDefault from '@cospired/eslint-config-base'; + +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'; + +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))]; + +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', 'tool.config.ts', 'tool.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')); +}); + +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); +}); 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..457e088 --- /dev/null +++ b/packages/eslint-config-typescript/test/node.test.js @@ -0,0 +1,49 @@ +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 { createTypescriptConfig } from '../src/index.js'; +import { createNodeConfig } from '../src/node.js'; + +const root = fixtureProject; +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-typescript/test/typescript.test.js b/packages/eslint-config-typescript/test/typescript.test.js new file mode 100644 index 0000000..0753ef8 --- /dev/null +++ b/packages/eslint-config-typescript/test/typescript.test.js @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; + +import { fixtureProject, lintText, ruleIds, ruleSeverity } from '../../../test-utils/lint.js'; +import { createTypescriptConfig } from '../src/index.js'; + +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); +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); +}; + +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'), root), 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('@cospired/eslint-config-base'); + + assert.deepEqual([...loadedRules(js)].filter((id) => id.startsWith('@typescript-eslint/')), []); +}); 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/smoke-pack.js b/scripts/smoke-pack.js new file mode 100644 index 0000000..6a73160 --- /dev/null +++ b/scripts/smoke-pack.js @@ -0,0 +1,58 @@ +// 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 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); +}); + +const peers = {}; + +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'); + +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', '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'], +]; +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 new file mode 100644 index 0000000..d1cf2dd --- /dev/null +++ b/test-utils/completeness.test.js @@ -0,0 +1,25 @@ +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/fixtures/README.md b/test-utils/fixtures/README.md new file mode 100644 index 0000000..5136946 --- /dev/null +++ b/test-utils/fixtures/README.md @@ -0,0 +1,14 @@ +# 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. | diff --git a/test-utils/fixtures/ts-project/other/c.test.ts b/test-utils/fixtures/ts-project/other/c.test.ts new file mode 100644 index 0000000..351d3e8 --- /dev/null +++ b/test-utils/fixtures/ts-project/other/c.test.ts @@ -0,0 +1 @@ +export const c = 1; 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/test-utils/fixtures/ts-project/src/a.test.tsx b/test-utils/fixtures/ts-project/src/a.test.tsx new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/test-utils/fixtures/ts-project/src/a.test.tsx @@ -0,0 +1 @@ +export const a = 1; diff --git a/test-utils/fixtures/ts-project/src/a.ts b/test-utils/fixtures/ts-project/src/a.ts new file mode 100644 index 0000000..73ea5dd --- /dev/null +++ b/test-utils/fixtures/ts-project/src/a.ts @@ -0,0 +1 @@ +export const y = 1; diff --git a/test-utils/fixtures/ts-project/src/b.ts b/test-utils/fixtures/ts-project/src/b.ts new file mode 100644 index 0000000..b85663f --- /dev/null +++ b/test-utils/fixtures/ts-project/src/b.ts @@ -0,0 +1 @@ +export const b = 1; diff --git a/test-utils/fixtures/ts-project/src/component.tsx b/test-utils/fixtures/ts-project/src/component.tsx new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/test-utils/fixtures/ts-project/src/component.tsx @@ -0,0 +1 @@ +export const a = 1; diff --git a/test-utils/fixtures/ts-project/src/shared/x.ts b/test-utils/fixtures/ts-project/src/shared/x.ts new file mode 100644 index 0000000..ad1d380 --- /dev/null +++ b/test-utils/fixtures/ts-project/src/shared/x.ts @@ -0,0 +1 @@ +export const x = 1; diff --git a/test-utils/fixtures/ts-project/test/a.test.ts b/test-utils/fixtures/ts-project/test/a.test.ts new file mode 100644 index 0000000..fe74263 --- /dev/null +++ b/test-utils/fixtures/ts-project/test/a.test.ts @@ -0,0 +1 @@ +export const t = 1; diff --git a/test-utils/fixtures/ts-project/tool.config.ts b/test-utils/fixtures/ts-project/tool.config.ts new file mode 100644 index 0000000..0e811a8 --- /dev/null +++ b/test-utils/fixtures/ts-project/tool.config.ts @@ -0,0 +1 @@ +export default { test: true }; diff --git a/test-utils/fixtures/ts-project/tool.shared.ts b/test-utils/fixtures/ts-project/tool.shared.ts new file mode 100644 index 0000000..cf20619 --- /dev/null +++ b/test-utils/fixtures/ts-project/tool.shared.ts @@ -0,0 +1 @@ +export const shared = 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..8e274d5 --- /dev/null +++ b/test-utils/fixtures/ts-project/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/test-utils/fixtures/ts-project/tsconfig.test.json b/test-utils/fixtures/ts-project/tsconfig.test.json new file mode 100644 index 0000000..fc1e6f5 --- /dev/null +++ b/test-utils/fixtures/ts-project/tsconfig.test.json @@ -0,0 +1 @@ +{ "extends": "./tsconfig.json", "include": ["test"] } diff --git a/test-utils/lint.js b/test-utils/lint.js new file mode 100644 index 0000000..80e23cf --- /dev/null +++ b/test-utils/lint.js @@ -0,0 +1,115 @@ +// 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'); + +/** Lint `code` with a flat config array; returns the flattened messages. */ +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; +} + +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; +} + +/** 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)]; + }); +} + +/** The severity (0|1|2) a rule has for `filePath` once all config objects are merged. */ +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]; + + 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, 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); + + 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/lint.test.js b/test-utils/lint.test.js new file mode 100644 index 0000000..9d33db7 --- /dev/null +++ b/test-utils/lint.test.js @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { loadedRules, lintText, missingRules, 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']); +}); + +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']); +}); 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']; 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')), []); + }); +}