refactor: upgrade ESLint 9→10 and simplify React lint toolchain

- Upgrade eslint and @eslint/js to v10
- Replace eslint-plugin-react-x + eslint-plugin-react-dom + eslint-plugin-react-hooks
  with unified @eslint-react/eslint-plugin
- Raise dev Node.js requirement to ^22.13.0 || >=24 (runtime packages unaffected)
- Add .npmrc with engine-strict=true
- Move all @eslint-react rule overrides to eslint.config.js,
  eliminating plugin-specific inline eslint-disable comments
- Fix real issues caught by new rules: useless assignments,
  leaked setTimeout, ref naming, useState setter naming
This commit is contained in:
Simon
2026-04-08 20:31:31 +08:00
parent a43e653a74
commit 4f80ec1459
17 changed files with 1649 additions and 1687 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -1,8 +1,6 @@
import eslintReact from '@eslint-react/eslint-plugin'
import js from '@eslint/js'
import reactDom from 'eslint-plugin-react-dom'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import reactX from 'eslint-plugin-react-x'
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
@@ -15,44 +13,25 @@ export default defineConfig([
'**/.wxt',
'**/.output',
]),
{
plugins: {
'react-hooks': reactHooks,
},
rules: reactHooks.configs.recommended.rules,
},
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
// reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
eslintReact.configs['recommended-typescript'],
],
languageOptions: {
parserOptions: {
// project: ['./tsconfig.json'],
// project: ['./packages/*/tsconfig.json'],
// tsconfigRootDir: import.meta.dirname,
projectService: true,
},
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
// Add any additional rules here
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
@@ -72,14 +51,14 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
// 'require-await': 'off',
'@typescript-eslint/require-await': 'off',
'@eslint-react/dom-no-missing-button-type': 'off',
'@eslint-react/no-nested-component-definitions': 'off',
'@eslint-react/no-array-index-key': 'off',
'@eslint-react/dom-no-dangerously-set-innerhtml': 'off',
},
},
])

3237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
"node": "^22.13.0 || >=24"
},
"scripts": {
"start": "npm run dev --workspace=@page-agent/website",
@@ -40,7 +40,8 @@
"devDependencies": {
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^9.39.2",
"@eslint-react/eslint-plugin": "^4.2.1",
"@eslint/js": "^10.0.0",
"@microsoft/api-extractor": "^7.58.1",
"@tailwindcss/vite": "^4.2.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
@@ -49,12 +50,9 @@
"chalk": "^5.6.2",
"concurrently": "^9.2.1",
"dotenv": "^17.4.1",
"eslint": "^9.39.2",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.13.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-react-x": "^2.13.0",
"globals": "^17.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",

View File

@@ -19,7 +19,7 @@ const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
* - etc.
*/
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
let resolvedArguments = null as any
let resolvedArguments: any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')

View File

@@ -56,7 +56,7 @@ export class RemotePageController {
}
async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState
let browserState: BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()

View File

@@ -49,7 +49,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
useEffect(() => {
const [prevConfig, setPrevConfig] = useState(config)
if (prevConfig !== config) {
setPrevConfig(config)
setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL)
setApiKey(config?.apiKey)
@@ -59,7 +61,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config])
}
// Poll for user auth token every second until found
useEffect(() => {

View File

@@ -71,7 +71,6 @@ export function HistoryDetail({
{/* Events (read-only) */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{session.history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
</div>

View File

@@ -34,7 +34,6 @@ export function HistoryList({
}, [])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load()
}, [load])

View File

@@ -133,7 +133,6 @@ export default function App() {
)}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}

View File

@@ -206,9 +206,9 @@ export function useHubWs(
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
const hubWsRef = useRef<HubWs | null>(null)
const latest = useRef({ execute, stop, configure, config })
const latestRef = useRef({ execute, stop, configure, config })
useEffect(() => {
latest.current = { execute, stop, configure, config }
latestRef.current = { execute, stop, configure, config }
})
useEffect(() => {
@@ -218,14 +218,14 @@ export function useHubWs(
Number(wsPort),
{
onExecute: async (task, incomingConfig) => {
const { execute, configure, config } = latest.current
const { execute, configure, config } = latestRef.current
if (incomingConfig) {
await configure({ ...config, ...incomingConfig } as ExtConfig)
}
const result = await execute(task)
return { success: result.success, data: result.data }
},
onStop: () => latest.current.stop(),
onStop: () => latestRef.current.stop(),
},
setWsState
)

View File

@@ -178,7 +178,6 @@ export default function App() {
{showEmptyState && <EmptyState />}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}

View File

@@ -169,7 +169,6 @@ function highlightSyntax(code: string): string {
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
}

View File

@@ -140,6 +140,7 @@ function JSConsole({
// 全局console拦截处理
useEffect(() => {
const interceptor = ConsoleInterceptor.getInstance()
let scrollTimer: ReturnType<typeof setTimeout>
const handleGlobalConsole = (type: string, args: unknown[]) => {
const content = args.map((arg) => formatResult(arg)).join(' ')
@@ -152,8 +153,8 @@ function JSConsole({
setOutputs((prev) => [...prev, outputItem])
// 自动滚动到底部
setTimeout(() => {
clearTimeout(scrollTimer)
scrollTimer = setTimeout(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
}
@@ -164,6 +165,7 @@ function JSConsole({
return () => {
interceptor.unsubscribe(handleGlobalConsole)
clearTimeout(scrollTimer)
}
}, [])

View File

@@ -9,19 +9,19 @@ const LanguageContext = createContext<{
} | null>(null)
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLang] = useState<Lang>(() => {
const [language, setLanguage] = useState<Lang>(() => {
const stored = localStorage.getItem('language') as Lang
if (stored === 'zh-CN' || stored === 'en-US') return stored
return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'
})
const setLanguage = (lang: Lang) => {
setLang(lang)
const switchLanguage = (lang: Lang) => {
setLanguage(lang)
localStorage.setItem('language', lang)
}
return (
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage }}>
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage: switchLanguage }}>
{children}
</LanguageContext>
)

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import type { PageAgent as PageAgentType } from 'page-agent'
import { useEffect, useState } from 'react'
import { Link, useSearchParams } from 'wouter'
@@ -46,10 +45,11 @@ export default function HeroSection() {
: 'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown'
const [task, setTask] = useState(() => defaultTask)
useEffect(() => {
const [prevDefaultTask, setPrevDefaultTask] = useState(defaultTask)
if (prevDefaultTask !== defaultTask) {
setPrevDefaultTask(defaultTask)
setTask(defaultTask)
}, [defaultTask])
}
const [params] = useSearchParams()
const isOther = params.has('try_other')

View File

@@ -18,10 +18,12 @@ function ScrollToTop() {
export default function Router() {
useEffect(() => {
const schedule = globalThis.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1))
const cancel = globalThis.cancelIdleCallback ?? clearTimeout
const id = schedule(() => docsImport())
return () => cancel(id)
if ('requestIdleCallback' in globalThis) {
const id = requestIdleCallback(() => docsImport())
return () => cancelIdleCallback(id)
}
const id = setTimeout(() => docsImport(), 1)
return () => clearTimeout(id)
}, [])
return (