metro: allow dynamic dependencies from within node_modules

Summary: Tries to adress https://github.com/facebook/metro/issues/65. We need a reasonnable workaround to support modules like `moment.js` that do dynamic requires but only in some cases. By replacing the call by a function that throws, we move the exception at runtime instead of happening at compile time. We don't want to do that for non-node_modules file because they are fixable directly, while `node_modules` are not fixable by people and they get completely blocked by the error at compile time.

Reviewed By: rafeca

Differential Revision: D6736989

fbshipit-source-id: a6e1fd9b56fa83907400884efd8f8594018b7c37
This commit is contained in:
Jean Lauliac 2018-01-18 07:50:53 -08:00 committed by Facebook Github Bot
parent 40497ee118
commit 46545d4f5c
17 changed files with 183 additions and 48 deletions

View File

@ -28,6 +28,7 @@ const {
import type {PostProcessModules} from '../DeltaBundler';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type {DynamicRequiresBehavior} from '../ModuleGraph/worker/collectDependencies';
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
import type {TransformCache} from '../lib/TransformCaching';
import type {Reporter} from '../lib/reporting';
@ -82,6 +83,7 @@ export type Options = {|
+assetRegistryPath: string,
+blacklistRE?: RegExp,
+cacheVersion: string,
+dynamicDepsInPackages: DynamicRequiresBehavior,
+enableBabelRCLookup: boolean,
+extraNodeModules: {},
+getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
@ -117,17 +119,18 @@ class Bundler {
opts.projectRoots.forEach(verifyRootExists);
this._transformer = new Transformer(
opts.transformModulePath,
opts.maxWorkers,
{
this._transformer = new Transformer({
maxWorkers: opts.maxWorkers,
reporters: {
stdoutChunk: chunk =>
opts.reporter.update({type: 'worker_stdout_chunk', chunk}),
stderrChunk: chunk =>
opts.reporter.update({type: 'worker_stderr_chunk', chunk}),
},
opts.workerPath || undefined,
);
transformModulePath: opts.transformModulePath,
dynamicDepsInPackages: opts.dynamicDepsInPackages,
workerPath: opts.workerPath || undefined,
});
this._depGraphPromise = DependencyGraph.load({
assetExts: opts.assetExts,
@ -137,6 +140,7 @@ class Bundler {
getPolyfills: opts.getPolyfills,
getTransformCacheKey: getTransformCacheKeyFn({
cacheVersion: opts.cacheVersion,
dynamicDepsInPackages: opts.dynamicDepsInPackages,
projectRoots: opts.projectRoots,
transformModulePath: opts.transformModulePath,
}),

View File

@ -24,6 +24,7 @@ import type {
import type {PostProcessModules} from './DeltaBundler';
import type {PostProcessModules as PostProcessModulesForBuck} from './ModuleGraph/types.flow.js';
import type {TransformVariants} from './ModuleGraph/types.flow';
import type {DynamicRequiresBehavior} from './ModuleGraph/worker/collectDependencies';
import type {HasteImpl} from './node-haste/Module';
import type {IncomingMessage, ServerResponse} from 'http';
@ -39,6 +40,9 @@ export type ConfigT = {
enhanceMiddleware: Middleware => Middleware,
extraNodeModules: {[id: string]: string},
+dynamicDepsInPackages: DynamicRequiresBehavior,
/**
* Specify any additional asset file extensions to be used by the packager.
* For example, if you want to include a .ttf file, you would return ['ttf']
@ -160,6 +164,7 @@ const DEFAULT = ({
enhanceMiddleware: middleware => middleware,
extraNodeModules: {},
assetTransforms: false,
dynamicDepsInPackages: 'throwAtRuntime',
getAssetExts: () => [],
getBlacklistRE: () => blacklist(),
getEnableBabelRCLookup: () => false,

View File

@ -26,6 +26,14 @@ describe('Transformer', function() {
const localPath = 'arbitrary/file.js';
const transformModulePath = __filename;
const opts = {
maxWorkers: 4,
reporters: {},
transformModulePath,
dynamicDepsInPackages: 'reject',
workerPath: null,
};
beforeEach(function() {
Cache = jest.fn();
Cache.prototype.get = jest.fn((a, b, c) => c());
@ -55,7 +63,7 @@ describe('Transformer', function() {
const transformOptions = {arbitrary: 'options'};
const code = 'arbitrary(code)';
new Transformer(transformModulePath, 4).transformFile(
new Transformer(opts).transformFile(
fileName,
localPath,
code,
@ -74,11 +82,12 @@ describe('Transformer', function() {
transformOptions,
[],
'',
'reject',
);
});
it('should add file info to parse errors', () => {
const transformer = new Transformer(transformModulePath, 4);
const transformer = new Transformer(opts);
const message = 'message';
const snippet = 'snippet';

View File

@ -21,6 +21,7 @@ import type {BabelSourceMap} from 'babel-core';
import type {Options, TransformedCode} from './worker';
import type {LocalPath} from '../node-haste/lib/toLocalPath';
import type {ResultWithMap} from './worker/minify';
import type {DynamicRequiresBehavior} from '../ModuleGraph/worker/collectDependencies';
import typeof {minify as Minify, transform as Transform} from './worker';
@ -37,31 +38,37 @@ type Reporters = {
module.exports = class Transformer {
_worker: WorkerInterface;
_transformModulePath: string;
_dynamicDepsInPackages: DynamicRequiresBehavior;
constructor(
transformModulePath: string,
maxWorkers: number,
reporters: Reporters,
workerPath: string = require.resolve('./worker'),
) {
this._transformModulePath = transformModulePath;
constructor(options: {|
+maxWorkers: number,
+reporters: Reporters,
+transformModulePath: string,
+dynamicDepsInPackages: DynamicRequiresBehavior,
+workerPath: ?string,
|}) {
this._transformModulePath = options.transformModulePath;
this._dynamicDepsInPackages = options.dynamicDepsInPackages;
const {workerPath = require.resolve('./worker')} = options;
if (maxWorkers > 1) {
if (options.maxWorkers > 1) {
this._worker = this._makeFarm(
workerPath,
this._computeWorkerKey,
['minify', 'transform'],
maxWorkers,
options.maxWorkers,
);
const {reporters} = options;
this._worker.getStdout().on('data', chunk => {
reporters.stdoutChunk(chunk.toString('utf8'));
});
this._worker.getStderr().on('data', chunk => {
reporters.stderrChunk(chunk.toString('utf8'));
});
} else {
// eslint-disable-next-line flow-no-fixme
// $FlowFixMe: Flow doesn't support dynamic requires
this._worker = require(workerPath);
}
}
@ -101,6 +108,7 @@ module.exports = class Transformer {
options,
assetExts,
assetRegistryPath,
this._dynamicDepsInPackages,
);
debug('Done transforming file', filename);

View File

@ -34,6 +34,7 @@ describe('code transformation worker:', () => {
},
[],
'',
'reject',
);
expect(result.code).toBe(
@ -60,6 +61,7 @@ describe('code transformation worker:', () => {
},
[],
'',
'reject',
);
expect(result.code).toBe(
@ -91,6 +93,7 @@ describe('code transformation worker:', () => {
},
[],
'',
'reject',
);
expect(result.code).toBe(
@ -130,11 +133,31 @@ describe('code transformation worker:', () => {
},
[],
'',
'reject',
);
throw new Error('should not reach this');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
if (!(error instanceof InvalidRequireCallError)) {
throw error;
}
expect(error.message).toMatchSnapshot();
}
});
it('supports dynamic dependencies from within `node_modules`', async () => {
await transformCode(
'/root/node_modules/bar/file.js',
`node_modules/bar/file.js`,
'require(global.something);\n',
path.join(__dirname, '../../../transformer.js'),
false,
{
dev: true,
transform: {},
},
[],
'',
'throwAtRuntime',
);
});
});

View File

@ -32,6 +32,7 @@ import type {MetroSourceMapSegmentTuple} from 'metro-source-map';
import type {LocalPath} from '../../node-haste/lib/toLocalPath';
import type {ResultWithMap} from './minify';
import type {Ast, Plugins as BabelPlugins} from 'babel-core';
import type {DynamicRequiresBehavior} from '../../ModuleGraph/worker/collectDependencies';
export type TransformedCode = {
code: string,
@ -97,6 +98,7 @@ function postTransform(
isScript: boolean,
options: Options,
transformFileStartLogEntry: LogEntry,
dynamicDepsInPackages: DynamicRequiresBehavior,
receivedAst: ?Ast,
): Data {
// Transformers can ouptut null ASTs (if they ignore the file). In that case
@ -125,7 +127,13 @@ function postTransform(
} else {
let dependencyMapName;
try {
({dependencies, dependencyMapName} = collectDependencies(ast));
const opts = {
dynamicRequires: getDynamicDepsBehavior(
dynamicDepsInPackages,
filename,
),
};
({dependencies, dependencyMapName} = collectDependencies(ast, opts));
} catch (error) {
if (error instanceof collectDependencies.InvalidRequireCallError) {
throw new InvalidRequireCallError(error, filename);
@ -162,6 +170,24 @@ function postTransform(
};
}
function getDynamicDepsBehavior(
inPackages: DynamicRequiresBehavior,
filename: string,
): DynamicRequiresBehavior {
switch (inPackages) {
case 'reject':
return 'reject';
case 'throwAtRuntime':
const isPackage = /(?:^|[/\\])node_modules[/\\]/.test(filename);
return isPackage ? inPackages : 'reject';
default:
(inPackages: empty);
throw new Error(
`invalid value for dynamic deps behavior: \`${inPackages}\``,
);
}
}
function transformCode(
filename: string,
localPath: LocalPath,
@ -171,6 +197,7 @@ function transformCode(
options: Options,
assetExts: $ReadOnlyArray<string>,
assetRegistryPath: string,
dynamicDepsInPackages: DynamicRequiresBehavior,
): Data | Promise<Data> {
const isJson = filename.endsWith('.json');
@ -216,6 +243,7 @@ function transformCode(
isScript,
options,
transformFileStartLogEntry,
dynamicDepsInPackages,
];
return transformResult instanceof Promise

View File

@ -21,6 +21,7 @@ const {codeFromAst, comparableCode} = require('../../test-helpers');
const {any} = expect;
const {InvalidRequireCallError} = collectDependencies;
const opts = {dynamicRequires: 'reject'};
it('collects unique dependency identifiers and transforms the AST', () => {
const ast = astFromCode(`
@ -31,7 +32,7 @@ it('collects unique dependency identifiers and transforms the AST', () => {
}
require('do');
`);
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{name: 'b/lib/a', isAsync: false},
{name: 'do', isAsync: false},
@ -53,7 +54,7 @@ it('collects asynchronous dependencies', () => {
const ast = astFromCode(`
import("some/async/module").then(foo => {});
`);
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{name: 'some/async/module', isAsync: true},
{name: 'asyncRequire', isAsync: false},
@ -70,7 +71,7 @@ it('collects mixed dependencies as being sync', () => {
const a = require("some/async/module");
import("some/async/module").then(foo => {});
`);
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{name: 'some/async/module', isAsync: false},
{name: 'asyncRequire', isAsync: false},
@ -88,7 +89,7 @@ it('collects mixed dependencies as being sync; reverse order', () => {
import("some/async/module").then(foo => {});
const a = require("some/async/module");
`);
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([
{name: 'some/async/module', isAsync: false},
{name: 'asyncRequire', isAsync: false},
@ -104,7 +105,7 @@ it('collects mixed dependencies as being sync; reverse order', () => {
describe('Evaluating static arguments', () => {
it('supports template literals as arguments', () => {
const ast = astFromCode('require(`left-pad`)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([{name: 'left-pad', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`require(${dependencyMapName}[0], \`left-pad\`);`),
@ -113,7 +114,7 @@ describe('Evaluating static arguments', () => {
it('supports template literals with static interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([{name: 'left-pad', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`require(${dependencyMapName}[0], \`left\${"-"}pad\`);`),
@ -123,10 +124,12 @@ describe('Evaluating static arguments', () => {
it('throws template literals with dyncamic interpolations', () => {
const ast = astFromCode('let foo;require(`left${foo}pad`)');
try {
collectDependencies(ast);
collectDependencies(ast, opts);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
if (!(error instanceof InvalidRequireCallError)) {
throw error;
}
expect(error.message).toMatchSnapshot();
}
});
@ -134,17 +137,19 @@ describe('Evaluating static arguments', () => {
it('throws on tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
try {
collectDependencies(ast);
collectDependencies(ast, opts);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
if (!(error instanceof InvalidRequireCallError)) {
throw error;
}
expect(error.message).toMatchSnapshot();
}
});
it('supports multiple static strings concatenated', () => {
const ast = astFromCode('require("foo_" + "bar")');
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([{name: 'foo_bar', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`require(${dependencyMapName}[0], "foo_" + "bar");`),
@ -153,7 +158,7 @@ describe('Evaluating static arguments', () => {
it('supports concatenating strings and template literasl', () => {
const ast = astFromCode('require("foo_" + "bar" + `_baz`)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([{name: 'foo_bar_baz', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(
@ -164,7 +169,7 @@ describe('Evaluating static arguments', () => {
it('supports using static variables in require statements', () => {
const ast = astFromCode('const myVar="my";require("foo_" + myVar)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
const {dependencies, dependencyMapName} = collectDependencies(ast, opts);
expect(dependencies).toEqual([{name: 'foo_my', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(
@ -176,18 +181,34 @@ describe('Evaluating static arguments', () => {
it('throws when requiring non-strings', () => {
const ast = astFromCode('require(1)');
try {
collectDependencies(ast);
collectDependencies(ast, opts);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
if (!(error instanceof InvalidRequireCallError)) {
throw error;
}
expect(error.message).toMatchSnapshot();
}
});
it('throws at runtime when requiring non-strings with special option', () => {
const ast = astFromCode('require(1)');
const opts = {dynamicRequires: 'throwAtRuntime'};
const {dependencies} = collectDependencies(ast, opts);
expect(dependencies).toEqual([]);
expect(codeFromAst(ast)).toEqual(
comparableCode(
"(function (name) { throw new Error('Module `' + name " +
"+ '` was required dynamically. This is not supported by " +
"Metro bundler.'); })(1);",
),
);
});
});
it('exposes a string as `dependencyMapName` even without collecting dependencies', () => {
const ast = astFromCode('');
expect(collectDependencies(ast).dependencyMapName).toEqual(any(String));
expect(collectDependencies(ast, opts).dependencyMapName).toEqual(any(String));
});
function astFromCode(code) {

View File

@ -20,9 +20,13 @@ const prettyPrint = require('babel-generator').default;
import type {TransformResultDependency} from '../types.flow';
export type DynamicRequiresBehavior = 'throwAtRuntime' | 'reject';
type Options = {|+dynamicRequires: DynamicRequiresBehavior|};
type Context = {
nameToIndex: Map<string, number>,
dependencies: Array<{|+name: string, isAsync: boolean|}>,
+dynamicRequires: DynamicRequiresBehavior,
};
type CollectedDependencies = {|
@ -38,9 +42,13 @@ type CollectedDependencies = {|
* know the actual module ID. The second argument is only provided for debugging
* purposes.
*/
function collectDependencies(ast: Ast): CollectedDependencies {
function collectDependencies(
ast: Ast,
options: Options,
): CollectedDependencies {
const visited = new WeakSet();
const context = {nameToIndex: new Map(), dependencies: []};
const {dynamicRequires} = options;
const context = {nameToIndex: new Map(), dependencies: [], dynamicRequires};
const visitor = {
Program(path, state) {
state.dependencyMapIdentifier = path.scope.generateUidIdentifier(
@ -77,6 +85,9 @@ function isRequireCall(callee) {
function processImportCall(context, path, node, depMapIdent) {
const [, name] = getModuleNameFromCallArgs('import', node, path);
if (name == null) {
throw invalidRequireOf('import', node);
}
const index = assignDependencyIndex(context, name, 'import');
const mapLookup = createDepMapLookup(depMapIdent, index);
const newImport = makeAsyncRequire({
@ -87,17 +98,32 @@ function processImportCall(context, path, node, depMapIdent) {
}
function processRequireCall(context, path, node, depMapIdent) {
const [nameExpression, name] = getModuleNameFromCallArgs(
'require',
node,
path,
);
const [nameExpr, name] = getModuleNameFromCallArgs('require', node, path);
if (name == null) {
const {dynamicRequires} = context;
switch (dynamicRequires) {
case 'reject':
throw invalidRequireOf('require', node);
case 'throwAtRuntime':
const newNode = makeDynamicRequireReplacement({NAME_EXPR: nameExpr});
path.replaceWith(newNode);
return newNode;
default:
(dynamicRequires: empty);
throw new Error(`invalid dyn requires spec \`${dynamicRequires}\``);
}
}
const index = assignDependencyIndex(context, name, 'require');
const mapLookup = createDepMapLookup(depMapIdent, index);
node.arguments = [mapLookup, nameExpression];
node.arguments = [mapLookup, nameExpr];
return node;
}
const makeDynamicRequireReplacement = babelTemplate(
"(function(name){throw new Error('Module `'+name+'` was required " +
"dynamically. This is not supported by Metro bundler.')})(NAME_EXPR)",
);
/**
* Extract the module name from `require` arguments. We support template
* literal, for example one could write `require(`foo`)`.
@ -115,8 +141,7 @@ function getModuleNameFromCallArgs(type, node, path) {
if (result.confident && typeof result.value === 'string') {
return [nameExpression, result.value];
}
throw invalidRequireOf(type, node);
return [nameExpression, null];
}
/**

View File

@ -188,7 +188,8 @@ function makeResult(ast: Ast, filename, sourceCode, isPolyfill = false) {
dependencies = [];
file = JsFileWrapping.wrapPolyfill(ast);
} else {
({dependencies, dependencyMapName} = collectDependencies(ast));
const opts = {dynamicRequires: 'reject'};
({dependencies, dependencyMapName} = collectDependencies(ast, opts));
file = JsFileWrapping.wrapModule(ast, dependencyMapName);
}

View File

@ -121,6 +121,7 @@ class Server {
assetRegistryPath: options.assetRegistryPath,
blacklistRE: options.blacklistRE,
cacheVersion: options.cacheVersion || '1.0',
dynamicDepsInPackages: options.dynamicDepsInPackages,
createModuleIdFactory: options.createModuleIdFactory,
enableBabelRCLookup:
options.enableBabelRCLookup != null

View File

@ -115,6 +115,7 @@ async function runMetro({
assetRegistryPath: normalizedConfig.assetRegistryPath,
blacklistRE: normalizedConfig.getBlacklistRE(),
createModuleIdFactory: normalizedConfig.createModuleIdFactory,
dynamicDepsInPackages: normalizedConfig.dynamicDepsInPackages,
extraNodeModules: normalizedConfig.extraNodeModules,
getPolyfills: normalizedConfig.getPolyfills,
getModulesRunBeforeMainModule:

View File

@ -93,6 +93,7 @@ describe('basic_bundle', () => {
const bundleWithPolyfills = await Metro.build(
{
assetRegistryPath: ASSET_REGISTRY_PATH,
dynamicDepsInPackages: 'reject',
getModulesRunBeforeMainModule: () => ['InitializeCore'],
getPolyfills: () => [polyfill1, polyfill2],
projectRoots: [INPUT_PATH, POLYFILLS_PATH],
@ -113,6 +114,7 @@ describe('basic_bundle', () => {
const bundleWithoutPolyfills = await Metro.build(
{
assetRegistryPath: ASSET_REGISTRY_PATH,
dynamicDepsInPackages: 'reject',
getModulesRunBeforeMainModule: () => ['InitializeCore'],
getPolyfills: () => [],
projectRoots: [INPUT_PATH, POLYFILLS_PATH],

View File

@ -171,6 +171,7 @@ function toServerOptions(options: Options): ServerOptions {
assetRegistryPath: options.assetRegistryPath,
blacklistRE: options.blacklistRE,
cacheVersion: options.cacheVersion,
dynamicDepsInPackages: options.dynamicDepsInPackages,
enableBabelRCLookup: options.enableBabelRCLookup,
extraNodeModules: options.extraNodeModules,
getModulesRunBeforeMainModule: options.getModulesRunBeforeMainModule,

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getTransformCacheKeyFn Should return always the same key for the same params 1`] = `"7b98b107d047d0c094f33c8c85d28a9c4edaf407"`;
exports[`getTransformCacheKeyFn Should return always the same key for the same params 1`] = `"b2ccafe92b59d525a827be33f7579633f4859b48"`;

View File

@ -21,6 +21,7 @@ describe('getTransformCacheKeyFn', () => {
expect(
getTransformCacheKeyFn({
cacheVersion: '1.0',
dynamicDepsInPackages: 'arbitrary',
projectRoots: [__dirname],
transformModulePath: path.resolve(
__dirname,
@ -33,6 +34,7 @@ describe('getTransformCacheKeyFn', () => {
it('Should return a different key when the params change', async () => {
const baseParams = {
cacheVersion: '1.0',
dynamicDepsInPackages: 'arbitrary',
projectRoots: [__dirname],
transformModulePath: path.resolve(__dirname, '../../defaultTransform.js'),
};

View File

@ -24,6 +24,7 @@ const VERSION = require('../../package.json').version;
*/
function getTransformCacheKeyFn(opts: {|
+cacheVersion: string,
+dynamicDepsInPackages: string,
+projectRoots: $ReadOnlyArray<string>,
+transformModulePath: string,
|}): (options: mixed) => string {
@ -45,6 +46,7 @@ function getTransformCacheKeyFn(opts: {|
.split(path.sep)
.join('-'),
transformModuleHash,
opts.dynamicDepsInPackages,
];
const transformCacheKey = crypto

View File

@ -17,6 +17,7 @@ import type {
PostProcessBundleSourcemap,
} from '../Bundler';
import type {PostProcessModules} from '../DeltaBundler';
import type {DynamicRequiresBehavior} from '../ModuleGraph/worker/collectDependencies';
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
import type {TransformCache} from '../lib/TransformCaching';
import type {Reporter} from '../lib/reporting';
@ -74,6 +75,7 @@ export type Options = {|
blacklistRE?: RegExp,
cacheVersion?: string,
createModuleIdFactory?: () => (path: string) => number,
+dynamicDepsInPackages: DynamicRequiresBehavior,
enableBabelRCLookup?: boolean,
extraNodeModules?: {},
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,