diff --git a/packages/metro-source-map/src/Consumer/AbstractConsumer.js b/packages/metro-source-map/src/Consumer/AbstractConsumer.js index 6932fc62d9..0a1d9e7889 100644 --- a/packages/metro-source-map/src/Consumer/AbstractConsumer.js +++ b/packages/metro-source-map/src/Consumer/AbstractConsumer.js @@ -54,10 +54,14 @@ class AbstractConsumer implements IConsumer { } } - // flowlint unsafe-getters-setters:off + // flowlint-next-line unsafe-getters-setters:off get file(): ?string { return this._sourceMap.file; } + + sourceContentFor(source: string, nullOnMissing: true): ?string { + invariant(false, 'Not implemented'); + } } module.exports = AbstractConsumer; diff --git a/packages/metro-source-map/src/Consumer/DelegatingConsumer.js b/packages/metro-source-map/src/Consumer/DelegatingConsumer.js index 09f82ab419..56bb37d8f7 100644 --- a/packages/metro-source-map/src/Consumer/DelegatingConsumer.js +++ b/packages/metro-source-map/src/Consumer/DelegatingConsumer.js @@ -64,10 +64,14 @@ class DelegatingConsumer implements IConsumer { return this._rootConsumer.eachMapping(callback, context, order); } - // flowlint unsafe-getters-setters:off + // flowlint-next-line unsafe-getters-setters:off get file(): ?string { return this._rootConsumer.file; } + + sourceContentFor(source: string, nullOnMissing: true): ?string { + return this._rootConsumer.sourceContentFor(source, nullOnMissing); + } } module.exports = DelegatingConsumer; diff --git a/packages/metro-source-map/src/Consumer/MappingsConsumer.js b/packages/metro-source-map/src/Consumer/MappingsConsumer.js index 14ba394c7e..6e202b3332 100644 --- a/packages/metro-source-map/src/Consumer/MappingsConsumer.js +++ b/packages/metro-source-map/src/Consumer/MappingsConsumer.js @@ -33,6 +33,7 @@ import type { Mapping, IConsumer, } from './types.flow'; +import type {Number0} from 'ob1'; /** * A source map consumer that supports "basic" source maps (that have a @@ -192,6 +193,28 @@ class MappingsConsumer extends AbstractConsumer implements IConsumer { generatedMappings(): Iterable { return this._decodeAndCacheMappings(); } + + _indexOfSource(source: string): ?Number0 { + const idx = this._normalizeAndCacheSources().indexOf( + normalizeSourcePath(source, this._sourceMap), + ); + if (idx === -1) { + return null; + } + return add0(idx); + } + + sourceContentFor(source: string, nullOnMissing: true): ?string { + const {sourcesContent} = this._sourceMap; + if (!sourcesContent) { + return null; + } + const idx = this._indexOfSource(source); + if (idx == null) { + return null; + } + return sourcesContent[get0(idx)] ?? null; + } } module.exports = MappingsConsumer; diff --git a/packages/metro-source-map/src/Consumer/SectionsConsumer.js b/packages/metro-source-map/src/Consumer/SectionsConsumer.js index 3ffe254541..7b531a9a64 100644 --- a/packages/metro-source-map/src/Consumer/SectionsConsumer.js +++ b/packages/metro-source-map/src/Consumer/SectionsConsumer.js @@ -111,6 +111,16 @@ class SectionsConsumer extends AbstractConsumer implements IConsumer { ); return index != null ? this._consumers[index] : null; } + + sourceContentFor(source: string, nullOnMissing: true): ?string { + for (const [_, consumer] of this._consumers) { + const content = consumer.sourceContentFor(source, nullOnMissing); + if (content != null) { + return content; + } + } + return null; + } } module.exports = SectionsConsumer; diff --git a/packages/metro-source-map/src/Consumer/types.flow.js b/packages/metro-source-map/src/Consumer/types.flow.js index f67860d0bb..722d65d306 100644 --- a/packages/metro-source-map/src/Consumer/types.flow.js +++ b/packages/metro-source-map/src/Consumer/types.flow.js @@ -59,6 +59,12 @@ export interface IConsumer { order?: IterationOrder, ): void; - // flowlint unsafe-getters-setters:off + // flowlint-next-line unsafe-getters-setters:off get file(): ?string; + + sourceContentFor( + source: string, + /* nullOnMissing = false behaves inconsistently upstream, so we don't support it */ + nullOnMissing: true, + ): ?string; } diff --git a/packages/metro-source-map/src/__tests__/Consumer-test.js b/packages/metro-source-map/src/__tests__/Consumer-test.js index 459de5ef18..90659b7ab0 100644 --- a/packages/metro-source-map/src/__tests__/Consumer-test.js +++ b/packages/metro-source-map/src/__tests__/Consumer-test.js @@ -194,6 +194,51 @@ describe('basic maps', () => { `); }); }); + + describe('sourceContentFor', () => { + test('missing sourcesContent', () => { + const consumer = new Consumer({ + version: 3, + mappings: '', + names: [], + sources: [], + }); + expect(consumer.sourceContentFor('a.js', true)).toBeNull(); + }); + + test('null in sourcesContent', () => { + const consumer = new Consumer({ + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: [null], + }); + expect(consumer.sourceContentFor('a.js', true)).toBeNull(); + }); + + test('sourcesContent too short', () => { + const consumer = new Consumer({ + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: [], + }); + expect(consumer.sourceContentFor('a.js', true)).toBeNull(); + }); + + test('string in sourcesContent', () => { + const consumer = new Consumer({ + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: ['content of a.js'], + }); + expect(consumer.sourceContentFor('a.js', true)).toBe('content of a.js'); + }); + }); }); describe('indexed (sectioned) maps', () => { @@ -374,6 +419,64 @@ describe('indexed (sectioned) maps', () => { `); }); }); + + describe('sourceContentFor', () => { + test('empty map', () => { + const consumer = new Consumer({ + version: 3, + sections: [], + }); + expect(consumer.sourceContentFor('a.js', true)).toBeNull(); + }); + + test('found in section', () => { + const consumer = new Consumer({ + version: 3, + sections: [ + { + offset: {line: 0, column: 0}, + map: { + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: ['content of a.js'], + }, + }, + ], + }); + expect(consumer.sourceContentFor('a.js', true)).toBe('content of a.js'); + }); + + test('found in multiple sections', () => { + const consumer = new Consumer({ + version: 3, + sections: [ + { + offset: {line: 0, column: 0}, + map: { + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: [null], + }, + }, + { + offset: {line: 1, column: 0}, + map: { + version: 3, + mappings: '', + names: [], + sources: ['a.js'], + sourcesContent: ['content of a.js'], + }, + }, + ], + }); + expect(consumer.sourceContentFor('a.js', true)).toBe('content of a.js'); + }); + }); }); describe('.file', () => { diff --git a/packages/metro-source-map/src/__tests__/composeSourceMaps-test.js b/packages/metro-source-map/src/__tests__/composeSourceMaps-test.js index ff605af8b2..e7613a8795 100644 --- a/packages/metro-source-map/src/__tests__/composeSourceMaps-test.js +++ b/packages/metro-source-map/src/__tests__/composeSourceMaps-test.js @@ -156,6 +156,40 @@ describe('composeSourceMaps', () => { ]); }); + it('preserves sourcesContent', () => { + const map1 = { + version: 3, + sections: [ + { + offset: {line: 0, column: 0}, + map: { + version: 3, + sources: ['1.js', '2.js'], + sourcesContent: ['content of 1.js', 'content of 2.js'], + names: [], + // One column from 2.js, one column from 1.js + mappings: 'ACAA,CDAA', + }, + }, + ], + }; + + const map2 = { + version: 3, + sources: ['transformed.js'], + names: [], + // Two consecutive columns from transformed.js + mappings: 'AAAA,CAAC', + }; + + const mergedMap = composeSourceMaps([map1, map2]); + + expect(mergedMap.sourcesContent).toEqual([ + 'content of 2.js', + 'content of 1.js', + ]); + }); + it('merges two maps', () => { const mergedMap = composeSourceMaps([ fixtures['1.json'], diff --git a/packages/metro-source-map/src/composeSourceMaps.js b/packages/metro-source-map/src/composeSourceMaps.js index 057d7d5d0b..a5238027e1 100644 --- a/packages/metro-source-map/src/composeSourceMaps.js +++ b/packages/metro-source-map/src/composeSourceMaps.js @@ -66,6 +66,12 @@ function composeSourceMaps( const composedMap = generator.toJSON(); + composedMap.sourcesContent = composedMap.sources.map(source => + consumers[consumers.length - 1].sourceContentFor(source, true), + ); + if (composedMap.sourcesContent.every(content => content == null)) { + delete composedMap.sourcesContent; + } const metadataConsumer = new SourceMetadataMapConsumer(firstMap); composedMap.x_facebook_sources = metadataConsumer.toArray( composedMap.sources, diff --git a/packages/metro-source-map/src/source-map.js b/packages/metro-source-map/src/source-map.js index 0e2b001a19..53cc4e56f7 100644 --- a/packages/metro-source-map/src/source-map.js +++ b/packages/metro-source-map/src/source-map.js @@ -74,11 +74,12 @@ export type IndexMapSection = { export type IndexMap = {| +file?: string, +mappings?: void, // avoids SourceMap being a disjoint union + +sourcesContent?: void, +sections: Array, +version: number, +x_facebook_offsets?: Array, +x_metro_module_paths?: Array, - +x_facebook_sources?: FBSourcesArray, + +x_facebook_sources?: void, +x_facebook_segments?: FBSegmentMap, +x_hermes_function_offsets?: HermesFunctionOffsets, |};