chore(eslint): add eslint-plugin-react-compiler (#5918)

* chore(eslint): add `eslint-plugin-react-compiler`

* refactor: fix eslint warnings and avoid in-render mutations

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
This commit is contained in:
Sukka
2025-12-22 12:51:20 +08:00
committed by GitHub
parent eafa08066d
commit 8e48e4ed10
8 changed files with 100 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ import configPrettier from "eslint-config-prettier";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
import pluginImportX from "eslint-plugin-import-x"; import pluginImportX from "eslint-plugin-import-x";
import pluginPrettier from "eslint-plugin-prettier"; import pluginPrettier from "eslint-plugin-prettier";
import pluginReactCompiler from "eslint-plugin-react-compiler";
import pluginReactHooks from "eslint-plugin-react-hooks"; import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh"; import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports"; import pluginUnusedImports from "eslint-plugin-unused-imports";
@@ -19,6 +20,7 @@ export default defineConfig([
js: eslintJS, js: eslintJS,
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543 // @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
"react-hooks": pluginReactHooks, "react-hooks": pluginReactHooks,
"react-compiler": pluginReactCompiler,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421 // @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
"import-x": pluginImportX, "import-x": pluginImportX,
"react-refresh": pluginReactRefresh, "react-refresh": pluginReactRefresh,
@@ -52,6 +54,7 @@ export default defineConfig([
// React // React
"react-hooks/rules-of-hooks": "error", "react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error", "react-hooks/exhaustive-deps": "error",
"react-compiler/react-compiler": "error",
"react-refresh/only-export-components": [ "react-refresh/only-export-components": [
"warn", "warn",
{ allowConstantExport: true }, { allowConstantExport: true },

View File

@@ -97,6 +97,7 @@
"eslint-plugin-import-x": "^4.16.1", "eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-unused-imports": "^4.3.0", "eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0", "glob": "^13.0.0",

51
pnpm-lock.yaml generated
View File

@@ -192,6 +192,9 @@ importers:
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.5.4 specifier: ^5.5.4
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4)
eslint-plugin-react-compiler:
specifier: ^19.1.0-rc.2
version: 19.1.0-rc.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -393,6 +396,13 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0 '@babel/core': ^7.0.0
'@babel/plugin-proposal-private-methods@7.18.6':
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -2549,6 +2559,12 @@ packages:
eslint-config-prettier: eslint-config-prettier:
optional: true optional: true
eslint-plugin-react-compiler@19.1.0-rc.2:
resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==}
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
peerDependencies:
eslint: '>=7'
eslint-plugin-react-dom@2.3.13: eslint-plugin-react-dom@2.3.13:
resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==} resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
@@ -4109,12 +4125,21 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod-validation-error@3.5.4:
resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.24.4
zod-validation-error@4.0.2: zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
peerDependencies: peerDependencies:
zod: ^3.25.0 || ^4.0.0 zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.2.1: zod@4.2.1:
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
@@ -4331,6 +4356,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.5)':
dependencies:
'@babel/core': 7.28.5
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)': '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)':
dependencies: dependencies:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
@@ -6626,6 +6659,18 @@ snapshots:
optionalDependencies: optionalDependencies:
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@babel/core': 7.28.5
'@babel/parser': 7.28.5
'@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.5)
eslint: 9.39.2(jiti@2.6.1)
hermes-parser: 0.25.1
zod: 3.25.76
zod-validation-error: 3.5.4(zod@3.25.76)
transitivePeerDependencies:
- supports-color
eslint-plugin-react-dom@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): eslint-plugin-react-dom@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies: dependencies:
'@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@@ -8583,10 +8628,16 @@ snapshots:
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod-validation-error@3.5.4(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-validation-error@4.0.2(zod@4.2.1): zod-validation-error@4.0.2(zod@4.2.1):
dependencies: dependencies:
zod: 4.2.1 zod: 4.2.1
zod@3.25.76: {}
zod@4.2.1: {} zod@4.2.1: {}
zwitch@2.0.4: {} zwitch@2.0.4: {}

View File

@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react"; import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react"; import UseRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
import { buildRegex, compileStringMatcher } from "@/utils/search-matcher"; import { buildRegex, compileStringMatcher } from "@/utils/search-matcher";
export type SearchState = { export type SearchState = {
@@ -223,7 +223,7 @@ export const BaseSearchBox = ({
<Tooltip title={t("shared.placeholders.useRegex")}> <Tooltip title={t("shared.placeholders.useRegex")}>
<div> <div>
<SvgIcon <SvgIcon
component={useRegularExpressionIcon} component={UseRegularExpressionIcon}
aria-label={useRegularExpression ? "active" : "inactive"} aria-label={useRegularExpression ? "active" : "inactive"}
{...iconStyle} {...iconStyle}
onClick={handleToggleUseRegularExpression} onClick={handleToggleUseRegularExpression}

View File

@@ -123,24 +123,6 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
useEffect(() => { useEffect(() => {
initialDataRef.current = initialData; initialDataRef.current = initialData;
}, [initialData]); }, [initialData]);
// Background refresh: when the dialog/model is ready and the underlying resource key changes,
// try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function
// identity because callers often pass inline lambdas that change every render.
useEffect(() => {
if (!open) return;
// Only attempt after initial model is ready to avoid racing the initial load
if (!modelPath) return;
// Avoid immediate double-load on open: the initial load has just completed.
if (skipNextRefreshRef.current) {
skipNextRefreshRef.current = false;
return;
}
// Only meaningful when a callable loader is provided (plain Promise cannot be "recalled")
if (typeof initialDataRef.current === "function") {
void reloadLatest();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, modelPath, dataKey]);
// Helper to (soft) reload latest source and apply only if the user hasn't typed yet // Helper to (soft) reload latest source and apply only if the user hasn't typed yet
const reloadLatest = useLockFn(async () => { const reloadLatest = useLockFn(async () => {
// Snapshot the model/doc identity and bump a token so older calls can't win // Snapshot the model/doc identity and bump a token so older calls can't win
@@ -201,6 +183,23 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
} }
} }
}); });
// Background refresh: when the dialog/model is ready and the underlying resource key changes,
// try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function
// identity because callers often pass inline lambdas that change every render.
useEffect(() => {
if (!open) return;
// Only attempt after initial model is ready to avoid racing the initial load
if (!modelPath) return;
// Avoid immediate double-load on open: the initial load has just completed.
if (skipNextRefreshRef.current) {
skipNextRefreshRef.current = false;
return;
}
// Only meaningful when a callable loader is provided (plain Promise cannot be "recalled")
if (typeof initialDataRef.current === "function") {
void reloadLatest();
}
}, [open, modelPath, dataKey, reloadLatest]);
const beforeMount = () => { const beforeMount = () => {
monacoInitialization(); monacoInitialization();

View File

@@ -88,10 +88,6 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
const handleOk = useLockFn( const handleOk = useLockFn(
handleSubmit(async (form) => { handleSubmit(async (form) => {
if (form.option?.timeout_seconds) {
form.option.timeout_seconds = +form.option.timeout_seconds;
}
setLoading(true); setLoading(true);
try { try {
// 基本验证 // 基本验证
@@ -101,17 +97,21 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
} }
// 处理表单数据 // 处理表单数据
if (form.option?.update_interval) { const option = form.option ? { ...form.option } : undefined;
form.option.update_interval = +form.option.update_interval; if (option?.timeout_seconds) {
} else { option.timeout_seconds = +option.timeout_seconds;
delete form.option?.update_interval;
} }
if (form.option?.user_agent === "") { if (option?.update_interval) {
delete form.option.user_agent; option.update_interval = +option.update_interval;
} else if (option) {
option.update_interval = undefined;
}
if (option?.user_agent === "") {
option.user_agent = undefined;
} }
const name = form.name || `${form.type} file`; const name = form.name || `${form.type} file`;
const item = { ...form, name }; const item = { ...form, name, option };
const isRemote = form.type === "remote"; const isRemote = form.type === "remote";
const isUpdate = openType === "edit"; const isUpdate = openType === "edit";

View File

@@ -88,25 +88,19 @@ export const WebUIItem = (props: Props) => {
); );
} }
const placeholderCounts: Record<string, number> = {}; const renderedParts = highlightedParts.map((part, index) => {
let textCounter = 0;
const renderedParts = highlightedParts.map((part) => {
const isPlaceholder = const isPlaceholder =
part === "%host" || part === "%port" || part === "%secret"; part === "%host" || part === "%port" || part === "%secret";
const repeatIndex = highlightedParts
.slice(0, index)
.filter((prev) => prev === part).length;
const key = `${part || "empty"}-${repeatIndex}`;
if (isPlaceholder) { return (
const count = placeholderCounts[part] ?? 0; <span key={key} className={isPlaceholder ? "placeholder" : undefined}>
placeholderCounts[part] = count + 1; {part}
return ( </span>
<span key={`placeholder-${part}-${count}`} className="placeholder"> );
{part}
</span>
);
}
const key = `text-${textCounter}-${part || "empty"}`;
textCounter += 1;
return <span key={key}>{part}</span>;
}); });
return ( return (

View File

@@ -62,18 +62,15 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
try { try {
if (current && !skipConfigSave) { if (current && !skipConfigSave) {
if (!current.selected) current.selected = []; const selected = current.selected ? [...current.selected] : [];
const index = selected.findIndex((item) => item.name === groupName);
const index = current.selected.findIndex(
(item) => item.name === groupName,
);
if (index < 0) { if (index < 0) {
current.selected.push({ name: groupName, now: proxyName }); selected.push({ name: groupName, now: proxyName });
} else { } else {
current.selected[index] = { name: groupName, now: proxyName }; selected[index] = { name: groupName, now: proxyName };
} }
await patchCurrent({ selected: current.selected }); await patchCurrent({ selected });
} }
await selectNodeForGroup(groupName, proxyName); await selectNodeForGroup(groupName, proxyName);