diff --git a/react-packager/src/Bundler/source-map/B64Builder.js b/react-packager/src/Bundler/source-map/B64Builder.js index f62b025f..f8de0870 100644 --- a/react-packager/src/Bundler/source-map/B64Builder.js +++ b/react-packager/src/Bundler/source-map/B64Builder.js @@ -45,6 +45,9 @@ class B64Builder { * Adds `n` markers for generated lines to the mappings. */ markLines(n: number) { + if (n < 1) { + return this; + } this.hasSegment = false; if (this.pos + n >= this.buffer.length) { this._realloc(); diff --git a/react-packager/src/Bundler/source-map/__tests__/B64Builder-test.js b/react-packager/src/Bundler/source-map/__tests__/B64Builder-test.js new file mode 100644 index 00000000..4f62614f --- /dev/null +++ b/react-packager/src/Bundler/source-map/__tests__/B64Builder-test.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.disableAutomock(); + +const B64Builder = require('../B64Builder'); + +let builder; +beforeEach(() => { + builder = new B64Builder(); +}); + +it('exposes a fluent interface', () => { + expect(builder.markLines(0)).toBe(builder); + expect(builder.markLines(3)).toBe(builder); + expect(builder.startSegment()).toBe(builder); + expect(builder.append(4)).toBe(builder); +}); + +it('can create an empty string', () => { + expect(builder.toString()).toEqual(''); +}); + +it('can mark a new line in the generated code', () => { + builder.markLines(1); + expect(builder.toString()).toEqual(';'); +}); + +it('can mark multiple new lines in the generated code', () => { + builder.markLines(4); + expect(builder.toString()).toEqual(';;;;'); +}); + +it('can mark zero new lines in the generated code', () => { + builder.markLines(0); + expect(builder.toString()).toEqual(''); +}); + +it('does not add commas when just starting a segment', () => { + builder.startSegment(0); + expect(builder.toString()).toEqual('A'); +}); + +it('adds a comma when starting a segment after another segment', () => { + builder.startSegment(0); + builder.startSegment(1); + expect(builder.toString()).toEqual('A,C'); +}); + +it('does not add a comma when starting a segment after marking a line', () => { + builder.startSegment(0); + builder.markLines(1); + builder.startSegment(0); + expect(builder.toString()).toEqual('A;A'); +}); + +it('adds a comma when starting a segment after calling `markLines(0)`', () => { + builder.startSegment(0); + builder.markLines(0); + builder.startSegment(1); + expect(builder.toString()).toEqual('A,C'); +}); + +it('can append values that fit within 5 bits (including sign bit)', () => { + builder.append(0b1111); + builder.append(-0b1111); + expect(builder.toString()).toEqual('ef'); +}); + +it('can append values that fit within 10 bits (including sign bit)', () => { + builder.append(0b111100110); + builder.append(-0b110110011); + expect(builder.toString()).toEqual('senb'); +}); + +it('can append values that fit within 15 bits (including sign bit)', () => { + builder.append(0b10011111011001); + builder.append(-0b11001010001001); + expect(builder.toString()).toEqual('y9TzoZ'); +}); + +it('can append values that fit within 20 bits (including sign bit)', () => { + builder.append(0b1110010011101110110); + builder.append(-0b1011000010100100110); + expect(builder.toString()).toEqual('s3zctyiW'); +}); + +it('can append values that fit within 25 bits (including sign bit)', () => { + builder.append(0b100010001111011010110111); + builder.append(-0b100100111100001110101111); + expect(builder.toString()).toEqual('ur7jR/6hvS'); +}); + +it('can append values that fit within 30 bits (including sign bit)', () => { + builder.append(0b10001100100001101010001011111); + builder.append(-0b11111000011000111110011111101); + expect(builder.toString()).toEqual('+lqjyR7v+xhf'); +}); + +it('can append values that fit within 32 bits (including sign bit)', () => { + builder.append(0b1001100101000101001011111110011); + builder.append(-0b1101101101011000110011001110000); + expect(builder.toString()).toEqual('m/rq0sChnzx1tD'); +}); + +it('can handle multiple operations', () => { + builder + .markLines(3) + .startSegment(4) + .append(2) + .append(2) + .append(0) + .append(2345) + .startSegment(12) + .append(987543) + .markLines(1) + .startSegment(0); + expect(builder.toString()).toEqual(';;;IEEAyyE,Yu5o8B;A'); +}); diff --git a/react-packager/src/Bundler/source-map/__tests__/Generator-test.js b/react-packager/src/Bundler/source-map/__tests__/Generator-test.js new file mode 100644 index 00000000..0cad1999 --- /dev/null +++ b/react-packager/src/Bundler/source-map/__tests__/Generator-test.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.disableAutomock(); + +const Generator = require('../Generator'); + +const {objectContaining} = expect; + +let generator; +beforeEach(() => { + generator = new Generator(); +}); + +it('adds file name and source code when starting a file', () => { + const file1 = 'just/a/file'; + const file2 = 'another/file'; + const source1 = 'var a = 1;'; + const source2 = 'var a = 2;'; + + generator.startFile(file1, source1); + generator.startFile(file2, source2); + + expect(generator.toMap()) + .toEqual(objectContaining({ + sources: [file1, file2], + sourcesContent: [source1, source2], + })); +}); + +it('throws when adding a mapping without starting a file', () => { + expect(() => generator.addSimpleMapping(1, 2)).toThrow(); +}); + +it('throws when adding a mapping after ending a file', () => { + generator.startFile('apples', 'pears'); + generator.endFile(); + expect(() => generator.addSimpleMapping(1, 2)).toThrow(); +}); + +it('can add a mapping for generated code without corresponding original source', () => { + generator.startFile('apples', 'pears'); + generator.addSimpleMapping(12, 87); + expect(generator.toMap()) + .toEqual(objectContaining({ + mappings: ';;;;;;;;;;;uF', + })); +}); + +it('can add a mapping with corresponding location in the original source', () => { + generator.startFile('apples', 'pears'); + generator.addSourceMapping(2, 3, 456, 7); + expect(generator.toMap()) + .toEqual(objectContaining({ + mappings: ';GAucO', + })); +}); + +it('can add a mapping with source location and symbol name', () => { + generator.startFile('apples', 'pears'); + generator.addNamedSourceMapping(9, 876, 54, 3, 'arbitrary'); + expect(generator.toMap()) + .toEqual(objectContaining({ + mappings: ';;;;;;;;42BAqDGA', + names: ['arbitrary'], + })); +}); + +describe('full map generation', () => { + beforeEach(() => { + generator.startFile('apples', 'pears'); + generator.addSimpleMapping(1, 2); + generator.addNamedSourceMapping(3, 4, 5, 6, 'plums'); + generator.endFile(); + generator.startFile('lemons', 'oranges'); + generator.addNamedSourceMapping(7, 8, 9, 10, 'tangerines'); + generator.addNamedSourceMapping(11, 12, 13, 14, 'tangerines'); + generator.addSimpleMapping(15, 16); + }); + + it('can add multiple mappings for each file', () => { + expect(generator.toMap()).toEqual({ + version: 3, + mappings: 'E;;IAIMA;;;;QCIIC;;;;YAIIA;;;;gB', + sources: ['apples', 'lemons'], + sourcesContent: ['pears', 'oranges'], + names: ['plums', 'tangerines'], + }); + }); + + it('can add a `file` property to the map', () => { + expect(generator.toMap('arbitrary')) + .toEqual(objectContaining({ + file: 'arbitrary', + })); + }); + + it('supports direct JSON serialization', () => { + expect(JSON.parse(generator.toString())).toEqual(generator.toMap()); + }); + + it('supports direct JSON serialization with a file name', () => { + const file = 'arbitrary/file'; + expect(JSON.parse(generator.toString(file))).toEqual(generator.toMap(file)); + }); +}); diff --git a/react-packager/src/Bundler/source-map/__tests__/source-map-test.js b/react-packager/src/Bundler/source-map/__tests__/source-map-test.js new file mode 100644 index 00000000..39258eae --- /dev/null +++ b/react-packager/src/Bundler/source-map/__tests__/source-map-test.js @@ -0,0 +1,85 @@ + /** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.disableAutomock(); + +const Generator = require('../Generator'); +const {compactMapping, fromRawMappings} = require('..'); + +describe('flattening mappings / compacting', () => { + it('flattens simple mappings', () => { + expect(compactMapping({generated: {line: 12, column: 34}})) + .toEqual([12, 34]); + }); + + it('flattens mappings with a source location', () => { + expect(compactMapping({ + generated: {column: 34, line: 12}, + original: {column: 78, line: 56}, + })).toEqual([12, 34, 56, 78]); + }); + + it('flattens mappings with a source location and a symbol name', () => { + expect(compactMapping({ + generated: {column: 34, line: 12}, + name: 'arbitrary', + original: {column: 78, line: 56}, + })).toEqual([12, 34, 56, 78, 'arbitrary']); + }); +}); + +describe('build map from raw mappings', () => { + it('returns a `Generator` instance', () => { + expect(fromRawMappings([])).toBeInstanceOf(Generator); + }); + + it('returns a working source map containing all mappings', () => { + const input = [{ + code: lines(11), + map: [ + [1, 2], + [3, 4, 5, 6, 'apples'], + [7, 8, 9, 10], + [11, 12, 13, 14, 'pears'] + ], + sourceCode: 'code1', + sourcePath: 'path1', + }, { + code: lines(3), + map: [ + [1, 2], + [3, 4, 15, 16, 'bananas'], + ], + sourceCode: 'code2', + sourcePath: 'path2', + }, { + code: lines(23), + map: [ + [11, 12], + [13, 14, 15, 16, 'bananas'], + [17, 18, 19, 110], + [21, 112, 113, 114, 'pears'] + ], + sourceCode: 'code3', + sourcePath: 'path3', + }]; + + expect(fromRawMappings(input).toMap()) + .toEqual({ + mappings: 'E;;IAIMA;;;;QAII;;;;YAIIC;E;;ICEEC;;;;;;;;;;;Y;;cCAAA;;;;kBAI8F;;;;gHA8FID', + names: ['apples', 'pears', 'bananas'], + sources: ['path1', 'path2', 'path3'], + sourcesContent: ['code1', 'code2', 'code3'], + version: 3, + }); + }); +}); + +const lines = n => Array(n).join('\n');