diff --git a/react-packager/src/Bundler/source-map/B64Builder.js b/react-packager/src/Bundler/source-map/B64Builder.js new file mode 100644 index 00000000..f62b025f --- /dev/null +++ b/react-packager/src/Bundler/source-map/B64Builder.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-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. + * + * @flow + */ + +'use strict'; + +const encode = require('./encode'); + +const MAX_SEGMENT_LENGTH = 7; +const ONE_MEG = 1024 * 1024; +const COMMA = 0x2c; +const SEMICOLON = 0x3b; + +/** + * Efficient builder for base64 VLQ mappings strings. + * + * This class uses a buffer that is preallocated with one megabyte and is + * reallocated dynamically as needed, doubling its size. + * + * Encoding never creates any complex value types (strings, objects), and only + * writes character values to the buffer. + * + * For details about source map terminology and specification, check + * https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit + */ +class B64Builder { + buffer: Buffer; + pos: number; + hasSegment: boolean; + + constructor() { + this.buffer = new Buffer(ONE_MEG); + this.pos = 0; + this.hasSegment = false; + } + + /** + * Adds `n` markers for generated lines to the mappings. + */ + markLines(n: number) { + this.hasSegment = false; + if (this.pos + n >= this.buffer.length) { + this._realloc(); + } + while (n--) { + this.buffer[this.pos++] = SEMICOLON; + } + return this; + } + + /** + * Starts a segment at the specified column offset in the current line. + */ + startSegment(column: number) { + if (this.hasSegment) { + this._writeByte(COMMA); + } else { + this.hasSegment = true; + } + + this.append(column); + return this; + } + + /** + * Appends a single number to the mappings. + */ + append(value: number) { + if (this.pos + MAX_SEGMENT_LENGTH >= this.buffer.length) { + this._realloc(); + } + + this.pos = encode(value, this.buffer, this.pos); + return this; + } + + /** + * Returns the string representation of the mappings. + */ + toString() { + return this.buffer.toString('ascii', 0, this.pos); + } + + _writeByte(byte) { + if (this.pos === this.buffer.length) { + this._realloc(); + } + this.buffer[this.pos++] = byte; + } + + _realloc() { + const {buffer} = this; + this.buffer = new Buffer(buffer.length * 2); + buffer.copy(this.buffer); + } +} + +module.exports = B64Builder; diff --git a/react-packager/src/Bundler/source-map/Generator.js b/react-packager/src/Bundler/source-map/Generator.js new file mode 100644 index 00000000..7d156c30 --- /dev/null +++ b/react-packager/src/Bundler/source-map/Generator.js @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2015-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. + * + * @flow + */ + +'use strict'; + +const B64Builder = require('./B64Builder'); + +import type {SourceMap} from 'babel-core'; + +/** + * Generates a source map from raw mappings. + * + * Raw mappings are a set of 2, 4, or five elements: + * + * - line and column number in the generated source + * - line and column number in the original source + * - symbol name in the original source + * + * Mappings have to be passed in the order appearance in the generated source. + */ +class Generator { + builder: B64Builder; + last: {| + generatedColumn: number, + generatedLine: number, + name: number, + source: number, + sourceColumn: number, + sourceLine: number, + |}; + names: IndexedSet; + source: number; + sources: Array; + sourcesContent: Array; + + constructor() { + this.builder = new B64Builder(); + this.last = { + generatedColumn: 0, + generatedLine: 1, // lines are passed in 1-indexed + name: 0, + source: 0, + sourceColumn: 0, + sourceLine: 1, + }; + this.names = new IndexedSet(); + this.source = -1; + this.sources = []; + this.sourcesContent = []; + } + + /** + * Mark the beginning of a new source file. + */ + startFile(file: string, code: string) { + this.source = this.sources.push(file) - 1; + this.sourcesContent.push(code); + } + + /** + * Mark the end of the current source file + */ + endFile() { + this.source = -1; + } + + /** + * Add a mapping that contains the first 2, 4, or all of the following values: + * + * 1. line offset in the generated source + * 2. column offset in the generated source + * 3. line offset in the original source + * 4. column offset in the original source + * 5. name of the symbol in the original source. + */ + addMapping( + generatedLine: number, + generatedColumn: number, + sourceLine?: number, + sourceColumn?: number, + name?: string, + ): void { + var {last} = this; + if (this.source === -1 || + generatedLine === last.generatedLine && + generatedColumn < last.generatedColumn || + generatedLine < last.generatedLine) { + const msg = this.source === -1 + ? 'Cannot add mapping before starting a file with `addFile()`' + : 'Mapping is for a position preceding an earlier mapping'; + throw new Error(msg); + } + + if (generatedLine > last.generatedLine) { + this.builder.markLines(generatedLine - last.generatedLine); + last.generatedLine = generatedLine; + last.generatedColumn = 0; + } + + this.builder.startSegment(generatedColumn - last.generatedColumn); + last.generatedColumn = generatedColumn; + + if (sourceLine != null) { + if (sourceColumn == null) { + throw new Error( + 'Received mapping with source line, but without source column'); + } + + this.builder + .append(this.source - last.source) + .append(sourceLine - last.sourceLine) + .append(sourceColumn - last.sourceColumn); + + last.source = this.source; + last.sourceColumn = sourceColumn; + last.sourceLine = sourceLine; + + if (name != null) { + const nameIndex = this.names.indexFor(name); + this.builder.append(nameIndex - last.name); + last.name = nameIndex; + } + } + } + + /** + * Return the source map as object. + */ + toMap(file?: string): SourceMap { + return { + version: 3, + file, + sources: this.sources.slice(), + sourcesContent: this.sourcesContent.slice(), + names: this.names.items(), + mappings: this.builder.toString(), + }; + } + + /** + * Return the source map as string. + * + * This is ~2.5x faster than calling `JSON.stringify(generator.toMap())` + */ + toString(file?: string): string { + return ('{' + + '"version":3,' + + (file ? `"file":${JSON.stringify(file)},` : '') + + `"sources":${JSON.stringify(this.sources)},` + + `"sourcesContent":${JSON.stringify(this.sourcesContent)},` + + `"names":${JSON.stringify(this.names.items())},` + + `"mappings":"${this.builder.toString()}"` + + '}'); + } +} + +class IndexedSet { + map: Map; + nextIndex: number; + + constructor() { + this.map = new Map(); + this.nextIndex = 0; + } + + indexFor(x: string) { + let index = this.map.get(x); + if (index == null) { + index = this.nextIndex++; + this.map.set(x, index); + } + return index; + } + + items() { + return Array.from(this.map.keys()); + } +} + +module.exports = Generator; diff --git a/react-packager/src/Bundler/source-map/encode.js b/react-packager/src/Bundler/source-map/encode.js new file mode 100644 index 00000000..f7aa0a96 --- /dev/null +++ b/react-packager/src/Bundler/source-map/encode.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2015-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. + * + * @flow + */ +/** + * Copyright 2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + * + * Based on the Base 64 VLQ implementation in Closure Compiler: + * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java + * + * Copyright 2011 The Closure Compiler Authors. All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @copyright + */ + +/* eslint-disable no-bitwise */ + +'use strict'; + +// A map of values to characters for the b64 encoding +const CHAR_MAP = [ + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, + 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f, +]; + +// A single base 64 digit can contain 6 bits of data. For the base 64 variable +// length quantities we use in the source map spec, the first bit is the sign, +// the next four bits are the actual value, and the 6th bit is the +// continuation bit. The continuation bit tells us whether there are more +// digits in this value following this digit. +// +// Continuation +// | Sign +// | | +// V V +// 101011 + +const VLQ_BASE_SHIFT = 5; + +// binary: 100000 +const VLQ_BASE = 1 << VLQ_BASE_SHIFT; + +// binary: 011111 +const VLQ_BASE_MASK = VLQ_BASE - 1; + +// binary: 100000 +const VLQ_CONTINUATION_BIT = VLQ_BASE; + +/** + * Converts from a two-complement value to a value where the sign bit is + * placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + */ +function toVLQSigned(value) { + return value < 0 + ? ((-value) << 1) + 1 + : (value << 1) + 0; +} + +/** + * Encodes a number to base64 VLQ format and appends it to the passed-in buffer + * + * DON'T USE COMPOUND OPERATORS (eg `>>>=`) ON `let`-DECLARED VARIABLES! + * V8 WILL DEOPTIMIZE THIS FUNCTION AND MAP CREATION WILL BE 25% SLOWER! + * + * DON'T ADD MORE COMMENTS TO THIS FUNCTION TO KEEP ITS LENGTH SHORT ENOUGH FOR + * V8 OPTIMIZATION! + */ +function encode(value: number, buffer: Buffer, position: number): number { + let digit, vlq = toVLQSigned(value); + do { + digit = vlq & VLQ_BASE_MASK; + vlq = vlq >>> VLQ_BASE_SHIFT; + if (vlq > 0) { + // There are still more digits in this value, so we must make sure the + // continuation bit is marked. + digit = digit | VLQ_CONTINUATION_BIT; + } + buffer[position++] = CHAR_MAP[digit]; + } while (vlq > 0); + + return position; +} + +module.exports = encode; diff --git a/react-packager/src/Bundler/source-map/package.json b/react-packager/src/Bundler/source-map/package.json new file mode 100644 index 00000000..be5a9ee4 --- /dev/null +++ b/react-packager/src/Bundler/source-map/package.json @@ -0,0 +1 @@ +{"main": "source-map.js"} diff --git a/react-packager/src/Bundler/source-map/source-map.js b/react-packager/src/Bundler/source-map/source-map.js new file mode 100644 index 00000000..cf22c50e --- /dev/null +++ b/react-packager/src/Bundler/source-map/source-map.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-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. + * + * @flow + */ + +'use strict'; + +const Generator = require('./Generator'); + +import type ModuleTransport from '../../lib/ModuleTransport'; + +/** + * Creates a source map from modules with "raw mappings", i.e. an array of + * tuples with either 2, 4, or 5 elements: + * generated line, generated column, source line, source line, symbol name. + */ +function fromRawMappings(modules: Array): string { + const generator = new Generator(); + let carryOver = 0; + + for (var j = 0, o = modules.length; j < o; ++j) { + var module = modules[j]; + var {code, map} = module; + + if (Array.isArray(map)) { + addMappingsForFile(generator, map, module, carryOver); + } else if (map != null) { + throw new Error( + `Unexpected module with full source map found: ${module.sourcePath}` + ); + } + + carryOver = carryOver + countLines(code); + } + + return generator.toString(); +} + +function addMappingsForFile(generator, mappings, module, carryOver) { + generator.startFile(module.sourcePath, module.sourceCode); + + const columnOffset = module.code.indexOf('{') + 1; + for (let i = 0, n = mappings.length; i < n; ++i) { + const mapping = mappings[i]; + generator.addMapping( + mapping[0] + carryOver, + // lines start at 1, columns start at 0 + mapping[0] === 1 && mapping[1] + columnOffset || mapping[1], + mapping[2], + mapping[3], + mapping[4], + ); + } + generator.endFile(); + +} + +function countLines(string) { + return string.split('\n').length; +} + +exports.fromRawMappings = fromRawMappings;