mirror of https://github.com/status-im/metro.git
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:
parent
40497ee118
commit
46545d4f5c
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"`;
|
||||||
|
|
|
@ -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'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
Loading…
Reference in New Issue