From ce589a8fc90fd7d5680c491b5ea336f98e77a84b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 27 Sep 2025 11:43:46 -0700 Subject: [PATCH] fix(webpack): es module source map resolution (#10860) Including source file remapping for newer runtimes. --- packages/core/inspector_modules.ts | 122 ++++++++++++-------- packages/webpack5/src/configuration/base.ts | 35 +++++- 2 files changed, 108 insertions(+), 49 deletions(-) diff --git a/packages/core/inspector_modules.ts b/packages/core/inspector_modules.ts index af69d6f88..9c39b37fc 100644 --- a/packages/core/inspector_modules.ts +++ b/packages/core/inspector_modules.ts @@ -1,25 +1,14 @@ import './globals'; - import './debugger/webinspector-network'; import './debugger/webinspector-dom'; import './debugger/webinspector-css'; -// require('./debugger/webinspector-network'); -// require('./debugger/webinspector-dom'); -// require('./debugger/webinspector-css'); - -/** - * Source map remapping for stack traces for the runtime in-flight error displays - * Currently this is very slow. Need to find much faster way to remap stack traces. - * NOTE: This likely should not be in core because errors can happen on boot before core is fully loaded. Ideally the runtime should provide this in full but unsure. - */ import { File, knownFolders } from './file-system'; // import/destructure style helps commonjs/esm build issues import * as sourceMapJs from 'source-map-js'; const { SourceMapConsumer } = sourceMapJs; -// note: webpack config can by default use 'source-map' files with runtimes v9+ +// note: bundlers can by default use 'source-map' files with runtimes v9+ // helps avoid having to decode the inline base64 source maps -// currently same performance on inline vs file source maps so file source maps may just be cleaner const usingSourceMapFiles = true; let loadedSourceMaps: Map; let consumerCache: Map; @@ -30,9 +19,13 @@ function getConsumer(mapPath: string, sourceMap: any) { } let c = consumerCache.get(mapPath); if (!c) { - // parse once - c = new SourceMapConsumer(sourceMap); - consumerCache.set(mapPath, c); + try { + c = new SourceMapConsumer(sourceMap); + consumerCache.set(mapPath, c); + } catch (error) { + console.error(`Failed to create SourceMapConsumer for ${mapPath}:`, error); + return null; + } } return c; } @@ -43,35 +36,43 @@ function loadAndExtractMap(mapPath: string) { loadedSourceMaps = new Map(); } let mapText = loadedSourceMaps.get(mapPath); - // Note: not sure if separate source map files or inline is better - // need to test build times one way or other with webpack, vite and rspack - // but this handles either way if (mapText) { return mapText; // already loaded } else { if (File.exists(mapPath)) { - const contents = File.fromPath(mapPath).readTextSync(); - if (usingSourceMapFiles) { - mapText = contents; - } else { - // parse out the inline base64 - const match = contents.match(/\/\/[#@] sourceMappingURL=data:application\/json[^,]+,(.+)$/); - const base64 = match[1]; - const binary = atob(base64); - // this is the raw text of the source map - // seems to work without doing the decodeURIComponent trick - mapText = binary; - // // escape each char code into %XX and let decodeURIComponent build the UTF-8 string - // mapText = decodeURIComponent( - // binary - // .split('') - // .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) - // .join('') - // ); + try { + const contents = File.fromPath(mapPath).readTextSync(); + + // Note: we may want to do this, keeping for reference if needed in future. + // Check size before processing (skip very large source maps) + // const maxSizeBytes = 10 * 1024 * 1024; // 10MB limit + // if (contents.length > maxSizeBytes) { + // console.warn(`Source map ${mapPath} is too large (${contents.length} bytes), skipping...`); + // return null; + // } + + if (usingSourceMapFiles) { + mapText = contents; + } else { + // parse out the inline base64 + const match = contents.match(/\/\/[#@] sourceMappingURL=data:application\/json[^,]+,(.+)$/); + if (!match) { + console.warn(`Invalid source map format in ${mapPath}`); + return null; + } + const base64 = match[1]; + const binary = atob(base64); + // this is the raw text of the source map + // seems to work without doing decodeURIComponent tricks + mapText = binary; + } + } catch (error) { + console.error(`Failed to load source map ${mapPath}:`, error); + return null; } } else { // no source maps - return { source: null, line: 0, column: 0 }; + return null; } } loadedSourceMaps.set(mapPath, mapText); // cache it @@ -80,9 +81,10 @@ function loadAndExtractMap(mapPath: string) { function remapFrame(file: string, line: number, column: number) { /** - * webpack config can use source map files or inline. + * bundlers can use source map files or inline. * To use source map files, run with `--env.sourceMap=source-map`. - * @nativescript/webpack 5.1 enables `source-map` files by default when using runtimes v9+. + * Notes: + * Starting with @nativescript/webpack 5.0.25, `source-map` files are used by default when using runtimes v9+. */ const appPath = knownFolders.currentApp().path; @@ -92,10 +94,23 @@ function remapFrame(file: string, line: number, column: number) { } const mapPath = `${appPath}/${file.replace('file:///app/', '')}${sourceMapFileExt}`; - // 3) hand it to the consumer const sourceMap = loadAndExtractMap(mapPath); + + if (!sourceMap) { + return { source: null, line: 0, column: 0 }; + } + const consumer = getConsumer(mapPath, sourceMap); - return consumer.originalPositionFor({ line, column }); + if (!consumer) { + return { source: null, line: 0, column: 0 }; + } + + try { + return consumer.originalPositionFor({ line, column }); + } catch (error) { + console.error(`Failed to get original position for ${file}:${line}:${column}:`, error); + return { source: null, line: 0, column: 0 }; + } } function remapStack(raw: string): string { @@ -103,21 +118,32 @@ function remapStack(raw: string): string { const out = lines.map((line) => { const m = /\((.+):(\d+):(\d+)\)/.exec(line); if (!m) return line; - 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})`); + + 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 + } }); return out.join('\n'); } /** - * Added in 9.0 runtimes. - * Allows the runtime to remap stack traces before displaying them in the in-flight error screens. + * Added with 9.0 runtimes. + * Allows the runtime to remap stack traces before displaying them via in-flight error screens. */ (global as any).__ns_remapStack = (rawStack: string) => { // console.log('Remapping stack trace...'); - return remapStack(rawStack); + try { + return remapStack(rawStack); + } catch (error) { + console.error('Failed to remap stack trace, returning original:', error); + return rawStack; // fallback to original stack trace + } }; /** * End of source map remapping for stack traces diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index fcc846ad1..5f65f90a7 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -171,7 +171,40 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { return map as Config.DevTool; }; - config.devtool(getSourceMapType(env.sourceMap)); + const sourceMapType = getSourceMapType(env.sourceMap); + + // Use devtool for both CommonJS and ESM - let webpack handle source mapping properly + config.devtool(sourceMapType); + + // 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); + } // when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder if (env.sourceMap === 'hidden-source-map') {