mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
test(dev): classify boundary exports with the refresh runtime's own predicate
The hand-rolled `^[A-Z]` name check called `export class Foo {}` a component;
the runtime rejects any class whose prototype carries extra members, so that
shape restored the two-pass split undetected. Use react-refresh's exported
`isLikelyComponentType` and mirror `isCompoundComponent` instead of a third
approximation. react-refresh was already resolvable only via shamefully-hoist,
so it is now an explicit devDependency.
This commit is contained in:
@@ -218,6 +218,7 @@
|
||||
"react-dom": "^19.2.7",
|
||||
"react-grab": "^0.1.33",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-refresh": "^0.18.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
Generated
+3
@@ -324,6 +324,9 @@ importers:
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.17)(react@19.2.7)
|
||||
react-refresh:
|
||||
specifier: ^0.18.0
|
||||
version: 0.18.0
|
||||
rehype-highlight:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createRequire } from 'node:module'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -7,39 +8,58 @@ import * as providerModule from '@/components/confirmation-dialog'
|
||||
import { ConfirmationDialogProvider } from '@/components/confirmation-dialog'
|
||||
import { useConfirmationDialog } from '@/components/confirmation-dialog-context'
|
||||
|
||||
// Why: crash reports 42d63029 / 5ee3b8b6. A module exporting both the provider and
|
||||
// the hook is not a Fast Refresh boundary, so Vite applied its updates in two passes
|
||||
// under two `?t=` stamps, `createContext` ran twice, and the provider published a
|
||||
// different context object than `ChecksPanel` read. Asserting on the *module
|
||||
// namespace* rather than source text is what makes re-exports and default exports
|
||||
// visible here -- `export { useConfirmationDialog } from ...` reintroduces the crash
|
||||
// and no amount of source-shape regex sees it.
|
||||
function isComponentLike(value: unknown): boolean {
|
||||
if (typeof value === 'function') {
|
||||
return /^[A-Z]/.test(value.name)
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const tag = (value as { $$typeof?: symbol }).$$typeof
|
||||
return tag === Symbol.for('react.forward_ref') || tag === Symbol.for('react.memo')
|
||||
}
|
||||
return false
|
||||
// Why: crash reports 42d63029 / 5ee3b8b6. A module exporting both the provider and the
|
||||
// hook is not a Fast Refresh boundary, so Vite applied its updates in two passes under two
|
||||
// `?t=` stamps, `createContext` ran twice, and the provider published a different context
|
||||
// object than `ChecksPanel` read.
|
||||
//
|
||||
// Two earlier versions of this test were evaded, both by approximating the runtime instead
|
||||
// of asking it. Matching source text missed a re-export of the hook; a hand-rolled `^[A-Z]`
|
||||
// name check called `export class ConfirmationDialogQueue {}` a component when the runtime
|
||||
// rejects it. Hence the real predicate below rather than a third approximation.
|
||||
|
||||
// react-refresh ships no types; take the one binding this needs.
|
||||
const { isLikelyComponentType } = createRequire(import.meta.url)('react-refresh/runtime') as {
|
||||
isLikelyComponentType: (value: unknown) => boolean
|
||||
}
|
||||
|
||||
function nonComponentExports(moduleNamespace: object): string[] {
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
Object.prototype.toString.call(value) === '[object Object]' &&
|
||||
((value as object).constructor === Object || (value as object).constructor === undefined)
|
||||
)
|
||||
}
|
||||
|
||||
// Mirrors `isCompoundComponent` in @vitejs/plugin-react's refresh-runtime, which accepts a
|
||||
// plain object whose every member is a component (`export const Dialog = { Root, Trigger }`).
|
||||
// The runtime's third accept term, `prevExports[key] === nextExports[key]`, is deliberately
|
||||
// left out: it holds only for exports whose identity survives a re-execution, which nothing
|
||||
// but a primitive does, and an unchanged primitive breaks the boundary the moment it changes.
|
||||
function isRefreshSafeExport(value: unknown): boolean {
|
||||
if (isLikelyComponentType(value)) {
|
||||
return true
|
||||
}
|
||||
if (!isPlainObject(value)) {
|
||||
return false
|
||||
}
|
||||
return Object.values(value).every((member) => isLikelyComponentType(member))
|
||||
}
|
||||
|
||||
function unsafeExports(moduleNamespace: object): string[] {
|
||||
return Object.entries(moduleNamespace)
|
||||
.filter(([, value]) => !isComponentLike(value))
|
||||
.filter(([, value]) => !isRefreshSafeExport(value))
|
||||
.map(([name]) => name)
|
||||
}
|
||||
|
||||
describe('confirmation dialog Fast Refresh boundary', () => {
|
||||
it('exports only components from the provider module', () => {
|
||||
it('exports nothing from the provider module that invalidates the refresh boundary', () => {
|
||||
expect(Object.keys(providerModule).length).toBeGreaterThan(0)
|
||||
expect(nonComponentExports(providerModule)).toEqual([])
|
||||
expect(unsafeExports(providerModule)).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the context and hook in a component-free module', () => {
|
||||
const components = Object.entries(contextModule)
|
||||
.filter(([, value]) => isComponentLike(value))
|
||||
.filter(([, value]) => isLikelyComponentType(value))
|
||||
.map(([name]) => name)
|
||||
|
||||
expect(components).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user