Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
npm run install:all
- name: Lint
run: npm run lint:all
- name: Test webview lint configuration
run: npm run test:lint --prefix webview-ui
- name: Smoke test (Linux)
run: xvfb-run -a npm run test
if: runner.os == 'Linux'
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/fuzzing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ jobs:
cache: "npm"

- name: Install dependencies
run: npm ci --legacy-peer-deps
run: npm ci

- name: Install webview dependencies
run: |
cd webview-ui
npm ci --legacy-peer-deps
npm ci
cd ..

- name: Build webview
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
- name: Install webview dependencies
run: |
cd webview-ui
npm ci --legacy-peer-deps
npm ci
cd ..

- name: Build webview
Expand Down
25 changes: 25 additions & 0 deletions docs/book/src/development/webview-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ For commands that require a webview (see [guidance](https://code.visualstudio.co

Run `npm run install:all` to install package dependencies for both the extension and webview project.

### Linting

Use Node.js 22 or newer. Run `npm run lint:all` from the repository root and
`npm run test:lint --prefix webview-ui` for the lint configuration regression tests.

The webview uses `@eslint-react/eslint-plugin` with ESLint 10 instead of
`eslint-plugin-react`, whose published peer dependency range does not support ESLint 10.
Both projects install with normal npm peer validation; no package downgrade or peer override is needed.

The React configuration explicitly maps the previous recommended checks rather than enabling
the replacement plugin's broader recommended preset. `eslint-plugin-react-hooks` remains
responsible for Hooks checks, preserving existing rule names and suppression comments.

This migration is not exact rule parity:

- Missing keys, JSX comment text, children props, unsafe target links, unknown DOM properties,
state mutation, conflicting inner HTML/children, and deprecated lifecycle/DOM API checks remain enabled.
- Display-name checks cover anonymous wrapped components and now also contexts, rather than all
component forms checked by the old plugin.
- TypeScript and React 19 types cover duplicate explicit JSX attributes, unresolved JSX names,
typed props, string refs, removed `isMounted` calls, and missing class render returns. These checks
require the TypeScript build, not just lint, and cannot protect untyped `any` usage.
- The old `react/no-unescaped-entities` check has no equivalent in the replacement plugin and is
no longer enforced. TypeScript does not cover valid JSX text containing unescaped quotes/apostrophes.

## Development/Debugging

### File structure
Expand Down
118 changes: 59 additions & 59 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@
]
},
"scripts": {
"install:all": "npm install --legacy-peer-deps && cd webview-ui && npm install --legacy-peer-deps",
"install:all": "npm install && cd webview-ui && npm install",
"lint:all": "npx eslint . && cd webview-ui && npm run lint",
"lint-fix:all": "npx eslint . --fix && cd webview-ui && npm run lint-fix",
"prettier-format": "prettier --config .prettierrc . --write",
Expand Down
38 changes: 25 additions & 13 deletions webview-ui/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import eslintReact from "@eslint-react/eslint-plugin";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
Expand All @@ -20,14 +21,31 @@ export default [
ignores: ["**/node_modules/", "**/dist/", "**/*.js", "**/*.cjs"],
},
...fixupConfigRules(
compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
),
compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"),
),
{
files: ["**/*.{ts,tsx}"],
plugins: { "@eslint-react": eslintReact },
// Map the previous React checks; migration gaps are documented in webview-development.md.
rules: {
"@eslint-react/no-missing-component-display-name": "error",
"@eslint-react/no-missing-context-display-name": "error",
"@eslint-react/no-missing-key": "error",
"@eslint-react/dom-no-unsafe-target-blank": "error",
"@eslint-react/dom-no-unknown-property": "error",
"@eslint-react/jsx-no-comment-textnodes": "error",
"@eslint-react/jsx-no-children-prop": "error",
"@eslint-react/dom-no-dangerously-set-innerhtml-with-children": "error",
"@eslint-react/no-direct-mutation-state": "error",
"@eslint-react/no-component-will-mount": "error",
"@eslint-react/no-component-will-receive-props": "error",
"@eslint-react/no-component-will-update": "error",
"@eslint-react/dom-no-find-dom-node": "error",
"@eslint-react/dom-no-render": "error",
"@eslint-react/dom-no-hydrate": "error",
"@eslint-react/dom-no-render-return-value": "error",
},
},
{
plugins: {
"@typescript-eslint": fixupPluginRules(typescriptEslint),
Expand All @@ -51,12 +69,6 @@ export default [
},
},

settings: {
react: {
version: "detect",
},
},

rules: {
"@typescript-eslint/naming-convention": [
"error",
Expand Down
58 changes: 58 additions & 0 deletions webview-ui/eslint.config.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { ESLint } from "eslint";

// CI providers set `CI=true`, which makes typescript-eslint infer "single run" mode. In that mode the
// type-aware program is compiled ahead of time from the files on disk, so the first `lintText` call for
// a given path is parsed from the on-disk contents instead of the snippet we pass in. Disabling the
// inference keeps `lintText` authoritative locally and on CI alike.
const eslint = new ESLint({
overrideConfig: {
languageOptions: {
parserOptions: {
disallowAutomaticSingleRunInference: true,
},
},
},
});
const filePath = "src/components/ProgressRing.tsx";

for (const [name, code, ruleId] of [
[
"missing fragment keys",
"export const Items = () => [1, 2].map((value) => <><span>{value}</span></>);",
"@eslint-react/no-missing-key",
],
[
"unsafe target links",
'export const Link = () => <a href="https://example.com" target="_blank">Link</a>;',
"@eslint-react/dom-no-unsafe-target-blank",
],
[
"unknown DOM properties",
'export const Label = () => <div class="label" />;',
"@eslint-react/dom-no-unknown-property",
],
[
"conditional Hooks",
'import { useState } from "react"; export function Item({ active }: { active: boolean }) { if (active) { useState(0); } return <div />; }',
"react-hooks/rules-of-hooks",
],
]) {
test(`reports ${name}`, async () => {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0);
assert.ok(
result.messages.some((message) => message.ruleId === ruleId),
JSON.stringify(result.messages),
);
});
}

test("accepts keyed JSX without legacy React scope rules", async () => {
const [result] = await eslint.lintText(
'export function Items() { return ["a", "b"].map((value) => <span key={value}>{value}</span>); }',
{ filePath },
);
assert.deepEqual(result.messages, []);
});
Loading
Loading