Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
83 changes: 83 additions & 0 deletions packages/common/refresh-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export const runtimePublicPath = '/@react-refresh'

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

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

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
97 changes: 18 additions & 79 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,12 @@ 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<SourceMapPayload>(
result.code,
result.map!,
'@vitejs/plugin-react-swc',
id,
)
},
},
options.plugins
Expand Down Expand Up @@ -280,30 +245,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