Skip to content

Commit 2042459

Browse files
committed
Upgrade ESLint v9 to v10 and replace React plugins with @eslint-react
- Bump eslint to ^10.0.2 and @eslint/js to ^10.0.1 - Replace eslint-plugin-react + eslint-plugin-react-hooks with @eslint-react/eslint-plugin (recommended-type-checked config) - Use globalIgnores() for idiomatic v10 global ignore patterns - Fix preserve-caught-error violations (new v10 recommended rule) by adding { cause } to re-thrown errors in catch blocks - Fix @eslint-react naming-convention warnings: rename refs to use Ref suffix, rename useState destructuring to match convention - Give LogEntry objects stable IDs so WorkspaceLogs avoids array-index-key; derive ActionMenu keys in a pre-processing step - Disable package-json rules that don't apply to VS Code extensions (scoped to root only so workspace packages are fully linted)
1 parent 667ca8c commit 2042459

File tree

16 files changed

+562
-1261
lines changed

16 files changed

+562
-1261
lines changed

eslint.config.mjs

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
// @ts-check
22
import eslint from "@eslint/js";
3-
import { defineConfig } from "eslint/config";
3+
import { defineConfig, globalIgnores } from "eslint/config";
44
import markdown from "@eslint/markdown";
55
import tseslint from "typescript-eslint";
66
import prettierConfig from "eslint-config-prettier";
77
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
88
import { flatConfigs as importXFlatConfigs } from "eslint-plugin-import-x";
99
import packageJson from "eslint-plugin-package-json";
10-
import reactPlugin from "eslint-plugin-react";
11-
import reactHooksPlugin from "eslint-plugin-react-hooks";
10+
import eslintReact from "@eslint-react/eslint-plugin";
1211
import globals from "globals";
1312

1413
export default defineConfig(
15-
// Global ignores
16-
{
17-
ignores: [
18-
"out/**",
19-
"dist/**",
20-
"packages/*/dist/**",
21-
"**/*.d.ts",
22-
"vitest.config.ts",
23-
"**/vite.config*.ts",
24-
"**/createWebviewConfig.ts",
25-
".vscode-test/**",
26-
],
27-
},
14+
globalIgnores([
15+
"out/**",
16+
"dist/**",
17+
"packages/*/dist/**",
18+
"**/*.d.ts",
19+
"vitest.config.ts",
20+
"**/vite.config*.ts",
21+
"**/createWebviewConfig.ts",
22+
".vscode-test/**",
23+
]),
2824

2925
// Base ESLint recommended rules (for JS/TS/TSX files only)
3026
{
@@ -176,38 +172,31 @@ export default defineConfig(
176172
},
177173
},
178174

179-
// React hooks and compiler rules (covers .ts hook files too)
175+
// React rules with type-checked analysis (covers hooks, JSX, DOM)
180176
{
181177
files: ["packages/**/*.{ts,tsx}"],
182-
...reactHooksPlugin.configs.flat.recommended,
178+
extends: [eslintReact.configs["recommended-type-checked"]],
183179
rules: {
184-
...reactHooksPlugin.configs.flat.recommended.rules,
185180
// React Compiler auto-memoizes; exhaustive-deps false-positives on useCallback
186-
"react-hooks/exhaustive-deps": "off",
181+
"@eslint-react/exhaustive-deps": "off",
187182
},
188183
},
189184

190-
// TSX files - React JSX rules
185+
// Package.json linting
186+
packageJson.configs.recommended,
191187
{
192-
files: ["**/*.tsx"],
193-
plugins: {
194-
react: reactPlugin,
195-
},
196-
settings: {
197-
react: {
198-
version: "detect",
199-
},
200-
},
188+
// The root package.json is a VS Code extension (not an npm package),
189+
// so these publishing-oriented rules don't apply.
190+
files: ["package.json"],
191+
ignores: ["packages/**/package.json"],
201192
rules: {
202-
...reactPlugin.configs.recommended.rules,
203-
...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
204-
"react/prop-types": "off", // Using TypeScript
193+
"package-json/require-exports": "off",
194+
"package-json/require-files": "off",
195+
"package-json/require-sideEffects": "off",
196+
"package-json/require-attribution": "off",
205197
},
206198
},
207199

208-
// Package.json linting
209-
packageJson.configs.recommended,
210-
211200
// Markdown linting with GitHub-flavored admonitions allowed
212201
...markdown.configs.recommended,
213202
{

package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,8 @@
489489
"zod": "^4.3.6"
490490
},
491491
"devDependencies": {
492-
"@eslint/js": "^9.39.2",
492+
"@eslint-react/eslint-plugin": "^2.13.0",
493+
"@eslint/js": "^10.0.1",
493494
"@eslint/markdown": "^7.5.1",
494495
"@tanstack/react-query": "catalog:",
495496
"@testing-library/jest-dom": "^6.9.1",
@@ -518,13 +519,11 @@
518519
"dayjs": "^1.11.19",
519520
"electron": "^40.6.1",
520521
"esbuild": "^0.27.3",
521-
"eslint": "^9.39.2",
522+
"eslint": "^10.0.2",
522523
"eslint-config-prettier": "^10.1.8",
523524
"eslint-import-resolver-typescript": "^4.4.4",
524525
"eslint-plugin-import-x": "^4.16.1",
525526
"eslint-plugin-package-json": "^0.89.2",
526-
"eslint-plugin-react": "^7.37.5",
527-
"eslint-plugin-react-hooks": "^7.0.1",
528527
"globals": "^17.4.0",
529528
"jsdom": "^28.1.0",
530529
"jsonc-eslint-parser": "^3.1.0",

packages/tasks/src/components/ActionMenu.tsx

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -74,39 +74,44 @@ export function ActionMenu({ items }: ActionMenuProps) {
7474
tabIndex={-1}
7575
onKeyDown={(e) => isEscape(e) && close()}
7676
>
77-
{items.map((item, index) =>
78-
item.separator ? (
79-
<div
80-
key={`sep-${index}`}
81-
className="action-menu-separator"
82-
role="separator"
83-
/>
84-
) : (
85-
<button
86-
key={`${item.label}-${index}`}
87-
type="button"
88-
className={[
89-
"action-menu-item",
90-
item.danger && "danger",
91-
item.loading && "loading",
92-
]
93-
.filter(Boolean)
94-
.join(" ")}
95-
onClick={() => {
96-
item.onClick();
97-
close();
98-
}}
99-
disabled={item.disabled === true || item.loading === true}
100-
>
101-
{item.loading ? (
102-
<VscodeProgressRing className="action-menu-spinner" />
103-
) : (
104-
<VscodeIcon name={item.icon} className="action-menu-icon" />
105-
)}
106-
<span>{item.label}</span>
107-
</button>
108-
),
109-
)}
77+
{items
78+
.map((item, i) => ({
79+
item,
80+
key: item.separator ? `separator-${i}` : `${item.label}-${i}`,
81+
}))
82+
.map(({ item, key }) =>
83+
item.separator ? (
84+
<div
85+
key={key}
86+
className="action-menu-separator"
87+
role="separator"
88+
/>
89+
) : (
90+
<button
91+
key={key}
92+
type="button"
93+
className={[
94+
"action-menu-item",
95+
item.danger && "danger",
96+
item.loading && "loading",
97+
]
98+
.filter(Boolean)
99+
.join(" ")}
100+
onClick={() => {
101+
item.onClick();
102+
close();
103+
}}
104+
disabled={item.disabled === true || item.loading === true}
105+
>
106+
{item.loading ? (
107+
<VscodeProgressRing className="action-menu-spinner" />
108+
) : (
109+
<VscodeIcon name={item.icon} className="action-menu-icon" />
110+
)}
111+
<span>{item.label}</span>
112+
</button>
113+
),
114+
)}
110115
</div>
111116
)}
112117
</div>

packages/tasks/src/components/WorkspaceLogs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function WorkspaceLogs({ task }: { task: Task }) {
1919
{lines.length === 0 ? (
2020
<LogViewerPlaceholder>Waiting for logs...</LogViewerPlaceholder>
2121
) : (
22-
lines.map((line, i) => <LogLine key={i}>{line}</LogLine>)
22+
lines.map((entry) => <LogLine key={entry.id}>{entry.text}</LogLine>)
2323
)}
2424
</LogViewer>
2525
);

packages/tasks/src/hooks/useFollowScroll.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface ScrollableElement extends HTMLElement {
1717
*/
1818
export function useFollowScroll(): RefObject<HTMLDivElement | null> {
1919
const ref = useRef<HTMLDivElement>(null);
20-
const atBottom = useRef(true);
20+
const atBottomRef = useRef(true);
2121

2222
useEffect(() => {
2323
const sentinel = ref.current;
@@ -28,7 +28,7 @@ export function useFollowScroll(): RefObject<HTMLDivElement | null> {
2828
const container = parent as ScrollableElement;
2929

3030
function onScroll() {
31-
atBottom.current =
31+
atBottomRef.current =
3232
container.scrollMax - container.scrollPos <= BOTTOM_THRESHOLD;
3333
}
3434

@@ -41,7 +41,7 @@ export function useFollowScroll(): RefObject<HTMLDivElement | null> {
4141
});
4242

4343
const mo = new MutationObserver(() => {
44-
if (atBottom.current) {
44+
if (atBottomRef.current) {
4545
scrollToBottom();
4646
}
4747
});

packages/tasks/src/hooks/useWorkspaceLogs.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import { useEffect, useState } from "react";
22

33
import { useTasksApi } from "./useTasksApi";
44

5+
export interface LogEntry {
6+
id: number;
7+
text: string;
8+
}
9+
510
/**
611
* Subscribes to workspace log lines pushed from the extension.
712
* Batches updates per animation frame to avoid excessive re-renders
813
* when many lines arrive in quick succession.
914
*/
10-
export function useWorkspaceLogs(): string[] {
15+
export function useWorkspaceLogs(): LogEntry[] {
1116
const { onWorkspaceLogsAppend, stopStreamingWorkspaceLogs } = useTasksApi();
12-
const [lines, setLines] = useState<string[]>([]);
17+
const [lines, setLines] = useState<LogEntry[]>([]);
1318

1419
useEffect(() => {
1520
let pending: string[] = [];
@@ -22,7 +27,13 @@ export function useWorkspaceLogs(): string[] {
2227
const batch = pending;
2328
pending = [];
2429
frame = 0;
25-
setLines((prev) => prev.concat(batch));
30+
setLines((prev) => {
31+
const entries = batch.map((text, i) => ({
32+
id: prev.length + i,
33+
text,
34+
}));
35+
return prev.concat(entries);
36+
});
2637
});
2738
}
2839
});

packages/webview-shared/src/react/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function useMessage<T>(handler: (message: T) => void): void {
2222
* Hook to manage webview state with VS Code's state API
2323
*/
2424
export function useVsCodeState<T>(initialState: T): [T, (state: T) => void] {
25-
const [state, setLocalState] = useState<T>((): T => {
25+
const [localState, setLocalState] = useState<T>((): T => {
2626
const saved = getState<T>();
2727
return saved ?? initialState;
2828
});
@@ -32,5 +32,5 @@ export function useVsCodeState<T>(initialState: T): [T, (state: T) => void] {
3232
setState(newState);
3333
}, []);
3434

35-
return [state, setVsCodeState];
35+
return [localState, setVsCodeState];
3636
}

packages/webview-shared/src/react/useIpc.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,20 @@ type NotificationHandler = (data: unknown) => void;
4343
*/
4444
export function useIpc(options: UseIpcOptions = {}) {
4545
const { timeoutMs = DEFAULT_TIMEOUT_MS } = options;
46-
const pendingRequests = useRef<Map<string, PendingRequest>>(new Map());
47-
const notificationHandlers = useRef<Map<string, Set<NotificationHandler>>>(
46+
const pendingRequestsRef = useRef<Map<string, PendingRequest>>(new Map());
47+
const notificationHandlersRef = useRef<Map<string, Set<NotificationHandler>>>(
4848
new Map(),
4949
);
5050

5151
// Cleanup on unmount
5252
useEffect(() => {
5353
return () => {
54-
for (const req of pendingRequests.current.values()) {
54+
for (const req of pendingRequestsRef.current.values()) {
5555
clearTimeout(req.timeout);
5656
req.reject(new Error("Component unmounted"));
5757
}
58-
pendingRequests.current.clear();
59-
notificationHandlers.current.clear();
58+
pendingRequestsRef.current.clear();
59+
notificationHandlersRef.current.clear();
6060
};
6161
}, []);
6262

@@ -71,11 +71,11 @@ export function useIpc(options: UseIpcOptions = {}) {
7171

7272
// Response handling (has requestId + success)
7373
if ("requestId" in msg && "success" in msg) {
74-
const pending = pendingRequests.current.get(msg.requestId);
74+
const pending = pendingRequestsRef.current.get(msg.requestId);
7575
if (!pending) return;
7676

7777
clearTimeout(pending.timeout);
78-
pendingRequests.current.delete(msg.requestId);
78+
pendingRequestsRef.current.delete(msg.requestId);
7979

8080
if (msg.success) {
8181
pending.resolve(msg.data);
@@ -87,7 +87,7 @@ export function useIpc(options: UseIpcOptions = {}) {
8787

8888
// Notification handling (has type, no requestId)
8989
if ("type" in msg && !("requestId" in msg)) {
90-
const handlers = notificationHandlers.current.get(msg.type);
90+
const handlers = notificationHandlersRef.current.get(msg.type);
9191
if (handlers) {
9292
for (const h of handlers) {
9393
h(msg.data);
@@ -113,13 +113,13 @@ export function useIpc(options: UseIpcOptions = {}) {
113113

114114
return new Promise((resolve, reject) => {
115115
const timeout = setTimeout(() => {
116-
if (pendingRequests.current.has(requestId)) {
117-
pendingRequests.current.delete(requestId);
116+
if (pendingRequestsRef.current.has(requestId)) {
117+
pendingRequestsRef.current.delete(requestId);
118118
reject(new Error(`Request timeout: ${definition.method}`));
119119
}
120120
}, timeoutMs);
121121

122-
pendingRequests.current.set(requestId, {
122+
pendingRequestsRef.current.set(requestId, {
123123
resolve: resolve as (value: unknown) => void,
124124
reject,
125125
timeout,
@@ -162,20 +162,20 @@ export function useIpc(options: UseIpcOptions = {}) {
162162
callback: (data: D) => void,
163163
): () => void {
164164
const method = definition.method;
165-
let handlers = notificationHandlers.current.get(method);
165+
let handlers = notificationHandlersRef.current.get(method);
166166
if (!handlers) {
167167
handlers = new Set();
168-
notificationHandlers.current.set(method, handlers);
168+
notificationHandlersRef.current.set(method, handlers);
169169
}
170170
handlers.add(callback as NotificationHandler);
171171

172172
// Return unsubscribe function
173173
return () => {
174-
const h = notificationHandlers.current.get(method);
174+
const h = notificationHandlersRef.current.get(method);
175175
if (h) {
176176
h.delete(callback as NotificationHandler);
177177
if (h.size === 0) {
178-
notificationHandlers.current.delete(method);
178+
notificationHandlersRef.current.delete(method);
179179
}
180180
}
181181
};

0 commit comments

Comments
 (0)