// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import * as fs from "fs"; import * as path from "path"; import { SourceMapConsumer, RawSourceMap, SourceMapGenerator, MappingItem, Mapping, Position, NullableMappedPosition, } from "source-map"; import * as sourceMapResolve from "source-map-resolve"; const DISK_LETTER_RE = /^(?:[a-z]{2,}:\/{3})?[a-z]:/i; export class SourceMapsCombinator { public convert(rawBundleSourcemap: RawSourceMap): RawSourceMap { // Find user files from bundle files list const consumers: { [key: string]: SourceMapConsumer } = rawBundleSourcemap.sources.reduce( (result: { [key: string]: SourceMapConsumer }, file) => { // Skip files inside node_modules if (file.includes("node_modules")) return result; try { const consumer: SourceMapConsumer | null = this.getSourceMapConsumerFrom(file); if (consumer) result[file] = consumer; } finally { return result; } }, {}, ); if (Object.keys(consumers).length === 0) { // Sourcemaps not found, so return original bundle sourcemap return rawBundleSourcemap; } const generator = new SourceMapGenerator(); const bundleConsumer = new SourceMapConsumer(rawBundleSourcemap); bundleConsumer.eachMapping((item: MappingItem) => { if (item.source === null) { // Some mappings in react native bundle have no sources return; } // Copy mappings const mapping: Mapping = { generated: { line: item.generatedLine, column: item.generatedColumn }, original: { line: item.originalLine, column: item.originalColumn }, source: item.source, name: item.name, }; if (consumers[item.source]) { const jsPosition: Position = { line: item.originalLine, column: item.originalColumn, }; const tsPosition: NullableMappedPosition = consumers[item.source].originalPositionFor(jsPosition); if (tsPosition.source === null) { // Some positions from react native generated bundle can not translate to TS source positions // skip them return; } // Resolve TS source path to absolute because it might be relative to generated JS // (this depends on whether "sourceRoot" option is specified in tsconfig.json) if (!tsPosition.source.match(DISK_LETTER_RE)) { // This check for Windows tests which were run on MacOs tsPosition.source = path.resolve( rawBundleSourcemap.sourceRoot, path.dirname(item.source), tsPosition.source, ); } // Update mapping w/ mapped position values mapping.source = tsPosition.source; mapping.name = tsPosition.name || mapping.name; if (tsPosition.line !== null && tsPosition.column !== null) { mapping.original = { line: tsPosition.line, column: tsPosition.column }; } } try { generator.addMapping(mapping); } catch (err) {} }); return generator.toJSON(); } private getSourceMapConsumerFrom(generatedFile: string): SourceMapConsumer | null { const code = fs.readFileSync(generatedFile); const consumer = this.readSourcemap(generatedFile, code.toString()); return consumer; } private readSourcemap(file: string, code: string): SourceMapConsumer | null { const result = sourceMapResolve.resolveSync( code, file, readFileSync.bind(null, getDiskLetter(file)), ); if (result === null) { return null; } return new SourceMapConsumer(result.map); } } // Hack for source-map-resolve and cutted disk letter // https://github.com/lydell/source-map-resolve/issues/9 function readFileSync(diskLetter: string, filePath: string) { return filePath.match(DISK_LETTER_RE) ? fs.readFileSync(filePath) : fs.readFileSync(`${diskLetter}${filePath}`); } function getDiskLetter(filePath: string): string { const matched = filePath.match(DISK_LETTER_RE); return matched ? matched[0] : ""; }