From cfa3e209d8d0f24af9525a4f7b1906506b06b3dc Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Mon, 3 Nov 2025 19:25:47 -0800 Subject: [PATCH] fix(webpack): es module source mapping improvements --- packages/core/inspector_modules.ts | 49 +++++--- packages/webpack5/src/configuration/base.ts | 30 +---- .../src/plugins/FixSourceMapUrlPlugin.ts | 112 ++++++++++++++++++ 3 files changed, 150 insertions(+), 41 deletions(-) create mode 100644 packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts diff --git a/packages/core/inspector_modules.ts b/packages/core/inspector_modules.ts index 9c39b37fc..e631503f7 100644 --- a/packages/core/inspector_modules.ts +++ b/packages/core/inspector_modules.ts @@ -23,7 +23,8 @@ 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; } } @@ -67,7 +68,7 @@ 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 { @@ -108,7 +109,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 +117,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; - - 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 + // 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.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 +160,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 } }; diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index f7bcdf887..803ea02ce 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -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 diff --git a/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts b/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts new file mode 100644 index 000000000..1aed1bc98 --- /dev/null +++ b/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts @@ -0,0 +1,112 @@ +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) => { + const stage = wp.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING; + compilation.hooks.processAssets.tap( + { name: 'FixSourceMapUrlPlugin', stage }, + (assets: Record) => { + 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), + ); + }); + } + } +}