fix(webpack): es module source map resolution (#10860)

Including source file remapping for newer runtimes.
This commit is contained in:
Nathan Walker
2025-09-27 11:43:46 -07:00
committed by GitHub
parent 54c069f519
commit ce589a8fc9
2 changed files with 108 additions and 49 deletions

View File

@ -1,25 +1,14 @@
import './globals'; import './globals';
import './debugger/webinspector-network'; import './debugger/webinspector-network';
import './debugger/webinspector-dom'; import './debugger/webinspector-dom';
import './debugger/webinspector-css'; 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 { File, knownFolders } from './file-system';
// import/destructure style helps commonjs/esm build issues // import/destructure style helps commonjs/esm build issues
import * as sourceMapJs from 'source-map-js'; import * as sourceMapJs from 'source-map-js';
const { SourceMapConsumer } = sourceMapJs; 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 // 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; const usingSourceMapFiles = true;
let loadedSourceMaps: Map<string, any>; let loadedSourceMaps: Map<string, any>;
let consumerCache: Map<string, any>; let consumerCache: Map<string, any>;
@ -30,9 +19,13 @@ function getConsumer(mapPath: string, sourceMap: any) {
} }
let c = consumerCache.get(mapPath); let c = consumerCache.get(mapPath);
if (!c) { if (!c) {
// parse once try {
c = new SourceMapConsumer(sourceMap); c = new SourceMapConsumer(sourceMap);
consumerCache.set(mapPath, c); consumerCache.set(mapPath, c);
} catch (error) {
console.error(`Failed to create SourceMapConsumer for ${mapPath}:`, error);
return null;
}
} }
return c; return c;
} }
@ -43,35 +36,43 @@ function loadAndExtractMap(mapPath: string) {
loadedSourceMaps = new Map(); loadedSourceMaps = new Map();
} }
let mapText = loadedSourceMaps.get(mapPath); 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) { if (mapText) {
return mapText; // already loaded return mapText; // already loaded
} else { } else {
if (File.exists(mapPath)) { if (File.exists(mapPath)) {
const contents = File.fromPath(mapPath).readTextSync(); try {
if (usingSourceMapFiles) { const contents = File.fromPath(mapPath).readTextSync();
mapText = contents;
} else { // Note: we may want to do this, keeping for reference if needed in future.
// parse out the inline base64 // Check size before processing (skip very large source maps)
const match = contents.match(/\/\/[#@] sourceMappingURL=data:application\/json[^,]+,(.+)$/); // const maxSizeBytes = 10 * 1024 * 1024; // 10MB limit
const base64 = match[1]; // if (contents.length > maxSizeBytes) {
const binary = atob(base64); // console.warn(`Source map ${mapPath} is too large (${contents.length} bytes), skipping...`);
// this is the raw text of the source map // return null;
// seems to work without doing the decodeURIComponent trick // }
mapText = binary;
// // escape each char code into %XX and let decodeURIComponent build the UTF-8 string if (usingSourceMapFiles) {
// mapText = decodeURIComponent( mapText = contents;
// binary } else {
// .split('') // parse out the inline base64
// .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) const match = contents.match(/\/\/[#@] sourceMappingURL=data:application\/json[^,]+,(.+)$/);
// .join('') 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 { } else {
// no source maps // no source maps
return { source: null, line: 0, column: 0 }; return null;
} }
} }
loadedSourceMaps.set(mapPath, mapText); // cache it loadedSourceMaps.set(mapPath, mapText); // cache it
@ -80,9 +81,10 @@ function loadAndExtractMap(mapPath: string) {
function remapFrame(file: string, line: number, column: number) { 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`. * 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; 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}`; const mapPath = `${appPath}/${file.replace('file:///app/', '')}${sourceMapFileExt}`;
// 3) hand it to the consumer
const sourceMap = loadAndExtractMap(mapPath); const sourceMap = loadAndExtractMap(mapPath);
if (!sourceMap) {
return { source: null, line: 0, column: 0 };
}
const consumer = getConsumer(mapPath, sourceMap); 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 { function remapStack(raw: string): string {
@ -103,21 +118,32 @@ function remapStack(raw: string): string {
const out = lines.map((line) => { const out = lines.map((line) => {
const m = /\((.+):(\d+):(\d+)\)/.exec(line); const m = /\((.+):(\d+):(\d+)\)/.exec(line);
if (!m) return line; if (!m) return line;
const [_, file, l, c] = m;
const orig = remapFrame(file, +l, +c); try {
if (!orig.source) return line; const [_, file, l, c] = m;
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`); 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'); return out.join('\n');
} }
/** /**
* Added in 9.0 runtimes. * Added with 9.0 runtimes.
* Allows the runtime to remap stack traces before displaying them in the in-flight error screens. * Allows the runtime to remap stack traces before displaying them via in-flight error screens.
*/ */
(global as any).__ns_remapStack = (rawStack: string) => { (global as any).__ns_remapStack = (rawStack: string) => {
// console.log('Remapping stack trace...'); // 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 * End of source map remapping for stack traces

View File

@ -171,7 +171,40 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
return map as Config.DevTool; 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 // when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder
if (env.sourceMap === 'hidden-source-map') { if (env.sourceMap === 'hidden-source-map') {