react-native
Version:
A framework for building native apps using React
820 lines (735 loc) • 22.5 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 assert = require('assert');
const crypto = require('crypto');
const debug = require('debug')('RNP:Bundler');
const fs = require('fs');
const Cache = require('../node-haste').Cache;
const Transformer = require('../JSTransformer');
const Resolver = require('../Resolver');
const Bundle = require('./Bundle');
const HMRBundle = require('./HMRBundle');
const ModuleTransport = require('../lib/ModuleTransport');
const imageSize = require('image-size');
const path = require('path');
const denodeify = require('denodeify');
const {
sep: pathSeparator,
join: joinPath,
relative: relativePath,
dirname: pathDirname,
extname,
} = require('path');
const VERSION = require('../../package.json').version;
import type AssetServer from '../AssetServer';
import type Module, {HasteImpl} from '../node-haste/Module';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type {
Options as JSTransformerOptions,
TransformOptions,
} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
import type GlobalTransformCache from '../lib/GlobalTransformCache';
export type GetTransformOptions = (
mainModuleName: string,
options: {},
getDependencies: string => Promise<Array<string>>,
) => {} | Promise<{}>;
type Asset = {
__packager_asset: boolean,
fileSystemLocation: string,
httpServerLocation: string,
width: ?number,
height: ?number,
scales: number,
files: Array<string>,
hash: string,
name: string,
type: string,
};
const sizeOf = denodeify(imageSize);
const noop = () => {};
const {
createActionStartEntry,
createActionEndEntry,
log,
} = require('../Logger');
const assetPropertyBlacklist = new Set([
'files',
'fileSystemLocation',
'path',
]);
type Options = {
allowBundleUpdates: boolean,
assetExts: Array<string>,
assetServer: AssetServer,
blacklistRE?: RegExp,
cacheVersion: string,
extraNodeModules: {},
getTransformOptions?: GetTransformOptions,
globalTransformCache: ?GlobalTransformCache,
hasteImpl?: HasteImpl,
moduleFormat: string,
platforms: Array<string>,
polyfillModuleNames: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache: boolean,
transformModulePath?: string,
transformTimeoutInterval: ?number,
watch: boolean,
};
class Bundler {
_opts: Options;
_getModuleId: (opts: Module) => number;
_cache: Cache;
_transformer: Transformer;
_resolver: Resolver;
_projectRoots: Array<string>;
_assetServer: AssetServer;
_getTransformOptions: void | GetTransformOptions;
constructor(opts: Options) {
this._opts = opts;
opts.projectRoots.forEach(verifyRootExists);
let transformModuleHash;
try {
/* $FlowFixMe: if transformModulePath is null it'll just be caught */
const transformModuleStr = fs.readFileSync(opts.transformModulePath);
transformModuleHash =
crypto.createHash('sha1').update(transformModuleStr).digest('hex');
} catch (error) {
transformModuleHash = '';
}
const stableProjectRoots = opts.projectRoots.map(p => {
return path.relative(path.join(__dirname, '../../../..'), p);
});
const cacheKeyParts = [
'react-packager-cache',
VERSION,
opts.cacheVersion,
stableProjectRoots.join(',').split(pathSeparator).join('-'),
transformModuleHash,
];
this._getModuleId = createModuleIdFactory();
let getCacheKey = () => '';
if (opts.transformModulePath) {
/* $FlowFixMe: dynamic requires prevent static typing :'( */
const transformer = require(opts.transformModulePath);
if (typeof transformer.getCacheKey !== 'undefined') {
getCacheKey = transformer.getCacheKey;
}
}
const transformCacheKey = crypto.createHash('sha1').update(
cacheKeyParts.join('$'),
).digest('hex');
debug(`Using transform cache key "${transformCacheKey}"`);
this._cache = new Cache({
resetCache: opts.resetCache,
cacheKey: transformCacheKey,
});
/* $FlowFixMe: in practice it's always here. */
this._transformer = new Transformer(opts.transformModulePath);
const getTransformCacheKey = (src, filename, options) => {
return transformCacheKey + getCacheKey(src, filename, options);
};
this._resolver = new Resolver({
assetExts: opts.assetExts,
blacklistRE: opts.blacklistRE,
cache: this._cache,
extraNodeModules: opts.extraNodeModules,
getTransformCacheKey,
globalTransformCache: opts.globalTransformCache,
hasteImpl: opts.hasteImpl,
minifyCode: this._transformer.minify,
moduleFormat: opts.moduleFormat,
platforms: opts.platforms,
polyfillModuleNames: opts.polyfillModuleNames,
projectRoots: opts.projectRoots,
providesModuleNodeModules: opts.providesModuleNodeModules,
reporter: opts.reporter,
resetCache: opts.resetCache,
transformCode:
(module, code, transformCodeOptions) => this._transformer.transformFile(
module.path,
code,
transformCodeOptions,
),
watch: opts.watch,
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
this._getTransformOptions = opts.getTransformOptions;
}
end() {
this._transformer.kill();
return Promise.all([
this._cache.end(),
this.getResolver().getDependencyGraph().getWatcher().end(),
]);
}
bundle(options: {
dev: boolean,
minify: boolean,
unbundle: boolean,
sourceMapUrl: string,
}) {
const {dev, minify, unbundle} = options;
const moduleSystemDeps =
this._resolver.getModuleSystemDependencies({dev, unbundle});
return this._bundle({
...options,
bundle: new Bundle({dev, minify, sourceMapUrl: options.sourceMapUrl}),
moduleSystemDeps,
});
}
_sourceHMRURL(platform: ?string, hmrpath: string) {
return this._hmrURL(
'',
platform,
'bundle',
hmrpath,
);
}
_sourceMappingHMRURL(platform: ?string, hmrpath: string) {
// Chrome expects `sourceURL` when eval'ing code
return this._hmrURL(
'\/\/# sourceURL=',
platform,
'map',
hmrpath,
);
}
_hmrURL(prefix: string, platform: ?string, extensionOverride: string, filePath: string) {
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 (pathSeparator === '\\') {
filePath = filePath.replace(/\\/g, '/');
}
const extensionStart = filePath.lastIndexOf('.');
const resource = filePath.substring(
matchingRoot.length,
extensionStart !== -1 ? extensionStart : undefined,
);
return (
prefix + resource +
'.' + extensionOverride + '?' +
'platform=' + (platform || '') + '&runModule=false&entryModuleOnly=true&hot=true'
);
}
hmrBundle(options: {platform: ?string}, host: string, port: number) {
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({
assetPlugins,
bundle,
dev,
entryFile,
entryModuleOnly,
generateSourceMaps,
hot,
isolateModuleIDs,
minify,
moduleSystemDeps = [],
onProgress,
platform,
resolutionResponse,
runBeforeMainModule,
runModule,
unbundle,
}: {
assetPlugins?: Array<string>,
bundle: Bundle,
dev: boolean,
entryFile?: string,
entryModuleOnly?: boolean,
generateSourceMaps?: boolean,
hot?: boolean,
isolateModuleIDs?: boolean,
minify?: boolean,
moduleSystemDeps?: Array<Module>,
onProgress?: () => void,
platform?: ?string,
resolutionResponse?: ResolutionResponse,
runBeforeMainModule?: boolean,
runModule?: boolean,
unbundle?: boolean,
}) {
const onResolutionResponse = (response: ResolutionResponse) => {
/* $FlowFixMe: looks like ResolutionResponse is monkey-patched
* with `getModuleId`. */
bundle.setMainModuleId(response.getModuleId(getMainModule(response)));
if (entryModuleOnly && entryFile) {
response.dependencies = response.dependencies.filter(module =>
module.path.endsWith(entryFile || '')
);
} else {
response.dependencies = moduleSystemDeps.concat(response.dependencies);
}
};
const finalizeBundle = ({bundle: finalBundle, transformedModules, response, modulesByName}: {
bundle: Bundle,
transformedModules: Array<{module: Module, transformed: ModuleTransport}>,
response: ResolutionResponse,
modulesByName: {[name: string]: Module},
}) =>
Promise.all(
transformedModules.map(({module, transformed}) =>
finalBundle.addModule(this._resolver, response, module, transformed)
)
).then(() => {
const runBeforeMainModuleIds = Array.isArray(runBeforeMainModule)
? runBeforeMainModule
.map(name => modulesByName[name])
.filter(Boolean)
.map(response.getModuleId)
: undefined;
finalBundle.finalize({
runModule,
runBeforeMainModule: runBeforeMainModuleIds,
allowUpdates: this._opts.allowBundleUpdates,
});
return finalBundle;
});
return this._buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
onResolutionResponse,
finalizeBundle,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onProgress,
});
}
_buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onResolutionResponse = noop,
onModuleTransformed = noop,
finalizeBundle = noop,
onProgress = noop,
}: *) {
const transformingFilesLogEntry =
log(createActionStartEntry({
action_name: 'Transforming files',
entry_point: entryFile,
environment: dev ? 'dev' : 'prod',
}));
const modulesByName = Object.create(null);
if (!resolutionResponse) {
resolutionResponse = this.getDependencies({
entryFile,
dev,
platform,
hot,
onProgress,
minify,
isolateModuleIDs,
generateSourceMaps: unbundle || minify || generateSourceMaps,
});
}
return Promise.resolve(resolutionResponse).then(response => {
bundle.setRamGroups(response.transformOptions.transform.ramGroups);
log(createActionEndEntry(transformingFilesLogEntry));
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,
assetPlugins,
transformOptions: response.transformOptions,
/* $FlowFixMe: `getModuleId` is monkey-patched */
getModuleId: (response.getModuleId: () => number),
dependencyPairs: response.getResolvedDependencyPairs(module),
}).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: string) {
this._cache.invalidate(filePath);
}
getShallowDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
generateSourceMaps = false,
}: {
entryFile: string,
platform: string,
dev?: boolean,
minify?: boolean,
hot?: boolean,
generateSourceMaps?: boolean,
}) {
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);
});
}
getModuleForPath(entryFile: string) {
return this._resolver.getModuleForPath(entryFile);
}
getDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
recursive = true,
generateSourceMaps = false,
isolateModuleIDs = false,
onProgress,
}: {
entryFile: string,
platform: string,
dev?: boolean,
minify?: boolean,
hot?: boolean,
recursive?: boolean,
generateSourceMaps?: boolean,
isolateModuleIDs?: boolean,
onProgress?: ?(finishedModules: number, totalModules: number) => mixed,
}) {
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,
isolateModuleIDs ? createModuleIdFactory() : this._getModuleId,
);
});
}
getOrderedDependencyPaths({entryFile, dev, platform}: {
entryFile: string,
dev: boolean,
platform: string,
}) {
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,
getModuleId,
dependencyPairs,
assetPlugins,
}: {
module: Module,
bundle: Bundle,
entryFilePath: string,
transformOptions: JSTransformerOptions,
getModuleId: () => number,
dependencyPairs: Array<[mixed, {path: string}]>,
assetPlugins: Array<string>,
}): Promise<ModuleTransport> {
let moduleTransport;
const moduleId = getModuleId(module);
if (module.isAsset()) {
moduleTransport = this._generateAssetModule(
bundle, module, moduleId, assetPlugins, 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: moduleId,
code,
map,
meta: {dependencies, dependencyOffsets, preloaded, dependencyPairs},
sourceCode: source,
sourcePath: module.path,
});
});
}
_generateAssetObjAndCode(
module: Module,
assetPlugins: Array<string>,
platform: ?string = null,
) {
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
var assetUrlPath = joinPath('/assets', pathDirname(relPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (pathSeparator === '\\') {
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.
const isImage = [
'png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff',
].indexOf(extname(module.path).slice(1)) !== -1;
return this._assetServer.getAssetData(relPath, platform).then(assetData => {
return Promise.all([isImage ? sizeOf(assetData.files[0]) : null, assetData]);
}).then(res => {
const dimensions = res[0];
const assetData = res[1];
const scale = assetData.scales[0];
const asset = {
__packager_asset: true,
fileSystemLocation: pathDirname(module.path),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetData.scales,
files: assetData.files,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
return this._applyAssetPlugins(assetPlugins, asset);
}).then(asset => {
const json = JSON.stringify(filterObject(asset, assetPropertyBlacklist));
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},
};
});
}
_applyAssetPlugins(
assetPlugins: Array<string>,
asset: Asset,
) {
if (!assetPlugins.length) {
return asset;
}
const [currentAssetPlugin, ...remainingAssetPlugins] = assetPlugins;
/* $FlowFixMe: dynamic requires prevent static typing :'( */
const assetPluginFunction = require(currentAssetPlugin);
const result = assetPluginFunction(asset);
// If the plugin was an async function, wait for it to fulfill before
// applying the remaining plugins
if (typeof result.then === 'function') {
return result.then(resultAsset =>
this._applyAssetPlugins(remainingAssetPlugins, resultAsset)
);
} else {
return this._applyAssetPlugins(remainingAssetPlugins, result);
}
}
_generateAssetModule(
bundle: Bundle,
module: Module,
moduleId: number,
assetPlugins: Array<string> = [],
platform: ?string = null,
) {
return Promise.all([
module.getName(),
this._generateAssetObjAndCode(module, assetPlugins, platform),
]).then(([name, {asset, code, meta}]) => {
bundle.addAsset(asset);
return new ModuleTransport({
name,
id: moduleId,
code,
meta,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
getTransformOptions(
mainModuleName: string,
options: {
dev?: boolean,
generateSourceMaps?: boolean,
hot?: boolean,
platform: string,
projectRoots: Array<string>,
},
): Promise<TransformOptions> {
const getDependencies = (entryFile: string) =>
this.getDependencies({...options, entryFile})
.then(r => r.dependencies.map(d => d.path));
const extraOptions = this._getTransformOptions
? this._getTransformOptions(mainModuleName, options, getDependencies)
: null;
return Promise.resolve(extraOptions)
.then(extraOpts => {
return {...options, ...extraOpts};
});
}
getResolver() {
return this._resolver;
}
}
function getPathRelativeToRoot(roots, absPath) {
for (let i = 0; i < roots.length; i++) {
const relPath = relativePath(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: modulePath}) => {
if (!(modulePath in fileToIdMap)) {
fileToIdMap[modulePath] = nextId;
nextId += 1;
}
return fileToIdMap[modulePath];
};
}
function getMainModule({dependencies, numPrependedDependencies = 0}) {
return dependencies[numPrependedDependencies];
}
function filterObject(object, blacklist) {
const copied = Object.assign({}, object);
for (const key of blacklist) {
delete copied[key];
}
return copied;
}
module.exports = Bundler;