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

View File

@@ -97,6 +97,7 @@
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"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-unused-imports": "^4.3.0",
"glob": "^13.0.0",

51
pnpm-lock.yaml generated
View File

@@ -192,6 +192,9 @@ importers:
eslint-plugin-prettier:
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)
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:
specifier: ^7.0.1
version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -393,6 +396,13 @@ packages:
peerDependencies:
'@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':
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
engines: {node: '>=6.9.0'}
@@ -2549,6 +2559,12 @@ packages:
eslint-config-prettier:
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:
resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==}
engines: {node: '>=20.19.0'}
@@ -4109,12 +4125,21 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
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:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.2.1:
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
@@ -4331,6 +4356,14 @@ snapshots:
transitivePeerDependencies:
- 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)':
dependencies:
'@babel/core': 7.28.5
@@ -6626,6 +6659,18 @@ snapshots:
optionalDependencies:
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):
dependencies:
'@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: {}
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):
dependencies:
zod: 4.2.1
zod@3.25.76: {}
zod@4.2.1: {}
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 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";
export type SearchState = {
@@ -223,7 +223,7 @@ export const BaseSearchBox = ({
<Tooltip title={t("shared.placeholders.useRegex")}>
<div>
<SvgIcon
component={useRegularExpressionIcon}
component={UseRegularExpressionIcon}
aria-label={useRegularExpression ? "active" : "inactive"}
{...iconStyle}
onClick={handleToggleUseRegularExpression}

View File

@@ -123,24 +123,6 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
useEffect(() => {
initialDataRef.current = 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
const reloadLatest = useLockFn(async () => {
// 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 = () => {
monacoInitialization();

View File

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

View File

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

View File

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