test(studio): ratchet React Compiler bail-outs per file - #3773
Draft
miguel-heygen wants to merge 2 commits into
Draft
test(studio): ratchet React Compiler bail-outs per file#3773miguel-heygen wants to merge 2 commits into
miguel-heygen wants to merge 2 commits into
Conversation
This was referenced Sep 8, 2026
Prints, for every React Compiler diagnostic, the file, the cause, and oxc-transform-react's own codeframe (line, column, caret) so an engineer lands on the exact statement instead of grepping the file for the cause. Default report and --json output are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands unit C1 (bail-out ratchet) of the Studio React Compiler coverage plan. Stacked on the Vite 8 PR (#3620). Baseline: 126 files bail out today (54 components, 72 hooks); the gate fails on any rise and the units that follow lower it to zero.
What
A per-file ratchet that fails the Studio suite when any non-test file under
packages/studio/srcgains a React Compiler bail-out.Four files, no component changed:
packages/studio/scripts/compiler-bailouts.mjs, the scan. Runsoxc-transform-reactover every non-test.ts/.tsxundersrc, groups what the compiler declines by file and cause.packages/studio/src/styles/compilerBailouts.test.ts, the gate. Same shape as the hex ratchet: per-file counts, atotal, a rise fails naming the file, a fall passes and prints the command that banks it.packages/studio/src/styles/compiler-bailouts.json, the committed baseline. 126 files, 126 bail-outs.bun run compiler:bailoutsinpackages/studio, which prints the grouped report for a human.--jsonprints the same data as{ total, files: { path: [causes] } }.The baseline is only ever written by
COMPILER_BAILOUTS_WRITE=1 bunx vitest run src/styles/compilerBailouts.test.ts. A normal run never regenerates it, and a missing baseline fails loudly naming that flag, so the number in git is always one a person chose to accept.Why
react({ compiler: true })is silent about what it declines. A component the compiler skips is emitted exactly as written: it still renders, still passes its tests, still builds, and simply is not memoized. There is no warning in the build log and nothing in a diff to notice. Turning the compiler on without a gate means the covered set can shrink for months and the only symptom is that Studio is slower than it should be.This is the alarm, and it is a vitest test rather than a lint rule because Test is a required check on
mainand Lint is not.How
Three facts about
oxc-transform-react0.149.0, each measured against this versionoutputMode: "lint"at the defaultpanicThresholdreturns zero errors even for a component written specifically to violate the rules of React. EscalatingpanicThresholdto"all_errors"is the only way a skip surfaces. This confirms the earlier finding thatlogDiagnosticsreports nothing; that option does not exist on the type at all in this version."use no memo"/"use no forget"directive is never reported. It is honoured before any diagnostic is produced, so with the escalation on, a file whose only problem is the directive looks clean. The scan counts directives from the source text instead. Areact-hookssuppression is the opposite case: the compiler already reports it as "React rule suppression prevents optimization", foreslint-disableandoxlint-disable, next-line and file-level, and for bothexhaustive-depsandrules-of-hooks. Counting those separately would double count, so the scan does not. Aneslint-disablefor an unrelated rule is correctly ignored by the compiler.The count is one per file, not one per diagnostic
Fact 2 means a diagnostic count is not monotone, and a ratchet needs monotone.
App.tsxreports 21 ref reads, all from one function; a second declined function further down reports nothing. Fixing that first function would uncover the second and the number could go up while the code got better. A gate that fires on an improvement gets disabled.So
countis one per opt-out the scan can see: one per directive, plus one if the compiler declines the file at all. That is monotone. It catches every file going 0 to 1, which is the transition that matters while the baseline is driven down, and once a file reaches 0 the gate on that file is exact. The hole it accepts, stated plainly: a second bail-out added to a file that already bails does not fire. A true per-function count needs the compiler to expose non-fatal diagnostics; it is not something a cleverer caller can recover.Scope
Scanned: every
.tsand.tsxunderpackages/studio/src. Excluded:*.test.ts(x),*.stories.tsx,*.d.ts,src/test-setup.ts, and anything outsidesrc. 737 files scanned, 126 bail out.A file the baseline has never seen has a baseline of zero, so a rename cannot smuggle a bail-out past the gate.
Files that fail to parse are not counted as bail-outs: when the escalated pass errors, the scan re-runs that one file with the compiler off, and throws naming the file if it still errors. Only the files that already errored pay for the second pass.
Reconciling with the count measured when the compiler was turned on
That measurement reported 53 non-test
.tsxfiles. This scan finds 126, and the difference is entirely scope, not disagreement:.tsxfiles. The same 53, pluscomponents/renders/renderQueueTestHarness.tsx. That file is imported only by four*.test.tsxfiles but its own name does not match*.test.tsx, so this scan's exclusion rule keeps it. Being a superset is the safe direction for a ratchet..tsfiles. Hooks and helpers, which the earlier table did not enumerate.useInspectorGestureTransaction.ts,useDomEditNudge.ts,useCaptionSync.tsand others bail for the same reasons the components do. They are listed in full below, because the follow-up units were scoped from a.tsx-only list and these are not in any of them yet.Baseline by cause
Cannot access refs during render (70)
App.tsxcaptions/components/CaptionOverlay.tsxcaptions/hooks/useCaptionSync.tscomponents/TimelineToolbar.tsxcomponents/editor/BlockParamsPanel.tsxcomponents/editor/DomEditOverlay.tsxcomponents/editor/EaseCurveSection.tsxcomponents/editor/MotionPathOverlay.tsxcomponents/editor/SnapGuideOverlay.tsxcomponents/editor/SourceEditor.tsxcomponents/editor/TopologyLens.tsxcomponents/editor/Transform3DCube.tsxcomponents/editor/propertyPanelColor.tsxcomponents/editor/propertyPanelColorGradingSlider.tsxcomponents/editor/propertyPanelCommitField.tsxcomponents/editor/propertyPanelFlatPrimitives.tsxcomponents/editor/propertyPanelPrimitives.tsxcomponents/editor/propertyPanelSections.tsxcomponents/editor/useColorGradingScopes.tscomponents/editor/useDomEditNudge.tscomponents/editor/useFxAudition.tscomponents/editor/useInspectorGestureTransaction.tscomponents/editor/useLayerRevealOverride.tscomponents/editor/useMotionPathData.tscomponents/feedback/StudioFeedbackCard.tsxcomponents/nle/NLEContext.tsxcomponents/nle/NLEPreview.tsxcomponents/nle/PreviewPane.tsxcomponents/nle/TimelineResizeDivider.tsxcomponents/nle/useCanvasZOrderTimelineMirror.tscomponents/sidebar/LeftSidebar.tsxcomponents/sidebar/PromptPreviewModal.tsxcomponents/ui/useDialogBehavior.tscontexts/DomEditContext.tsxhooks/useAppHotkeys.tshooks/useClipboard.tshooks/useDomEditPreviewSync.tshooks/useDomSelection.tshooks/useElementPicker.tshooks/useExternalFileChangeCoordinator.tshooks/useFileManager.tshooks/useGestureRecording.tshooks/useGsapPropertyDebounce.tshooks/useGsapScriptCommits.tshooks/useGsapSelectionHandlers.tshooks/useLivePlayheadTime.tshooks/useMusicBeatAnalysis.tshooks/usePanelLayout.tshooks/usePersistentEditHistory.tshooks/usePreviewPersistence.tshooks/useProjectSignaturePoll.tshooks/useRazorSplit.tshooks/useSdkSession.tshooks/useThumbnailLease.tshooks/useTimelineEditing.tsplayer/components/PlayerControls.tsxplayer/components/Timeline.tsxplayer/components/TimelineCanvas.tsxplayer/components/TimelineClipDiamonds.tsxplayer/components/TimelineGestureOverlay.tsxplayer/components/TimelineLanes.tsxplayer/components/useTimelineClipDrag.tsplayer/components/useTimelineFocusCoordinator.tsplayer/components/useTimelinePlayhead.tsplayer/components/useTimelineRangeSelection.tsplayer/components/useTimelineSelectionLifecycle.tsplayer/components/useTimelineTrackLayout.tsplayer/hooks/usePlaybackKeyboard.tsplayer/hooks/useTimelinePlayer.tswebmcp/useStudioAgentTools.tsReact rule suppression prevents optimization (20)
components/editor/GestureTrailOverlay.tsxcomponents/editor/PropertyPanel.tsxcomponents/editor/PropertyPanelFlat.tsxcomponents/editor/propertyPanelAudioFxGroup.tsxcomponents/editor/propertyPanelFxSection.tsxcomponents/editor/useColorGradingController.tscomponents/editor/useFxCarve.tscomponents/nle/useCompositionStack.tscomponents/nle/useTimelineEditCallbacks.tscomponents/panels/VariablesPanel.tsxcontexts/TimelineEditContext.tsxcontexts/VariablePromoteContext.tsxhooks/useBlockHandlers.tshooks/useGsapTweenCache.tshooks/useMountEffect.tshooks/useProjectCompositionVariables.tshooks/useSlideshowTabState.tshooks/useTimelineSelectionPreviewSync.tsplayer/components/useTimelineGeometry.tsplayer/components/useTimelineScrollViewport.ts(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause (14)
components/editor/propertyPanelColorSecondary.tsxcomponents/editor/propertyPanelFill.tsxcomponents/editor/propertyPanelFlatMediaSection.tsxcomponents/editor/propertyPanelFont.tsxcomponents/editor/propertyPanelMediaSection.tsxcomponents/editor/useColorGradingPreviews.tscomponents/editor/useFxLevelling.tscomponents/panels/SlideshowPanel.tsxcomponents/storyboard/StoryboardFrameFocus.tsxcomponents/storyboard/useFrameComments.tshooks/useBlockCatalog.tshooks/useFrameCapture.tshooks/useGestureCommit.tshooks/useLintModal.tsExisting memoization could not be preserved (9)
App.tsxcomponents/editor/useInspectorGestureTransaction.tscomponents/storyboard/StoryboardLoaded.tsxhooks/useFileManager.tsplayer/components/useAutomationLaneGestures.tsplayer/components/useTimelineClipDrag.tsplayer/components/useTimelinePlayhead.tsplayer/components/useTimelineRangeSelection.tsplayer/components/useTimelineStackingSync.ts(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch (8)
captions/hooks/useCaptionSync.tscomponents/editor/propertyPanelColorSecondary.tsxcomponents/editor/useColorGradingScopes.tshooks/timelineAudioGroupCreate.tshooks/useAnimatedPropertyCommit.tshooks/useElementLifecycleOps.tshooks/useFrameCapture.tshooks/useTimelineDeleteOps.tsLogical assignment operators (||=, &&=, ??=) are not yet supported (5)
components/editor/marqueeCommit.tscomponents/editor/useFxAudition.tshooks/useDomEditSession.tshooks/useRazorSplit.tsplayer/hooks/useTimelinePlayer.tstry/finallywithoutcatchis not supported by React Compiler (5)components/sidebar/AssetsTab.tsxcomponents/storyboard/StoryboardSourceEditor.tsxhooks/useElementLifecycleOps.tshooks/useRemoveBackground.tshooks/useTimelineGroupEditing.tsThis value cannot be modified (3)
components/editor/DomEditOverlay.tsxhooks/useConsoleErrorCapture.tsplayer/components/useTimelineRangeSelection.tsCannot modify local variables after render completes (3)
components/editor/DomEditOverlay.tsxhooks/useConsoleErrorCapture.tsplayer/components/useTimelineRangeSelection.ts(BuildHIR::node.lowerReorderableExpression) Expression type
MemberExpressioncannot be safely reordered (3)components/editor/useColorGradingPreviews.tshooks/usePreviewPersistence.tsplayer/components/TimelineClipDiamonds.tsx(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global (3)
hooks/useGestureCommit.tshooks/useGsapAwareEditing.tshooks/useToast.tsCannot access variable while it is being initialized (3)
player/components/useTimelineClipDrag.tsplayer/components/useTimelinePlayhead.tsplayer/components/useTimelineRangeSelection.ts(BuildHIR::lowerExpression) Handle Import expressions (2)
components/editor/OffCanvasIndicators.tsxplayer/components/Player.tsxHooks may not be referenced as normal values; they must be called (2)
player/components/PlayerControls.tsxplayer/hooks/useTimelinePlayer.tsHooks must be called at the top level of a function component or custom Hook (1)
components/ExternalFileConflictBanner.tsxExpected a node for all identifiers, none found for
0(1)components/editor/FileTreeNodes.tsxCannot reassign variables declared outside of the component/hook (1)
components/renders/renderQueueTestHarness.tsxSupport non-trivial for..of inits (1)
hooks/useGsapAwareEditing.tsUse of incompatible library (1)
player/components/useTimelineVirtualRows.tsTest plan
Ten tests in
src/styles/compilerBailouts.test.ts, seven of them the scenarios this gate has to get right.ref.currentduring render is reported with the file and the cause"use no memo"directive is counted, which the compiler itself never reportsProved end to end, not only against synthetic maps. A temporary component reading a ref during render was appended to a currently clean file, and the gate failed with
src/components/AskAgentModal.tsx: 1 bail-outs, baseline 0plus the lowering command. The file was restored.Proved non-vacuous. The scan was broken on purpose by dropping
panicThreshold: "all_errors"back to the default. The ref-during-render test failed, along with the whole-tree gate. Restored, both pass.Proved the missing-baseline path for real. The gate was run before the baseline existed and failed with
src/styles/compiler-bailouts.json is missing. Write it with: COMPILER_BAILOUTS_WRITE=1 bunx vitest run src/styles/compilerBailouts.test.ts.Commands:
bunx vitest run src/styles/compilerBailouts.test.ts --maxWorkers=4: 10 passed, 0.96 s duration, 2.6 s wall including startup. The whole-tree scan inside it is 0.73 s over 737 files.bunx vitest run --maxWorkers=4frompackages/studio, no Studio dev server running:428 passed | 1 skipped (429)files,4753 passed | 18 todo (4771)tests, 64.8 s. The branch before this one was427 passed | 1 skipped (428)files and4743 passed | 18 todo (4761)tests, so the delta is exactly this one new file and its ten tests. Nothing else moved.bun run typecheck: clean.bun run build: succeeds.bunx oxlinton the two new source files:Found 0 warnings and 0 errors.bunx oxfmt: clean.No issues in 4 changed files. No.fallowrc.jsoncentry was needed for the new script.Not run, and why: the render-parity capture and the two Studio CI gates. This change adds a test file, a script and a JSON baseline. No file that ships in the bundle is touched, so there is nothing whose rendered output could differ.
Not covered
.tsfiles are not in any follow-up unit's file list yet. The follow-up units were scoped from a.tsx-only list. They are enumerated above so they can be assigned.components/renders/renderQueueTestHarness.tsxis a test-only harness that this scan counts as source, because its name does not match*.test.tsx. It is in the baseline at 1. Either fix it or rename it; the gate does not care which.oxc-transform-reactupgrade that rewords a message changes the report and not the gate, which is the intended split.*.test.tsxfiles that drive re-renders by mutating capturedletbindings are a separate unit.