build(frontend): React 18 -> 19 with the React Compiler (E27 S1)
The compiler auto-memoises, which is the point: it infers what the app's 112 `useCallback` and 48 `useMemo` call sites were hand-written for and applies the equivalent itself. It is on repo-wide with no opt-out list — it bails on anything it cannot prove safe, and says so through the lint rules. Verified in the built bundle rather than assumed: 369 `react.memo_cache_sentinel` markers across 35 of the 76 chunks. ONE transform, shared by vite.config.ts and vitest.config.ts through reactPlugin.ts. Not two `react()` calls: if the compiler ran only in the production build, every vitest run would exercise code the user never receives and a compiler-induced change would ship green — the same gate-vs-image split 0.93.2 was released to fix. The plugin is imported rather than named by the usual string, because a string is invisible to knip and, worse, a typo in it is a silent no-op that ships an uncompiled bundle, while a bad import is a tsc error. eslint-plugin-react-hooks 5 -> 7 brings the whole compiler rule set: 283 findings, triaged rather than bulk-suppressed, because the two halves needed different answers. react-hooks/refs (217) is OFF. 153 of those 217 are this app's popover architecture, not its history: 79 are @floating-ui/react's documented `pop.refs.setReference` / `getReferenceProps` callback-ref setters, and 74 more are passing the popover handle down as a prop, which is what components/ui's popover engine is for. Suppressing them would not help the next popover anyone writes. The remaining ~64 are genuine render-phase `ref.current = x` writes and are real, but the rule cannot tell them apart here; they are E27 S2's work. The other 66 go into eslint-suppressions.json as a ratchet — new violations fail the gate, and `--prune-suppressions` makes a fixed one fail too until it is removed. 45 of them are `set-state-in-effect`, which IS E27 S2's subject, so that sprint now has a measured worklist. react-i18next deliberately stays on 14. It never declared a React-19 peer — every version from 14 to 17 asks only for `react >= 16.8.0` — and what the newer lines actually require is i18next 25/26 against our 23. That is an i18n runtime migration wearing a React hat, and it buys this sprint nothing. Two React 19 type changes: `useRef<T>(null)` is now `RefObject<T | null>` (the honest shape — the element is null until it mounts), so PlexPlayerSettings' prop follows; and reactPlugin.ts joins tsconfig.node.json's include. The compiler skips memoising VirtualGrid and reports it: TanStack Virtual's `useVirtualizer()` returns functions that cannot be memoised without going stale. That is the right trade for a component whose cost is its row window, and it is the one such site in the app, so it carries a targeted disable with the reason — a second incompatible library anywhere else must still stop the gate. Nine lanes green, vitest 1558. Browser UAT on the dev server against the still-running React 18 build as a control: boot, feed (virtualized grid, popover menu, a filter change that moved the list 195,187 -> 64 items with the URL mirrored, so no stale memoised UI), command palette, player modal — no console errors on any of it.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+78
-56
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }]],
|
||||
},
|
||||
});
|
||||
@@ -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<HTMLDivElement>;
|
||||
// `| null` is not optional under React 19's types: `useRef<T>(null)` is now `RefObject<T | null>`,
|
||||
// which is the honest shape — the element is null until the div mounts. The old `RefObject<T>` lied
|
||||
// about that and 18's types let it.
|
||||
menuRef: RefObject<HTMLDivElement | null>;
|
||||
settingsTabs: { key: SettingsTab; label: string }[];
|
||||
activeTab: SettingsTab;
|
||||
setSettingsTab: (tab: SettingsTab) => void;
|
||||
|
||||
@@ -127,6 +127,15 @@ export default function VirtualGrid<T>({
|
||||
|
||||
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,
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user