From c934d898b4d1bd6daf7279dd95137de404c99348 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 2 Aug 2025 17:46:33 -0700 Subject: [PATCH] feat: source map remapping for runtime stack trace displays --- packages/core/inspector_modules.ts | 114 ++++++++++++++++++++ packages/core/package.json | 1 + packages/webpack5/src/configuration/base.ts | 14 ++- 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/packages/core/inspector_modules.ts b/packages/core/inspector_modules.ts index ca9c177a1..c9e283d69 100644 --- a/packages/core/inspector_modules.ts +++ b/packages/core/inspector_modules.ts @@ -6,3 +6,117 @@ 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 { SourceMapConsumer } from 'source-map-js'; + +// note: webpack config 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; + +function getConsumer(mapPath: string, sourceMap: any): SourceMapConsumer { + if (!consumerCache) { + consumerCache = new Map(); + } + let c = consumerCache.get(mapPath); + if (!c) { + // parse once + c = new SourceMapConsumer(sourceMap); + consumerCache.set(mapPath, c); + } + return c; +} + +function loadAndExtractMap(mapPath: string) { + // check cache first + if (!loadedSourceMaps) { + 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('') + // ); + } + } else { + // no source maps + return { source: null, line: 0, column: 0 }; + } + } + loadedSourceMaps.set(mapPath, mapText); // cache it + return mapText; +} + +function remapFrame(file: string, line: number, column: number) { + /** + * webpack config 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+. + */ + + const appPath = knownFolders.currentApp().path; + let sourceMapFileExt = ''; + if (usingSourceMapFiles) { + sourceMapFileExt = '.map'; + } + const mapPath = `${appPath}/${file.replace('file:///app/', '')}${sourceMapFileExt}`; + + // 3) hand it to the consumer + const sourceMap = loadAndExtractMap(mapPath); + const consumer = getConsumer(mapPath, sourceMap); + return consumer.originalPositionFor({ line, column }); +} + +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; + 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})`); + }); + 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. + */ +(global as any).__ns_remapStack = (rawStack: string) => { + console.log('Remapping stack trace...'); + return remapStack(rawStack); +}; +/** + * End of source map remapping for stack traces + */ diff --git a/packages/core/package.json b/packages/core/package.json index bc46c89bb..5bb4ea766 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,6 +66,7 @@ "css-what": "^6.1.0", "emoji-regex": "^10.2.1", "source-map": "0.6.1", + "source-map-js": "^1.2.0", "tslib": "^2.0.0" }, "nativescript": { diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index 3a363de03..b1114f1c0 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -44,6 +44,14 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // set mode config.mode(mode); + // use source map files with v9+ + function useSourceMapFiles() { + if (mode === 'development') { + // in development we always use source-map files with v9+ runtimes + // they are parsed and mapped to display in-flight app error screens + env.sourceMap = 'source-map'; + } + } // determine target output by @nativescript/core version // v9+ supports ESM output, anything below uses CommonJS if (hasDependency('@nativescript/core')) { @@ -51,9 +59,13 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // ensure alpha/beta/rc versions are considered as well if (coreVersion && !coreVersion.includes('9.0.0')) { if (!satisfies(coreVersion, '>=9.0.0')) { - // @nativescript/core < 9.0.0 uses CommonJS output + // @nativescript/core < 9 uses CommonJS output env.commonjs = true; + } else { + useSourceMapFiles(); } + } else { + useSourceMapFiles(); } }