Skip to content

Commit bbeca52

Browse files
committed
fix(webpack): es module source mapping improvements
1 parent 2325e33 commit bbeca52

File tree

3 files changed

+226
-45
lines changed

3 files changed

+226
-45
lines changed

packages/core/inspector_modules.ts

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,58 @@ function getConsumer(mapPath: string, sourceMap: any) {
2323
c = new SourceMapConsumer(sourceMap);
2424
consumerCache.set(mapPath, c);
2525
} catch (error) {
26-
console.error(`Failed to create SourceMapConsumer for ${mapPath}:`, error);
26+
// Keep quiet in production-like console; failures just fall back to original stack
27+
console.debug && console.debug(`SourceMapConsumer failed for ${mapPath}:`, error);
2728
return null;
2829
}
2930
}
3031
return c;
3132
}
3233

33-
function loadAndExtractMap(mapPath: string) {
34+
function safeReadText(path: string): string | null {
35+
try {
36+
if (File.exists(path)) {
37+
return File.fromPath(path).readTextSync();
38+
}
39+
} catch (_) {}
40+
return null;
41+
}
42+
43+
function findInlineOrLinkedMapFromJs(jsPath: string): { key: string; text: string } | null {
44+
const jsText = safeReadText(jsPath);
45+
if (!jsText) return null;
46+
47+
// Look for the last sourceMappingURL directive
48+
// Supports both //# and /*# */ styles; capture up to line end or */
49+
const re = /[#@]\s*sourceMappingURL=([^\s*]+)(?:\s*\*\/)?/g;
50+
let match: RegExpExecArray | null = null;
51+
let last: RegExpExecArray | null = null;
52+
while ((match = re.exec(jsText))) last = match;
53+
if (!last) return null;
54+
55+
const url = last[1];
56+
if (url.startsWith('data:application/json')) {
57+
const base64 = url.split(',')[1];
58+
if (!base64) return null;
59+
try {
60+
const text = atob(base64);
61+
return { key: `inline:${jsPath}`, text };
62+
} catch (_) {
63+
return null;
64+
}
65+
}
66+
67+
// Linked .map file (relative)
68+
const jsDir = jsPath.substring(0, jsPath.lastIndexOf('/'));
69+
const mapPath = `${jsDir}/${url}`;
70+
const text = safeReadText(mapPath);
71+
if (text) {
72+
return { key: mapPath, text };
73+
}
74+
return null;
75+
}
76+
77+
function loadAndExtractMap(mapPath: string, fallbackJsPath?: string) {
3478
// check cache first
3579
if (!loadedSourceMaps) {
3680
loadedSourceMaps = new Map();
@@ -67,12 +111,24 @@ function loadAndExtractMap(mapPath: string) {
67111
mapText = binary;
68112
}
69113
} catch (error) {
70-
console.error(`Failed to load source map ${mapPath}:`, error);
114+
console.debug && console.debug(`Failed to load source map ${mapPath}:`, error);
71115
return null;
72116
}
73117
} else {
74-
// no source maps
75-
return null;
118+
// Try fallback: read inline or linked map from the JS file itself
119+
if (fallbackJsPath) {
120+
const alt = findInlineOrLinkedMapFromJs(fallbackJsPath);
121+
if (alt && alt.text) {
122+
mapText = alt.text;
123+
// Cache under both the requested key and the alt key so future lookups are fast
124+
loadedSourceMaps.set(alt.key, alt.text);
125+
} else {
126+
return null;
127+
}
128+
} else {
129+
// no source maps
130+
return null;
131+
}
76132
}
77133
}
78134
loadedSourceMaps.set(mapPath, mapText); // cache it
@@ -92,9 +148,19 @@ function remapFrame(file: string, line: number, column: number) {
92148
if (usingSourceMapFiles) {
93149
sourceMapFileExt = '.map';
94150
}
95-
const mapPath = `${appPath}/${file.replace('file:///app/', '')}${sourceMapFileExt}`;
151+
const rel = file.replace('file:///app/', '');
152+
const jsPath = `${appPath}/${rel}`;
153+
let mapPath = `${jsPath}${sourceMapFileExt}`; // default: same name + .map
154+
155+
// Fallback: if .mjs.map missing, try .js.map
156+
if (!File.exists(mapPath) && rel.endsWith('.mjs')) {
157+
const jsMapFallback = `${appPath}/${rel.replace(/\.mjs$/, '.js.map')}`;
158+
if (File.exists(jsMapFallback)) {
159+
mapPath = jsMapFallback;
160+
}
161+
}
96162

97-
const sourceMap = loadAndExtractMap(mapPath);
163+
const sourceMap = loadAndExtractMap(mapPath, jsPath);
98164

99165
if (!sourceMap) {
100166
return { source: null, line: 0, column: 0 };
@@ -108,26 +174,44 @@ function remapFrame(file: string, line: number, column: number) {
108174
try {
109175
return consumer.originalPositionFor({ line, column });
110176
} catch (error) {
111-
console.error(`Failed to get original position for ${file}:${line}:${column}:`, error);
177+
console.debug && console.debug(`Remap failed for ${file}:${line}:${column}:`, error);
112178
return { source: null, line: 0, column: 0 };
113179
}
114180
}
115181

116182
function remapStack(raw: string): string {
117183
const lines = raw.split('\n');
118184
const out = lines.map((line) => {
119-
const m = /\((.+):(\d+):(\d+)\)/.exec(line);
120-
if (!m) return line;
185+
// 1) Parenthesized frame: at fn (file:...:L:C)
186+
let m = /\((.+):(\d+):(\d+)\)/.exec(line);
187+
if (m) {
188+
try {
189+
const [_, file, l, c] = m;
190+
const orig = remapFrame(file, +l, +c);
191+
if (!orig.source) return line;
192+
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`);
193+
} catch (error) {
194+
console.debug && console.debug('Remap failed for frame:', line, error);
195+
return line;
196+
}
197+
}
121198

122-
try {
123-
const [_, file, l, c] = m;
124-
const orig = remapFrame(file, +l, +c);
125-
if (!orig.source) return line;
126-
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`);
127-
} catch (error) {
128-
console.error('Failed to remap stack frame:', line, error);
129-
return line; // return original line if remapping fails
199+
// 2) Bare frame: at file:///app/vendor.js:L:C (no parentheses)
200+
const bare = /(\s+at\s+)([^\s()]+):(\d+):(\d+)/.exec(line);
201+
if (bare) {
202+
try {
203+
const [, prefix, file, l, c] = bare;
204+
const orig = remapFrame(file, +l, +c);
205+
if (!orig.source) return line;
206+
const replacement = `${prefix}${orig.source}:${orig.line}:${orig.column}`;
207+
return line.replace(bare[0], replacement);
208+
} catch (error) {
209+
console.debug && console.debug('Remap failed for bare frame:', line, error);
210+
return line;
211+
}
130212
}
213+
214+
return line;
131215
});
132216
return out.join('\n');
133217
}
@@ -141,7 +225,7 @@ function remapStack(raw: string): string {
141225
try {
142226
return remapStack(rawStack);
143227
} catch (error) {
144-
console.error('Failed to remap stack trace, returning original:', error);
228+
console.debug && console.debug('Remap failed, returning original:', error);
145229
return rawStack; // fallback to original stack trace
146230
}
147231
};

packages/webpack5/src/configuration/base.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { applyDotEnvPlugin } from '../helpers/dotEnv';
2424
import { env as _env, IWebpackEnv } from '../index';
2525
import { getValue } from '../helpers/config';
2626
import { getIPS } from '../helpers/host';
27+
import FixSourceMapUrlPlugin from '../plugins/FixSourceMapUrlPlugin';
2728
import {
2829
getAvailablePlatforms,
2930
getAbsoluteDistPath,
@@ -178,32 +179,9 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
178179

179180
// For ESM builds, fix the sourceMappingURL to use correct paths
180181
if (!env.commonjs && sourceMapType && sourceMapType !== 'hidden-source-map') {
181-
class FixSourceMapUrlPlugin {
182-
apply(compiler) {
183-
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation) => {
184-
const leadingCharacter = process.platform === 'win32' ? '/' : '';
185-
Object.keys(compilation.assets).forEach((filename) => {
186-
if (filename.endsWith('.mjs') || filename.endsWith('.js')) {
187-
const asset = compilation.assets[filename];
188-
let source = asset.source();
189-
190-
// Replace sourceMappingURL to use file:// protocol pointing to actual location
191-
source = source.replace(
192-
/\/\/# sourceMappingURL=(.+\.map)/g,
193-
`//# sourceMappingURL=file://${leadingCharacter}${outputPath}/$1`,
194-
);
195-
196-
compilation.assets[filename] = {
197-
source: () => source,
198-
size: () => source.length,
199-
};
200-
}
201-
});
202-
});
203-
}
204-
}
205-
206-
config.plugin('FixSourceMapUrlPlugin').use(FixSourceMapUrlPlugin);
182+
config
183+
.plugin('FixSourceMapUrlPlugin')
184+
.use(FixSourceMapUrlPlugin as any, [{ outputPath }]);
207185
}
208186

209187
// when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { Compiler } from 'webpack';
2+
import { sources } from 'webpack';
3+
4+
export interface FixSourceMapUrlPluginOptions {
5+
outputPath: string;
6+
}
7+
8+
/**
9+
* Ensures sourceMappingURL points to the actual file:// location on device/emulator.
10+
* Handles Webpack 5 asset sources (string/Buffer/Source objects).
11+
*/
12+
export default class FixSourceMapUrlPlugin {
13+
constructor(private readonly options: FixSourceMapUrlPluginOptions) {}
14+
15+
apply(compiler: Compiler) {
16+
const wp: any = (compiler as any).webpack;
17+
const hasProcessAssets =
18+
!!wp?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING &&
19+
!!(compiler as any).hooks?.thisCompilation;
20+
21+
const leadingCharacter = process.platform === 'win32' ? '/' : '';
22+
23+
const toStringContent = (content: any): string => {
24+
if (typeof content === 'string') return content;
25+
if (Buffer.isBuffer(content)) return content.toString('utf-8');
26+
if (content && typeof content.source === 'function') {
27+
const inner = content.source();
28+
if (typeof inner === 'string') return inner;
29+
if (Buffer.isBuffer(inner)) return inner.toString('utf-8');
30+
try {
31+
return String(inner);
32+
} catch {
33+
return '';
34+
}
35+
}
36+
try {
37+
return String(content);
38+
} catch {
39+
return '';
40+
}
41+
};
42+
43+
const processFile = (filename: string, compilation: any) => {
44+
if (!(filename.endsWith('.mjs') || filename.endsWith('.js'))) return;
45+
46+
// Support both legacy compilation.assets and v5 Asset API
47+
let rawSource: any;
48+
if (typeof (compilation as any).getAsset === 'function') {
49+
const assetObj = (compilation as any).getAsset(filename);
50+
if (assetObj && assetObj.source) {
51+
rawSource = (assetObj.source as any).source
52+
? (assetObj.source as any).source()
53+
: (assetObj.source as any)();
54+
}
55+
}
56+
if (
57+
rawSource === undefined &&
58+
(compilation as any).assets &&
59+
(compilation as any).assets[filename]
60+
) {
61+
const asset = (compilation as any).assets[filename];
62+
rawSource = typeof asset.source === 'function' ? asset.source() : asset;
63+
}
64+
65+
let source = toStringContent(rawSource);
66+
// Replace sourceMappingURL to use file:// protocol pointing to actual location
67+
source = source.replace(
68+
/\/\/\# sourceMappingURL=(.+\.map)/g,
69+
`//# sourceMappingURL=file://${leadingCharacter}${this.options.outputPath}/$1`,
70+
);
71+
72+
// Prefer Webpack 5 updateAsset with RawSource when available
73+
const RawSourceCtor =
74+
wp?.sources?.RawSource || (sources as any)?.RawSource;
75+
if (
76+
typeof (compilation as any).updateAsset === 'function' &&
77+
RawSourceCtor
78+
) {
79+
(compilation as any).updateAsset(filename, new RawSourceCtor(source));
80+
} else {
81+
(compilation as any).assets[filename] = {
82+
source: () => source,
83+
size: () => source.length,
84+
};
85+
}
86+
};
87+
88+
if (hasProcessAssets) {
89+
compiler.hooks.thisCompilation.tap(
90+
'FixSourceMapUrlPlugin',
91+
(compilation: any) => {
92+
// IMPORTANT:
93+
// Run AFTER SourceMapDevToolPlugin has emitted external map assets.
94+
// If we run at DEV_TOOLING and replace sources, we may drop mapping info
95+
// before Webpack has a chance to write .map files. SUMMARIZE happens later.
96+
const stage =
97+
wp.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE ||
98+
// Fallback to DEV_TOOLING if summarize is unavailable
99+
wp.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING;
100+
compilation.hooks.processAssets.tap(
101+
{ name: 'FixSourceMapUrlPlugin', stage },
102+
(assets: Record<string, any>) => {
103+
Object.keys(assets).forEach((filename) =>
104+
processFile(filename, compilation),
105+
);
106+
},
107+
);
108+
},
109+
);
110+
} else {
111+
// Fallback for older setups: use emit (may log deprecation in newer webpack)
112+
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation: any) => {
113+
Object.keys((compilation as any).assets).forEach((filename) =>
114+
processFile(filename, compilation),
115+
);
116+
});
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)