react-native
Version:
A framework for building native apps using React
722 lines (646 loc) • 19.1 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.
*/
;
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const Promise = require('promise');
const ProgressBar = require('progress');
const Cache = require('node-haste').Cache;
const Transformer = require('../JSTransformer');
const Resolver = require('../Resolver');
const Bundle = require('./Bundle');
const HMRBundle = require('./HMRBundle');
const PrepackBundle = require('./PrepackBundle');
const Activity = require('../Activity');
const ModuleTransport = require('../lib/ModuleTransport');
const declareOpts = require('../lib/declareOpts');
const imageSize = require('image-size');
const version = require('../../../../package.json').version;
const sizeOf = Promise.denodeify(imageSize);
const noop = () => {};
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
},
blacklistRE: {
type: 'object', // typeof regex is object
},
moduleFormat: {
type: 'string',
default: 'haste',
},
polyfillModuleNames: {
type: 'array',
default: [],
},
cacheVersion: {
type: 'string',
default: '1.0',
},
resetCache: {
type: 'boolean',
default: false,
},
transformModulePath: {
type:'string',
required: false,
},
extraNodeModules: {
type: 'object',
required: false,
},
nonPersistent: {
type: 'boolean',
default: false,
},
assetRoots: {
type: 'array',
required: false,
},
assetExts: {
type: 'array',
default: ['png'],
},
fileWatcher: {
type: 'object',
required: true,
},
assetServer: {
type: 'object',
required: true,
},
transformTimeoutInterval: {
type: 'number',
required: false,
},
silent: {
type: 'boolean',
default: false,
},
});
class Bundler {
constructor(options) {
const opts = this._opts = validateOpts(options);
opts.projectRoots.forEach(verifyRootExists);
let mtime;
try {
({mtime} = fs.statSync(opts.transformModulePath));
mtime = String(mtime.getTime());
} catch (error) {
mtime = '';
}
const cacheKeyParts = [
'react-packager-cache',
version,
opts.cacheVersion,
opts.projectRoots.join(',').split(path.sep).join('-'),
mtime,
];
this._getModuleId = createModuleIdFactory();
if (opts.transformModulePath) {
const transformer = require(opts.transformModulePath);
if (typeof transformer.cacheKey !== 'undefined') {
cacheKeyParts.push(transformer.cacheKey);
}
}
this._cache = new Cache({
resetCache: opts.resetCache,
cacheKey: cacheKeyParts.join('$'),
});
this._transformer = new Transformer({
transformModulePath: opts.transformModulePath,
});
this._resolver = new Resolver({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
polyfillModuleNames: opts.polyfillModuleNames,
moduleFormat: opts.moduleFormat,
assetRoots: opts.assetRoots,
fileWatcher: opts.fileWatcher,
assetExts: opts.assetExts,
cache: this._cache,
getModuleId: this._getModuleId,
transformCode:
(module, code, options) =>
this._transformer.transformFile(module.path, code, options),
extraNodeModules: opts.extraNodeModules,
minifyCode: this._transformer.minify,
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
if (opts.getTransformOptionsModulePath) {
this._transformOptionsModule = require(
opts.getTransformOptionsModulePath
);
}
}
kill() {
this._transformer.kill();
return this._cache.end();
}
bundle(options) {
const {dev, minify, unbundle} = options;
const moduleSystemDeps =
this._resolver.getModuleSystemDependencies({dev, unbundle});
return this._bundle({
bundle: new Bundle({minify, sourceMapUrl: options.sourceMapUrl}),
moduleSystemDeps,
...options,
});
}
_sourceHMRURL(platform, path) {
return this._hmrURL(
'',
platform,
'bundle',
path,
);
}
_sourceMappingHMRURL(platform, path) {
// Chrome expects `sourceURL` when eval'ing code
return this._hmrURL(
'\/\/# sourceURL=',
platform,
'map',
path,
);
}
_hmrURL(prefix, platform, extensionOverride, filePath) {
const matchingRoot = this._projectRoots.find(root => filePath.startsWith(root));
if (!matchingRoot) {
throw new Error('No matching project root for ', filePath);
}
// Replaces '\' with '/' for Windows paths.
if (path.sep === '\\') {
filePath = filePath.replace(/\\/g, '/');
}
const extensionStart = filePath.lastIndexOf('.');
let resource = filePath.substring(
matchingRoot.length,
extensionStart !== -1 ? extensionStart : undefined,
);
return (
prefix + resource +
'.' + extensionOverride + '?' +
'platform=' + platform + '&runModule=false&entryModuleOnly=true&hot=true'
);
}
hmrBundle(options, host, port) {
return this._bundle({
...options,
bundle: new HMRBundle({
sourceURLFn: this._sourceHMRURL.bind(this, options.platform),
sourceMappingURLFn: this._sourceMappingHMRURL.bind(
this,
options.platform,
),
}),
hot: true,
dev: true,
});
}
_bundle({
bundle,
entryFile,
runModule: runMainModule,
runBeforeMainModule,
dev,
minify,
platform,
moduleSystemDeps = [],
hot,
unbundle,
entryModuleOnly,
resolutionResponse,
}) {
if (dev && runBeforeMainModule) { // no runBeforeMainModule for hmr bundles
// `require` calls in the require polyfill itself are not extracted and
// replaced with numeric module IDs, but the require polyfill
// needs Systrace.
// Therefore, we include the Systrace module before the main module, and
// it will set itself as property on the require function.
// TODO(davidaurelio) Scan polyfills for dependencies, too (t9759686)
runBeforeMainModule = runBeforeMainModule.concat(['Systrace']);
}
const onResolutionResponse = response => {
bundle.setMainModuleId(this._getModuleId(getMainModule(response)));
if (bundle.setNumPrependedModules) {
bundle.setNumPrependedModules(
response.numPrependedDependencies + moduleSystemDeps.length
);
}
if (entryModuleOnly) {
response.dependencies = response.dependencies.filter(module =>
module.path.endsWith(entryFile)
);
} else {
response.dependencies = moduleSystemDeps.concat(response.dependencies);
}
};
const finalizeBundle = ({bundle, transformedModules, response, modulesByName}) =>
Promise.all(
transformedModules.map(({module, transformed}) =>
bundle.addModule(this._resolver, response, module, transformed)
)
).then(() => {
const runBeforeMainModuleIds = Array.isArray(runBeforeMainModule)
? runBeforeMainModule
.map(name => modulesByName[name])
.filter(Boolean)
.map(this._getModuleId, this)
: undefined;
bundle.finalize({
runMainModule,
runBeforeMainModule: runBeforeMainModuleIds,
});
return bundle;
});
return this._buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
onResolutionResponse,
finalizeBundle,
});
}
prepackBundle({
entryFile,
runModule: runMainModule,
runBeforeMainModule,
sourceMapUrl,
dev,
platform,
}) {
const onModuleTransformed = ({module, transformed, response, bundle}) => {
const deps = Object.create(null);
const pairs = response.getResolvedDependencyPairs(module);
if (pairs) {
pairs.forEach(pair => {
deps[pair[0]] = pair[1].path;
});
}
return module.getName().then(name => {
bundle.addModule(name, transformed, deps, module.isPolyfill());
});
};
const finalizeBundle = ({bundle, response}) => {
const {mainModuleId} = response;
bundle.finalize({runBeforeMainModule, runMainModule, mainModuleId});
return bundle;
};
return this._buildBundle({
entryFile,
dev,
platform,
onModuleTransformed,
finalizeBundle,
minify: false,
bundle: new PrepackBundle(sourceMapUrl),
});
}
_buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
onResolutionResponse = noop,
onModuleTransformed = noop,
finalizeBundle = noop,
}) {
const findEventId = Activity.startEvent('find dependencies');
const modulesByName = Object.create(null);
if (!resolutionResponse) {
let onProgress = noop;
if (process.stdout.isTTY && !this._opts.silent) {
const bar = new ProgressBar(
'transformed :current/:total (:percent)',
{complete: '=', incomplete: ' ', width: 40, total: 1},
);
onProgress = (_, total) => {
bar.total = total;
bar.tick();
};
}
resolutionResponse = this.getDependencies({
entryFile,
dev,
platform,
hot,
onProgress,
minify,
generateSourceMaps: unbundle,
});
}
return Promise.resolve(resolutionResponse).then(response => {
Activity.endEvent(findEventId);
onResolutionResponse(response);
// get entry file complete path (`entryFile` is relative to roots)
let entryFilePath;
if (response.dependencies.length > 1) { // skip HMR requests
const numModuleSystemDependencies =
this._resolver.getModuleSystemDependencies({dev, unbundle}).length;
const dependencyIndex = (response.numPrependedDependencies || 0) + numModuleSystemDependencies;
if (dependencyIndex in response.dependencies) {
entryFilePath = response.dependencies[dependencyIndex].path;
}
}
const toModuleTransport = module =>
this._toModuleTransport({
module,
bundle,
entryFilePath,
transformOptions: response.transformOptions,
}).then(transformed => {
modulesByName[transformed.name] = module;
onModuleTransformed({
module,
response,
bundle,
transformed,
});
return {module, transformed};
});
return Promise.all(response.dependencies.map(toModuleTransport))
.then(transformedModules =>
Promise.resolve(
finalizeBundle({bundle, transformedModules, response, modulesByName})
).then(() => bundle)
);
});
}
invalidateFile(filePath) {
this._cache.invalidate(filePath);
}
getShallowDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
generateSourceMaps = false,
}) {
return this.getTransformOptions(
entryFile,
{
dev,
platform,
hot,
generateSourceMaps,
projectRoots: this._projectRoots,
},
).then(transformSpecificOptions => {
const transformOptions = {
minify,
dev,
platform,
transform: transformSpecificOptions,
};
return this._resolver.getShallowDependencies(entryFile, transformOptions);
});
}
stat(filePath) {
return this._resolver.stat(filePath);
}
getModuleForPath(entryFile) {
return this._resolver.getModuleForPath(entryFile);
}
getDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
recursive = true,
generateSourceMaps = false,
onProgress,
}) {
return this.getTransformOptions(
entryFile,
{
dev,
platform,
hot,
generateSourceMaps,
projectRoots: this._projectRoots,
},
).then(transformSpecificOptions => {
const transformOptions = {
minify,
dev,
platform,
transform: transformSpecificOptions,
};
return this._resolver.getDependencies(
entryFile,
{dev, platform, recursive},
transformOptions,
onProgress,
);
});
}
getOrderedDependencyPaths({ entryFile, dev, platform }) {
return this.getDependencies({entryFile, dev, platform}).then(
({ dependencies }) => {
const ret = [];
const promises = [];
const placeHolder = {};
dependencies.forEach(dep => {
if (dep.isAsset()) {
const relPath = getPathRelativeToRoot(
this._projectRoots,
dep.path
);
promises.push(
this._assetServer.getAssetData(relPath, platform)
);
ret.push(placeHolder);
} else {
ret.push(dep.path);
}
});
return Promise.all(promises).then(assetsData => {
assetsData.forEach(({ files }) => {
const index = ret.indexOf(placeHolder);
ret.splice(index, 1, ...files);
});
return ret;
});
}
);
}
_toModuleTransport({module, bundle, entryFilePath, transformOptions}) {
let moduleTransport;
if (module.isAsset_DEPRECATED()) {
moduleTransport = this._generateAssetModule_DEPRECATED(bundle, module);
} else if (module.isAsset()) {
moduleTransport = this._generateAssetModule(
bundle, module, transformOptions.platform);
}
if (moduleTransport) {
return Promise.resolve(moduleTransport);
}
return Promise.all([
module.getName(),
module.read(transformOptions),
]).then((
[name, {code, dependencies, dependencyOffsets, map, source}]
) => {
const {preloadedModules} = transformOptions.transform;
const preloaded =
module.path === entryFilePath ||
module.isPolyfill() ||
preloadedModules && preloadedModules.hasOwnProperty(module.path);
return new ModuleTransport({
name,
id: this._getModuleId(module),
code,
map,
meta: {dependencies, dependencyOffsets, preloaded},
sourceCode: source,
sourcePath: module.path
});
});
}
getGraphDebugInfo() {
return this._resolver.getDebugInfo();
}
_generateAssetModule_DEPRECATED(bundle, module) {
return Promise.all([
sizeOf(module.path),
module.getName(),
]).then(([dimensions, id]) => {
const img = {
__packager_asset: true,
path: module.path,
uri: id.replace(/^[^!]+!/, ''),
width: dimensions.width / module.resolution,
height: dimensions.height / module.resolution,
deprecated: true,
};
bundle.addAsset(img);
const code = 'module.exports=' + JSON.stringify(img) + ';';
return new ModuleTransport({
name: id,
id: this._getModuleId(module),
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
_generateAssetObjAndCode(module, platform = null) {
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
var assetUrlPath = path.join('/assets', path.dirname(relPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (path.sep === '\\') {
assetUrlPath = assetUrlPath.replace(/\\/g, '/');
}
// Test extension against all types supported by image-size module.
// If it's not one of these, we won't treat it as an image.
let isImage = [
'png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'
].indexOf(path.extname(module.path).slice(1)) !== -1;
return Promise.all([
isImage ? sizeOf(module.path) : null,
this._assetServer.getAssetData(relPath, platform),
]).then(function(res) {
const dimensions = res[0];
const assetData = res[1];
const asset = {
__packager_asset: true,
fileSystemLocation: path.dirname(module.path),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / module.resolution : undefined,
height: dimensions ? dimensions.height / module.resolution : undefined,
scales: assetData.scales,
files: assetData.files,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
const json = JSON.stringify(asset);
const assetRegistryPath = 'react-native/Libraries/Image/AssetRegistry';
const code =
`module.exports = require(${JSON.stringify(assetRegistryPath)}).registerAsset(${json});`;
const dependencies = [assetRegistryPath];
const dependencyOffsets = [code.indexOf(assetRegistryPath) - 1];
return {
asset,
code,
meta: {dependencies, dependencyOffsets}
};
});
}
_generateAssetModule(bundle, module, platform = null) {
return Promise.all([
module.getName(),
this._generateAssetObjAndCode(module, platform),
]).then(([name, {asset, code, meta}]) => {
bundle.addAsset(asset);
return new ModuleTransport({
name,
id: this._getModuleId(module),
code,
meta: meta,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
getTransformOptions(mainModuleName, options) {
const extraOptions = this._transformOptionsModule
? this._transformOptionsModule(mainModuleName, options, this)
: null;
return Promise.resolve(extraOptions)
.then(extraOptions => Object.assign(options, extraOptions));
}
}
function getPathRelativeToRoot(roots, absPath) {
for (let i = 0; i < roots.length; i++) {
const relPath = path.relative(roots[i], absPath);
if (relPath[0] !== '.') {
return relPath;
}
}
throw new Error(
'Expected root module to be relative to one of the project roots'
);
}
function verifyRootExists(root) {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
function createModuleIdFactory() {
const fileToIdMap = Object.create(null);
let nextId = 0;
return ({path}) => {
if (!(path in fileToIdMap)) {
fileToIdMap[path] = nextId;
nextId += 1;
}
return fileToIdMap[path];
};
}
function getMainModule({dependencies, numPrependedDependencies = 0}) {
return dependencies[numPrependedDependencies];
}
module.exports = Bundler;