diff --git a/e2e/cases/output/source-map/index.test.ts b/e2e/cases/output/source-map/index.test.ts index fd3f92bdeb..eb02a4b51e 100644 --- a/e2e/cases/output/source-map/index.test.ts +++ b/e2e/cases/output/source-map/index.test.ts @@ -43,31 +43,26 @@ async function testSourceMapType( const AppContentIndex = outputCode.indexOf('Hello Rsbuild!'); const indexContentIndex = outputCode.indexOf('window.test'); - const positions = ( - await mapSourceMapPositions(sourceMap, [ - { - line: 1, - column: AppContentIndex, - }, - { - line: 1, - column: indexContentIndex, - }, - ]) - ).map((o) => ({ - ...o, - source: o.source?.split('webpack:///')[1] || o.source, - })); + const positions = await mapSourceMapPositions(sourceMap, [ + { + line: 1, + column: AppContentIndex, + }, + { + line: 1, + column: indexContentIndex, + }, + ]); expect(positions).toEqual([ { - source: 'src/App.jsx', + source: '../../../src/App.jsx', line: 2, column: appSourceCode.split('\n')[1].indexOf('Hello Rsbuild!'), name: null, }, { - source: 'src/index.js', + source: '../../../src/index.js', line: 7, column: indexSourceCode.split('\n')[6].indexOf('window'), name: 'window', diff --git a/e2e/cases/plugin-api/plugin-transform-merge-source-map/index.test.ts b/e2e/cases/plugin-api/plugin-transform-merge-source-map/index.test.ts index 97593798d9..e8d3c01e2e 100644 --- a/e2e/cases/plugin-api/plugin-transform-merge-source-map/index.test.ts +++ b/e2e/cases/plugin-api/plugin-transform-merge-source-map/index.test.ts @@ -33,13 +33,13 @@ const expectSourceMap = async (files: Record) => { const sourceLines = sourceCode.split('\n'); expect(positions).toEqual([ { - source: 'webpack:///src/index.ts', + source: '../../../src/index.ts', line: 2, column: sourceLines[1].indexOf(`'args'`), name: null, }, { - source: 'webpack:///src/index.ts', + source: '../../../src/index.ts', line: 5, column: sourceLines[4].indexOf(`'hello'`), name: null, diff --git a/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap b/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap index f3a8e81561..8752d65f25 100644 --- a/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap +++ b/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap @@ -677,6 +677,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when produ "output": { "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", + "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", "path": "/packages/compat/webpack/tests/dist", @@ -1066,6 +1067,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe "output": { "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "[name].js", + "devtoolModuleFilenameTemplate": [Function], "filename": "[name].js", "hashFunction": "xxhash64", "library": { @@ -1429,6 +1431,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe "output": { "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", + "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", "path": "/packages/compat/webpack/tests/dist", diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index 1d2735ad45..e02b496fc7 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -43,6 +43,7 @@ import { pluginResourceHints } from './plugins/resourceHints'; import { pluginRsdoctor } from './plugins/rsdoctor'; import { pluginRspackProfile } from './plugins/rspackProfile'; import { pluginServer } from './plugins/server'; +import { pluginSourceMap } from './plugins/sourceMap'; import { pluginSplitChunks } from './plugins/splitChunks'; import { pluginSri } from './plugins/sri'; import { pluginSwc } from './plugins/swc'; @@ -78,6 +79,7 @@ function applyDefaultPlugins( pluginManager.addPlugins([ pluginBasic(), pluginEntry(), + pluginSourceMap(), pluginCache(), pluginTarget(), pluginOutput(), diff --git a/packages/core/src/plugins/basic.ts b/packages/core/src/plugins/basic.ts index 45478ba110..dc4de6a9dd 100644 --- a/packages/core/src/plugins/basic.ts +++ b/packages/core/src/plugins/basic.ts @@ -1,25 +1,4 @@ -import { toPosixPath } from '../helpers/path'; -import type { - NormalizedEnvironmentConfig, - RsbuildPlugin, - Rspack, -} from '../types'; - -const getDevtool = (config: NormalizedEnvironmentConfig): Rspack.DevTool => { - const { sourceMap } = config.output; - const isProd = config.mode === 'production'; - - if (sourceMap === false) { - return false; - } - if (sourceMap === true) { - return isProd ? 'source-map' : 'cheap-module-source-map'; - } - if (sourceMap.js === undefined) { - return isProd ? false : 'cheap-module-source-map'; - } - return sourceMap.js; -}; +import type { RsbuildPlugin } from '../types'; /** * Set some basic Rspack configs @@ -34,21 +13,6 @@ export const pluginBasic = (): RsbuildPlugin => ({ chain.name(environment.name); - const devtool = getDevtool(config); - chain.devtool(devtool); - - // When JS source map is disabled, but CSS source map is enabled, - // add `SourceMapDevToolPlugin` to let Rspack generate CSS source map. - const { sourceMap } = config.output; - if (!devtool && typeof sourceMap === 'object' && sourceMap.css) { - chain.plugin('source-map-css').use(bundler.SourceMapDevToolPlugin, [ - { - test: /\.css$/, - filename: '[file].map[query]', - }, - ]); - } - // The base directory for resolving entry points and loaders from the configuration. chain.context(api.context.rootPath); @@ -82,15 +46,6 @@ export const pluginBasic = (): RsbuildPlugin => ({ .use(bundler.HotModuleReplacementPlugin); } - if (isDev) { - // Set correct path for source map - // this helps VS Code break points working correctly in monorepo - chain.output.devtoolModuleFilenameTemplate( - (info: { absoluteResourcePath: string }) => - toPosixPath(info.absoluteResourcePath), - ); - } - if (api.context.bundlerType === 'rspack') { chain.module.parser.merge({ javascript: { diff --git a/packages/core/src/plugins/sourceMap.ts b/packages/core/src/plugins/sourceMap.ts new file mode 100644 index 0000000000..5cde5a4801 --- /dev/null +++ b/packages/core/src/plugins/sourceMap.ts @@ -0,0 +1,118 @@ +import path from 'node:path'; +import { toPosixPath } from '../helpers/path'; +import type { + NormalizedEnvironmentConfig, + RsbuildPlugin, + Rspack, +} from '../types'; + +const getDevtool = (config: NormalizedEnvironmentConfig): Rspack.DevTool => { + const { sourceMap } = config.output; + const isProd = config.mode === 'production'; + + if (sourceMap === false) { + return false; + } + if (sourceMap === true) { + return isProd ? 'source-map' : 'cheap-module-source-map'; + } + if (sourceMap.js === undefined) { + return isProd ? false : 'cheap-module-source-map'; + } + return sourceMap.js; +}; + +export const pluginSourceMap = (): RsbuildPlugin => ({ + name: 'rsbuild:source-map', + + setup(api) { + const sourceMapFilenameTemplate = ({ + absoluteResourcePath, + }: { + absoluteResourcePath: string; + }) => absoluteResourcePath; + + api.modifyBundlerChain((chain, { bundler, environment }) => { + const { config } = environment; + const devtool = getDevtool(config); + + chain + .devtool(devtool) + .output.devtoolModuleFilenameTemplate(sourceMapFilenameTemplate); + + // When JS source map is disabled, but CSS source map is enabled, + // add `SourceMapDevToolPlugin` to let Rspack generate CSS source map. + const { sourceMap } = config.output; + if (!devtool && typeof sourceMap === 'object' && sourceMap.css) { + chain.plugin('source-map-css').use(bundler.SourceMapDevToolPlugin, [ + { + test: /\.css$/, + filename: '[file].map[query]', + }, + ]); + } + }); + + // Use project-relative POSIX paths in source maps: + // - Prevents leaking absolute system paths + // - Keeps maps portable across environments + // - Matches source map spec and debugger expectations + api.processAssets( + // Source maps has been extracted to separate files on this stage + { stage: 'optimize-transfer' }, + ({ assets, compilation, sources }) => { + // If devtoolModuleFilenameTemplate is not the default template, + // which means users want to customize it, skip the default processing. + if ( + compilation.outputOptions.devtoolModuleFilenameTemplate !== + sourceMapFilenameTemplate + ) { + return; + } + + const { distPath } = api.context; + + for (const [filename, asset] of Object.entries(assets)) { + if (!filename.endsWith('.map')) { + continue; + } + + const rawSource = asset.source(); + let map: Rspack.RawSourceMap; + try { + map = JSON.parse( + Buffer.isBuffer(rawSource) ? rawSource.toString() : rawSource, + ); + } catch { + continue; + } + + if (!Array.isArray(map.sources)) { + continue; + } + + const mapDir = path.dirname(path.join(distPath, filename)); + + let isSourcesUpdated = false; + + map.sources = map.sources.map((source) => { + if (path.isAbsolute(source)) { + isSourcesUpdated = true; + return toPosixPath(path.relative(mapDir, source)); + } + return source; + }); + + if (!isSourcesUpdated) { + continue; + } + + compilation.updateAsset( + filename, + new sources.RawSource(JSON.stringify(map)), + ); + } + }, + ); + }, +}); diff --git a/packages/core/tests/__snapshots__/basic.test.ts.snap b/packages/core/tests/__snapshots__/basic.test.ts.snap index ded8526af9..d6ea715079 100644 --- a/packages/core/tests/__snapshots__/basic.test.ts.snap +++ b/packages/core/tests/__snapshots__/basic.test.ts.snap @@ -3,7 +3,6 @@ exports[`plugin-basic > should apply basic config correctly in development 1`] = ` { "context": "/packages/core/tests", - "devtool": "cheap-module-source-map", "experiments": { "inlineConst": false, "inlineEnum": false, @@ -28,9 +27,6 @@ exports[`plugin-basic > should apply basic config correctly in development 1`] = }, }, "name": "web", - "output": { - "devtoolModuleFilenameTemplate": [Function], - }, "performance": { "hints": false, }, @@ -52,7 +48,6 @@ exports[`plugin-basic > should apply basic config correctly in development 1`] = exports[`plugin-basic > should apply basic config correctly in production 1`] = ` { "context": "/packages/core/tests", - "devtool": false, "experiments": { "inlineConst": true, "inlineEnum": true, diff --git a/packages/core/tests/__snapshots__/default.test.ts.snap b/packages/core/tests/__snapshots__/default.test.ts.snap index b169e6ef9f..cd8f5779eb 100644 --- a/packages/core/tests/__snapshots__/default.test.ts.snap +++ b/packages/core/tests/__snapshots__/default.test.ts.snap @@ -962,6 +962,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when prod "output": { "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", + "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", "path": "/packages/core/tests/dist",