diff --git a/packages/plugin-react/src/compilerRefresh.test.ts b/packages/plugin-react/src/compilerRefresh.test.ts new file mode 100644 index 000000000..1de6f3030 --- /dev/null +++ b/packages/plugin-react/src/compilerRefresh.test.ts @@ -0,0 +1,83 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createServer } from 'vite' +import { expect, test } from 'vitest' +import react from './index' + +const workerCode = ` +const Factory = { Values: (values: Value): Value => values } +export enum MessageKind { Ready = 'ready', Complete = 'complete' } +export const MessageKinds = Factory.Values(Object.values(MessageKind)) +` + +test('native compiler serves non-React TypeScript without refresh registrations', async () => { + const root = await mkdtemp(join(tmpdir(), 'react-compiler-refresh-')) + await writeFile(join(root, 'message.ts'), workerCode) + const server = await createServer({ + root, + configFile: false, + logLevel: 'silent', + plugins: [react({ compiler: true })], + }) + try { + const output = await server.transformRequest('/message.ts') + expect(output?.code).not.toContain('$RefreshReg$') + const module = await import( + `data:text/javascript,${encodeURIComponent(output!.code)}` + ) + expect(module.MessageKinds).toEqual(['ready', 'complete']) + } finally { + await server.close() + await rm(root, { recursive: true, force: true }) + } +}) + +test.each([ + [ + 'component.tsx?query', + 'export function App() { return
}', + undefined, + ], + [ + 'component.ts', + 'import { jsx } from "react/jsx-runtime"; export function App() { return jsx("div", {}) }', + undefined, + ], + [ + 'component.ts', + 'import { jsxDEV } from "react/jsx-dev-runtime"; export function App() { return jsxDEV("div", {}) }', + undefined, + ], + [ + 'component.ts', + 'import { jsx } from "@emotion/react/jsx-runtime"; export function App() { return jsx("div", {}) }', + '@emotion/react', + ], +])( + 'keeps refresh for %s with a JSX runtime', + async (id, code, jsxImportSource) => { + const server = await createServer({ + configFile: false, + logLevel: 'silent', + plugins: [react({ compiler: true, jsxImportSource })], + }) + try { + const plugin = server.config.plugins.find( + (p) => p.name === 'vite:react-compiler', + )! + const hook = plugin.transform! + const handler = typeof hook === 'function' ? hook : hook.handler + const output = await handler.call( + { environment: server.environments.client } as any, + code, + `/src/${id}`, + ) + expect( + output && typeof output === 'object' ? output.code : output, + ).toContain('$RefreshReg$') + } finally { + await server.close() + } + }, +) diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 778858962..fb413e26a 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -349,6 +349,14 @@ function createReactCompilerPlugin( }, async handler(code, id) { const isClient = this.environment?.config.consumer !== 'server' + const filepath = id.split('?')[0]! + const jsxImportSource = reactOptions.jsxImportSource ?? 'react' + const useFastRefresh = + isClient && + isFastRefreshEnabled() && + (filepath.endsWith('x') || + code.includes(`${jsxImportSource}/jsx-runtime`) || + code.includes(`${jsxImportSource}/jsx-dev-runtime`)) const shouldCompile = isClient && (reactCompilerOptions.compilationMode === 'annotation' @@ -358,12 +366,12 @@ function createReactCompilerPlugin( const { transform } = compiler ?? (await loadCompiler((message) => this.error(message))) - const result = await transform(id.split('?')[0]!, code, { + const result = await transform(filepath, code, { jsx: { runtime: reactOptions.jsxRuntime, development: jsxDevelopment, importSource: reactOptions.jsxImportSource, - refresh: isClient && isFastRefreshEnabled(), + refresh: useFastRefresh, }, reactCompiler: shouldCompile ? reactCompilerOptions : false, sourcemap: this.environment