diff --git a/packages/metro-transform-plugins/src/__tests__/__snapshots__/inline-requires-plugin-test.js.snap b/packages/metro-transform-plugins/src/__tests__/__snapshots__/inline-requires-plugin-test.js.snap index a1742ded0d..95cfb5aaf2 100644 --- a/packages/metro-transform-plugins/src/__tests__/__snapshots__/inline-requires-plugin-test.js.snap +++ b/packages/metro-transform-plugins/src/__tests__/__snapshots__/inline-requires-plugin-test.js.snap @@ -7,6 +7,16 @@ var foo = require(\\"foo\\"); ↓ ↓ ↓ ↓ ↓ ↓ +" +`; + +exports[`memoizeCalls=false: inline-requires deletes unused destructured requires: deletes unused destructured requires 1`] = ` +" +const {a} = require(\\"foo\\"); + + ↓ ↓ ↓ ↓ ↓ ↓ + + " `; @@ -91,6 +101,25 @@ function test() { " `; +exports[`memoizeCalls=false: inline-requires ignores destructured properties that are re-assigned: ignores destructured properties that are re-assigned 1`] = ` +" +var X = require(\\"X\\"); +const {a} = require(\\"X\\"); +X.a = function() { + a(); +}; + + ↓ ↓ ↓ ↓ ↓ ↓ + +const { + a +} = require(\\"X\\"); +require(\\"X\\").a = function () { + a(); +}; +" +`; + exports[`memoizeCalls=false: inline-requires ignores require properties (as identifiers) that are re-assigned: ignores require properties (as identifiers) that are re-assigned 1`] = ` " var X = require(\\"X\\"); @@ -137,6 +166,46 @@ require(\\"foo\\").method(); " `; +exports[`memoizeCalls=false: inline-requires inlines destructured requires referenced across scopes: inlines destructured requires referenced across scopes 1`] = ` +" +const {a, b} = require(\\"foo\\"); +function f() { + return a() + b(); +} +function g() { + if (b()) { + a(); + } +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +function f() { + return require(\\"foo\\").a() + require(\\"foo\\").b(); +} +function g() { + if (require(\\"foo\\").b()) { + require(\\"foo\\").a(); + } +} +" +`; + +exports[`memoizeCalls=false: inline-requires inlines destructured requires: inlines destructured requires 1`] = ` +" +const {a, b: c, \\"d-e\\": de} = require(\\"foo\\"); +a(); +c.x(); +de(); + + ↓ ↓ ↓ ↓ ↓ ↓ + +require(\\"foo\\").a(); +require(\\"foo\\").b.x(); +require(\\"foo\\")[\\"d-e\\"](); +" +`; + exports[`memoizeCalls=false: inline-requires inlines functions provided via \`inlineableCalls\`: inlines functions provided via \`inlineableCalls\` 1`] = ` " const inlinedCustom = customStuff(\\"foo\\"); @@ -244,6 +313,16 @@ var foo = require(\\"foo\\"); ↓ ↓ ↓ ↓ ↓ ↓ +" +`; + +exports[`memoizeCalls=true: inline-requires deletes unused destructured requires: deletes unused destructured requires 1`] = ` +" +const {a} = require(\\"foo\\"); + + ↓ ↓ ↓ ↓ ↓ ↓ + + " `; @@ -331,6 +410,26 @@ function test() { " `; +exports[`memoizeCalls=true: inline-requires ignores destructured properties that are re-assigned: ignores destructured properties that are re-assigned 1`] = ` +" +var X = require(\\"X\\"); +const {a} = require(\\"X\\"); +X.a = function() { + a(); +}; + + ↓ ↓ ↓ ↓ ↓ ↓ + +var X; +const { + a +} = require(\\"X\\"); +(X || (X = require(\\"X\\"))).a = function () { + a(); +}; +" +`; + exports[`memoizeCalls=true: inline-requires ignores require properties (as identifiers) that are re-assigned: ignores require properties (as identifiers) that are re-assigned 1`] = ` " var X = require(\\"X\\"); @@ -380,6 +479,48 @@ var baz = 4; " `; +exports[`memoizeCalls=true: inline-requires inlines destructured requires referenced across scopes: inlines destructured requires referenced across scopes 1`] = ` +" +const {a, b} = require(\\"foo\\"); +function f() { + return a() + b(); +} +function g() { + if (b()) { + a(); + } +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +var _foo; +function f() { + return (_foo || (_foo = require(\\"foo\\"))).a() + _foo.b(); +} +function g() { + if ((_foo || (_foo = require(\\"foo\\"))).b()) { + (_foo || (_foo = require(\\"foo\\"))).a(); + } +} +" +`; + +exports[`memoizeCalls=true: inline-requires inlines destructured requires: inlines destructured requires 1`] = ` +" +const {a, b: c, \\"d-e\\": de} = require(\\"foo\\"); +a(); +c.x(); +de(); + + ↓ ↓ ↓ ↓ ↓ ↓ + +var _foo; +(_foo || (_foo = require(\\"foo\\"))).a(); +_foo.b.x(); +_foo[\\"d-e\\"](); +" +`; + exports[`memoizeCalls=true: inline-requires inlines functions provided via \`inlineableCalls\`: inlines functions provided via \`inlineableCalls\` 1`] = ` " const inlinedCustom = customStuff(\\"foo\\"); diff --git a/packages/metro-transform-plugins/src/__tests__/inline-requires-plugin-test.js b/packages/metro-transform-plugins/src/__tests__/inline-requires-plugin-test.js index 45d4f3b9fa..3b1c13a26f 100644 --- a/packages/metro-transform-plugins/src/__tests__/inline-requires-plugin-test.js +++ b/packages/metro-transform-plugins/src/__tests__/inline-requires-plugin-test.js @@ -238,6 +238,97 @@ const TEST_CASES: TestCases = { code: ['const a = require("Foo", "Bar", 47);', '', 'a();'].join('\n'), snapshot: true, }, + + 'inlines destructured requires': { + code: [ + 'const {a, b: c, "d-e": de} = require("foo");', + 'a();', + 'c.x();', + 'de();', + ].join('\n'), + snapshot: true, + }, + + 'inlines destructured requires referenced across scopes': { + code: [ + 'const {a, b} = require("foo");', + 'function f() {', + ' return a() + b();', + '}', + 'function g() {', + ' if (b()) {', + ' a();', + ' }', + '}', + ].join('\n'), + snapshot: true, + }, + + 'deletes unused destructured requires': { + code: ['const {a} = require("foo");'].join('\n'), + snapshot: true, + }, + + 'ignores destructured requires with defaults': { + code: ['const {', ' a = 1', '} = require("foo");', 'a();'].join('\n'), + snapshot: false, + }, + + 'ignores destructured requires with rest elements': { + code: [ + 'const {', + ' a,', + ' ...rest', + '} = require("foo");', + 'a(rest);', + ].join('\n'), + snapshot: false, + }, + + 'ignores nested destructured requires': { + code: [ + 'const {', + ' a: {', + ' b', + ' }', + '} = require("foo");', + 'b();', + ].join('\n'), + snapshot: false, + }, + + 'ignores destructured requires with computed keys': { + code: ['const {', ' [key]: a', '} = require("foo");', 'a();'].join('\n'), + snapshot: false, + }, + + 'ignores destructured requires that are re-assigned': { + code: [ + 'let {', + ' a,', + ' b', + '} = require("foo");', + 'b = 1;', + 'a(b);', + ].join('\n'), + snapshot: false, + }, + + 'ignores destructured properties that are re-assigned': { + code: [ + 'var X = require("X");', + 'const {a} = require("X");', + 'X.a = function() {', + ' a();', + '};', + ].join('\n'), + snapshot: true, + }, + + 'ignores destructured requires in `ignoredRequires`': { + code: ['const {', ' a', '} = require("CommonFoo");', 'a();'].join('\n'), + snapshot: false, + }, }; describe.each([true, false])('memoizeCalls=%s:', memoizeCalls => { diff --git a/packages/metro-transform-plugins/src/inline-requires-plugin.js b/packages/metro-transform-plugins/src/inline-requires-plugin.js index 04d2cee4d9..d276e9199c 100644 --- a/packages/metro-transform-plugins/src/inline-requires-plugin.js +++ b/packages/metro-transform-plugins/src/inline-requires-plugin.js @@ -58,11 +58,15 @@ export type State = { * g(require('foo').bar); * * Destructuring also works the same way. For instance: - * const {Baz} = require('foo'); - * h(Baz); + * const {Baz, qux: Qux} = require('foo'); + * h(Baz, Qux); * * Is also successfully inlined into: - * g(require('foo').Baz); + * h(require('foo').Baz, require('foo').qux); + * + * With `memoizeCalls`, destructured properties share one memoized module. + * Destructuring with defaults, rest elements, nesting or computed keys is not + * inlined. */ export default ({types: t, traverse}: Babel): PluginObj => ({ name: 'inline-requires', @@ -89,10 +93,146 @@ export default ({types: t, traverse}: Babel): PluginObj => ({ if (programNode.type !== 'Program') { return; } + const program: Program = programNode; + + // Inlines `const {a, b: c} = require('m')` by replacing each reference + // to `a` or `c` with `require('m').a` or `require('m').b`. When calls + // are memoized, all properties share one memoized module, so each + // scope requires `m` at most once. + function inlineDestructuredRequire( + { + declarationPath, + moduleName, + requireFnName, + properties, + }: InlineableDestructuring, + state: State, + ): void { + const references: Array<{key: string, referencePath: NodePath<>}> = + []; + for (const {key, localName} of properties) { + const binding = declarationPath.scope.getBinding(localName); + if ( + binding == null || + binding.constantViolations.length > 0 || + isExcludedMemberAssignment(moduleName, key, state) + ) { + return; + } + for (const referencePath of binding.referencePaths) { + references.push({key, referencePath}); + } + } + // A memoized require is only assigned by the first reference in a + // scope, so references are replaced in source order. + references.sort( + (a, b) => + (a.referencePath.node.start ?? 0) - + (b.referencePath.node.start ?? 0), + ); + + const declarator = declarationPath.node; + const initPath = declarationPath.get('init'); + if ( + declarator.type !== 'VariableDeclarator' || + declarator.init == null || + Array.isArray(initPath) + ) { + return; + } + const init: Expression = declarator.init; + const initLoc = getNearestLocFromPath(initPath); + deleteLocation(init); + traverse(init, { + noScope: true, + enter: path => deleteLocation(path.node), + }); + + const getRequireExpr = () => { + const refExpr = t.cloneDeep(init); + // $FlowFixMe[prop-missing] + refExpr.METRO_INLINE_REQUIRES_INIT_LOC = initLoc; + return refExpr; + }; + const getMember = (object: Expression, key: string) => + t.isValidIdentifier(key) + ? t.memberExpression(object, t.identifier(key)) + : t.memberExpression(object, t.stringLiteral(key), true); + + let memoVarName: ?string = null; + if ( + memoizeCalls && + references.length > 0 && + !nonMemoizedModules.has(moduleName) + ) { + const name = path.scope.generateUid(moduleName); + declarationPath.remove(); + if ( + addStmtToBlock( + program, + t.variableDeclaration('var', [ + t.variableDeclarator(t.identifier(name)), + ]), + 0, + ) + ) { + memoVarName = name; + } + } + + let thrown = false; + const scopesWithInlinedRequire = new Set(); + for (const {key, referencePath} of references) { + try { + referencePath.scope.rename(requireFnName); + if (memoVarName == null) { + referencePath.replaceWith(getMember(getRequireExpr(), key)); + continue; + } + referencePath.scope.rename(memoVarName); + const memoOrCall = t.logicalExpression( + '||', + t.identifier(memoVarName), + t.assignmentExpression( + '=', + t.identifier(memoVarName), + getRequireExpr(), + ), + ); + // Reuse the memoized module directly once this scope has + // assigned it, as for identifier aliases above. + if (!isDirectlyEnclosedByBlock(t, referencePath)) { + referencePath.replaceWith(getMember(memoOrCall, key)); + } else if (scopesWithInlinedRequire.has(referencePath.scope)) { + referencePath.replaceWith( + getMember(t.identifier(memoVarName), key), + ); + } else { + referencePath.replaceWith(getMember(memoOrCall, key)); + scopesWithInlinedRequire.add(referencePath.scope); + } + } catch (error) { + thrown = true; + } + } + + // If a replacement failed (e.g. replacing a type annotation), + // avoid removing the initial require just to be safe. + if (!thrown && declarationPath.node != null) { + declarationPath.remove(); + } + } + path.scope.crawl(); path.traverse( { CallExpression(path, state) { + const destructuring = parseInlineableDestructuring(path, state); + if (destructuring != null) { + inlineDestructuredRequire(destructuring, state); + return; + } + const parseResult = parseInlineableAlias(path, state) || parseInlineableMemberAlias(path, state); @@ -389,6 +529,62 @@ function parseInlineableMemberAlias( }; } +type InlineableDestructuring = { + declarationPath: NodePath, + moduleName: string, + requireFnName: string, + properties: ReadonlyArray<{key: string, localName: string}>, +}; + +// Matches a top-level `const {a, 'b': c} = require('m')`. Patterns with +// defaults, rest elements, nesting or computed keys are left alone. +function parseInlineableDestructuring( + path: NodePath, + state: State, +): ?InlineableDestructuring { + const module = getInlineableModule(path, state); + if (module == null) { + return null; + } + const declarationPath = path.parentPath; + if ( + declarationPath == null || + declarationPath.node.type !== 'VariableDeclarator' || + declarationPath.node.id.type !== 'ObjectPattern' || + declarationPath.parent.type !== 'VariableDeclaration' || + declarationPath.parentPath?.parent.type !== 'Program' + ) { + return null; + } + + const properties: Array<{key: string, localName: string}> = []; + for (const property of declarationPath.node.id.properties) { + if ( + property.type !== 'ObjectProperty' || + property.computed === true || + property.value.type !== 'Identifier' + ) { + return null; + } + let key; + if (property.key.type === 'Identifier') { + key = property.key.name; + } else if (property.key.type === 'StringLiteral') { + key = property.key.value; + } else { + return null; + } + properties.push({key, localName: property.value.name}); + } + + return { + declarationPath, + moduleName: module.moduleName, + requireFnName: module.requireFnName, + properties, + }; +} + function getInlineableModule( path: NodePath, state: State,