Skip to content

Commit 099719f

Browse files
authored
refactor: extract common code in common package (#443)
1 parent f26c213 commit 099719f

File tree

17 files changed

+358
-1108
lines changed

17 files changed

+358
-1108
lines changed

packages/common/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './refresh-utils'
2+
export * from './warning'

packages/common/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@vitejs/react-common",
3+
"version": "0.0.0",
4+
"type": "module",
5+
"private": true,
6+
"exports": {
7+
".": "./index.ts",
8+
"./refresh-runtime": "./refresh-runtime.js"
9+
},
10+
"peerDependencies": {
11+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
12+
},
13+
"devDependencies": {
14+
"vite": "^6.2.2"
15+
}
16+
}

packages/plugin-react-swc/src/refresh-runtime.js renamed to packages/common/refresh-runtime.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ export function validateRefreshBoundaryAndEnqueueUpdate(
624624
if (hasExports && allExportsAreComponentsOrUnchanged === true) {
625625
enqueueUpdate()
626626
} else {
627-
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports`
627+
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at __README_URL__#consistent-components-exports`
628628
}
629629
}
630630

packages/common/refresh-utils.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export const runtimePublicPath = '/@react-refresh'
2+
3+
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
4+
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
5+
6+
// NOTE: this is exposed publicly via plugin-react
7+
export const preambleCode = `import { injectIntoGlobalHook } from "__BASE__${runtimePublicPath.slice(
8+
1,
9+
)}"
10+
injectIntoGlobalHook(window);
11+
window.$RefreshReg$ = () => {};
12+
window.$RefreshSig$ = () => (type) => type;`
13+
14+
export const getPreambleCode = (base: string): string =>
15+
preambleCode.replace('__BASE__', base)
16+
17+
export function addRefreshWrapper<M extends { mappings: string } | undefined>(
18+
code: string,
19+
map: M | string,
20+
pluginName: string,
21+
id: string,
22+
): { code: string; map: M | string } {
23+
const hasRefresh = refreshContentRE.test(code)
24+
const onlyReactComp = !hasRefresh && reactCompRE.test(code)
25+
if (!hasRefresh && !onlyReactComp) return { code, map }
26+
27+
const newMap = typeof map === 'string' ? (JSON.parse(map) as M) : map
28+
let newCode = code
29+
if (hasRefresh) {
30+
newCode = `let prevRefreshReg;
31+
let prevRefreshSig;
32+
33+
if (import.meta.hot && !inWebWorker) {
34+
if (!window.$RefreshReg$) {
35+
throw new Error(
36+
"${pluginName} can't detect preamble. Something is wrong."
37+
);
38+
}
39+
40+
prevRefreshReg = window.$RefreshReg$;
41+
prevRefreshSig = window.$RefreshSig$;
42+
window.$RefreshReg$ = RefreshRuntime.getRefreshReg(${JSON.stringify(id)});
43+
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
44+
}
45+
46+
${newCode}
47+
48+
if (import.meta.hot && !inWebWorker) {
49+
window.$RefreshReg$ = prevRefreshReg;
50+
window.$RefreshSig$ = prevRefreshSig;
51+
}
52+
`
53+
if (newMap) {
54+
newMap.mappings = ';'.repeat(17) + newMap.mappings
55+
}
56+
}
57+
58+
newCode = `import * as RefreshRuntime from "${runtimePublicPath}";
59+
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
60+
61+
${newCode}
62+
63+
if (import.meta.hot && !inWebWorker) {
64+
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
65+
RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(
66+
id,
67+
)}, currentExports);
68+
import.meta.hot.accept((nextExports) => {
69+
if (!nextExports) return;
70+
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify(
71+
id,
72+
)}, currentExports, nextExports);
73+
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
74+
});
75+
});
76+
}
77+
`
78+
if (newMap) {
79+
newMap.mappings = ';;;' + newMap.mappings
80+
}
81+
82+
return { code: newCode, map: newMap }
83+
}

packages/common/warning.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { BuildOptions, UserConfig } from 'vite'
2+
3+
export const silenceUseClientWarning = (
4+
userConfig: UserConfig,
5+
): BuildOptions => ({
6+
rollupOptions: {
7+
onwarn(warning, defaultHandler) {
8+
if (
9+
warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
10+
warning.message.includes('use client')
11+
) {
12+
return
13+
}
14+
// https://github.com/vitejs/vite/issues/15012
15+
if (
16+
warning.code === 'SOURCEMAP_ERROR' &&
17+
warning.message.includes('resolve original location') &&
18+
warning.pos === 0
19+
) {
20+
return
21+
}
22+
if (userConfig.build?.rollupOptions?.onwarn) {
23+
userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
24+
} else {
25+
defaultHandler(warning)
26+
}
27+
},
28+
},
29+
})

packages/plugin-react-swc/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"devDependencies": {
3939
"@playwright/test": "^1.51.1",
4040
"@types/fs-extra": "^11.0.4",
41-
"@types/node": "22.13.10",
41+
"@types/node": "^22.13.15",
42+
"@vitejs/react-common": "workspace:*",
4243
"@vitejs/release-scripts": "^1.3.3",
4344
"esbuild": "^0.25.1",
4445
"fs-extra": "^11.3.0",

packages/plugin-react-swc/playground/hmr/__tests__/hmr.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test('HMR invalidate', async ({ page }) => {
3535
// Edit export
3636
editFile('src/TitleWithExport.tsx', ['React', 'React!'])
3737
await waitForLogs(
38-
'[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',
38+
'[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',
3939
'[vite] hot updated: /src/App.tsx',
4040
)
4141
await expect(page.locator('h1')).toHaveText('Vite * React!')

packages/plugin-react-swc/playground/react-18/__tests__/react-18.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test('HMR invalidate', async ({ page }) => {
3535
// Edit export
3636
editFile('src/TitleWithExport.tsx', ['React', 'React!'])
3737
await waitForLogs(
38-
'[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',
38+
'[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',
3939
'[vite] hot updated: /src/App.tsx',
4040
)
4141
await expect(page.locator('h1')).toHaveText('Vite * React!')

packages/plugin-react-swc/scripts/bundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const buildOrWatch = async (options: BuildOptions) => {
2727

2828
Promise.all([
2929
buildOrWatch({
30-
entryPoints: ['src/refresh-runtime.js'],
30+
entryPoints: ['@vitejs/react-common/refresh-runtime'],
3131
outdir: 'dist',
3232
platform: 'browser',
3333
format: 'esm',

packages/plugin-react-swc/src/index.ts

Lines changed: 18 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ import {
1111
type Options as SWCOptions,
1212
transform,
1313
} from '@swc/core'
14-
import type { BuildOptions, PluginOption, UserConfig } from 'vite'
15-
16-
const runtimePublicPath = '/@react-refresh'
17-
18-
const preambleCode = `import { injectIntoGlobalHook } from "__PATH__";
19-
injectIntoGlobalHook(window);
20-
window.$RefreshReg$ = () => {};
21-
window.$RefreshSig$ = () => (type) => type;`
14+
import type { PluginOption } from 'vite'
15+
import {
16+
addRefreshWrapper,
17+
getPreambleCode,
18+
runtimePublicPath,
19+
silenceUseClientWarning,
20+
} from '@vitejs/react-common'
2221

2322
/* eslint-disable no-restricted-globals */
2423
const _dirname =
@@ -30,9 +29,6 @@ const resolve = createRequire(
3029
).resolve
3130
/* eslint-enable no-restricted-globals */
3231

33-
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
34-
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
35-
3632
type Options = {
3733
/**
3834
* Control where the JSX factory is imported from.
@@ -94,7 +90,10 @@ const react = (_options?: Options): PluginOption[] => {
9490
resolveId: (id) => (id === runtimePublicPath ? id : undefined),
9591
load: (id) =>
9692
id === runtimePublicPath
97-
? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8')
93+
? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8').replace(
94+
/__README_URL__/g,
95+
'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc',
96+
)
9897
: undefined,
9998
},
10099
{
@@ -126,10 +125,7 @@ const react = (_options?: Options): PluginOption[] => {
126125
{
127126
tag: 'script',
128127
attrs: { type: 'module' },
129-
children: preambleCode.replace(
130-
'__PATH__',
131-
config.server!.config.base + runtimePublicPath.slice(1),
132-
),
128+
children: getPreambleCode(config.server!.config.base),
133129
},
134130
],
135131
async transform(code, _id, transformOptions) {
@@ -151,43 +147,12 @@ const react = (_options?: Options): PluginOption[] => {
151147
if (!result) return
152148
if (!refresh) return result
153149

154-
const hasRefresh = refreshContentRE.test(result.code)
155-
if (!hasRefresh && !reactCompRE.test(result.code)) return result
156-
157-
const sourceMap: SourceMapPayload = JSON.parse(result.map!)
158-
sourceMap.mappings = ';;' + sourceMap.mappings
159-
160-
result.code = `import * as RefreshRuntime from "${runtimePublicPath}";
161-
162-
${result.code}`
163-
164-
if (hasRefresh) {
165-
sourceMap.mappings = ';;;;;;' + sourceMap.mappings
166-
result.code = `if (!window.$RefreshReg$) throw new Error("React refresh preamble was not loaded. Something is wrong.");
167-
const prevRefreshReg = window.$RefreshReg$;
168-
const prevRefreshSig = window.$RefreshSig$;
169-
window.$RefreshReg$ = RefreshRuntime.getRefreshReg("${id}");
170-
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
171-
172-
${result.code}
173-
174-
window.$RefreshReg$ = prevRefreshReg;
175-
window.$RefreshSig$ = prevRefreshSig;
176-
`
177-
}
178-
179-
result.code += `
180-
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
181-
RefreshRuntime.registerExportsForReactRefresh("${id}", currentExports);
182-
import.meta.hot.accept((nextExports) => {
183-
if (!nextExports) return;
184-
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate("${id}", currentExports, nextExports);
185-
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
186-
});
187-
});
188-
`
189-
190-
return { code: result.code, map: sourceMap }
150+
return addRefreshWrapper<SourceMapPayload>(
151+
result.code,
152+
result.map!,
153+
'@vitejs/plugin-react-swc',
154+
id,
155+
)
191156
},
192157
},
193158
options.plugins
@@ -280,30 +245,4 @@ const transformWithOptions = async (
280245
return result
281246
}
282247

283-
const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({
284-
rollupOptions: {
285-
onwarn(warning, defaultHandler) {
286-
if (
287-
warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
288-
warning.message.includes('use client')
289-
) {
290-
return
291-
}
292-
// https://github.com/vitejs/vite/issues/15012
293-
if (
294-
warning.code === 'SOURCEMAP_ERROR' &&
295-
warning.message.includes('resolve original location') &&
296-
warning.pos === 0
297-
) {
298-
return
299-
}
300-
if (userConfig.build?.rollupOptions?.onwarn) {
301-
userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
302-
} else {
303-
defaultHandler(warning)
304-
}
305-
},
306-
},
307-
})
308-
309248
export default react

0 commit comments

Comments
 (0)