Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ Type: `Array<string>`

Additional platforms to look out for, For example, if you want to add a "custom" platform, and use modules ending in .custom.js, you would return ['custom'] here.

#### `requireCycleIgnorePatterns`

Type: `Array<RegExp>` (default: `[/(^|\/|\\)node_modules($|\/|\\)/]`)

In development mode, suppress require cycle warnings for any cycle involving a module that matches any of these expressions. This is useful for third-party code and first-party expected cycles.

Note that if you specify your own value for this config option it will replace (not concatenate with) Metro's default.

---
### Transformer Options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ Object {
"windows",
"web",
],
"requireCycleIgnorePatterns": Array [
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
],
"resolveRequest": null,
"resolverMainFields": Array [
"browser",
Expand Down Expand Up @@ -204,6 +207,9 @@ Object {
"windows",
"web",
],
"requireCycleIgnorePatterns": Array [
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
],
"resolveRequest": null,
"resolverMainFields": Array [
"browser",
Expand Down Expand Up @@ -350,6 +356,9 @@ Object {
"windows",
"web",
],
"requireCycleIgnorePatterns": Array [
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
],
"resolveRequest": null,
"resolverMainFields": Array [
"browser",
Expand Down Expand Up @@ -496,6 +505,9 @@ Object {
"windows",
"web",
],
"requireCycleIgnorePatterns": Array [
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
],
"resolveRequest": null,
"resolverMainFields": Array [
"browser",
Expand Down
1 change: 1 addition & 0 deletions packages/metro-config/src/configTypes.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type ResolverConfigT = {
resolverMainFields: $ReadOnlyArray<string>,
sourceExts: $ReadOnlyArray<string>,
useWatchman: boolean,
requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
};

type SerializerConfigT = {
Expand Down
1 change: 1 addition & 0 deletions packages/metro-config/src/defaults/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({
resolveRequest: null,
resolverMainFields: ['browser', 'main'],
useWatchman: true,
requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/],
},

serializer: {
Expand Down
53 changes: 52 additions & 1 deletion packages/metro-runtime/src/polyfills/__tests__/require-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ function createModule(
verboseName,
factory,
dependencyMap = [],
globalPrefix = '',
) {
moduleSystem.__d(factory, moduleId, dependencyMap, verboseName);
moduleSystem[globalPrefix + '__d'](
factory,
moduleId,
dependencyMap,
verboseName,
);
}

describe('require', () => {
Expand Down Expand Up @@ -665,6 +671,51 @@ describe('require', () => {
console.warn = warn;
});

it('does not log warning for cyclic dependency in ignore list', () => {
moduleSystem.__customPrefix__requireCycleIgnorePatterns = [/foo/];
createModuleSystem(moduleSystem, true, '__customPrefix');

createModule(
moduleSystem,
0,
'foo.js',
(global, require) => {
require(1);
},
[],
'__customPrefix',
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require) => {
require(2);
},
[],
'__customPrefix',
);

createModule(
moduleSystem,
2,
'baz.js',
(global, require) => {
require(0);
},
[],
'__customPrefix',
);

const warn = console.warn;
console.warn = jest.fn();

moduleSystem.__r(0);
expect(console.warn).toHaveBeenCalledTimes(0);
console.warn = warn;
});

it('sets the exports value to their current value', () => {
createModuleSystem(moduleSystem, false, '');

Expand Down
32 changes: 25 additions & 7 deletions packages/metro-runtime/src/polyfills/require.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,15 @@ function metroRequire(moduleId: ModuleID | VerboseModuleNameForDev): Exports {
.map((id: number) =>
modules[id] ? modules[id].verboseName : '[unknown]',
);
// We want to show A -> B -> A:
cycle.push(cycle[0]);
console.warn(
`Require cycle: ${cycle.join(' -> ')}\n\n` +
'Require cycles are allowed, but can result in uninitialized values. ' +
'Consider refactoring to remove the need for a cycle.',
);

if (shouldPrintRequireCycle(cycle)) {
cycle.push(cycle[0]); // We want to print A -> B -> A:
console.warn(
`Require cycle: ${cycle.join(' -> ')}\n\n` +
'Require cycles are allowed, but can result in uninitialized values. ' +
'Consider refactoring to remove the need for a cycle.',
);
}
}
}

Expand All @@ -194,6 +196,22 @@ function metroRequire(moduleId: ModuleID | VerboseModuleNameForDev): Exports {
: guardedLoadModule(moduleIdReallyIsNumber, module);
}

// We print require cycles unless they match a pattern in the
// `requireCycleIgnorePatterns` configuration.
function shouldPrintRequireCycle(modules: $ReadOnlyArray<?string>): boolean {
const regExps =
global[__METRO_GLOBAL_PREFIX__ + '__requireCycleIgnorePatterns'];
if (!regExps || !Array.isArray(regExps)) {
return true;
}

const isIgnored = module =>
module != null && regExps.some(regExp => regExp.test(module));

// Print the cycle unless any part of it is ignored
return modules.every(module => !isIgnored(module));
}

function metroImportDefault(moduleId: ModuleID | VerboseModuleNameForDev) {
if (__DEV__ && typeof moduleId === 'string') {
const verboseName = moduleId;
Expand Down
62 changes: 55 additions & 7 deletions packages/metro/src/lib/__tests__/getPreludeCode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ const vm = require('vm');
['development', 'production'].forEach((mode: string) => {
describe(`${mode} mode`, () => {
const isDev = mode === 'development';
const globalPrefix = '';
const globalPrefix = '__metro';
const requireCycleIgnorePatterns = [];

it('sets up `process.env.NODE_ENV` and `__DEV__`', () => {
const sandbox: $FlowFixMe = {};
vm.createContext(sandbox);
vm.runInContext(getPreludeCode({isDev, globalPrefix}), sandbox);
vm.runInContext(
getPreludeCode({isDev, globalPrefix, requireCycleIgnorePatterns}),
sandbox,
);
expect(sandbox.process.env.NODE_ENV).toEqual(mode);
expect(sandbox.__DEV__).toEqual(isDev);
});
Expand All @@ -31,17 +35,51 @@ const vm = require('vm');
const sandbox: $FlowFixMe = {};
vm.createContext(sandbox);
vm.runInContext(
getPreludeCode({isDev, globalPrefix: '__metro'}),
getPreludeCode({
isDev,
globalPrefix: '__customPrefix',
requireCycleIgnorePatterns,
}),
sandbox,
);
expect(sandbox.__METRO_GLOBAL_PREFIX__).toBe('__customPrefix');
});

it('sets up `${globalPrefix}__requireCycleIgnorePatterns` in development', () => {
const sandbox: $FlowFixMe = {};
vm.createContext(sandbox);
vm.runInContext(
getPreludeCode({
isDev,
globalPrefix,
requireCycleIgnorePatterns: [
/blah/,
/(^|\/|\\)node_modules($|\/|\\)/,
],
}),
sandbox,
);
expect(sandbox.__METRO_GLOBAL_PREFIX__).toBe('__metro');

if (isDev) {
expect(sandbox[`${globalPrefix}__requireCycleIgnorePatterns`]).toEqual([
/blah/,
/(^|\/|\\)node_modules($|\/|\\)/,
]);
} else {
expect(
sandbox[`${globalPrefix}__requireCycleIgnorePatterns`],
).not.toBeDefined();
}
});

it('does not override an existing `process.env`', () => {
const nextTick = () => {};
const sandbox: $FlowFixMe = {process: {nextTick, env: {FOOBAR: 123}}};
vm.createContext(sandbox);
vm.runInContext(getPreludeCode({isDev, globalPrefix}), sandbox);
vm.runInContext(
getPreludeCode({isDev, globalPrefix, requireCycleIgnorePatterns}),
sandbox,
);
expect(sandbox.process.env.NODE_ENV).toEqual(mode);
expect(sandbox.process.env.FOOBAR).toEqual(123);
expect(sandbox.process.nextTick).toEqual(nextTick);
Expand All @@ -53,7 +91,12 @@ const vm = require('vm');
const BAR = 2;
vm.createContext(sandbox);
vm.runInContext(
getPreludeCode({isDev, globalPrefix, extraVars: {FOO, BAR}}),
getPreludeCode({
isDev,
globalPrefix,
requireCycleIgnorePatterns,
extraVars: {FOO, BAR},
}),
sandbox,
);
expect(sandbox.FOO).toBe(FOO);
Expand All @@ -64,7 +107,12 @@ const vm = require('vm');
const sandbox: $FlowFixMe = {};
vm.createContext(sandbox);
vm.runInContext(
getPreludeCode({isDev, globalPrefix, extraVars: {__DEV__: 123}}),
getPreludeCode({
isDev,
globalPrefix,
requireCycleIgnorePatterns,
extraVars: {__DEV__: 123},
}),
sandbox,
);
expect(sandbox.__DEV__).toBe(isDev);
Expand Down
15 changes: 15 additions & 0 deletions packages/metro/src/lib/getPreludeCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,33 @@ function getPreludeCode({
extraVars,
isDev,
globalPrefix,
requireCycleIgnorePatterns,
}: {
+extraVars?: {[string]: mixed, ...},
+isDev: boolean,
+globalPrefix: string,
+requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
}): string {
const vars = [
// Ensure these variable names match the ones referenced in metro-runtime
// require.js
'__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()',
`__DEV__=${String(isDev)}`,
...formatExtraVars(extraVars),
'process=this.process||{}',
`__METRO_GLOBAL_PREFIX__='${globalPrefix}'`,
];

if (isDev) {
// Ensure these variable names match the ones referenced in metro-runtime
// require.js
vars.push(
`${globalPrefix}__requireCycleIgnorePatterns=[${requireCycleIgnorePatterns
.map(regex => regex.toString())
.join(',')}]`,
);
}

return `var ${vars.join(',')};${processEnv(
isDev ? 'development' : 'production',
)}`;
Expand Down
9 changes: 8 additions & 1 deletion packages/metro/src/lib/getPrependedScripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ async function getPrependedScripts(
_getPrelude({
dev: options.dev,
globalPrefix: config.transformer.globalPrefix,
requireCycleIgnorePatterns: config.resolver.requireCycleIgnorePatterns,
}),
...dependencies.values(),
];
Expand All @@ -77,12 +78,18 @@ async function getPrependedScripts(
function _getPrelude({
dev,
globalPrefix,
requireCycleIgnorePatterns,
}: {
dev: boolean,
globalPrefix: string,
requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
...
}): Module<> {
const code = getPreludeCode({isDev: dev, globalPrefix});
const code = getPreludeCode({
isDev: dev,
globalPrefix,
requireCycleIgnorePatterns,
});
const name = '__prelude__';

return {
Expand Down