test(frontend): a React Compiler ratchet over everything the build ships
compilerCoverage.contract.test.ts used to check two files. It now runs the real compiler over all of src (minus tests) and holds the exact set it skips today, file -> function -> category: 93 functions. A new skip or a category change is red, and so is a listed skip that has gone, so the list only shrinks. Channels and DataTable must also show up as compiled, so a harness that compiled nothing cannot pass. Measured against the audit's scan: no function this sprint touched started to skip. PlexPersonView and useOverlayOrder now compile. reactPlugin.ts claimed the compiler's rules were linted, which was false while react-hooks/refs was off. That comment and the refs block in eslint.config.js now say what holds the line and why refs is still off: 245 real hits, pinned by the ratchet, burned down from S3.
This commit is contained in:
@@ -1,66 +1,142 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { transformSync } from "@babel/core";
|
||||
import reactCompiler from "babel-plugin-react-compiler";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { compileReport, shippedSources, type CompileReport } from "./compilerSkips.testkit";
|
||||
|
||||
// The React Compiler bails out of a component it cannot prove safe WITHOUT a word: the component
|
||||
// still works, it just re-renders everything under it on every render. Its main tripwire, a ref read
|
||||
// or written during render, is not linted here (`react-hooks/refs` is off, see eslint.config.js), so
|
||||
// nothing caught it. The channel manager is where it hurt (the owner, 2026-10-03): Channels and
|
||||
// DataTable both bailed out, every open, close and reposition of a row's right-click menu rebuilt
|
||||
// the rows and columns, and on "All" rows the menu took ~0.9 s to open and ~0.6 s to close.
|
||||
// still works, it just re-renders everything under it on every render. The channel manager is where
|
||||
// it hurt (the owner, 2026-10-03): Channels and DataTable both bailed out, every open, close and
|
||||
// reposition of a row's right-click menu rebuilt the rows and columns, and on "All" rows the menu
|
||||
// took ~0.9 s to open and ~0.6 s to close. The E32 audit then found 95 such functions across src.
|
||||
//
|
||||
// This runs the real compiler (the options reactPlugin.ts gives it) over the files that must stay
|
||||
// compiled and fails on any function it skips, with the compiler's own reason.
|
||||
const MUST_COMPILE = ["components/Channels.tsx", "components/ui/DataTable.tsx"];
|
||||
// A RATCHET over everything the build compiles (E32 S1). KNOWN_SKIPS is the exact set the compiler
|
||||
// skips today, `file → function → the compiler's category`; everything else must compile. Both
|
||||
// directions are red:
|
||||
// • a NEW skip (or a known one changing category) — fix the function; if it truly cannot be
|
||||
// compiled, adding it here is a decision for the owner, with the reason in docs/gates.md;
|
||||
// • a skip that is GONE — delete its entry, so the list only ever shrinks.
|
||||
// E32 S3+ burns it down: the popover engine (Refs: `getReferenceProps()` / `refs.setReference`
|
||||
// passed during render), render-phase ref writes, try/finally (Todo), the two exhaustive-deps
|
||||
// suppressions. The end state is IncompatibleLibrary alone (VirtualGrid on TanStack Virtual).
|
||||
const KNOWN_SKIPS: CompileReport["skipped"] = {
|
||||
"components/AboutCell.tsx": { AboutCell: "Refs" },
|
||||
"components/ActiveFilterChips.tsx": { OverflowPopover: "Refs" },
|
||||
"components/AddToPlaylist.tsx": { AddToPlaylist: "Refs", MembershipPanel: "Todo" },
|
||||
"components/BootGate.tsx": { BootGate: "Refs" },
|
||||
"components/ChannelStatsPopover.tsx": { ChannelStatsPopover: "Refs" },
|
||||
"components/ChannelTags.tsx": {
|
||||
ChannelTagsControl: "Refs",
|
||||
SuggestedChipsLane: "Refs",
|
||||
TagEditorMenu: "Refs",
|
||||
},
|
||||
"components/ChannelsProvider.tsx": { ChannelsProvider: "Refs" },
|
||||
"components/ChatThread.tsx": { ChatThread: "Refs" },
|
||||
"components/ConfigPanel.tsx": { ConfigPanel: "Refs" },
|
||||
"components/DateFilterSelect.tsx": { DateFilterSelect: "Refs" },
|
||||
"components/DownloadCenter.tsx": { DownloadCenter: "Todo" },
|
||||
"components/DownloadDialog.tsx": { DownloadDialog: "Todo" },
|
||||
"components/Feed.tsx": { Feed: "Immutability" },
|
||||
"components/IssueComposer.tsx": { Attachments: "Immutability", ComposerBody: "Refs" },
|
||||
"components/KeyGate.tsx": { KeyGate: "Todo" },
|
||||
"components/LanguageSwitcher.tsx": { LanguageSwitcher: "Refs" },
|
||||
"components/Messages.tsx": { ConversationList: "Refs" },
|
||||
"components/NavSidebar.tsx": { NavSidebar: "Refs" },
|
||||
"components/NavigationProvider.tsx": { NavigationProvider: "Refs" },
|
||||
"components/OnboardingWizard.tsx": { OnboardingWizard: "Refs" },
|
||||
"components/PersonalViewsModal.tsx": { PersonalViewsModal: "Refs" },
|
||||
"components/PlayerModal.tsx": { PlayerModal: "Todo" },
|
||||
"components/PlaylistLimitsHint.tsx": { PlaylistLimitsHint: "Refs" },
|
||||
"components/Playlists.tsx": { Playlists: "Immutability" },
|
||||
"components/PlaylistsRail.tsx": { PlaylistsRail: "Todo" },
|
||||
"components/PlexBrowse.tsx": { PlexBrowse: "Immutability" },
|
||||
"components/PlexCollectionEditor.tsx": { PlexCollectionEditor: "Todo" },
|
||||
"components/PlexCollectionView.tsx": { PlexCollectionView: "Todo" },
|
||||
"components/PlexPlayer.tsx": { PlexPlayer: "Suppression" },
|
||||
"components/PlexPlaylistAdd.tsx": { PlexPlaylistAdd: "Todo" },
|
||||
"components/PlexPlaylistView.tsx": { PlexPlaylistView: "Refs" },
|
||||
"components/PlexStatsOverlay.tsx": { PlexStatsOverlay: "Todo" },
|
||||
"components/SavedViewsPopover.tsx": { SavedViewsPopover: "Refs" },
|
||||
"components/SavedViewsProvider.tsx": { SavedViewsProvider: "Todo" },
|
||||
"components/SettingsPanel.tsx": { SignInMethods: "Todo" },
|
||||
"components/SetupWizard.tsx": { SetupWizard: "Todo" },
|
||||
"components/SyncStatus.tsx": { SyncStatus: "Refs" },
|
||||
"components/TagFilterChips.tsx": { TagFilterChips: "Refs" },
|
||||
"components/TagManager.tsx": { TagManager: "Refs", TagRow: "Refs" },
|
||||
"components/UpNextCard.tsx": { UpNextCard: "Refs" },
|
||||
"components/UploadDialog.tsx": { UploadDialog: "Todo" },
|
||||
"components/VideoEditor.tsx": { VideoEditor: "Refs" },
|
||||
"components/ViewSwitcher.tsx": { ViewSwitcher: "Refs" },
|
||||
"components/WatchPage.tsx": { WatchPage: "Todo", useMediaSession: "Todo" },
|
||||
"components/Welcome.tsx": { AuthCard: "Todo", Lightbox: "Refs" },
|
||||
"components/YearFilter.tsx": { YearFilter: "Refs" },
|
||||
"components/plexViews.tsx": { PlexSeasonView: "Todo", PlexShowView: "Todo" },
|
||||
"components/ui/ActionMenu.tsx": { ActionMenu: "Refs" },
|
||||
"components/ui/AnchoredPopover.tsx": { PopoverSurfaceInner: "Refs" },
|
||||
"components/ui/Combobox.tsx": { Combobox: "Refs" },
|
||||
"components/ui/FilterSelect.tsx": { FilterSelect: "Refs" },
|
||||
"components/ui/FocusSafeButton.tsx": { FocusSafeButton: "Refs" },
|
||||
"components/ui/Modal.tsx": { Modal: "Refs" },
|
||||
"components/ui/NavRail.tsx": { NavRailGroup: "Refs" },
|
||||
"components/ui/PageScroller.tsx": { PageScroller: "Refs" },
|
||||
"components/ui/PriorityStepper.tsx": { PriorityStepper: "Refs" },
|
||||
"components/ui/SidePanel.tsx": { SidePanel: "Refs" },
|
||||
"components/ui/SortControl.tsx": { SortControl: "Refs" },
|
||||
"components/ui/Toaster.tsx": { Toaster: "Globals" },
|
||||
"components/ui/Tooltip.tsx": { Tooltip: "Refs" },
|
||||
"components/ui/VirtualGrid.tsx": { VirtualGrid: "IncompatibleLibrary" },
|
||||
"components/ui/anchoredPopoverHooks.ts": { useAnchoredPopover: "Refs" },
|
||||
"components/ui/sortableA11y.ts": { useSortableA11y: "Immutability" },
|
||||
"components/ui/tableHeader.tsx": { FilterFunnel: "Refs", FilterOptionList: "Refs" },
|
||||
"components/useDebugRecording.ts": { useDebugRecording: "Todo" },
|
||||
"components/useDownloadActions.tsx": { useDownloadActions: "Todo" },
|
||||
"components/usePlaylistActions.tsx": { usePlaylistActions: "Todo" },
|
||||
"components/usePlexPlaylistEdits.ts": { usePlexPlaylistEdits: "Todo" },
|
||||
"lib/history.ts": { useBackToClose: "Refs", useBackToExit: "Refs", useHistorySubview: "Refs" },
|
||||
"lib/keymap.ts": { useGlobalKeymap: "Refs" },
|
||||
"lib/layerStack.ts": { useLayer: "Refs" },
|
||||
"lib/listNav.ts": { useListNav: "Refs" },
|
||||
"lib/playerModalHooks.ts": { useYtBigView: "Refs", useYtProgress: "Suppression" },
|
||||
"lib/plexPlayerHooks.ts": { useAutoSkip: "Refs", usePlexTracks: "Todo" },
|
||||
"lib/useChannelPriority.ts": { useChannelPriority: "Refs" },
|
||||
"lib/useChipOverflowFit.ts": { useChipOverflowFit: "Refs" },
|
||||
"lib/useDismiss.ts": { useDismiss: "Refs" },
|
||||
"lib/usePref.ts": { usePref: "Refs" },
|
||||
"lib/useUndoable.ts": { useUndoable: "Refs" },
|
||||
"lib/useUrlMirror.ts": { useUrlMirror: "Refs" },
|
||||
};
|
||||
|
||||
function bailouts(path: string): string[] {
|
||||
const source = readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
||||
const found: string[] = [];
|
||||
transformSync(source, {
|
||||
filename: path,
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
parserOpts: { plugins: ["jsx", "typescript"] },
|
||||
plugins: [
|
||||
[
|
||||
reactCompiler,
|
||||
{
|
||||
target: "19",
|
||||
logger: {
|
||||
logEvent: (
|
||||
_file: string | null,
|
||||
e: { kind: string; fnLoc?: unknown; detail?: unknown },
|
||||
) => {
|
||||
if (
|
||||
e.kind !== "CompileError" &&
|
||||
e.kind !== "CompileSkip" &&
|
||||
e.kind !== "PipelineError"
|
||||
)
|
||||
return;
|
||||
const loc = e.fnLoc as { start?: { line?: number } } | undefined;
|
||||
const d = e.detail as
|
||||
| { options?: { reason?: string; details?: { message?: string }[] } }
|
||||
| { reason?: string; details?: { message?: string }[] }
|
||||
| undefined;
|
||||
const o =
|
||||
d && "options" in d
|
||||
? d.options
|
||||
: (d as { reason?: string; details?: { message?: string }[] });
|
||||
found.push(
|
||||
`line ${loc?.start?.line}: ${o?.details?.[0]?.message ?? o?.reason ?? e.kind}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
return found;
|
||||
}
|
||||
// The files a regression hurt most: compiled, and they must stay so (a positive check too, so a
|
||||
// harness that compiled nothing could not pass as "no skips").
|
||||
const MUST_COMPILE: Record<string, string[]> = {
|
||||
"components/Channels.tsx": ["Channels"],
|
||||
"components/ui/DataTable.tsx": ["DataTable"],
|
||||
};
|
||||
|
||||
type Row = [file: string, fn: string, category: string];
|
||||
const rows = (skips: CompileReport["skipped"]): Row[] =>
|
||||
Object.entries(skips).flatMap(([file, fns]) =>
|
||||
Object.entries(fns).map(([fn, category]): Row => [file, fn, category]),
|
||||
);
|
||||
|
||||
describe("the React Compiler", () => {
|
||||
it.each(MUST_COMPILE)("compiles every function in %s", (path) => {
|
||||
expect(bailouts(path)).toEqual([]);
|
||||
// ~8 s for the whole tree, once, shared by the checks below.
|
||||
const report = compileReport(shippedSources());
|
||||
const actual = rows(report.skipped);
|
||||
const known = rows(KNOWN_SKIPS);
|
||||
const key = ([f, fn]: Row) => `${f} → ${fn}`;
|
||||
|
||||
it("skips nothing new (fix it, or the owner decides to list it)", () => {
|
||||
const knownAs = new Map(known.map((r) => [key(r), r[2]]));
|
||||
expect(
|
||||
actual.filter((r) => knownAs.get(key(r)) !== r[2]).map((r) => `${key(r)}: ${r[2]}`),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("lists no skip that has gone (delete it — the list only shrinks)", () => {
|
||||
const actualAs = new Map(actual.map((r) => [key(r), r[2]]));
|
||||
expect(known.filter((r) => !actualAs.has(key(r))).map(key)).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(Object.entries(MUST_COMPILE))("compiles %s", (file, fns) => {
|
||||
expect(report.skipped[file] ?? {}).toEqual({});
|
||||
for (const fn of fns) expect(report.compiled[file]).toContain(fn);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user