mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
Two new React Compiler compat hazards: - discarded-subscription: a useSyncExternalStore call whose result is thrown away, so the reads after it get cached on unrelated inputs. - store-read-in-memo-block: a render-level getState() that the compiler places in a cache-guarded block. Reads inside the source's own useMemo or callbacks keep their written deps and are skipped. The compiled output is parsed once for hook and store reads. Current sites are grandfathered in the hazard baseline. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
104 lines
3.2 KiB
JavaScript
104 lines
3.2 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
findMemoizedHookCalls,
|
|
findMemoizedStoreReads,
|
|
findSourceHazards,
|
|
renderStoreReadLines
|
|
} from './react-compiler-hazards.mjs'
|
|
|
|
describe('findSourceHazards', () => {
|
|
it('flags bare reads, postfix member values and opt-outs only inside components and hooks', () => {
|
|
const src = [
|
|
'export function useQueue(q, tick) {',
|
|
' void tick',
|
|
' void el?.offsetWidth',
|
|
' void save()',
|
|
' const next = q.items[q.head++]',
|
|
' q.head++',
|
|
' let i = 0',
|
|
' const v = q.items[i++]',
|
|
'}',
|
|
'export const Row = memo(() => {',
|
|
" 'use no memo'",
|
|
' return null',
|
|
'})',
|
|
'function flush(el) {',
|
|
' void el.offsetWidth',
|
|
' return q.items[q.head++]',
|
|
'}'
|
|
].join('\n')
|
|
expect(
|
|
findSourceHazards('a.tsx', src).map(({ kind, fn, line }) => `${fn}:${kind}:${line}`)
|
|
).toEqual([
|
|
'useQueue:void-read:2',
|
|
'useQueue:void-read:3',
|
|
'useQueue:member-postfix-value:5',
|
|
'Row:opt-out-directive:11'
|
|
])
|
|
})
|
|
|
|
it('flags a useSyncExternalStore result that is thrown away, not one that is kept', () => {
|
|
const src = [
|
|
'export function useRows(id) {',
|
|
' useSyncExternalStore(subscribe, getSnapshot)',
|
|
' const version = useSyncExternalStore(subscribe, getSnapshot)',
|
|
' return read(id, version)',
|
|
'}'
|
|
].join('\n')
|
|
expect(
|
|
findSourceHazards('a.ts', src).map(({ kind, fn, line }) => `${fn}:${kind}:${line}`)
|
|
).toEqual(['useRows:discarded-subscription:2'])
|
|
})
|
|
})
|
|
|
|
describe('findMemoizedHookCalls', () => {
|
|
it('finds hooks inside cache-guarded blocks, not hoisted ones or ones in closures', () => {
|
|
const compiled = [
|
|
'import { c as _c } from "react/compiler-runtime";',
|
|
'export function useThing(n) {',
|
|
' const $ = _c(2);',
|
|
' const s = useState(n);',
|
|
' let t0;',
|
|
' if ($[0] !== n) {',
|
|
' t0 = { n, s, r: useRef(n), f: () => useNever() };',
|
|
' $[0] = n;',
|
|
' $[1] = t0;',
|
|
' } else {',
|
|
' t0 = $[1];',
|
|
' }',
|
|
' return t0;',
|
|
'}'
|
|
].join('\n')
|
|
expect(findMemoizedHookCalls('a.js', compiled)).toEqual([{ fn: 'useThing', hook: 'useRef' }])
|
|
})
|
|
})
|
|
|
|
describe('store reads in memo blocks', () => {
|
|
it('finds getState() in a guarded block but not in a closure, and skips source useMemo reads', () => {
|
|
const compiled = [
|
|
'import { c as _c } from "react/compiler-runtime";',
|
|
'export function useRoute(n) {',
|
|
' const $ = _c(2);',
|
|
' let t0;',
|
|
' if ($[0] !== n) {',
|
|
' t0 = { r: route(store.getState(), n), f: () => store.getState() };',
|
|
' $[0] = n;',
|
|
' $[1] = t0;',
|
|
' } else {',
|
|
' t0 = $[1];',
|
|
' }',
|
|
' return t0;',
|
|
'}'
|
|
].join('\n')
|
|
expect(findMemoizedStoreReads('a.js', compiled)).toEqual([{ fn: 'useRoute' }])
|
|
const src = [
|
|
'export function useRoute(n) {',
|
|
' const paused = useMemo(() => store.getState(), [n])',
|
|
' return { r: route(store.getState(), n), paused }',
|
|
'}'
|
|
].join('\n')
|
|
expect(renderStoreReadLines('a.ts', src, 'useRoute')).toEqual([3])
|
|
})
|
|
})
|