fix(webpack): es module source mapping improvements

This commit is contained in:
Nathan Walker
2025-11-03 19:25:47 -08:00
parent 2325e338be
commit bbeca526f0
3 changed files with 227 additions and 46 deletions

View File

@@ -23,14 +23,58 @@ function getConsumer(mapPath: string, sourceMap: any) {
c = new SourceMapConsumer(sourceMap);
consumerCache.set(mapPath, c);
} catch (error) {
console.error(`Failed to create SourceMapConsumer for ${mapPath}:`, error);
// Keep quiet in production-like console; failures just fall back to original stack
console.debug && console.debug(`SourceMapConsumer failed for ${mapPath}:`, error);
return null;
}
}
return c;
}
function loadAndExtractMap(mapPath: string) {
function safeReadText(path: string): string | null {
try {
if (File.exists(path)) {
return File.fromPath(path).readTextSync();
}
} catch (_) {}
return null;
}
function findInlineOrLinkedMapFromJs(jsPath: string): { key: string; text: string } | null {
const jsText = safeReadText(jsPath);
if (!jsText) return null;
// Look for the last sourceMappingURL directive
// Supports both //# and /*# */ styles; capture up to line end or */
const re = /[#@]\s*sourceMappingURL=([^\s*]+)(?:\s*\*\/)?/g;
let match: RegExpExecArray | null = null;
let last: RegExpExecArray | null = null;
while ((match = re.exec(jsText))) last = match;
if (!last) return null;
const url = last[1];
if (url.startsWith('data:application/json')) {
const base64 = url.split(',')[1];
if (!base64) return null;
try {
const text = atob(base64);
return { key: `inline:${jsPath}`, text };
} catch (_) {
return null;
}
}
// Linked .map file (relative)
const jsDir = jsPath.substring(0, jsPath.lastIndexOf('/'));
const mapPath = `${jsDir}/${url}`;
const text = safeReadText(mapPath);
if (text) {
return { key: mapPath, text };
}
return null;
}
function loadAndExtractMap(mapPath: string, fallbackJsPath?: string) {
// check cache first
if (!loadedSourceMaps) {
loadedSourceMaps = new Map();
@@ -67,7 +111,18 @@ function loadAndExtractMap(mapPath: string) {
mapText = binary;
}
} catch (error) {
console.error(`Failed to load source map ${mapPath}:`, error);
console.debug && console.debug(`Failed to load source map ${mapPath}:`, error);
return null;
}
} else {
// Try fallback: read inline or linked map from the JS file itself
if (fallbackJsPath) {
const alt = findInlineOrLinkedMapFromJs(fallbackJsPath);
if (alt && alt.text) {
mapText = alt.text;
// Cache under both the requested key and the alt key so future lookups are fast
loadedSourceMaps.set(alt.key, alt.text);
} else {
return null;
}
} else {
@@ -75,6 +130,7 @@ function loadAndExtractMap(mapPath: string) {
return null;
}
}
}
loadedSourceMaps.set(mapPath, mapText); // cache it
return mapText;
}
@@ -92,9 +148,19 @@ function remapFrame(file: string, line: number, column: number) {
if (usingSourceMapFiles) {
sourceMapFileExt = '.map';
}
const mapPath = `${appPath}/${file.replace('file:///app/', '')}${sourceMapFileExt}`;
const rel = file.replace('file:///app/', '');
const jsPath = `${appPath}/${rel}`;
let mapPath = `${jsPath}${sourceMapFileExt}`; // default: same name + .map
const sourceMap = loadAndExtractMap(mapPath);
// Fallback: if .mjs.map missing, try .js.map
if (!File.exists(mapPath) && rel.endsWith('.mjs')) {
const jsMapFallback = `${appPath}/${rel.replace(/\.mjs$/, '.js.map')}`;
if (File.exists(jsMapFallback)) {
mapPath = jsMapFallback;
}
}
const sourceMap = loadAndExtractMap(mapPath, jsPath);
if (!sourceMap) {
return { source: null, line: 0, column: 0 };
@@ -108,7 +174,7 @@ function remapFrame(file: string, line: number, column: number) {
try {
return consumer.originalPositionFor({ line, column });
} catch (error) {
console.error(`Failed to get original position for ${file}:${line}:${column}:`, error);
console.debug && console.debug(`Remap failed for ${file}:${line}:${column}:`, error);
return { source: null, line: 0, column: 0 };
}
}
@@ -116,18 +182,36 @@ function remapFrame(file: string, line: number, column: number) {
function remapStack(raw: string): string {
const lines = raw.split('\n');
const out = lines.map((line) => {
const m = /\((.+):(\d+):(\d+)\)/.exec(line);
if (!m) return line;
// 1) Parenthesized frame: at fn (file:...:L:C)
let m = /\((.+):(\d+):(\d+)\)/.exec(line);
if (m) {
try {
const [_, file, l, c] = m;
const orig = remapFrame(file, +l, +c);
if (!orig.source) return line;
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`);
} catch (error) {
console.error('Failed to remap stack frame:', line, error);
return line; // return original line if remapping fails
console.debug && console.debug('Remap failed for frame:', line, error);
return line;
}
}
// 2) Bare frame: at file:///app/vendor.js:L:C (no parentheses)
const bare = /(\s+at\s+)([^\s()]+):(\d+):(\d+)/.exec(line);
if (bare) {
try {
const [, prefix, file, l, c] = bare;
const orig = remapFrame(file, +l, +c);
if (!orig.source) return line;
const replacement = `${prefix}${orig.source}:${orig.line}:${orig.column}`;
return line.replace(bare[0], replacement);
} catch (error) {
console.debug && console.debug('Remap failed for bare frame:', line, error);
return line;
}
}
return line;
});
return out.join('\n');
}
@@ -141,7 +225,7 @@ function remapStack(raw: string): string {
try {
return remapStack(rawStack);
} catch (error) {
console.error('Failed to remap stack trace, returning original:', error);
console.debug && console.debug('Remap failed, returning original:', error);
return rawStack; // fallback to original stack trace
}
};

View File

@@ -24,6 +24,7 @@ import { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index';
import { getValue } from '../helpers/config';
import { getIPS } from '../helpers/host';
import FixSourceMapUrlPlugin from '../plugins/FixSourceMapUrlPlugin';
import {
getAvailablePlatforms,
getAbsoluteDistPath,
@@ -178,32 +179,9 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// For ESM builds, fix the sourceMappingURL to use correct paths
if (!env.commonjs && sourceMapType && sourceMapType !== 'hidden-source-map') {
class FixSourceMapUrlPlugin {
apply(compiler) {
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation) => {
const leadingCharacter = process.platform === 'win32' ? '/' : '';
Object.keys(compilation.assets).forEach((filename) => {
if (filename.endsWith('.mjs') || filename.endsWith('.js')) {
const asset = compilation.assets[filename];
let source = asset.source();
// Replace sourceMappingURL to use file:// protocol pointing to actual location
source = source.replace(
/\/\/# sourceMappingURL=(.+\.map)/g,
`//# sourceMappingURL=file://${leadingCharacter}${outputPath}/$1`,
);
compilation.assets[filename] = {
source: () => source,
size: () => source.length,
};
}
});
});
}
}
config.plugin('FixSourceMapUrlPlugin').use(FixSourceMapUrlPlugin);
config
.plugin('FixSourceMapUrlPlugin')
.use(FixSourceMapUrlPlugin as any, [{ outputPath }]);
}
// when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder

View File

@@ -0,0 +1,119 @@
import type { Compiler } from 'webpack';
import { sources } from 'webpack';
export interface FixSourceMapUrlPluginOptions {
outputPath: string;
}
/**
* Ensures sourceMappingURL points to the actual file:// location on device/emulator.
* Handles Webpack 5 asset sources (string/Buffer/Source objects).
*/
export default class FixSourceMapUrlPlugin {
constructor(private readonly options: FixSourceMapUrlPluginOptions) {}
apply(compiler: Compiler) {
const wp: any = (compiler as any).webpack;
const hasProcessAssets =
!!wp?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING &&
!!(compiler as any).hooks?.thisCompilation;
const leadingCharacter = process.platform === 'win32' ? '/' : '';
const toStringContent = (content: any): string => {
if (typeof content === 'string') return content;
if (Buffer.isBuffer(content)) return content.toString('utf-8');
if (content && typeof content.source === 'function') {
const inner = content.source();
if (typeof inner === 'string') return inner;
if (Buffer.isBuffer(inner)) return inner.toString('utf-8');
try {
return String(inner);
} catch {
return '';
}
}
try {
return String(content);
} catch {
return '';
}
};
const processFile = (filename: string, compilation: any) => {
if (!(filename.endsWith('.mjs') || filename.endsWith('.js'))) return;
// Support both legacy compilation.assets and v5 Asset API
let rawSource: any;
if (typeof (compilation as any).getAsset === 'function') {
const assetObj = (compilation as any).getAsset(filename);
if (assetObj && assetObj.source) {
rawSource = (assetObj.source as any).source
? (assetObj.source as any).source()
: (assetObj.source as any)();
}
}
if (
rawSource === undefined &&
(compilation as any).assets &&
(compilation as any).assets[filename]
) {
const asset = (compilation as any).assets[filename];
rawSource = typeof asset.source === 'function' ? asset.source() : asset;
}
let source = toStringContent(rawSource);
// Replace sourceMappingURL to use file:// protocol pointing to actual location
source = source.replace(
/\/\/\# sourceMappingURL=(.+\.map)/g,
`//# sourceMappingURL=file://${leadingCharacter}${this.options.outputPath}/$1`,
);
// Prefer Webpack 5 updateAsset with RawSource when available
const RawSourceCtor =
wp?.sources?.RawSource || (sources as any)?.RawSource;
if (
typeof (compilation as any).updateAsset === 'function' &&
RawSourceCtor
) {
(compilation as any).updateAsset(filename, new RawSourceCtor(source));
} else {
(compilation as any).assets[filename] = {
source: () => source,
size: () => source.length,
};
}
};
if (hasProcessAssets) {
compiler.hooks.thisCompilation.tap(
'FixSourceMapUrlPlugin',
(compilation: any) => {
// IMPORTANT:
// Run AFTER SourceMapDevToolPlugin has emitted external map assets.
// If we run at DEV_TOOLING and replace sources, we may drop mapping info
// before Webpack has a chance to write .map files. SUMMARIZE happens later.
const stage =
wp.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE ||
// Fallback to DEV_TOOLING if summarize is unavailable
wp.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING;
compilation.hooks.processAssets.tap(
{ name: 'FixSourceMapUrlPlugin', stage },
(assets: Record<string, any>) => {
Object.keys(assets).forEach((filename) =>
processFile(filename, compilation),
);
},
);
},
);
} else {
// Fallback for older setups: use emit (may log deprecation in newer webpack)
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation: any) => {
Object.keys((compilation as any).assets).forEach((filename) =>
processFile(filename, compilation),
);
});
}
}
}