UNPKG

react-native

Version:

A framework for building native apps using React

328 lines (277 loc) • 9.12 kB
/** * 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. */ 'use strict'; const _ = require('lodash'); const base64VLQ = require('./base64-vlq'); const BundleBase = require('./BundleBase'); const ModuleTransport = require('../lib/ModuleTransport'); const crypto = require('crypto'); const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL='; class Bundle extends BundleBase { constructor({sourceMapUrl, minify} = {}) { super(); this._sourceMap = false; this._sourceMapUrl = sourceMapUrl; this._shouldCombineSourceMaps = false; this._numPrependedModules = 0; this._numRequireCalls = 0; this._minify = minify; this._ramBundle = null; // cached RAM Bundle } addModule(resolver, resolutionResponse, module, moduleTransport) { const index = super.addModule(moduleTransport); return resolver.wrapModule({ resolutionResponse, module, name: moduleTransport.name, code: moduleTransport.code, map: moduleTransport.map, meta: moduleTransport.meta, minify: this._minify, }).then(({code, map}) => { // If we get a map from the transformer we'll switch to a mode // were we're combining the source maps as opposed to if (!this._shouldCombineSourceMaps && map != null) { this._shouldCombineSourceMaps = true; } this.replaceModuleAt( index, new ModuleTransport({...moduleTransport, code, map})); }); } setNumPrependedModules(n) { this._numPrependedModules = n; } finalize(options) { options = options || {}; if (options.runMainModule) { options.runBeforeMainModule.forEach(this._addRequireCall, this); this._addRequireCall(super.getMainModuleId()); } super.finalize(); } _addRequireCall(moduleId) { const code = `;require(${JSON.stringify(moduleId)});`; const name = 'require-' + moduleId; super.addModule(new ModuleTransport({ name, id: this._numRequireCalls - 1, code, virtual: true, sourceCode: code, sourcePath: name + '.js', meta: {preloaded: true}, })); this._numRequireCalls += 1; } _getInlineSourceMap(dev) { if (this._inlineSourceMap == null) { const sourceMap = this.getSourceMap({excludeSource: true, dev}); /*eslint-env node*/ const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64'); this._inlineSourceMap = 'data:application/json;base64,' + encoded; } return this._inlineSourceMap; } getSource(options) { super.assertFinalized(); options = options || {}; let source = super.getSource(); if (options.inlineSourceMap) { source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev); } else if (this._sourceMapUrl) { source += SOURCEMAPPING_URL + this._sourceMapUrl; } return source; } getUnbundle(type) { this.assertFinalized(); if (!this._ramBundle) { const modules = this.getModules().slice(); // separate modules we need to preload from the ones we don't const [startupModules, lazyModules] = partition(modules, shouldPreload); this._ramBundle = { startupModules, lazyModules, allModules: modules, }; } return this._ramBundle; } /** * Combine each of the sourcemaps multiple modules have into a single big * one. This works well thanks to a neat trick defined on the sourcemap spec * that makes use of of the `sections` field to combine sourcemaps by adding * an offset. This is supported only by Chrome for now. */ _getCombinedSourceMaps(options) { const result = { version: 3, file: this._getSourceMapFile(), sections: [], }; let line = 0; this.getModules().forEach(module => { let map = module.map; if (module.virtual) { map = generateSourceMapForVirtualModule(module); } if (options.excludeSource) { if (map.sourcesContent && map.sourcesContent.length) { map = Object.assign({}, map, {sourcesContent: []}); } } result.sections.push({ offset: { line: line, column: 0 }, map: map, }); line += module.code.split('\n').length; }); return result; } getSourceMap(options) { super.assertFinalized(); if (this._shouldCombineSourceMaps) { return this._getCombinedSourceMaps(options); } const mappings = this._getMappings(); const modules = this.getModules(); const map = { file: this._getSourceMapFile(), sources: modules.map(module => module.sourcePath), version: 3, names: [], mappings: mappings, sourcesContent: options.excludeSource ? [] : modules.map(module => module.sourceCode) }; return map; } getEtag() { var eTag = crypto.createHash('md5').update(this.getSource()).digest('hex'); return eTag; } _getSourceMapFile() { return this._sourceMapUrl ? this._sourceMapUrl.replace('.map', '.bundle') : 'bundle.js'; } _getMappings() { const modules = super.getModules(); // The first line mapping in our package is basically the base64vlq code for // zeros (A). const firstLine = 'AAAA'; // Most other lines in our mappings are all zeros (for module, column etc) // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. const line = 'AACA'; const moduleLines = Object.create(null); let mappings = ''; for (let i = 0; i < modules.length; i++) { const module = modules[i]; const code = module.code; let lastCharNewLine = false; moduleLines[module.sourcePath] = 0; for (let t = 0; t < code.length; t++) { if (t === 0 && i === 0) { mappings += firstLine; } else if (t === 0) { mappings += 'AC'; // This is the only place were we actually don't know the mapping ahead // of time. When it's a new module (and not the first) the lineno // mapping is 0 (current) - number of lines in prev module. mappings += base64VLQ.encode( 0 - moduleLines[modules[i - 1].sourcePath] ); mappings += 'A'; } else if (lastCharNewLine) { moduleLines[module.sourcePath]++; mappings += line; } lastCharNewLine = code[t] === '\n'; if (lastCharNewLine) { mappings += ';'; } } if (i !== modules.length - 1) { mappings += ';'; } } return mappings; } getJSModulePaths() { return this.getModules() // Filter out non-js files. Like images etc. .filter(module => !module.virtual) .map(module => module.sourcePath); } getDebugInfo() { return [ '<div><h3>Main Module:</h3> ' + super.getMainModuleId() + '</div>', '<style>', 'pre.collapsed {', ' height: 10px;', ' width: 100px;', ' display: block;', ' text-overflow: ellipsis;', ' overflow: hidden;', ' cursor: pointer;', '}', '</style>', '<h3> Module paths and transformed code: </h3>', this.getModules().map(function(m) { return '<div> <h4> Path: </h4>' + m.sourcePath + '<br/> <h4> Source: </h4>' + '<code><pre class="collapsed" onclick="this.classList.remove(\'collapsed\')">' + _.escape(m.code) + '</pre></code></div>'; }).join('\n'), ].join('\n'); } toJSON() { this.assertFinalized('Cannot serialize bundle unless finalized'); return { ...super.toJSON(), sourceMapUrl: this._sourceMapUrl, numPrependedModules: this._numPrependedModules, numRequireCalls: this._numRequireCalls, shouldCombineSourceMaps: this._shouldCombineSourceMaps, }; } static fromJSON(json) { const bundle = new Bundle({sourceMapUrl: json.sourceMapUrl}); bundle._sourceMapUrl = json.sourceMapUrl; bundle._numPrependedModules = json.numPrependedModules; bundle._numRequireCalls = json.numRequireCalls; bundle._shouldCombineSourceMaps = json.shouldCombineSourceMaps; BundleBase.fromJSON(bundle, json); return bundle; } } function generateSourceMapForVirtualModule(module) { // All lines map 1-to-1 let mappings = 'AAAA;'; for (let i = 1; i < module.code.split('\n').length; i++) { mappings += 'AACA;'; } return { version: 3, sources: [ module.sourcePath ], names: [], mappings: mappings, file: module.sourcePath, sourcesContent: [ module.sourceCode ], }; } function shouldPreload({meta}) { return meta && meta.preloaded; } function partition(array, predicate) { const included = []; const excluded = []; array.forEach(item => (predicate(item) ? included : excluded).push(item)); return [included, excluded]; } module.exports = Bundle;