Skip to content

Commit 100ca6f

Browse files
hsourcefacebook-github-bot
authored andcommitted
Add configuration option to suppress require cycle warnings (#707)
Summary: In many projects there are require cycles in the `node_modules` dependencies that are outside the developer's control, or cycles in first-party code that are known not to be problematic. This adds an option to selectively suppress require cycle warnings, and defaults to suppressing any cycle within `node_modules`. Closes #28 Pull Request resolved: #707 Test Plan: See D37157514 **Static Docs Preview: metro** |[Full Site](https://our.intern.facebook.com/intern/staticdocs/eph/1655421690/metro/)| |**Modified Pages**| |[docs/configuration](https://our.intern.facebook.com/intern/staticdocs/eph/1655421690/metro/docs/configuration/)||[docs/configuration](https://our.intern.facebook.com/intern/staticdocs/eph/D36778288/V10/metro/docs/configuration/)||[docs/configuration](https://our.intern.facebook.com/intern/staticdocs/eph/1655419713/metro/docs/configuration/)| Reviewed By: huntie Differential Revision: D36778288 Pulled By: robhogan fbshipit-source-id: b2588c1f3f74175af3ccb05ed7b0efe36f0eebb5
1 parent 7007e48 commit 100ca6f

9 files changed

Lines changed: 179 additions & 16 deletions

File tree

docs/Configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ Type: `Array<string>`
217217

218218
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.
219219

220+
#### `requireCycleIgnorePatterns`
221+
222+
Type: `Array<RegExp>`
223+
224+
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.
225+
226+
Note that if you specify your own value for this config option it will replace (not concatenate with) Metro's default.
227+
228+
Defaults to `[/(^|\/|\\)node_modules($|\/|\\)/]`.
229+
220230
---
221231
### Transformer Options
222232

packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ Object {
5858
"windows",
5959
"web",
6060
],
61+
"requireCycleIgnorePatterns": Array [
62+
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
63+
],
6164
"resolveRequest": null,
6265
"resolverMainFields": Array [
6366
"browser",
@@ -211,6 +214,9 @@ Object {
211214
"windows",
212215
"web",
213216
],
217+
"requireCycleIgnorePatterns": Array [
218+
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
219+
],
214220
"resolveRequest": null,
215221
"resolverMainFields": Array [
216222
"browser",
@@ -364,6 +370,9 @@ Object {
364370
"windows",
365371
"web",
366372
],
373+
"requireCycleIgnorePatterns": Array [
374+
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
375+
],
367376
"resolveRequest": null,
368377
"resolverMainFields": Array [
369378
"browser",
@@ -517,6 +526,9 @@ Object {
517526
"windows",
518527
"web",
519528
],
529+
"requireCycleIgnorePatterns": Array [
530+
/\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/,
531+
],
520532
"resolveRequest": null,
521533
"resolverMainFields": Array [
522534
"browser",

packages/metro-config/src/configTypes.flow.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ type ResolverConfigT = {
102102
resolverMainFields: $ReadOnlyArray<string>,
103103
sourceExts: $ReadOnlyArray<string>,
104104
useWatchman: boolean,
105+
requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
105106
};
106107

107108
type SerializerConfigT = {

packages/metro-config/src/defaults/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({
4646
resolveRequest: null,
4747
resolverMainFields: ['browser', 'main'],
4848
useWatchman: true,
49+
requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/],
4950
},
5051

5152
serializer: {

packages/metro-runtime/src/polyfills/__tests__/require-test.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ function createModule(
1919
verboseName,
2020
factory,
2121
dependencyMap = [],
22+
globalPrefix = '',
2223
) {
23-
moduleSystem.__d(factory, moduleId, dependencyMap, verboseName);
24+
moduleSystem[globalPrefix + '__d'](
25+
factory,
26+
moduleId,
27+
dependencyMap,
28+
verboseName,
29+
);
2430
}
2531

2632
describe('require', () => {
@@ -665,6 +671,51 @@ describe('require', () => {
665671
console.warn = warn;
666672
});
667673

674+
it('does not log warning for cyclic dependency in ignore list', () => {
675+
moduleSystem.__customPrefix__requireCycleIgnorePatterns = [/foo/];
676+
createModuleSystem(moduleSystem, true, '__customPrefix');
677+
678+
createModule(
679+
moduleSystem,
680+
0,
681+
'foo.js',
682+
(global, require) => {
683+
require(1);
684+
},
685+
[],
686+
'__customPrefix',
687+
);
688+
689+
createModule(
690+
moduleSystem,
691+
1,
692+
'bar.js',
693+
(global, require) => {
694+
require(2);
695+
},
696+
[],
697+
'__customPrefix',
698+
);
699+
700+
createModule(
701+
moduleSystem,
702+
2,
703+
'baz.js',
704+
(global, require) => {
705+
require(0);
706+
},
707+
[],
708+
'__customPrefix',
709+
);
710+
711+
const warn = console.warn;
712+
console.warn = jest.fn();
713+
714+
moduleSystem.__r(0);
715+
expect(console.warn).toHaveBeenCalledTimes(0);
716+
console.warn = warn;
717+
});
718+
668719
it('sets the exports value to their current value', () => {
669720
createModuleSystem(moduleSystem, false, '');
670721

packages/metro-runtime/src/polyfills/require.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,15 @@ function metroRequire(moduleId: ModuleID | VerboseModuleNameForDev): Exports {
177177
.map((id: number) =>
178178
modules[id] ? modules[id].verboseName : '[unknown]',
179179
);
180-
// We want to show A -> B -> A:
181-
cycle.push(cycle[0]);
182-
console.warn(
183-
`Require cycle: ${cycle.join(' -> ')}\n\n` +
184-
'Require cycles are allowed, but can result in uninitialized values. ' +
185-
'Consider refactoring to remove the need for a cycle.',
186-
);
180+
181+
if (shouldPrintRequireCycle(cycle)) {
182+
cycle.push(cycle[0]); // We want to print A -> B -> A:
183+
console.warn(
184+
`Require cycle: ${cycle.join(' -> ')}\n\n` +
185+
'Require cycles are allowed, but can result in uninitialized values. ' +
186+
'Consider refactoring to remove the need for a cycle.',
187+
);
188+
}
187189
}
188190
}
189191

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

199+
// We print require cycles unless they match a pattern in the
200+
// `requireCycleIgnorePatterns` configuration.
201+
function shouldPrintRequireCycle(modules: $ReadOnlyArray<?string>): boolean {
202+
const regExps =
203+
global[__METRO_GLOBAL_PREFIX__ + '__requireCycleIgnorePatterns'];
204+
if (!Array.isArray(regExps)) {
205+
return true;
206+
}
207+
208+
const isIgnored = module =>
209+
module != null && regExps.some(regExp => regExp.test(module));
210+
211+
// Print the cycle unless any part of it is ignored
212+
return modules.every(module => !isIgnored(module));
213+
}
214+
197215
function metroImportDefault(moduleId: ModuleID | VerboseModuleNameForDev) {
198216
if (__DEV__ && typeof moduleId === 'string') {
199217
const verboseName = moduleId;

packages/metro/src/lib/__tests__/getPreludeCode-test.js

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ const vm = require('vm');
1717
['development', 'production'].forEach((mode: string) => {
1818
describe(`${mode} mode`, () => {
1919
const isDev = mode === 'development';
20-
const globalPrefix = '';
20+
const globalPrefix = '__metro';
21+
const requireCycleIgnorePatterns = [];
2122

2223
it('sets up `process.env.NODE_ENV` and `__DEV__`', () => {
2324
const sandbox: $FlowFixMe = {};
2425
vm.createContext(sandbox);
25-
vm.runInContext(getPreludeCode({isDev, globalPrefix}), sandbox);
26+
vm.runInContext(
27+
getPreludeCode({isDev, globalPrefix, requireCycleIgnorePatterns}),
28+
sandbox,
29+
);
2630
expect(sandbox.process.env.NODE_ENV).toEqual(mode);
2731
expect(sandbox.__DEV__).toEqual(isDev);
2832
});
@@ -31,17 +35,51 @@ const vm = require('vm');
3135
const sandbox: $FlowFixMe = {};
3236
vm.createContext(sandbox);
3337
vm.runInContext(
34-
getPreludeCode({isDev, globalPrefix: '__metro'}),
38+
getPreludeCode({
39+
isDev,
40+
globalPrefix: '__customPrefix',
41+
requireCycleIgnorePatterns,
42+
}),
43+
sandbox,
44+
);
45+
expect(sandbox.__METRO_GLOBAL_PREFIX__).toBe('__customPrefix');
46+
});
47+
48+
it('sets up `${globalPrefix}__requireCycleIgnorePatterns` in development', () => {
49+
const sandbox: $FlowFixMe = {};
50+
vm.createContext(sandbox);
51+
vm.runInContext(
52+
getPreludeCode({
53+
isDev,
54+
globalPrefix,
55+
requireCycleIgnorePatterns: [
56+
/blah/,
57+
/(^|\/|\\)node_modules($|\/|\\)/,
58+
],
59+
}),
3560
sandbox,
3661
);
37-
expect(sandbox.__METRO_GLOBAL_PREFIX__).toBe('__metro');
62+
63+
if (isDev) {
64+
expect(sandbox[`${globalPrefix}__requireCycleIgnorePatterns`]).toEqual([
65+
/blah/,
66+
/(^|\/|\\)node_modules($|\/|\\)/,
67+
]);
68+
} else {
69+
expect(
70+
sandbox[`${globalPrefix}__requireCycleIgnorePatterns`],
71+
).not.toBeDefined();
72+
}
3873
});
3974

4075
it('does not override an existing `process.env`', () => {
4176
const nextTick = () => {};
4277
const sandbox: $FlowFixMe = {process: {nextTick, env: {FOOBAR: 123}}};
4378
vm.createContext(sandbox);
44-
vm.runInContext(getPreludeCode({isDev, globalPrefix}), sandbox);
79+
vm.runInContext(
80+
getPreludeCode({isDev, globalPrefix, requireCycleIgnorePatterns}),
81+
sandbox,
82+
);
4583
expect(sandbox.process.env.NODE_ENV).toEqual(mode);
4684
expect(sandbox.process.env.FOOBAR).toEqual(123);
4785
expect(sandbox.process.nextTick).toEqual(nextTick);
@@ -53,7 +91,12 @@ const vm = require('vm');
5391
const BAR = 2;
5492
vm.createContext(sandbox);
5593
vm.runInContext(
56-
getPreludeCode({isDev, globalPrefix, extraVars: {FOO, BAR}}),
94+
getPreludeCode({
95+
isDev,
96+
globalPrefix,
97+
requireCycleIgnorePatterns,
98+
extraVars: {FOO, BAR},
99+
}),
57100
sandbox,
58101
);
59102
expect(sandbox.FOO).toBe(FOO);
@@ -64,7 +107,12 @@ const vm = require('vm');
64107
const sandbox: $FlowFixMe = {};
65108
vm.createContext(sandbox);
66109
vm.runInContext(
67-
getPreludeCode({isDev, globalPrefix, extraVars: {__DEV__: 123}}),
110+
getPreludeCode({
111+
isDev,
112+
globalPrefix,
113+
requireCycleIgnorePatterns,
114+
extraVars: {__DEV__: 123},
115+
}),
68116
sandbox,
69117
);
70118
expect(sandbox.__DEV__).toBe(isDev);

packages/metro/src/lib/getPreludeCode.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,33 @@ function getPreludeCode({
1414
extraVars,
1515
isDev,
1616
globalPrefix,
17+
requireCycleIgnorePatterns,
1718
}: {
1819
+extraVars?: {[string]: mixed, ...},
1920
+isDev: boolean,
2021
+globalPrefix: string,
22+
+requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
2123
}): string {
2224
const vars = [
25+
// Ensure these variable names match the ones referenced in metro-runtime
26+
// require.js
2327
'__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()',
2428
`__DEV__=${String(isDev)}`,
2529
...formatExtraVars(extraVars),
2630
'process=this.process||{}',
2731
`__METRO_GLOBAL_PREFIX__='${globalPrefix}'`,
2832
];
33+
34+
if (isDev) {
35+
// Ensure these variable names match the ones referenced in metro-runtime
36+
// require.js
37+
vars.push(
38+
`${globalPrefix}__requireCycleIgnorePatterns=[${requireCycleIgnorePatterns
39+
.map(regex => regex.toString())
40+
.join(',')}]`,
41+
);
42+
}
43+
2944
return `var ${vars.join(',')};${processEnv(
3045
isDev ? 'development' : 'production',
3146
)}`;

packages/metro/src/lib/getPrependedScripts.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ async function getPrependedScripts(
7171
_getPrelude({
7272
dev: options.dev,
7373
globalPrefix: config.transformer.globalPrefix,
74+
requireCycleIgnorePatterns: config.resolver.requireCycleIgnorePatterns,
7475
}),
7576
...dependencies.values(),
7677
];
@@ -79,12 +80,18 @@ async function getPrependedScripts(
7980
function _getPrelude({
8081
dev,
8182
globalPrefix,
83+
requireCycleIgnorePatterns,
8284
}: {
8385
dev: boolean,
8486
globalPrefix: string,
87+
requireCycleIgnorePatterns: $ReadOnlyArray<RegExp>,
8588
...
8689
}): Module<> {
87-
const code = getPreludeCode({isDev: dev, globalPrefix});
90+
const code = getPreludeCode({
91+
isDev: dev,
92+
globalPrefix,
93+
requireCycleIgnorePatterns,
94+
});
8895
const name = '__prelude__';
8996

9097
return {

0 commit comments

Comments
 (0)