diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 5f5df9bce4..474aa2d5c3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -9,7 +9,7 @@ body: value: | ## Feature Request - # Thank you for suggesting a new feature! Before submitting, please ensure your request meets our contribution guidelines. + Thank you for suggesting a new feature! Before submitting, please ensure your request meets our contribution guidelines. - type: textarea id: problem attributes: diff --git a/.github/ISSUE_TEMPLATE/rule_request.yml b/.github/ISSUE_TEMPLATE/rule_request.yml index 9a10f7797c..9c918a558e 100644 --- a/.github/ISSUE_TEMPLATE/rule_request.yml +++ b/.github/ISSUE_TEMPLATE/rule_request.yml @@ -9,7 +9,7 @@ body: value: | ## Rule Request - # Thank you for suggesting a new rule! Before submitting, please ensure your request meets our rule writing standards. + Thank you for suggesting a new rule! Before submitting, please ensure your request meets our rule writing standards. ## References on Rule Design - [What makes a good rule](https://package.elm-lang.org/packages/jfmengels/elm-review/latest/Review-Rule#what-makes-a-good-rule) @@ -42,8 +42,6 @@ body: > A lint rule is an automated communication tool which sends messages to developers who have written patterns your rule wishes to prevent. As all communication, the message is important. placeholder: | - > A lint rule is an automated communication tool which sends messages to developers who have written patterns your rule wishes to prevent. As all communication, the message is important. - Rule name: [PluginName]/[YourRuleName] Error message: Briefly and clearly describe the problem diff --git a/.github/workflows/check-provenance.yml b/.github/workflows/check-provenance.yml new file mode 100644 index 0000000000..a4d85dfeea --- /dev/null +++ b/.github/workflows/check-provenance.yml @@ -0,0 +1,30 @@ +name: Check Provenance +on: + push: + branches: + - main + - "2.0.0" + pull_request: + types: [opened, edited, reopened, synchronize] + branches: + - main + - "2.0.0" +permissions: + contents: read +jobs: + check-provenance: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + - name: Check provenance downgrades + uses: danielroe/provenance-action@main + id: check + with: + fail-on-provenance-change: true # optional, default: false + lockfile: pnpm-lock.yaml # optional + # base-ref: origin/main # optional, default: origin/main + fail-on-downgrade: true # optional, default: true + - name: Print result + run: "echo 'Downgraded: ${{ steps.check.outputs.downgraded }}'" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c2c272d1aa..00478470b7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -2,7 +2,7 @@ name: Check on: push: branches: - - main + - "**" pull_request: types: [opened, edited, reopened, synchronize] branches: @@ -13,13 +13,14 @@ jobs: check: runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@v4 - - name: Setup node@23 - uses: actions/setup-node@v4 + - name: Setup node@24 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: 23 - - name: Enable Corepack - run: corepack enable + node-version: 24 + - name: Install pnpm + run: npm install -g pnpm + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install front-end dependencies run: pnpm install - name: Build front-end assets diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9443724cbf..48d1040c8d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - "2.0.0-next" + - "2.0.0" tags-ignore: - "**" paths-ignore: @@ -21,14 +21,15 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 - - name: Setup node@23 - uses: actions/setup-node@v4 + - name: Setup node@24 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: 23 + node-version: 24 registry-url: "https://registry.npmjs.org" - - name: Enable Corepack - run: corepack enable + - name: Install pnpm + run: npm install -g pnpm + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install dependencies run: pnpm install - name: Build front-end assets diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3fb4ebf62..4afa8dd27d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - "2.0.0-next" + - "2.0.0" pull_request: types: [opened, edited, reopened, synchronize] branches: @@ -14,24 +14,23 @@ jobs: test: runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@v4 - - name: Setup node@23 - uses: actions/setup-node@v4 + - name: Setup node@24 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: 23 - - name: Enable Corepack - run: corepack enable + node-version: 24 + - name: Install pnpm + run: npm install -g pnpm + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install front-end dependencies run: pnpm install - name: Build front-end assets run: pnpm run build - - name: Test on node@23 + - name: Test on node@24 run: pnpm run test - - name: Setup node@18 - uses: actions/setup-node@v4 + - name: Setup node@20 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: 18 - - name: Enable Corepack - run: corepack enable - - name: Test on node@18 + node-version: 20 + - name: Test on node@20 run: pnpm run test diff --git a/.gitignore b/.gitignore index bf4c207909..34f5128a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ coverage/ .eslintcache # misc +/log .DS_Store *.pem @@ -38,11 +39,9 @@ yarn-error.log* # vercel .vercel - stats.html *.config-*.mjs /eslint-config.json *.bundled_*.mjs *.tgz eslint-results.sarif -.tsup diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 8881abb756..0000000000 --- a/.markdownlint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "markdownlint/style/all", - "MD013": false, - "MD024": false, - "MD033": false, - "MD041": false -} diff --git a/.pkgs/configs/.gitignore b/.pkgs/configs/.gitignore index c5e7b4730b..87cee3c069 100644 --- a/.pkgs/configs/.gitignore +++ b/.pkgs/configs/.gitignore @@ -44,4 +44,3 @@ stats.html *.bundled_*.mjs *.tgz eslint-results.sarif -.tsup diff --git a/.pkgs/configs/eslint.js b/.pkgs/configs/eslint.js index b0bd52f6fe..fbb029ae29 100644 --- a/.pkgs/configs/eslint.js +++ b/.pkgs/configs/eslint.js @@ -172,8 +172,8 @@ export const strictTypeChecked = defineConfig([ ], "perfectionist/sort-intersection-types": "off", "perfectionist/sort-modules": "off", - "perfectionist/sort-named-exports": ["warn", { type: "natural", order: "asc" }], - "perfectionist/sort-named-imports": ["warn", { type: "natural", order: "asc" }], + "perfectionist/sort-named-exports": "off", + "perfectionist/sort-named-imports": "off", "perfectionist/sort-object-types": [ "warn", { ...p11tOptions, ...p11tGroups }, diff --git a/.pkgs/configs/eslint.ts b/.pkgs/configs/eslint.ts index e9440de976..30dded9353 100644 --- a/.pkgs/configs/eslint.ts +++ b/.pkgs/configs/eslint.ts @@ -185,8 +185,8 @@ export const strictTypeChecked = defineConfig([ ], "perfectionist/sort-intersection-types": "off", "perfectionist/sort-modules": "off", - "perfectionist/sort-named-exports": ["warn", { type: "natural", order: "asc" }], - "perfectionist/sort-named-imports": ["warn", { type: "natural", order: "asc" }], + "perfectionist/sort-named-exports": "off", + "perfectionist/sort-named-imports": "off", "perfectionist/sort-object-types": [ "warn", { ...p11tOptions, ...p11tGroups }, diff --git a/.pkgs/configs/package.json b/.pkgs/configs/package.json index d60ca9e2a1..64841fdc3f 100644 --- a/.pkgs/configs/package.json +++ b/.pkgs/configs/package.json @@ -19,18 +19,18 @@ "lint:ts": "tsc --noEmit" }, "dependencies": { - "@eslint/js": "^9.35.0", - "@stylistic/eslint-plugin": "^5.3.1", + "@eslint/js": "^9.36.0", + "@stylistic/eslint-plugin": "^5.4.0", "eslint-plugin-de-morgan": "^1.3.1", - "eslint-plugin-function": "^0.0.25", - "eslint-plugin-jsdoc": "^55.4.0", + "eslint-plugin-function": "^0.0.30", + "eslint-plugin-jsdoc": "^60.3.1", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-regexp": "^2.10.0", "eslint-plugin-unicorn": "^61.0.2", - "typescript-eslint": "^8.43.0" + "typescript-eslint": "^8.44.1" }, "peerDependencies": { - "eslint": "^9.35.0", - "typescript": "^4.9.5 || ^5.4.5" + "eslint": "^9.36.0", + "typescript": "^5.9.2" } } diff --git a/.pkgs/eslint-plugin-local/.gitignore b/.pkgs/eslint-plugin-local/.gitignore index c5e7b4730b..5a4c9838a9 100644 --- a/.pkgs/eslint-plugin-local/.gitignore +++ b/.pkgs/eslint-plugin-local/.gitignore @@ -43,5 +43,4 @@ stats.html /eslint-config.json *.bundled_*.mjs *.tgz -eslint-results.sarif -.tsup +eslint-results.sarif \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/dist/index.d.ts b/.pkgs/eslint-plugin-local/dist/index.d.ts index 59d5b613b6..b52ea362b8 100644 --- a/.pkgs/eslint-plugin-local/dist/index.d.ts +++ b/.pkgs/eslint-plugin-local/dist/index.d.ts @@ -1,20 +1,6 @@ -import "@typescript-eslint/utils/eslint-utils"; -import * as _typescript_eslint_utils_ts_eslint0 from "@typescript-eslint/utils/ts-eslint"; +import { CompatiblePlugin } from "@eslint-react/kit"; -//#region src/rules/prefer-eqeq-nullish-comparison.d.ts -type MessageID = "unexpectedComparison" | "useLooseComparisonSuggestion"; -//#endregion //#region src/index.d.ts -declare const _default: { - readonly meta: { - readonly name: string; - readonly version: string; - }; - readonly rules: { - readonly "avoid-multiline-template-expression": _typescript_eslint_utils_ts_eslint0.RuleModule<"avoidMultilineTemplateExpression", [], unknown, _typescript_eslint_utils_ts_eslint0.RuleListener>; - readonly "no-shadow-underscore": _typescript_eslint_utils_ts_eslint0.RuleModule<"noShadowUnderscore", [], unknown, _typescript_eslint_utils_ts_eslint0.RuleListener>; - readonly "prefer-eqeq-nullish-comparison": _typescript_eslint_utils_ts_eslint0.RuleModule; - }; -}; +declare const plugin: CompatiblePlugin; //#endregion -export { _default as default }; \ No newline at end of file +export { plugin as default }; \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/dist/index.js b/.pkgs/eslint-plugin-local/dist/index.js index bdca5e4a58..ce7bc06741 100644 --- a/.pkgs/eslint-plugin-local/dist/index.js +++ b/.pkgs/eslint-plugin-local/dist/index.js @@ -155,7 +155,7 @@ function create(context) { //#endregion //#region src/index.ts -var src_default = { +const plugin = { meta: { name, version @@ -166,6 +166,7 @@ var src_default = { "prefer-eqeq-nullish-comparison": prefer_eqeq_nullish_comparison_default } }; +var src_default = plugin; //#endregion export { src_default as default }; \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/package.json b/.pkgs/eslint-plugin-local/package.json index 9f84c13795..ab3aea83a2 100644 --- a/.pkgs/eslint-plugin-local/package.json +++ b/.pkgs/eslint-plugin-local/package.json @@ -16,7 +16,7 @@ "package.json" ], "scripts": { - "build": "tsdown", + "build": "tsdown --dts-resolve", "lint:publish": "publint", "lint:ts": "tsc --noEmit" }, @@ -26,14 +26,14 @@ "@eslint-react/kit": "workspace:*", "@eslint-react/shared": "workspace:*", "@eslint-react/var": "workspace:*", - "@eslint/js": "^9.35.0", - "@stylistic/eslint-plugin": "^5.3.1", - "@typescript-eslint/scope-manager": "^8.43.0", - "@typescript-eslint/type-utils": "^8.43.0", - "@typescript-eslint/types": "^8.43.0", - "@typescript-eslint/utils": "^8.43.0", + "@eslint/js": "^9.36.0", + "@stylistic/eslint-plugin": "^5.4.0", + "@typescript-eslint/scope-manager": "^8.44.1", + "@typescript-eslint/type-utils": "^8.44.1", + "@typescript-eslint/types": "^8.44.1", + "@typescript-eslint/utils": "^8.44.1", "eslint-plugin-de-morgan": "^1.3.1", - "eslint-plugin-jsdoc": "^55.4.0", + "eslint-plugin-jsdoc": "^60.3.1", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-regexp": "^2.10.0", "eslint-plugin-unicorn": "^61.0.2", @@ -42,15 +42,15 @@ }, "devDependencies": { "@local/configs": "workspace:*", - "@types/react": "^19.1.12", + "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", - "tsdown": "^0.15.0" + "tsdown": "^0.15.4" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "^4.9.5 || ^5.3.3" + "eslint": "^9.36.0", + "typescript": "^5.9.2" }, "engines": { - "node": ">=18.18.0" + "node": ">=20.19.0" } } diff --git a/.pkgs/eslint-plugin-local/src/index.ts b/.pkgs/eslint-plugin-local/src/index.ts index 73fc21f5af..3fa95e4970 100644 --- a/.pkgs/eslint-plugin-local/src/index.ts +++ b/.pkgs/eslint-plugin-local/src/index.ts @@ -1,9 +1,12 @@ import { name, version } from "../package.json"; + +import type { CompatiblePlugin } from "@eslint-react/kit"; + import avoidMultilineTemplateExpression from "./rules/avoid-multiline-template-expression"; import noShadowingUnderscore from "./rules/no-shadow-underscore"; import preferEqeqNullishComparison from "./rules/prefer-eqeq-nullish-comparison"; -export default { +const plugin: CompatiblePlugin = { meta: { name, version, @@ -13,4 +16,6 @@ export default { "no-shadow-underscore": noShadowingUnderscore, "prefer-eqeq-nullish-comparison": preferEqeqNullishComparison, }, -} as const; +}; + +export default plugin; diff --git a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts b/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts index d6942e1614..8ddbf46e9a 100644 --- a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts +++ b/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts @@ -2,7 +2,7 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; -import { nullThrows, NullThrowsReasons, type RuleListener } from "@typescript-eslint/utils/eslint-utils"; +import { NullThrowsReasons, type RuleListener, nullThrows } from "@typescript-eslint/utils/eslint-utils"; import { createRule } from "../utils"; diff --git a/.pkgs/eslint-plugin-local/tsdown.config.ts b/.pkgs/eslint-plugin-local/tsdown.config.ts index a616c43308..031761a959 100644 --- a/.pkgs/eslint-plugin-local/tsdown.config.ts +++ b/.pkgs/eslint-plugin-local/tsdown.config.ts @@ -10,6 +10,6 @@ export default { outDir: "dist", platform: "node", sourcemap: false, - target: "node18", + target: "node20", treeshake: true, } satisfies Options; diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f6f5027fbf..0d9a7a9e24 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "dprint.dprint", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "darkriszty.markdown-table-prettify" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d6ac8ee2..51f8667dd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,118 @@ +## v2.0.0 (2025-09-26) + +### ๐Ÿ’ฅ Breaking Changes + +**Target Environment Updates: Now ESM and ESLint Flat Config Only** + +- Drop support for CommonJS (CJS) module format, packages are now distributed only as ECMAScript Modules (ESM) +- Drop support for ESLint legacy config system, packages now support only ESLint Flat Config (`eslint.config.js`) +- Drop support for Node.js 16 and 18, minimum required version is now Node.js 20 +- Drop support for ESLint 8, minimum required version is now ESLint 9.3.6 +- Drop support for TypeScript 4, minimum required version is now TypeScript 5.9.2 + +**Removed Rules** + +| Rule | Replaced by | Reason | +| :--------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | :----------- | +| react-x/avoid-shorthand-boolean | [`react-x/jsx-shorthand-boolean`](/docs/rules/jsx-shorthand-boolean) | consolidated | +| react-x/avoid-shorthand-fragment | [`react-x/jsx-shorthand-fragment`](/docs/rules/jsx-shorthand-fragment) | consolidated | +| react-x/ensure-forward-ref-using-ref | [`react-x/no-useless-forward-ref`](/docs/rules/no-useless-forward-ref) | renamed | +| react-x/jsx-no-duplicate-props | [`react-x/jsx-no-duplicate-props`](/docs/rules/jsx-no-duplicate-props) | renamed | +| react-x/no-comment-textnodes | [`react-x/jsx-no-comment-textnodes`](/docs/rules/jsx-no-comment-textnodes) | renamed | +| react-x/no-complicated-conditional-rendering | | discontinued | +| react-x/no-nested-components | [`react-x/no-nested-component-definitions`](/docs/rules/no-nested-component-definitions) | renamed | +| react-x/prefer-react-namespace-import | [`react-x/prefer-namespace-import`](/docs/rules/prefer-namespace-import) | renamed | +| react-x/prefer-shorthand-boolean | [`react-x/jsx-shorthand-boolean`](/docs/rules/jsx-shorthand-boolean) | consolidated | +| react-x/prefer-shorthand-fragment | [`react-x/jsx-shorthand-fragment`](/docs/rules/jsx-shorthand-fragment) | consolidated | +| react-x/use-jsx-vars | [`react-x/jsx-uses-vars`](/docs/rules/jsx-uses-vars) | renamed | +| react-dom/no-children-in-void-dom-elements | [`react-dom/no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | renamed | +| react-hooks-extra/no-direct-set-state-in-use-layout-effect | [`react-hooks-extra/no-direct-set-state-in-use-effect`](/docs/rules/hooks-extra-no-direct-set-state-in-use-effect) | consolidated | +| react-hooks-extra/no-unnecessary-use-callback | [`react-x/no-unnecessary-use-callback`](/docs/rules/no-unnecessary-use-callback) | relocated | +| react-hooks-extra/no-unnecessary-use-memo | [`react-x/no-unnecessary-use-memo`](/docs/rules/no-unnecessary-use-memo) | relocated | +| react-hooks-extra/no-unnecessary-use-prefix | [`react-x/no-unnecessary-use-prefix`](/docs/rules/no-unnecessary-use-prefix) | relocated | +| react-hooks-extra/prefer-use-state-lazy-initialization | [`react-x/prefer-use-state-lazy-initialization`](/docs/rules/prefer-use-state-lazy-initialization) | relocated | + +**Removed Presets** + +| Preset | Replaced by | Reason | +| :-------------------------------- | :------------ | :----------- | +| `core` | `x` | renamed | +| `core-legacy` | | discontinued | +| `off-dom` | `disable-dom` | renamed | +| `off-dom-legacy` | | discontinued | +| `x-legacy` | | discontinued | +| `dom-legacy` | | discontinued | +| `web-api-legacy` | | discontinued | +| `recommended-legacy` | | discontinued | +| `recommended-typescript-legacy` | | discontinued | +| `recommended-type-checked-legacy` | | discontinued | + +**Removed Settings** + +| Setting | Replaced by | Reason | +| :--------------------- | :---------- | :----------- | +| `additionalComponents` | | discontinued | +| `additionalHooks` | | discontinued | +| `skipImportCheck` | | discontinued | + +The rule implementations have been refactored to improve performance and maintainability. + +### โœจ New + +**Added the following new rules:** + +- `react-x/jsx-shorthand-boolean`: Enforces a consistent style for boolean attributes +- `react-x/jsx-shorthand-fragment`: Enforces a consistent style for React Fragments +- `react-x/no-forbidden-props`: Disallows specific props on components +- `react-x/no-unnecessary-key`: Reports unnecessary `key` props on elements +- `react-x/no-unused-props`: Reports unused props in components +- `react-dom/no-string-style-prop`: Disallows string values for the `style` prop +- `react-dom/prefer-namespace-import`: Enforces using a namespace import for `react-dom` + +**Added the following new rule to the `recommended-type-checked` preset:** + +- `react-x/no-unused-props`: Reports unused props in components + +**The following rules now support Codemod features:** + +- `react-x/no-component-did-update` +- `react-x/no-component-will-receive-props` +- `react-x/no-component-will-update` +- `react-x/no-context-provider` +- `react-x/no-forward-ref` +- `react-x/no-string-refs` + +**The following rules now support auto-fix:** + +- `react-x/prefer-namespace-import` +- `react-dom/prefer-namespace-import` + +**The following rules now support suggestion fixes:** + +- `react-dom/no-missing-button-type` +- `react-dom/no-missing-iframe-sandbox` +- `react-dom/no-unsafe-target-blank` + +**New configuration preset added:** + +- `disable-conflict-eslint-plugin-react`: Disable rules in `eslint-plugin-react` that conflict with rules in our plugins + +### ๐Ÿž Fixes + +- fix(react-x/no-unnecessary-use-prefix): fix false positive of React Hooks defined within the callback function of `vi.mock(...)` in Vitest test files +- fix(react-web-api/no-leaked-event-listener): fix `useEffect` setup function check to handle `React.useEffect()` calls correctly +- fix(react-naming-convention/filename): fix false positive on well-known filenames like `404.tsx`, `_app.tsx`, `[slug].tsx` + +### ๐Ÿช„ Improvements + +- refactor: simplify React APIs detection logic +- refactor: cleanup utilities and simplify rule implementations +- docs: add comparison table between `eslint-plugin-react` and `eslint-react` rules +- docs: replace `tseslint.config` with `defineConfig` in all examples +- build: migrate build system from `tsup` to `tsdown` for better performance + +**Full Changelog**: https://github.com/Rel1cx/eslint-react/compare/v1.53.1...v2.0.0 + ## v1.53.1 (2025-09-11) ### ๐Ÿž Fixes diff --git a/README.md b/README.md index 21315ceab3..29fb6d4c8d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [TypeScript Specialized](#typescript-specialized) - [Other](#other) - [Rules](#rules) +- [Benchmark](#benchmark) - [FAQ](#faq) - [Roadmap](#roadmap) - [Contributing](#contributing) @@ -29,10 +30,10 @@ ## Features -- **Modern**: First-class support for TypeScript, React 19, and more. -- **Flexible**: Fully customizable rule severity levels, allowing you to enforce or relax rules as needed. -- **Performant**: Built with performance in mind, optimized for large codebases, **4-7x faster** than other ESLint plugins. -- **Context-aware Linting**: Rules that understand the context of your code and project configuration to provide more accurate linting. +- **Modern**: First-class support for **TypeScript**, **React 19**, and more. +- **Flexible**: Fully customizable rule severity levels, allowing you to **enforce** or **relax** rules as needed. +- **Performant**: Built with performance in mind, optimized for large codebases, [**4-7x faster**](https://github.com/Rel1cx/eslint-react-benchmark) than other ESLint plugins. +- **Context-aware Linting**: Rules that understand the context of your code and [project configuration](https://eslint-react.xyz/docs/configuration/configure-project-config) to provide more **accurate** linting. ## Public Packages @@ -53,8 +54,8 @@ > [!NOTE]\ > ESLint React requires the following minimum versions: > -> - Node.js: 18.18.0 -> - ESLint: 8.57.0 +> - Node.js: 20.19.0 +> - ESLint: 9.24.0 > - TypeScript: 4.9.5 ### Install @@ -68,46 +69,46 @@ npm install --save-dev typescript-eslint @eslint-react/eslint-plugin ```js // eslint.config.js -// @ts-check import eslintReact from "@eslint-react/eslint-plugin"; import eslintJs from "@eslint/js"; +import { defineConfig } from "eslint/config"; import tseslint from "typescript-eslint"; -export default tseslint.config({ - files: ["**/*.ts", "**/*.tsx"], - - // Extend recommended rule sets from: - // 1. ESLint JS's recommended rules - // 2. TypeScript ESLint recommended rules - // 3. ESLint React's recommended-typescript rules - extends: [ - eslintJs.configs.recommended, - tseslint.configs.recommended, - eslintReact.configs["recommended-typescript"], - ], - - // Configure language/parsing options - languageOptions: { - // Use TypeScript ESLint parser for TypeScript files - parser: tseslint.parser, - parserOptions: { - // Enable project service for better TypeScript integration - projectService: true, - tsconfigRootDir: import.meta.dirname, +export default defineConfig([ + { + files: ["**/*.ts", "**/*.tsx"], + + // Extend recommended rule sets from: + // 1. ESLint JS's recommended rules + // 2. TypeScript ESLint recommended rules + // 3. ESLint React's recommended-typescript rules + extends: [ + eslintJs.configs.recommended, + tseslint.configs.recommended, + eslintReact.configs["recommended-typescript"], + ], + + // Configure language/parsing options + languageOptions: { + // Use TypeScript ESLint parser for TypeScript files + parser: tseslint.parser, + parserOptions: { + // Enable project service for better TypeScript integration + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, }, - }, - // Custom rule overrides (modify rule levels or disable rules) - rules: { - "@eslint-react/no-missing-key": "warn", + // Custom rule overrides (modify rule levels or disable rules) + rules: { + "@eslint-react/no-missing-key": "warn", + }, }, -}); +]); ``` [Full Installation Guide โ†—](https://eslint-react.xyz/docs/getting-started/typescript) - - ## Presets ### Bare Bones @@ -141,6 +142,8 @@ export default tseslint.config({ Disable rules in the `web-api` preset. - `disable-type-checked`\ Disable rules that require type information. +- `disable-conflict-eslint-plugin-react`\ + Disable rules in `eslint-plugin-react` that conflict with rules in our plugins. - `off`\ Disable all rules in this plugin except for debug rules. @@ -150,6 +153,10 @@ export default tseslint.config({ [Rules Overview โ†—](https://eslint-react.xyz/docs/rules/overview) +## Benchmark + +[Benchmark Results โ†—](https://github.com/Rel1cx/eslint-react-benchmark) + ## FAQ [Frequently Asked Questions โ†—](https://eslint-react.xyz/docs/faq) @@ -162,7 +169,7 @@ export default tseslint.config({ Contributions are welcome! -Please follow our [contributing guidelines](./.github/CONTRIBUTING.md). +Please follow our [contributing guidelines](.github/CONTRIBUTING.md). ## License diff --git a/VERSION b/VERSION index 9adccc2f2f..227cea2156 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.53.1 +2.0.0 diff --git a/apps/website/app/base.css b/apps/website/app/app.css similarity index 90% rename from apps/website/app/base.css rename to apps/website/app/app.css index 31935e279f..8d5b98018a 100644 --- a/apps/website/app/base.css +++ b/apps/website/app/app.css @@ -1,6 +1,6 @@ @import "tailwindcss"; @import "tailwindcss-animated"; -@import "./theme.css"; +@import "./theme/theme.css"; @import "fumadocs-ui/css/preset.css"; @import "fumadocs-twoslash/twoslash.css"; diff --git a/apps/website/app/overrides.css b/apps/website/app/app.override.css similarity index 100% rename from apps/website/app/overrides.css rename to apps/website/app/app.override.css diff --git a/apps/website/app/docs/layout.tsx b/apps/website/app/docs/layout.tsx index 34901d384d..84d6545576 100644 --- a/apps/website/app/docs/layout.tsx +++ b/apps/website/app/docs/layout.tsx @@ -15,25 +15,3 @@ export default function Layout({ children }: { children: ReactNode }) { ); } - -// Notebook layout -// import type { ReactNode } from "react"; -// import { baseOptions } from "#/app/layout.config"; -// import { source } from "#/lib/source"; -// import { DocsLayout } from "fumadocs-ui/layouts/notebook"; - -// export default function Layout({ children }: { children: ReactNode }) { -// return ( -// -// {children} -// -// ); -// } diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 0b6c2c678d..3af13a8f12 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -1,19 +1,12 @@ +import { baseUrl } from "#/lib/metadata"; +import { RootProvider } from "fumadocs-ui/provider"; import type { Metadata } from "next"; import type { ReactNode } from "react"; -import { RootProvider } from "fumadocs-ui/provider"; import { ViewTransitions } from "next-view-transitions"; -import { IBM_Plex_Mono } from "next/font/google"; -import { baseUrl } from "../lib/metadata"; -import "./base.css"; -import "./overrides.css"; - -const ibm_plex_mono = IBM_Plex_Mono({ - subsets: ["latin"], - variable: "--font-ibm_plex_mono", - weight: ["400", "500", "700"], -}); +import "#/app/app.css"; +import "#/app/app.override.css"; const themeOptions = { enabled: true, @@ -22,19 +15,19 @@ const themeOptions = { // forcedTheme: "dark", }; -export const metadata = { +export const metadata: Metadata = { description: "4-7x faster composable ESLint rules for React and friends.", title: { default: "ESLint React", template: "%s | ESLint React", }, metadataBase: baseUrl, -} as const satisfies Metadata; +}; export default function Layout({ children }: { children: ReactNode }) { return ( - + diff --git a/apps/website/app/theme.css b/apps/website/app/theme.css deleted file mode 100644 index 5e793e9975..0000000000 --- a/apps/website/app/theme.css +++ /dev/null @@ -1,185 +0,0 @@ -@theme { - --color-fd-background: hsl(0, 0%, 100%); - --color-fd-foreground: hsl(240, 6%, 25%); - --color-fd-card: hsl(0, 0%, 100%); - --color-fd-card-foreground: hsl(0, 0%, 3.9%); - --color-fd-popover: hsl(0, 0%, 100%); - --color-fd-popover-foreground: hsl(0, 0%, 15.1%); - --color-fd-primary: hsl(210, 100%, 44%); - --color-fd-primary-foreground: hsl(0, 0%, 98%); - --color-fd-secondary: hsl(240, 6%, 97%); - --color-fd-secondary-foreground: hsl(0, 0%, 9%); - --color-fd-border: hsl(0, 0%, 89.8%); - --color-fd-ring: hsl(0, 0%, 63.9%); - --color-fd-muted: hsl(0, 0%, 96%); - --color-fd-muted-foreground: hsl(240, 6%, 50%); - --color-fd-accent: hsl(0, 0%, 94.1%); - --color-fd-accent-foreground: hsl(240, 6%, 25%); - - --color-fd-prose-body: - color-mix(in oklab, var(--color-fd-foreground) 80%, transparent); -} - -.dark { - --color-fd-background: hsl(0, 0%, 7.04%); - --color-fd-foreground: hsl(0, 0%, 92%); - --color-fd-card: hsl(0, 0%, 9.8%); - --color-fd-card-foreground: hsl(0, 0%, 98%); - --color-fd-popover: hsl(0, 0%, 9.8%); - --color-fd-popover-foreground: hsl(0, 0%, 88%); - --color-fd-primary: hsl(0, 0%, 98%); - --color-fd-primary-foreground: hsl(0, 0%, 9%); - --color-fd-secondary: hsl(0, 0%, 12.9%); - --color-fd-secondary-foreground: hsl(0, 0%, 98%); - --color-fd-border: hsl(0, 0%, 14%); - --color-fd-ring: hsl(0, 0%, 54.9%); - --color-fd-muted: hsl(0, 0%, 12.9%); - --color-fd-muted-foreground: hsl(0, 0%, 60.9%); - --color-fd-accent: hsl(0, 0%, 16.9%); - --color-fd-accent-foreground: hsl(0, 0%, 90%); - - --color-fd-prose-body: - color-mix(in oklab, var(--color-fd-foreground) 80%, transparent); -} - -:root { - --font-family-body: - "SF Pro Text", - "SF Pro Icons", - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - "Noto Sans", - Ubuntu, - Cantarell, - "Helvetica Neue", - Arial, - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; - - --font-family-ui: - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - "Noto Sans", - Ubuntu, - Cantarell, - "Helvetica Neue", - Arial, - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; - - --font-family-mono: - var(--font-ibm_plex_mono), - ui-monospace, - Menlo, - Monaco, - Consolas, - monospace; -} - -body { - font-family: var(--font-family-body); -} - -button { - cursor: pointer; -} - -:focus-visible { - outline: none; -} - -:focus-visible:not([class*="outline-none"]) { - outline: 2px solid var(--color-fd-primary); - outline-offset: -2px; -} - -[role="tabpanel"]:focus-visible { - outline: none; -} - -#nd-docs-layout #nd-sidebar { - a[data-active] { - font-weight: 400; - } - - p.mt-8.mb-2[style^="padding-inline-start:"] { - margin-top: 28px; - margin-bottom: 16px; - } - - div[data-state], - a[data-active] { - margin-top: 4px; - margin-bottom: 4px; - border-radius: 6px; - } -} - -.dark #nd-sidebar { - --color-fd-muted: hsl(0, 0%, 16%); - --color-fd-secondary: hsl(0, 0%, 18%); - --color-fd-muted-foreground: hsl(0, 0%, 72%); -} - -.prose { - font-family: var(--font-family-body); - --tw-prose-links: var(--color-fd-primary); - - .fd-codeblock.shiki { - background-color: color-mix(in oklab, var(--color-fd-secondary) 25%, transparent); - } - - .fd-codeblock, - [data-radix-scroll-area-viewport], - [data-radix-scroll-area-viewport]>div { - outline: none; - } - - :where(code):not(:where([class~='not-prose'], [class~='not-prose'] *)) { - border: none; - padding: .125rem .25em; - font-size: 14px; - } - - table { - font-size: 1em; - - thead tr th code, - tbody tr td code { - white-space: nowrap; - } - } -} - -.dark .prose { - --tw-prose-body: color-mix(in oklab, var(--color-fd-foreground) 80%, transparent); - - :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) { - color: #ffffff; - } -} - -.dark { - - .shadow-2xs, - .shadow-xs, - .shadow-sm, - .shadow-md, - .shadow-lg, - .shadow-xl, - .shadow-2xl { - box-shadow: 0 0 #0000; - } -} \ No newline at end of file diff --git a/apps/website/app/theme/theme.css b/apps/website/app/theme/theme.css index 80bb552c57..e8ef2d61db 100644 --- a/apps/website/app/theme/theme.css +++ b/apps/website/app/theme/theme.css @@ -80,7 +80,6 @@ "Noto Color Emoji"; --font-family-mono: - var(--font-ibm_plex_mono), ui-monospace, Menlo, Monaco, diff --git a/apps/website/atoms/location.ts b/apps/website/atoms/location.ts new file mode 100644 index 0000000000..9a6f75d509 --- /dev/null +++ b/apps/website/atoms/location.ts @@ -0,0 +1,21 @@ +import { Atom } from "@effect-atom/atom-react"; +import * as Option from "effect/Option"; + +function getHash() { + const hash = location.hash.slice(1); + if (hash.length > 0) { + return Option.some(hash); + } + return Option.none(); +} + +export const hashAtom = Atom.make>((get) => { + function onHashChange() { + get.setSelf(getHash()); + } + window.addEventListener("hashchange", onHashChange); + get.addFinalizer(() => { + window.removeEventListener("hashchange", onHashChange); + }); + return getHash(); +}); diff --git a/apps/website/atoms/theme.ts b/apps/website/atoms/theme.ts new file mode 100644 index 0000000000..5b8ec9de87 --- /dev/null +++ b/apps/website/atoms/theme.ts @@ -0,0 +1,22 @@ +import { Atom } from "@effect-atom/atom-react"; + +function getTheme(): "light" | "dark" { + const selected = localStorage?.getItem("starlight-theme") ?? "system"; + if (selected === "light" || selected === "dark") { + return selected; + } + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export const themeAtom = Atom.make<"light" | "dark">((get) => { + const observer = new MutationObserver(function() { + get.setSelf(getTheme()); + }); + get.addFinalizer(() => { + observer.disconnect(); + }); + observer.observe(document.documentElement, { + attributeFilter: ["data-theme"], + }); + return getTheme(); +}); diff --git a/apps/website/content/docs/changelog.md b/apps/website/content/docs/changelog.md index 5105860448..4464b73651 100644 --- a/apps/website/content/docs/changelog.md +++ b/apps/website/content/docs/changelog.md @@ -2,6 +2,121 @@ title: Changelog --- +## v2.0.0 (2025-09-26) + +### ๐Ÿ’ฅ Breaking Changes + +**Target Environment Updates: Now ESM and ESLint Flat Config Only** + +- Drop support for CommonJS (CJS) module format, packages are now distributed only as ECMAScript Modules (ESM) +- Drop support for ESLint legacy config system, packages now support only ESLint Flat Config (`eslint.config.js`) +- Drop support for Node.js 16 and 18, minimum required version is now Node.js 20 +- Drop support for ESLint 8, minimum required version is now ESLint 9.3.6 +- Drop support for TypeScript 4, minimum required version is now TypeScript 5.9.2 + +**Removed Rules** + +| Rule | Replaced by | Reason | +| :--------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | :----------- | +| react-x/avoid-shorthand-boolean | [`react-x/jsx-shorthand-boolean`](/docs/rules/jsx-shorthand-boolean) | consolidated | +| react-x/avoid-shorthand-fragment | [`react-x/jsx-shorthand-fragment`](/docs/rules/jsx-shorthand-fragment) | consolidated | +| react-x/ensure-forward-ref-using-ref | [`react-x/no-useless-forward-ref`](/docs/rules/no-useless-forward-ref) | renamed | +| react-x/jsx-no-duplicate-props | [`react-x/jsx-no-duplicate-props`](/docs/rules/jsx-no-duplicate-props) | renamed | +| react-x/no-comment-textnodes | [`react-x/jsx-no-comment-textnodes`](/docs/rules/jsx-no-comment-textnodes) | renamed | +| react-x/no-complicated-conditional-rendering | | discontinued | +| react-x/no-nested-components | [`react-x/no-nested-component-definitions`](/docs/rules/no-nested-component-definitions) | renamed | +| react-x/prefer-react-namespace-import | [`react-x/prefer-namespace-import`](/docs/rules/prefer-namespace-import) | renamed | +| react-x/prefer-shorthand-boolean | [`react-x/jsx-shorthand-boolean`](/docs/rules/jsx-shorthand-boolean) | consolidated | +| react-x/prefer-shorthand-fragment | [`react-x/jsx-shorthand-fragment`](/docs/rules/jsx-shorthand-fragment) | consolidated | +| react-x/use-jsx-vars | [`react-x/jsx-uses-vars`](/docs/rules/jsx-uses-vars) | renamed | +| react-dom/no-children-in-void-dom-elements | [`react-dom/no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | renamed | +| react-hooks-extra/no-direct-set-state-in-use-layout-effect | [`react-hooks-extra/no-direct-set-state-in-use-effect`](/docs/rules/hooks-extra-no-direct-set-state-in-use-effect) | consolidated | +| react-hooks-extra/no-unnecessary-use-callback | [`react-x/no-unnecessary-use-callback`](/docs/rules/no-unnecessary-use-callback) | relocated | +| react-hooks-extra/no-unnecessary-use-memo | [`react-x/no-unnecessary-use-memo`](/docs/rules/no-unnecessary-use-memo) | relocated | +| react-hooks-extra/no-unnecessary-use-prefix | [`react-x/no-unnecessary-use-prefix`](/docs/rules/no-unnecessary-use-prefix) | relocated | +| react-hooks-extra/prefer-use-state-lazy-initialization | [`react-x/prefer-use-state-lazy-initialization`](/docs/rules/prefer-use-state-lazy-initialization) | relocated | + +**Removed Presets** + +| Preset | Replaced by | Reason | +| :-------------------------------- | :------------ | :----------- | +| `core` | `x` | renamed | +| `core-legacy` | | discontinued | +| `off-dom` | `disable-dom` | renamed | +| `off-dom-legacy` | | discontinued | +| `x-legacy` | | discontinued | +| `dom-legacy` | | discontinued | +| `web-api-legacy` | | discontinued | +| `recommended-legacy` | | discontinued | +| `recommended-typescript-legacy` | | discontinued | +| `recommended-type-checked-legacy` | | discontinued | + +**Removed Settings** + +| Setting | Replaced by | Reason | +| :--------------------- | :---------- | :----------- | +| `additionalComponents` | | discontinued | +| `additionalHooks` | | discontinued | +| `skipImportCheck` | | discontinued | + +The rule implementations have been refactored to improve performance and maintainability. + +### โœจ New + +**Added the following new rules:** + +- `react-x/jsx-shorthand-boolean`: Enforces a consistent style for boolean attributes +- `react-x/jsx-shorthand-fragment`: Enforces a consistent style for React Fragments +- `react-x/no-forbidden-props`: Disallows specific props on components +- `react-x/no-unnecessary-key`: Reports unnecessary `key` props on elements +- `react-x/no-unused-props`: Reports unused props in components +- `react-dom/no-string-style-prop`: Disallows string values for the `style` prop +- `react-dom/prefer-namespace-import`: Enforces using a namespace import for `react-dom` + +**Added the following new rule to the `recommended-type-checked` preset:** + +- `react-x/no-unused-props`: Reports unused props in components + +**The following rules now support Codemod features:** + +- `react-x/no-component-did-update` +- `react-x/no-component-will-receive-props` +- `react-x/no-component-will-update` +- `react-x/no-context-provider` +- `react-x/no-forward-ref` +- `react-x/no-string-refs` + +**The following rules now support auto-fix:** + +- `react-x/prefer-namespace-import` +- `react-dom/prefer-namespace-import` + +**The following rules now support suggestion fixes:** + +- `react-dom/no-missing-button-type` +- `react-dom/no-missing-iframe-sandbox` +- `react-dom/no-unsafe-target-blank` + +**New configuration preset added:** + +- `disable-conflict-eslint-plugin-react`: Disable rules in `eslint-plugin-react` that conflict with rules in our plugins + +### ๐Ÿž Fixes + +- fix(react-x/no-unnecessary-use-prefix): fix false positive of React Hooks defined within the callback function of `vi.mock(...)` in Vitest test files +- fix(react-web-api/no-leaked-event-listener): fix `useEffect` setup function check to handle `React.useEffect()` calls correctly +- fix(react-naming-convention/filename): fix false positive on well-known filenames like `404.tsx`, `_app.tsx`, `[slug].tsx` + +### ๐Ÿช„ Improvements + +- refactor: simplify React APIs detection logic +- refactor: cleanup utilities and simplify rule implementations +- docs: add comparison table between `eslint-plugin-react` and `eslint-react` rules +- docs: replace `tseslint.config` with `defineConfig` in all examples +- build: migrate build system from `tsup` to `tsdown` for better performance + +**Full Changelog**: https://github.com/Rel1cx/eslint-react/compare/v1.53.1...v2.0.0 + ## v1.53.1 (2025-09-11) ### ๐Ÿž Fixes diff --git a/apps/website/content/docs/configuration/configure-analyzer.mdx b/apps/website/content/docs/configuration/configure-analyzer.mdx index e16a4596c5..6d7c991507 100644 --- a/apps/website/content/docs/configuration/configure-analyzer.mdx +++ b/apps/website/content/docs/configuration/configure-analyzer.mdx @@ -56,58 +56,6 @@ Example with `polymorphicPropName` set to `as`: // Evaluated as an h3 element ``` -### `additionalComponents` (Experimental) - - - Consider using `polymorphicPropName` instead when possible, as it's simpler - and more efficient. - - - - Experimental feature that may lack stability and documentation. - - -Maps components and their attributes for comprehensive analysis. Supports default attribute values. - -Example configuration: - -```json -[ - { - "name": "EmbedContent", - "as": "iframe", - "attributes": [ - { - "name": "sandbox", - "defaultValue": "" - } - ] - } -] -``` - -This makes `{:tsx}` evaluate as `