diff --git a/frontend/eslint-suppressions.json b/frontend/eslint-suppressions.json new file mode 100644 index 00000000..9eb1d0c3 --- /dev/null +++ b/frontend/eslint-suppressions.json @@ -0,0 +1,253 @@ +{ + "src/components/BootGate.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ChannelPage.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/components/ChannelTags.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/components/ChannelsProvider.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ConfigPanel.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/DateFilterSelect.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/DebugRecorder.dom.test.tsx": { + "react-hooks/globals": { + "count": 1 + } + }, + "src/components/DebugRecorder.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/DiagnosticsSettings.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/DownloadCenter.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/Feed.tsx": { + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/FeedFiltersProvider.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/GlassTuner.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + }, + "react-hooks/use-memo": { + "count": 1 + } + }, + "src/components/Home.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/components/IssueComposer.tsx": { + "react-hooks/immutability": { + "count": 2 + } + }, + "src/components/MyLogViewer.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/NavigationProvider.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/PersonalViewsModal.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/PlayerModal.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/Playlists.tsx": { + "react-hooks/immutability": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/PlexBrowse.tsx": { + "react-hooks/immutability": { + "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/PlexInfo.tsx": { + "react-hooks/static-components": { + "count": 3 + } + }, + "src/components/PlexPlayer.tsx": { + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/PlexSearchBox.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/SavedViewsProvider.tsx": { + "react-hooks/set-state-in-effect": { + "count": 3 + } + }, + "src/components/Scheduler.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/components/SetupWizard.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/TagFilterChips.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/WatchPage.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/Welcome.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/WizardProvider.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/comfortPin.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/downloadRow.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/Avatar.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/BackToTop.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/Combobox.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/CommandPalette.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/components/ui/Pager.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/Toaster.tsx": { + "react-hooks/globals": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + }, + "react-hooks/static-components": { + "count": 1 + } + }, + "src/components/ui/anchoredPopoverHooks.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/ui/sortableA11y.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/usePlexPlaylistEdits.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/plexPlayerHooks.subtitle.dom.test.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/lib/plexPlayerHooks.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/useCardPager.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/useFocusKeeper.restore.dom.test.tsx": { + "react-hooks/globals": { + "count": 1 + } + } +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 4dd46e4e..baf79b1d 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -187,6 +187,25 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, + // eslint-plugin-react-hooks 7's `recommended` is no longer two rules — it carries the whole + // React Compiler rule set (E27 S1). On this codebase that lands 283 findings, and they were + // triaged rather than bulk-suppressed, because the two answers are different: + // + // react-hooks/refs 217 — OFF, see below. + // the other 66 — real, and suppressed into eslint-suppressions.json as a RATCHET: + // new violations fail the gate, the existing list burns down in E27 S2 + // (45 of them are `set-state-in-effect`, which IS that sprint's subject). + // + // `refs` is off because it fires on this app's popover architecture, not on its history, so + // suppressing it would not help the next popover anyone writes. Measured over the 217: + // 79 are `pop.refs.setReference` / `getReferenceProps` — @floating-ui/react's callback-ref + // setters, which are the documented way to use it — and 74 more are passing the popover handle + // down as a prop (`popover={pop}`, `ref={fade.ref}`), which is what `components/ui`'s popover + // engine is FOR. That is 153 of 217 flagged for having the word `refs` in an object that is not + // a React ref. The remaining ~64 are genuine render-phase `someRef.current = x` writes and are + // real; they are E27 S2's work, not this rule's, because the rule cannot tell them apart here. + // Re-evaluate when the rule can: it shipped in 1.0.0 and this looks like over-approximation. + "react-hooks/refs": "off", "local/jsx-comment-text": "error", "local/focus-safe-disabled": "error", "local/literal-a11y-text": "error", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8d6a49e1..9294d8cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,8 +18,8 @@ "hls.js": "^1.6.16", "i18next": "^23.11.5", "lucide-react": "^0.408.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "19.3.0", + "react-dom": "19.3.0", "react-i18next": "^14.1.2" }, "devDependencies": { @@ -28,12 +28,13 @@ "@tailwindcss/postcss": "^4.3.3", "@testing-library/react": "^16.3.2", "@types/node": "^24.13.6", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "^4.3.1", + "babel-plugin-react-compiler": "1.0.0", "eslint": "9.39.1", "eslint-config-prettier": "10.1.8", - "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.4.24", "globals": "16.5.0", "jsdom": "^25.0.1", @@ -2922,32 +2923,24 @@ "undici-types": "~7.18.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "dev": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.3.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -3469,6 +3462,16 @@ "dev": true, "license": "MIT" }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4084,16 +4087,23 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { @@ -4586,6 +4596,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/hls.js": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", @@ -4776,6 +4803,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5279,18 +5307,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5824,28 +5840,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.3.0" } }, "node_modules/react-i18next": { @@ -6016,13 +6028,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -6836,6 +6845,19 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index ab3e43d6..904a0586 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,8 +34,8 @@ "hls.js": "^1.6.16", "i18next": "^23.11.5", "lucide-react": "^0.408.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "19.3.0", + "react-dom": "19.3.0", "react-i18next": "^14.1.2" }, "devDependencies": { @@ -44,12 +44,13 @@ "@tailwindcss/postcss": "^4.3.3", "@testing-library/react": "^16.3.2", "@types/node": "^24.13.6", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "^4.3.1", + "babel-plugin-react-compiler": "1.0.0", "eslint": "9.39.1", "eslint-config-prettier": "10.1.8", - "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.4.24", "globals": "16.5.0", "jsdom": "^25.0.1", diff --git a/frontend/reactPlugin.ts b/frontend/reactPlugin.ts new file mode 100644 index 00000000..044a1da6 --- /dev/null +++ b/frontend/reactPlugin.ts @@ -0,0 +1,26 @@ +import react from "@vitejs/plugin-react"; +import reactCompiler from "babel-plugin-react-compiler"; + +// ONE React transform, shared by vite.config.ts and vitest.config.ts (E27 S1). +// +// It is a shared factory rather than two `react()` calls because the two configs must not drift: +// if the compiler ran only in the production build, every vitest run would be exercising code the +// user never receives, and a compiler-induced behaviour change would ship green. That is precisely +// the defect `0.93.2` was released to fix — a gate and a build image that had quietly stopped being +// the same thing — so the rule here is that the gate compiles the code the same way the build does. +// +// The React Compiler auto-memoises: it infers what `useCallback`/`useMemo` were being hand-written +// for and applies the equivalent itself. It is enabled repo-wide with no opt-out list because it +// bails out on its own for anything it cannot prove safe (its rules are also linted, via +// eslint-plugin-react-hooks). `target` follows the installed React major and must move with it. +// +// The plugin is IMPORTED rather than named by the usual `"babel-plugin-react-compiler"` string. Two +// reasons, both about the gate: a string is invisible to knip, which then reports the dependency as +// unused, and — worse — a typo in that string is a silent no-op that ships an uncompiled bundle, +// while a bad import is a resolve error on the tsc lane. +export const reactWithCompiler = () => + react({ + babel: { + plugins: [[reactCompiler, { target: "19" }]], + }, + }); diff --git a/frontend/src/components/PlexPlayerSettings.tsx b/frontend/src/components/PlexPlayerSettings.tsx index 0a8e9211..994f1d86 100644 --- a/frontend/src/components/PlexPlayerSettings.tsx +++ b/frontend/src/components/PlexPlayerSettings.tsx @@ -12,7 +12,10 @@ type SettingsTab = "sync" | "playback" | "subtitle" | "clock"; // panel owns no state of its own. `now` is the live wall clock (passed in, not a second ticker) used // only for the clock-date-style preview buttons. export function PlexPlayerSettings(props: { - menuRef: RefObject; + // `| null` is not optional under React 19's types: `useRef(null)` is now `RefObject`, + // which is the honest shape — the element is null until the div mounts. The old `RefObject` lied + // about that and 18's types let it. + menuRef: RefObject; settingsTabs: { key: SettingsTab; label: string }[]; activeTab: SettingsTab; setSettingsTab: (tab: SettingsTab) => void; diff --git a/frontend/src/components/ui/VirtualGrid.tsx b/frontend/src/components/ui/VirtualGrid.tsx index 9180df48..98ff4a8e 100644 --- a/frontend/src/components/ui/VirtualGrid.tsx +++ b/frontend/src/components/ui/VirtualGrid.tsx @@ -127,6 +127,15 @@ export default function VirtualGrid({ const rows = useMemo(() => chunk(items, colCount), [items, colCount]); + // The React Compiler (E27 S1) SKIPS memoising this whole component, and says so: TanStack + // Virtual's `useVirtualizer()` hands back functions that cannot be memoised without going stale, + // so the compiler bails rather than risk a frozen viewport. That is the correct trade here — this + // component's cost is the row window it mounts, not its render closures — and it is the ONE such + // site in the app, which is why this is a targeted disable rather than the rule being turned off: + // a second incompatible library anywhere else must still stop the gate. The caution in the rule's + // own text applies, though: values taken from this API and handed to a memoised child can go + // stale, so keep the virtualizer's output inside this component. + // eslint-disable-next-line react-hooks/incompatible-library -- see above; @tanstack/react-virtual const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => scrollEl, diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 60b25c23..1c9ac7a5 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -30,5 +30,14 @@ "allowJs": true, "checkJs": true }, - "include": ["vite.config.ts", "vitest.config.ts", "tailwind.config.js", "postcss.config.js"] + // reactPlugin.ts is the ONE React transform both configs import (E27 S1) — it is build tooling, + // not app source, so it belongs here and not under tsconfig.json's ["src"]. A file imported by a + // listed one but absent from `include` is TS6307, not a silent pass. + "include": [ + "vite.config.ts", + "vitest.config.ts", + "reactPlugin.ts", + "tailwind.config.js", + "postcss.config.js" + ] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 970969ee..b49c66a3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { reactWithCompiler } from "./reactPlugin"; // During `vite dev` (host with Node), proxy API calls to the backend container. // Use 127.0.0.1 (not "localhost") so Node doesn't resolve to IPv6 ::1, which the @@ -20,7 +20,7 @@ const proxy = { }; export default defineConfig({ - plugins: [react()], + plugins: [reactWithCompiler()], server: { port: 5173, proxy }, build: { outDir: "dist" }, }); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 96c816ae..3545f74a 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "vitest/config"; -import react from "@vitejs/plugin-react"; +import { reactWithCompiler } from "./reactPlugin"; // Unit + component/hook tests, deliberately separate from vite.config.ts so the production build // never has to load vitest. The DEFAULT environment is node (the pure src/lib helpers — fast, no @@ -9,7 +9,7 @@ import react from "@vitejs/plugin-react"; // free React transform; it is a no-op for the plain .ts node tests (they carry no JSX). jsdom is a // devDependency referenced only via that docblock env string — knip's vitest plugin still sees it. export default defineConfig({ - plugins: [react()], + plugins: [reactWithCompiler()], test: { // .tsx included on purpose: a test file the glob misses does not fail, it is simply never // collected, and the gate still prints green.