diff --git a/packages/plugin-react/src/fast-refresh.ts b/packages/plugin-react/src/fast-refresh.ts index 3f9ffa60..5423b271 100644 --- a/packages/plugin-react/src/fast-refresh.ts +++ b/packages/plugin-react/src/fast-refresh.ts @@ -58,13 +58,17 @@ if (import.meta.hot && !inWebWorker) { window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; }` -const sharedFooter = ` +const sharedFooter = (id: string) => ` if (import.meta.hot && !inWebWorker) { RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => { - RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports); + RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify( + id, + )}, currentExports); import.meta.hot.accept((nextExports) => { if (!nextExports) return; - const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports); + const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify( + id, + )}, currentExports, nextExports); if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage); }); }); @@ -76,7 +80,7 @@ export function addRefreshWrapper(code: string, id: string): string { functionHeader.replace('__SOURCE__', JSON.stringify(id)) + code + functionFooter + - sharedFooter.replace('__SOURCE__', JSON.stringify(id)) + sharedFooter(id) ) } @@ -84,7 +88,5 @@ export function addClassComponentRefreshWrapper( code: string, id: string, ): string { - return ( - sharedHeader + code + sharedFooter.replace('__SOURCE__', JSON.stringify(id)) - ) + return sharedHeader + code + sharedFooter(id) } diff --git a/packages/plugin-react/src/refreshUtils.js b/packages/plugin-react/src/refreshUtils.js index 0ca2b011..0ac3c6ca 100644 --- a/packages/plugin-react/src/refreshUtils.js +++ b/packages/plugin-react/src/refreshUtils.js @@ -7,7 +7,14 @@ function debounce(fn, delay) { } /* eslint-disable no-undef */ -const enqueueUpdate = debounce(exports.performReactRefresh, 16) +const hooks = [] +window.__registerBeforePerformReactRefresh = (cb) => { + hooks.push(cb) +} +const enqueueUpdate = debounce(async () => { + if (hooks.length) await Promise.all(hooks.map((cb) => cb())) + exports.performReactRefresh() +}, 16) // Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141 // This allows to resister components not detected by SWC like styled component @@ -25,16 +32,30 @@ function registerExportsForReactRefresh(filename, moduleExports) { } } -function validateRefreshBoundaryAndEnqueueUpdate(prevExports, nextExports) { - if (!predicateOnExport(prevExports, (key) => key in nextExports)) { +function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExports) { + const ignoredExports = window.__getReactRefreshIgnoredExports?.({ id }) ?? [] + if ( + predicateOnExport( + ignoredExports, + prevExports, + (key) => key in nextExports, + ) !== true + ) { return 'Could not Fast Refresh (export removed)' } - if (!predicateOnExport(nextExports, (key) => key in prevExports)) { + if ( + predicateOnExport( + ignoredExports, + nextExports, + (key) => key in prevExports, + ) !== true + ) { return 'Could not Fast Refresh (new export)' } let hasExports = false const allExportsAreComponentsOrUnchanged = predicateOnExport( + ignoredExports, nextExports, (key, value) => { hasExports = true @@ -42,19 +63,20 @@ function validateRefreshBoundaryAndEnqueueUpdate(prevExports, nextExports) { return prevExports[key] === nextExports[key] }, ) - if (hasExports && allExportsAreComponentsOrUnchanged) { + if (hasExports && allExportsAreComponentsOrUnchanged === true) { enqueueUpdate() } else { - return 'Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports' + return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports` } } -function predicateOnExport(moduleExports, predicate) { +function predicateOnExport(ignoredExports, moduleExports, predicate) { for (const key in moduleExports) { if (key === '__esModule') continue + if (ignoredExports.includes(key)) continue const desc = Object.getOwnPropertyDescriptor(moduleExports, key) - if (desc && desc.get) return false - if (!predicate(key, moduleExports[key])) return false + if (desc && desc.get) return key + if (!predicate(key, moduleExports[key])) return key } return true } diff --git a/playground/react/__tests__/react.spec.ts b/playground/react/__tests__/react.spec.ts index 54180219..7b60914f 100644 --- a/playground/react/__tests__/react.spec.ts +++ b/playground/react/__tests__/react.spec.ts @@ -78,7 +78,7 @@ if (!isBuild) { code.replace('An Object', 'Updated'), ), [ - '[vite] invalidate /hmr/no-exported-comp.jsx: Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', + '[vite] invalidate /hmr/no-exported-comp.jsx: Could not Fast Refresh ("Foo" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', '[vite] hot updated: /hmr/no-exported-comp.jsx', '[vite] hot updated: /hmr/parent.jsx', 'Parent rendered', @@ -103,7 +103,7 @@ if (!isBuild) { code.replace('context provider', 'context provider updated'), ), [ - '[vite] invalidate /context/CountProvider.jsx: Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', + '[vite] invalidate /context/CountProvider.jsx: Could not Fast Refresh ("CountContext" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', '[vite] hot updated: /context/CountProvider.jsx', '[vite] hot updated: /App.jsx', '[vite] hot updated: /context/ContextButton.jsx',