react-native
Version:
A framework for building native apps using React
438 lines (377 loc) • 13 kB
JavaScript
/**
* 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
*/
;
const BundleBase = require('./BundleBase');
const ModuleTransport = require('../lib/ModuleTransport');
const _ = require('lodash');
const crypto = require('crypto');
const debug = require('debug')('RNP:Bundle');
const invariant = require('fbjs/lib/invariant');
const {fromRawMappings} = require('./source-map');
import type {SourceMap, CombinedSourceMap, MixedSourceMap} from '../lib/SourceMap';
import type {GetSourceOptions, FinalizeOptions} from './BundleBase';
export type Unbundle = {
startupModules: Array<*>,
lazyModules: Array<*>,
groups: Map<number, Set<number>>,
};
type SourceMapFormat = 'undetermined' | 'indexed' | 'flattened';
const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL=';
class Bundle extends BundleBase {
_dev: boolean | void;
_inlineSourceMap: string | void;
_minify: boolean | void;
_numRequireCalls: number;
_ramBundle: Unbundle | null;
_ramGroups: Array<string> | void;
_sourceMap: string | null;
_sourceMapFormat: SourceMapFormat;
_sourceMapUrl: string | void;
constructor({sourceMapUrl, dev, minify, ramGroups}: {
sourceMapUrl?: string,
dev?: boolean,
minify?: boolean,
ramGroups?: Array<string>,
} = {}) {
super();
this._sourceMap = null;
this._sourceMapFormat = 'undetermined';
this._sourceMapUrl = sourceMapUrl;
this._numRequireCalls = 0;
this._dev = dev;
this._minify = minify;
this._ramGroups = ramGroups;
this._ramBundle = null; // cached RAM Bundle
}
addModule(
/**
* $FlowFixMe: this code is inherently incorrect, because it modifies the
* signature of the base class function "addModule". That means callsites
* using an instance typed as the base class would be broken. This must be
* refactored.
*/
resolver: {wrapModule: (options: any) => Promise<{code: any, map: any}>},
resolutionResponse: mixed,
module: mixed,
/* $FlowFixMe: erroneous change of signature. */
moduleTransport: ModuleTransport,
/* $FlowFixMe: erroneous change of signature. */
): Promise<void> {
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,
dev: this._dev,
}).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 (map) {
const usesRawMappings = isRawMappings(map);
if (this._sourceMapFormat === 'undetermined') {
this._sourceMapFormat = usesRawMappings ? 'flattened' : 'indexed';
} else if (usesRawMappings && this._sourceMapFormat === 'indexed') {
throw new Error(
`Got at least one module with a full source map, but ${
moduleTransport.sourcePath} has raw mappings`
);
} else if (!usesRawMappings && this._sourceMapFormat === 'flattened') {
throw new Error(
`Got at least one module with raw mappings, but ${
moduleTransport.sourcePath} has a full source map`
);
}
}
this.replaceModuleAt(
index, new ModuleTransport({...moduleTransport, code, map}));
});
}
finalize(options: FinalizeOptions) {
options = options || {};
if (options.runModule) {
/* $FlowFixMe: this is unsound, as nothing enforces runBeforeMainModule
* to be available if `runModule` is true. Refactor. */
options.runBeforeMainModule.forEach(this._addRequireCall, this);
/* $FlowFixMe: this is unsound, as nothing enforces the module ID to have
* been set beforehand. */
this._addRequireCall(this.getMainModuleId());
}
super.finalize(options);
}
_addRequireCall(moduleId: string) {
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: ?boolean) {
if (this._inlineSourceMap == null) {
const sourceMap = this.getSourceMapString({excludeSource: true, dev});
/*eslint-env node*/
const encoded = new Buffer(sourceMap).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
}
return this._inlineSourceMap;
}
getSource(options: GetSourceOptions) {
this.assertFinalized();
options = options || {};
let source = super.getSource(options);
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev);
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
return source;
}
getUnbundle(): Unbundle {
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);
const ramGroups = this._ramGroups;
let groups;
this._ramBundle = {
startupModules,
lazyModules,
get groups() {
if (!groups) {
groups = createGroups(ramGroups || [], lazyModules);
}
return groups;
},
};
}
return this._ramBundle;
}
invalidateSource() {
debug('invalidating bundle');
super.invalidateSource();
this._sourceMap = null;
}
/**
* 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: {excludeSource?: boolean}): CombinedSourceMap {
const result = {
version: 3,
file: this._getSourceMapFile(),
sections: [],
};
let line = 0;
this.getModules().forEach(module => {
let map = module.map == null || module.virtual
? generateSourceMapForVirtualModule(module)
: module.map;
invariant(
!Array.isArray(map),
`Unexpected raw mappings for ${module.sourcePath}`,
);
if (options.excludeSource && 'sourcesContent' in map) {
map = {...map, sourcesContent: []};
}
result.sections.push({
offset: {line, column: 0},
map: (map: MixedSourceMap),
});
line += module.code.split('\n').length;
});
return result;
}
getSourceMap(options: {excludeSource?: boolean}): MixedSourceMap {
this.assertFinalized();
return this._sourceMapFormat === 'indexed'
? this._getCombinedSourceMaps(options)
: fromRawMappings(this.getModules()).toMap();
}
getSourceMapString(options: {excludeSource?: boolean}): string {
if (this._sourceMapFormat === 'indexed') {
return JSON.stringify(this.getSourceMap(options));
}
// The following code is an optimization specific to the development server:
// 1. generator.toSource() is faster than JSON.stringify(generator.toMap()).
// 2. caching the source map unless there are changes saves time in
// development settings.
let map = this._sourceMap;
if (map == null) {
debug('Start building flat source map');
map = this._sourceMap = fromRawMappings(this.getModules()).toString();
debug('End building flat source map');
} else {
debug('Returning cached source map');
}
return map;
}
getEtag() {
/* $FlowFixMe: we must pass options, or rename the
* base `getSource` function, as it does not actually need options. */
var eTag = crypto.createHash('md5').update(this.getSource()).digest('hex');
return eTag;
}
_getSourceMapFile() {
return this._sourceMapUrl
? this._sourceMapUrl.replace('.map', '.bundle')
: 'bundle.js';
}
getJSModulePaths() {
return this.getModules()
// Filter out non-js files. Like images etc.
.filter(module => !module.virtual)
.map(module => module.sourcePath);
}
getDebugInfo() {
return [
/* $FlowFixMe: this is unsound as the module ID could be unset. */
'<div><h3>Main Module:</h3> ' + this.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');
}
setRamGroups(ramGroups: Array<string>) {
this._ramGroups = ramGroups;
}
}
function generateSourceMapForVirtualModule(module): SourceMap {
// 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,
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];
}
function * filter(iterator, predicate) {
for (const value of iterator) {
if (predicate(value)) {
yield value;
}
}
}
function * subtree(moduleTransport: ModuleTransport, moduleTransportsByPath, seen = new Set()) {
seen.add(moduleTransport.id);
/* $FlowFixMe: there may not be a `meta` object */
for (const [, {path}] of moduleTransport.meta.dependencyPairs || []) {
const dependency = moduleTransportsByPath.get(path);
if (dependency && !seen.has(dependency.id)) {
yield dependency.id;
yield * subtree(dependency, moduleTransportsByPath, seen);
}
}
}
class ArrayMap extends Map {
get(key) {
let array = super.get(key);
if (!array) {
array = [];
this.set(key, array);
}
return array;
}
}
function createGroups(ramGroups: Array<string>, lazyModules) {
// build two maps that allow to lookup module data
// by path or (numeric) module id;
const byPath = new Map();
const byId = new Map();
lazyModules.forEach(m => {
byPath.set(m.sourcePath, m);
byId.set(m.id, m.sourcePath);
});
// build a map of group root IDs to an array of module IDs in the group
const result: Map<number, Set<number>> = new Map(
ramGroups
.map(modulePath => {
const root = byPath.get(modulePath);
if (!root) {
throw Error(`Group root ${modulePath} is not part of the bundle`);
}
return [
root.id,
// `subtree` yields the IDs of all transitive dependencies of a module
/* $FlowFixMe: assumes the module is always in the Map */
new Set(subtree(byPath.get(root.sourcePath), byPath)),
];
})
);
if (ramGroups.length > 1) {
// build a map of all grouped module IDs to an array of group root IDs
const all = new ArrayMap();
for (const [parent, children] of result) {
for (const module of children) {
all.get(module).push(parent);
}
}
// find all module IDs that are part of more than one group
const doubles = filter(all, ([, parents]) => parents.length > 1);
for (const [moduleId, parents] of doubles) {
// remove them from their groups
/* $FlowFixMe: this assumes the element exists. */
parents.forEach(p => result.get(p).delete(moduleId));
// print a warning for each removed module
const parentNames = parents.map(byId.get, byId);
const lastName = parentNames.pop();
console.warn(
/* $FlowFixMe: this assumes the element exists. */
`Module ${byId.get(moduleId)} belongs to groups ${
parentNames.join(', ')}, and ${lastName
}. Removing it from all groups.`
);
}
}
return result;
}
const isRawMappings = Array.isArray;
module.exports = Bundle;