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
29 changes: 12 additions & 17 deletions e2e/cases/output/source-map/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ const expectSourceMap = async (files: Record<string, string>) => {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<ROOT>/packages/compat/webpack/tests/dist",
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": "<ROOT>/packages/compat/webpack/tests/dist",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/createRsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,6 +79,7 @@ function applyDefaultPlugins(
pluginManager.addPlugins([
pluginBasic(),
pluginEntry(),
pluginSourceMap(),
pluginCache(),
pluginTarget(),
pluginOutput(),
Expand Down
47 changes: 1 addition & 46 deletions packages/core/src/plugins/basic.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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: {
Expand Down
118 changes: 118 additions & 0 deletions packages/core/src/plugins/sourceMap.ts
Original file line number Diff line number Diff line change
@@ -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')) {
Comment on lines +73 to +76

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compute relative source-map paths per environment output

The new source-map plugin derives mapDir from api.context.distPath, which is the common parent directory for all environments. When building multiple environments whose output.distPath values are nested (e.g. dist/web and dist/server), asset filenames are relative to each environment’s own output path, so combining them with the common root drops the environment subfolder. The subsequent path.relative(mapDir, source) therefore emits paths like ../../../src/foo.ts that resolve to <dist>/src/foo.ts instead of the real project source, breaking source map resolution for any environment that does not emit directly into the shared root. Use the environment-specific path (e.g. compilation.outputOptions.path or the environment.distPath passed to the hook) when computing mapDir to keep the relative paths correct.

Useful? React with 👍 / 👎.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will verify this issue and create another PR to fix it.

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)),
);
}
},
);
},
});
5 changes: 0 additions & 5 deletions packages/core/tests/__snapshots__/basic.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
exports[`plugin-basic > should apply basic config correctly in development 1`] = `
{
"context": "<ROOT>/packages/core/tests",
"devtool": "cheap-module-source-map",
"experiments": {
"inlineConst": false,
"inlineEnum": false,
Expand All @@ -28,9 +27,6 @@ exports[`plugin-basic > should apply basic config correctly in development 1`] =
},
},
"name": "web",
"output": {
"devtoolModuleFilenameTemplate": [Function],
},
"performance": {
"hints": false,
},
Expand All @@ -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": "<ROOT>/packages/core/tests",
"devtool": false,
"experiments": {
"inlineConst": true,
"inlineEnum": true,
Expand Down
1 change: 1 addition & 0 deletions packages/core/tests/__snapshots__/default.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<ROOT>/packages/core/tests/dist",
Expand Down
Loading