diff --git a/docs/Configuration.md b/docs/Configuration.md index 18fdea39c8..887db1ce6d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -217,6 +217,16 @@ Type: `Array` 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` + +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. + +Defaults to `[/(^|\/|\\)node_modules($|\/|\\)/]`. + --- ### Transformer Options diff --git a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap index 3f3f0ce653..ac7c971d42 100644 --- a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap +++ b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap @@ -58,6 +58,9 @@ Object { "windows", "web", ], + "requireCycleIgnorePatterns": Array [ + /\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/, + ], "resolveRequest": null, "resolverMainFields": Array [ "browser", @@ -211,6 +214,9 @@ Object { "windows", "web", ], + "requireCycleIgnorePatterns": Array [ + /\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/, + ], "resolveRequest": null, "resolverMainFields": Array [ "browser", @@ -364,6 +370,9 @@ Object { "windows", "web", ], + "requireCycleIgnorePatterns": Array [ + /\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/, + ], "resolveRequest": null, "resolverMainFields": Array [ "browser", @@ -517,6 +526,9 @@ Object { "windows", "web", ], + "requireCycleIgnorePatterns": Array [ + /\\(\\^\\|\\\\/\\|\\\\\\\\\\)node_modules\\(\\$\\|\\\\/\\|\\\\\\\\\\)/, + ], "resolveRequest": null, "resolverMainFields": Array [ "browser", diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index d537f2a377..e0bf41152b 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -102,6 +102,7 @@ type ResolverConfigT = { resolverMainFields: $ReadOnlyArray, sourceExts: $ReadOnlyArray, useWatchman: boolean, + requireCycleIgnorePatterns: $ReadOnlyArray, }; type SerializerConfigT = { diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index a631309e8b..3fe095f229 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -46,6 +46,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({ resolveRequest: null, resolverMainFields: ['browser', 'main'], useWatchman: true, + requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/], }, serializer: { diff --git a/packages/metro-runtime/src/polyfills/__tests__/require-test.js b/packages/metro-runtime/src/polyfills/__tests__/require-test.js index 46308eb03a..2d77907064 100644 --- a/packages/metro-runtime/src/polyfills/__tests__/require-test.js +++ b/packages/metro-runtime/src/polyfills/__tests__/require-test.js @@ -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', () => { @@ -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, ''); diff --git a/packages/metro-runtime/src/polyfills/require.js b/packages/metro-runtime/src/polyfills/require.js index 19fcc0d9e2..aea905d05b 100644 --- a/packages/metro-runtime/src/polyfills/require.js +++ b/packages/metro-runtime/src/polyfills/require.js @@ -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.', + ); + } } } @@ -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): boolean { + const regExps = + global[__METRO_GLOBAL_PREFIX__ + '__requireCycleIgnorePatterns']; + if (!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; diff --git a/packages/metro/src/lib/__tests__/getPreludeCode-test.js b/packages/metro/src/lib/__tests__/getPreludeCode-test.js index 117bde6c52..096a369e6f 100644 --- a/packages/metro/src/lib/__tests__/getPreludeCode-test.js +++ b/packages/metro/src/lib/__tests__/getPreludeCode-test.js @@ -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); }); @@ -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); @@ -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); @@ -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); diff --git a/packages/metro/src/lib/getPreludeCode.js b/packages/metro/src/lib/getPreludeCode.js index c76f10a331..b077a7a56e 100644 --- a/packages/metro/src/lib/getPreludeCode.js +++ b/packages/metro/src/lib/getPreludeCode.js @@ -14,18 +14,33 @@ function getPreludeCode({ extraVars, isDev, globalPrefix, + requireCycleIgnorePatterns, }: { +extraVars?: {[string]: mixed, ...}, +isDev: boolean, +globalPrefix: string, + +requireCycleIgnorePatterns: $ReadOnlyArray, }): 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', )}`; diff --git a/packages/metro/src/lib/getPrependedScripts.js b/packages/metro/src/lib/getPrependedScripts.js index 91d58deb66..fbe40a8660 100644 --- a/packages/metro/src/lib/getPrependedScripts.js +++ b/packages/metro/src/lib/getPrependedScripts.js @@ -71,6 +71,7 @@ async function getPrependedScripts( _getPrelude({ dev: options.dev, globalPrefix: config.transformer.globalPrefix, + requireCycleIgnorePatterns: config.resolver.requireCycleIgnorePatterns, }), ...dependencies.values(), ]; @@ -79,12 +80,18 @@ async function getPrependedScripts( function _getPrelude({ dev, globalPrefix, + requireCycleIgnorePatterns, }: { dev: boolean, globalPrefix: string, + requireCycleIgnorePatterns: $ReadOnlyArray, ... }): Module<> { - const code = getPreludeCode({isDev: dev, globalPrefix}); + const code = getPreludeCode({ + isDev: dev, + globalPrefix, + requireCycleIgnorePatterns, + }); const name = '__prelude__'; return {