Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './refresh-utils'
export * from './warning'
16 changes: 16 additions & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@vitejs/react-common",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
".": "./index.ts",
"./refresh-runtime": "./refresh-runtime.js"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
},
"devDependencies": {
"vite": "^6.2.2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ export function validateRefreshBoundaryAndEnqueueUpdate(
if (hasExports && allExportsAreComponentsOrUnchanged === true) {
enqueueUpdate()
} else {
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports`
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at __README_URL__#consistent-components-exports`
}
}

Expand Down
84 changes: 84 additions & 0 deletions packages/common/refresh-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
export const runtimePublicPath = '/@react-refresh'

const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/

// NOTE: this is currently exposed publicly via plugin-react
export const preambleCode = `import { injectIntoGlobalHook } from "__BASE__${runtimePublicPath.slice(
1,
)}"
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;`

// NOTE: this is exposed publicly via plugin-react
export const getPreambleCode = (base: string): string =>
preambleCode.replace('__BASE__', base)

export function addRefreshWrapper<M extends { mappings: string } | undefined>(
code: string,
map: M | string,
pluginName: string,
id: string,
): { code: string; map: M | string } {
const hasRefresh = refreshContentRE.test(code)
const onlyReactComp = !hasRefresh && reactCompRE.test(code)
if (!hasRefresh && !onlyReactComp) return { code, map }

const newMap = typeof map === 'string' ? (JSON.parse(map) as M) : map
let newCode = code
if (hasRefresh) {
newCode = `let prevRefreshReg;
let prevRefreshSig;

if (import.meta.hot && !inWebWorker) {
if (!window.$RefreshReg$) {
throw new Error(
"${pluginName} can't detect preamble. Something is wrong."
);
}

prevRefreshReg = window.$RefreshReg$;
prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = RefreshRuntime.getRefreshReg(${JSON.stringify(id)});
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}

${newCode}

if (import.meta.hot && !inWebWorker) {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
}
`
if (newMap) {
newMap.mappings = ';'.repeat(17) + newMap.mappings
}
}

newCode = `import * as RefreshRuntime from "${runtimePublicPath}";
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;

${newCode}

if (import.meta.hot && !inWebWorker) {
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(
id,
)}, currentExports);
import.meta.hot.accept((nextExports) => {
if (!nextExports) return;
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify(
id,
)}, currentExports, nextExports);
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
});
});
}
`
if (newMap) {
newMap.mappings = ';;;' + newMap.mappings
}

return { code: newCode, map: newMap }
}
29 changes: 29 additions & 0 deletions packages/common/warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { BuildOptions, UserConfig } from 'vite'

export const silenceUseClientWarning = (
userConfig: UserConfig,
): BuildOptions => ({
rollupOptions: {
onwarn(warning, defaultHandler) {
if (
warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
warning.message.includes('use client')
) {
return
}
// https://github.com/vitejs/vite/issues/15012
if (
warning.code === 'SOURCEMAP_ERROR' &&
warning.message.includes('resolve original location') &&
warning.pos === 0
) {
return
}
if (userConfig.build?.rollupOptions?.onwarn) {
userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
} else {
defaultHandler(warning)
}
},
},
})
3 changes: 2 additions & 1 deletion packages/plugin-react-swc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"devDependencies": {
"@playwright/test": "^1.51.1",
"@types/fs-extra": "^11.0.4",
"@types/node": "22.13.10",
"@types/node": "^22.13.15",
"@vitejs/react-common": "workspace:*",
"@vitejs/release-scripts": "^1.3.3",
"esbuild": "^0.25.1",
"fs-extra": "^11.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('HMR invalidate', async ({ page }) => {
// Edit export
editFile('src/TitleWithExport.tsx', ['React', 'React!'])
await waitForLogs(
'[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports',
'[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc#consistent-components-exports',
'[vite] hot updated: /src/App.tsx',
)
await expect(page.locator('h1')).toHaveText('Vite * React!')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('HMR invalidate', async ({ page }) => {
// Edit export
editFile('src/TitleWithExport.tsx', ['React', 'React!'])
await waitForLogs(
'[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports',
'[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc#consistent-components-exports',
'[vite] hot updated: /src/App.tsx',
)
await expect(page.locator('h1')).toHaveText('Vite * React!')
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-react-swc/scripts/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const buildOrWatch = async (options: BuildOptions) => {

Promise.all([
buildOrWatch({
entryPoints: ['src/refresh-runtime.js'],
entryPoints: ['@vitejs/react-common/refresh-runtime'],
outdir: 'dist',
platform: 'browser',
format: 'esm',
Expand Down
96 changes: 18 additions & 78 deletions packages/plugin-react-swc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import {
type Options as SWCOptions,
transform,
} from '@swc/core'
import type { BuildOptions, PluginOption, UserConfig } from 'vite'

const runtimePublicPath = '/@react-refresh'

const preambleCode = `import { injectIntoGlobalHook } from "__PATH__";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;`
import type { PluginOption } from 'vite'
import {
addRefreshWrapper,
getPreambleCode,
runtimePublicPath,
silenceUseClientWarning,
} from '@vitejs/react-common'

/* eslint-disable no-restricted-globals */
const _dirname =
Expand All @@ -30,9 +29,6 @@ const resolve = createRequire(
).resolve
/* eslint-enable no-restricted-globals */

const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/

type Options = {
/**
* Control where the JSX factory is imported from.
Expand Down Expand Up @@ -94,7 +90,10 @@ const react = (_options?: Options): PluginOption[] => {
resolveId: (id) => (id === runtimePublicPath ? id : undefined),
load: (id) =>
id === runtimePublicPath
? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8')
? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8').replace(
/__README_URL__/g,
'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc',
)
: undefined,
},
{
Expand Down Expand Up @@ -126,10 +125,7 @@ const react = (_options?: Options): PluginOption[] => {
{
tag: 'script',
attrs: { type: 'module' },
children: preambleCode.replace(
'__PATH__',
config.server!.config.base + runtimePublicPath.slice(1),
),
children: getPreambleCode(config.server!.config.base),
},
],
async transform(code, _id, transformOptions) {
Expand All @@ -151,43 +147,13 @@ const react = (_options?: Options): PluginOption[] => {
if (!result) return
if (!refresh) return result

const hasRefresh = refreshContentRE.test(result.code)
if (!hasRefresh && !reactCompRE.test(result.code)) return result

const sourceMap: SourceMapPayload = JSON.parse(result.map!)
sourceMap.mappings = ';;' + sourceMap.mappings

result.code = `import * as RefreshRuntime from "${runtimePublicPath}";

${result.code}`

if (hasRefresh) {
sourceMap.mappings = ';;;;;;' + sourceMap.mappings
result.code = `if (!window.$RefreshReg$) throw new Error("React refresh preamble was not loaded. Something is wrong.");
const prevRefreshReg = window.$RefreshReg$;
const prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = RefreshRuntime.getRefreshReg("${id}");
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

${result.code}

window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
`
}

result.code += `
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
RefreshRuntime.registerExportsForReactRefresh("${id}", currentExports);
import.meta.hot.accept((nextExports) => {
if (!nextExports) return;
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate("${id}", currentExports, nextExports);
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
});
});
`

return { code: result.code, map: sourceMap }
return addRefreshWrapper(
result.code,
sourceMap,
'@vitejs/plugin-react-swc',
id,
)
},
},
options.plugins
Expand Down Expand Up @@ -280,30 +246,4 @@ const transformWithOptions = async (
return result
}

const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({
rollupOptions: {
onwarn(warning, defaultHandler) {
if (
warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
warning.message.includes('use client')
) {
return
}
// https://github.com/vitejs/vite/issues/15012
if (
warning.code === 'SOURCEMAP_ERROR' &&
warning.message.includes('resolve original location') &&
warning.pos === 0
) {
return
}
if (userConfig.build?.rollupOptions?.onwarn) {
userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
} else {
defaultHandler(warning)
}
},
},
})

export default react
3 changes: 2 additions & 1 deletion packages/plugin-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild && pnpm run patch-cjs && tsx scripts/copyRefreshUtils.ts",
"build": "unbuild && pnpm run patch-cjs && tsx scripts/copyRefreshRuntime.ts",
"patch-cjs": "tsx ../../scripts/patchCJS.ts",
"prepublishOnly": "npm run build"
},
Expand All @@ -57,6 +57,7 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
},
"devDependencies": {
"@vitejs/react-common": "workspace:*",
"unbuild": "^3.5.0"
}
}
6 changes: 6 additions & 0 deletions packages/plugin-react/scripts/copyRefreshRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { copyFileSync } from 'node:fs'

copyFileSync(
'node_modules/@vitejs/react-common/refresh-runtime.js',
'dist/refresh-runtime.js',
)
3 changes: 0 additions & 3 deletions packages/plugin-react/scripts/copyRefreshUtils.ts

This file was deleted.

Loading
Loading