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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ describe('code transformation worker:', () => {
}, },
[], [],
'', '',
'reject',
); );
expect(result.code).toBe( expect(result.code).toBe(
@ -60,6 +61,7 @@ describe('code transformation worker:', () => {
}, },
[], [],
'', '',
'reject',
); );
expect(result.code).toBe( expect(result.code).toBe(
@ -91,6 +93,7 @@ describe('code transformation worker:', () => {
}, },
[], [],
'', '',
'reject',
); );
expect(result.code).toBe( expect(result.code).toBe(
@ -130,11 +133,31 @@ describe('code transformation worker:', () => {
}, },
[], [],
'', '',
'reject',
); );
throw new Error('should not reach this'); throw new Error('should not reach this');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError); if (!(error instanceof InvalidRequireCallError)) {
throw error;
}
expect(error.message).toMatchSnapshot(); 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 {LocalPath} from '../../node-haste/lib/toLocalPath';
import type {ResultWithMap} from './minify'; import type {ResultWithMap} from './minify';
import type {Ast, Plugins as BabelPlugins} from 'babel-core'; import type {Ast, Plugins as BabelPlugins} from 'babel-core';
import type {DynamicRequiresBehavior} from '../../ModuleGraph/worker/collectDependencies';
export type TransformedCode = { export type TransformedCode = {
code: string, code: string,
@ -97,6 +98,7 @@ function postTransform(
isScript: boolean, isScript: boolean,
options: Options, options: Options,
transformFileStartLogEntry: LogEntry, transformFileStartLogEntry: LogEntry,
dynamicDepsInPackages: DynamicRequiresBehavior,
receivedAst: ?Ast, receivedAst: ?Ast,
): Data { ): Data {
// Transformers can ouptut null ASTs (if they ignore the file). In that case // Transformers can ouptut null ASTs (if they ignore the file). In that case
@ -125,7 +127,13 @@ function postTransform(
} else { } else {
let dependencyMapName; let dependencyMapName;
try { try {
({dependencies, dependencyMapName} = collectDependencies(ast)); const opts = {
dynamicRequires: getDynamicDepsBehavior(
dynamicDepsInPackages,
filename,
),
};
({dependencies, dependencyMapName} = collectDependencies(ast, opts));
} catch (error) { } catch (error) {
if (error instanceof collectDependencies.InvalidRequireCallError) { if (error instanceof collectDependencies.InvalidRequireCallError) {
throw new InvalidRequireCallError(error, filename); 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( function transformCode(
filename: string, filename: string,
localPath: LocalPath, localPath: LocalPath,
@ -171,6 +197,7 @@ function transformCode(
options: Options, options: Options,
assetExts: $ReadOnlyArray<string>, assetExts: $ReadOnlyArray<string>,
assetRegistryPath: string, assetRegistryPath: string,
dynamicDepsInPackages: DynamicRequiresBehavior,
): Data | Promise<Data> { ): Data | Promise<Data> {
const isJson = filename.endsWith('.json'); const isJson = filename.endsWith('.json');
@ -216,6 +243,7 @@ function transformCode(
isScript, isScript,
options, options,
transformFileStartLogEntry, transformFileStartLogEntry,
dynamicDepsInPackages,
]; ];
return transformResult instanceof Promise return transformResult instanceof Promise

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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( expect(
getTransformCacheKeyFn({ getTransformCacheKeyFn({
cacheVersion: '1.0', cacheVersion: '1.0',
dynamicDepsInPackages: 'arbitrary',
projectRoots: [__dirname], projectRoots: [__dirname],
transformModulePath: path.resolve( transformModulePath: path.resolve(
__dirname, __dirname,
@ -33,6 +34,7 @@ describe('getTransformCacheKeyFn', () => {
it('Should return a different key when the params change', async () => { it('Should return a different key when the params change', async () => {
const baseParams = { const baseParams = {
cacheVersion: '1.0', cacheVersion: '1.0',
dynamicDepsInPackages: 'arbitrary',
projectRoots: [__dirname], projectRoots: [__dirname],
transformModulePath: path.resolve(__dirname, '../../defaultTransform.js'), transformModulePath: path.resolve(__dirname, '../../defaultTransform.js'),
}; };

View File

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

View File

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