diff --git a/src/renderer/src/components/confirmation-dialog-context.ts b/src/renderer/src/components/confirmation-dialog-context.ts index 1ec6eacfdb1..94a0ebc9984 100644 --- a/src/renderer/src/components/confirmation-dialog-context.ts +++ b/src/renderer/src/components/confirmation-dialog-context.ts @@ -1,10 +1,6 @@ import { createContext, useContext } from 'react' -// Why: the context stays in a component-free module so React Fast Refresh never -// rebuilds it. A module exporting both a component and this hook can never be a -// refresh boundary, so Vite applies its updates in two waves under different -// `?t=` stamps; importers refreshed in different waves then hold different -// context objects and consumers throw below until a full reload. +// Keep the context component-free so Fast Refresh preserves its identity. export type ConfirmationDialogOptions = { title: string diff --git a/src/renderer/src/components/confirmation-dialog-refresh-boundary.test.ts b/src/renderer/src/components/confirmation-dialog-refresh-boundary.test.ts index 37a28e21218..952cb21e23d 100644 --- a/src/renderer/src/components/confirmation-dialog-refresh-boundary.test.ts +++ b/src/renderer/src/components/confirmation-dialog-refresh-boundary.test.ts @@ -8,53 +8,15 @@ 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. -// -// 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 isPlainObject(value: unknown): value is Record { - 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]) => !isRefreshSafeExport(value)) - .map(([name]) => name) -} - describe('confirmation dialog Fast Refresh boundary', () => { it('exports nothing from the provider module that invalidates the refresh boundary', () => { - expect(Object.keys(providerModule).length).toBeGreaterThan(0) - expect(unsafeExports(providerModule)).toEqual([]) + expect(Object.keys(providerModule)).toEqual(['ConfirmationDialogProvider']) + expect(isLikelyComponentType(providerModule.ConfirmationDialogProvider)).toBe(true) }) it('keeps the context and hook in a component-free module', () => {