diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index bb0d146594d900..c21207996e4089 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -44,6 +44,7 @@ "@babel/plugin-transform-react-jsx-development": "^7.18.6", "@babel/plugin-transform-react-jsx-self": "^7.18.6", "@babel/plugin-transform-react-jsx-source": "^7.18.6", + "magic-string": "^0.26.2", "react-refresh": "^0.14.0" }, "peerDependencies": { diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 160e9ca1d2f883..f1adf81510bcef 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -3,6 +3,8 @@ import type { ParserOptions, TransformOptions, types as t } from '@babel/core' import * as babel from '@babel/core' import { createFilter, normalizePath } from 'vite' import type { Plugin, PluginOption, ResolvedConfig } from 'vite' +import MagicString from 'magic-string' +import type { SourceMap } from 'magic-string' import { addRefreshWrapper, isRefreshBoundary, @@ -88,11 +90,14 @@ declare module 'vite' { } } +const prependReactImportCode = "import React from 'react'; " + export default function viteReact(opts: Options = {}): PluginOption[] { // Provide default values for Rollup compat. let devBase = '/' let resolvedCacheDir: string let filter = createFilter(opts.include, opts.exclude) + let needHiresSourcemap = false let isProduction = true let projectRoot = process.cwd() let skipFastRefresh = opts.fastRefresh === false @@ -135,6 +140,8 @@ export default function viteReact(opts: Options = {}): PluginOption[] { filter = createFilter(opts.include, opts.exclude, { resolve: projectRoot }) + needHiresSourcemap = + config.command === 'build' && !!config.build.sourcemap isProduction = config.isProduction skipFastRefresh ||= isProduction || config.command === 'build' @@ -217,6 +224,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { } let ast: t.File | null | undefined + let prependReactImport = false if (!isProjectFile || isJSX) { if (useAutomaticRuntime) { // By reverse-compiling "React.createElement" calls into JSX, @@ -261,11 +269,23 @@ export default function viteReact(opts: Options = {}): PluginOption[] { // Even if the automatic JSX runtime is not used, we can still // inject the React import for .jsx and .tsx modules. if (!skipReactImport && !importReactRE.test(code)) { - code = `import React from 'react'; ` + code + prependReactImport = true } } } + let inputMap: SourceMap | undefined + if (prependReactImport) { + if (needHiresSourcemap) { + const s = new MagicString(code) + s.prepend(prependReactImportCode) + code = s.toString() + inputMap = s.generateMap({ hires: true, source: id }) + } else { + code = prependReactImportCode + code + } + } + // Plugins defined through this Vite plugin are only applied // to modules within the project root, but "babel.config.js" // files can define plugins that need to be applied to every @@ -275,10 +295,11 @@ export default function viteReact(opts: Options = {}): PluginOption[] { !babelOptions.configFile && !(isProjectFile && babelOptions.babelrc) + // Avoid parsing if no plugins exist. if (shouldSkip) { - // Avoid parsing if no plugins exist. return { - code + code, + map: inputMap ?? null } } @@ -326,7 +347,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { plugins, sourceMaps: true, // Vite handles sourcemap flattening - inputSourceMap: false as any + inputSourceMap: inputMap ?? (false as any) }) if (result) { diff --git a/playground/react-sourcemap/App.jsx b/playground/react-sourcemap/App.jsx new file mode 100644 index 00000000000000..ec47ca46ad212e --- /dev/null +++ b/playground/react-sourcemap/App.jsx @@ -0,0 +1,8 @@ +console.log('App.jsx 1') // for sourcemap +function App() { + return
foo
+} + +console.log('App.jsx 2') // for sourcemap + +export default App diff --git a/playground/react-sourcemap/__tests__/react-sourcemap.spec.ts b/playground/react-sourcemap/__tests__/react-sourcemap.spec.ts new file mode 100644 index 00000000000000..a1d35485760753 --- /dev/null +++ b/playground/react-sourcemap/__tests__/react-sourcemap.spec.ts @@ -0,0 +1,7 @@ +import { isBuild, serverLogs } from '~utils' + +test.runIf(isBuild)('should not output sourcemap warning', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch('Sourcemap is likely to be incorrect') + }) +}) diff --git a/playground/react-sourcemap/index.html b/playground/react-sourcemap/index.html new file mode 100644 index 00000000000000..d3ca9807c608ba --- /dev/null +++ b/playground/react-sourcemap/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/playground/react-sourcemap/main.jsx b/playground/react-sourcemap/main.jsx new file mode 100644 index 00000000000000..705d3340097aeb --- /dev/null +++ b/playground/react-sourcemap/main.jsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' + +ReactDOM.createRoot(document.getElementById('app')).render( + React.createElement(App) +) + +console.log('main.jsx') // for sourcemap diff --git a/playground/react-sourcemap/package.json b/playground/react-sourcemap/package.json new file mode 100644 index 00000000000000..91aa3331b877b4 --- /dev/null +++ b/playground/react-sourcemap/package.json @@ -0,0 +1,21 @@ +{ + "name": "test-react-sourcemap", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "dev:classic": "cross-env USE_CLASSIC=1 vite", + "build": "vite build", + "build:classic": "cross-env USE_CLASSIC=1 vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "workspace:*", + "cross-env": "^7.0.3" + } +} diff --git a/playground/react-sourcemap/vite.config.ts b/playground/react-sourcemap/vite.config.ts new file mode 100644 index 00000000000000..d8a2cc46b419b9 --- /dev/null +++ b/playground/react-sourcemap/vite.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react' +import type { UserConfig } from 'vite' + +const config: UserConfig = { + plugins: [ + react({ + jsxRuntime: process.env.USE_CLASSIC === '1' ? 'classic' : 'automatic' + }) + ], + build: { + sourcemap: true + } +} + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9b329c87ac56d..c6a4178e50ab9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,7 @@ importers: '@babel/plugin-transform-react-jsx-development': ^7.18.6 '@babel/plugin-transform-react-jsx-self': ^7.18.6 '@babel/plugin-transform-react-jsx-source': ^7.18.6 + magic-string: ^0.26.2 react-refresh: ^0.14.0 vite: workspace:* dependencies: @@ -170,6 +171,7 @@ importers: '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.18.6 '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.18.6 '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.18.6 + magic-string: 0.26.2 react-refresh: 0.14.0 devDependencies: vite: link:../vite @@ -821,6 +823,19 @@ importers: '@emotion/babel-plugin': 11.9.2 '@vitejs/plugin-react': link:../../packages/plugin-react + playground/react-sourcemap: + specifiers: + '@vitejs/plugin-react': workspace:* + cross-env: ^7.0.3 + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + devDependencies: + '@vitejs/plugin-react': link:../../packages/plugin-react + cross-env: 7.0.3 + playground/react/jsx-entry: specifiers: {} @@ -5710,7 +5725,7 @@ packages: dev: true /isexe/2.0.0: - resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true /jiti/1.13.0: