diff --git a/packages/metro-transform-plugins/src/__mocks__/test-helpers.js b/packages/metro-transform-plugins/src/__mocks__/test-helpers.js index 785dbdf50d..05e75ab994 100644 --- a/packages/metro-transform-plugins/src/__mocks__/test-helpers.js +++ b/packages/metro-transform-plugins/src/__mocks__/test-helpers.js @@ -20,7 +20,8 @@ const nullthrows = require('nullthrows'); function makeTransformOptions( plugins: ReadonlyArray, - options: OptionsT, + pluginOptions: OptionsT, + babelOptions?: BabelCoreOptions, ): BabelCoreOptions { return { ast: true, @@ -30,9 +31,12 @@ function makeTransformOptions( compact: true, configFile: false, plugins: plugins.length - ? plugins.map(plugin => [plugin, options]) + ? plugins.map(plugin => [plugin, pluginOptions]) : [() => ({visitor: {}})], sourceType: 'module', + filename: + '/Users/test/app/node_modules/react-native/Libraries/Components/Pressable/useAndroidRippleForView.js', + ...babelOptions, }; } @@ -55,10 +59,11 @@ function transformToAst( plugins: ReadonlyArray, code: string, options: T, + babelOptions?: BabelCoreOptions, ): BabelNodeFile { const transformResult = transformSync( code, - makeTransformOptions(plugins, options), + makeTransformOptions(plugins, options, babelOptions), ); const ast = nullthrows(transformResult.ast); validateOutputAst(ast); @@ -69,8 +74,9 @@ function transform( code: string, plugins: ReadonlyArray, options: ?EntryOptions, + babelOptions?: BabelCoreOptions, ) { - return generate(transformToAst(plugins, code, options)).code; + return generate(transformToAst(plugins, code, options, babelOptions)).code; } exports.compare = function ( @@ -78,8 +84,11 @@ exports.compare = function ( code: string, expected: string, options: ?EntryOptions = {}, + babelOptions?: BabelCoreOptions, ) { - expect(transform(code, plugins, options)).toBe(transform(expected, [], {})); + expect(transform(code, plugins, options, babelOptions)).toBe( + transform(expected, [], {}), + ); }; exports.transformToAst = transformToAst; diff --git a/packages/metro-transform-plugins/src/__tests__/inline-plugin-test.js b/packages/metro-transform-plugins/src/__tests__/inline-plugin-test.js index a1b1010354..7c9d77a685 100644 --- a/packages/metro-transform-plugins/src/__tests__/inline-plugin-test.js +++ b/packages/metro-transform-plugins/src/__tests__/inline-plugin-test.js @@ -934,4 +934,105 @@ describe('inline constants', () => { compare([stripFlow, inlinePlugin], code, expected, {dev: false}); }); + + test('replaces Platform.OS in the code if Platform is a top level relative Node.js require()', () => { + // Source code before `@react-native/babel-preset`: + // const Platform = require('../../Utilities/Platform').default; + const code = ` + var Platform = require('../../Utilities/Platform').default; + var test = Platform.OS === 'ios' ? 'ios' : 'not-ios'; + `; + + compare([inlinePlugin], code, code.replace('Platform.OS', '"android"'), { + inlinePlatform: true, + platform: 'android', + }); + }); + + test('replaces Platform.OS in the code if Platform is a top level relative ES import', () => { + // Source code before `@react-native/babel-preset`: + // import Platform from '../../Utilities/Platform'; + const code = ` + var _Platform = _interopRequireDefault(require("../../Utilities/Platform")); + var test = _Platform.default.OS === 'ios' ? 'ios' : 'not-ios'; + `; + + compare( + [inlinePlugin], + code, + code.replace('_Platform.default.OS', '"android"'), + { + inlinePlatform: true, + platform: 'android', + }, + ); + }); + + test('replaces Platform.select in the code if Platform is a top level relative Node.js require()', () => { + // Source code before `@react-native/babel-preset`: + // const Platform = require('../../Utilities/Platform').default; + const code = ` + var Platform = require('../../Utilities/Platform').default; + + function a() { + Platform.select({ios: 1, android: 2}); + var b = a.Platform.select({}); + } + `; + + compare([inlinePlugin], code, code.replace(/Platform\.select[^;]+/, '2'), { + inlinePlatform: 'true', + platform: 'android', + }); + }); + + test('replaces Platform.select in the code if Platform is a top level relative ES import', () => { + // Source code before `@react-native/babel-preset`: + // import Platform from '../../Utilities/Platform'; + const code = ` + var _Platform = _interopRequireDefault(require("../../Utilities/Platform")); + + function a() { + _Platform.default.select({ios: 1, android: 2}); + var b = a.Platform.select({}); + } + `; + + compare( + [inlinePlugin], + code, + code.replace(/_Platform\.default\.select[^;]+/, '2'), + { + inlinePlatform: 'true', + platform: 'android', + }, + ); + }); + + test('replaces Platform.select in the code if Platform is a top level relative ES import on Window', () => { + // Source code before `@react-native/babel-preset`: + // import Platform from '../../Utilities/Platform'; + const code = ` + var _Platform = _interopRequireDefault(require("../../Utilities/Platform")); + + function a() { + _Platform.default.select({ios: 1, android: 2}); + var b = a.Platform.select({}); + } + `; + + compare( + [inlinePlugin], + code, + code.replace(/_Platform\.default\.select[^;]+/, '2'), + { + inlinePlatform: 'true', + platform: 'android', + }, + { + filename: + 'C:\\Users\\test\\app\\node_modules\\react-native\\Libraries\\Components\\Pressable\\useAndroidRippleForView.js', + }, + ); + }); }); diff --git a/packages/metro-transform-plugins/src/inline-plugin.js b/packages/metro-transform-plugins/src/inline-plugin.js index 2cc055d260..a25c3316b2 100644 --- a/packages/metro-transform-plugins/src/inline-plugin.js +++ b/packages/metro-transform-plugins/src/inline-plugin.js @@ -31,7 +31,7 @@ export type Options = Readonly<{ platform: string, }>; -type State = {opts: Options}; +type State = {opts: Options, filename?: string}; const env = {name: 'env'}; const nodeEnv = {name: 'NODE_ENV'}; @@ -137,11 +137,12 @@ export default function inlinePlugin( const node = path.node; const scope = path.scope; const opts = state.opts; + const filename = state.filename; if (!isLeftHandSideOfAssignmentExpression(node, path.parent)) { if ( opts.inlinePlatform && - isPlatformNode(node, scope, !!opts.isWrapped) + isPlatformNode(node, scope, !!opts.isWrapped, filename) ) { path.replaceWith(t.stringLiteral(opts.platform)); } else if (!opts.dev && isProcessEnvNodeEnv(node, scope)) { @@ -156,10 +157,11 @@ export default function inlinePlugin( const scope = path.scope; const arg = node.arguments[0]; const opts = state.opts; + const filename = state.filename; if ( opts.inlinePlatform && - isPlatformSelectNode(node, scope, !!opts.isWrapped) && + isPlatformSelectNode(node, scope, !!opts.isWrapped, filename) && isObjectExpression(arg) ) { if (hasStaticProperties(arg)) { diff --git a/packages/metro-transform-plugins/src/utils/createInlinePlatformChecks.js b/packages/metro-transform-plugins/src/utils/createInlinePlatformChecks.js index 86121f16fa..a3da311be6 100644 --- a/packages/metro-transform-plugins/src/utils/createInlinePlatformChecks.js +++ b/packages/metro-transform-plugins/src/utils/createInlinePlatformChecks.js @@ -22,14 +22,21 @@ type PlatformChecks = { node: MemberExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ) => boolean, isPlatformSelectNode: ( node: CallExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ) => boolean, }; +const REACT_NATIVE_MODULES_REGEX = /[\/\\]node_modules[\/\\]react-native[\/\\]/; + +const isReactNativeFile = (filename?: string): boolean => + filename != null && REACT_NATIVE_MODULES_REGEX.test(filename); + export default function createInlinePlatformChecks( t: Types, requireName: string = 'require', @@ -45,30 +52,40 @@ export default function createInlinePlatformChecks( node: MemberExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => - isPlatformOS(node, scope, isWrappedModule) || - isReactPlatformOS(node, scope, isWrappedModule); + isPlatformOS(node, scope, isWrappedModule, filename) || + isReactPlatformOS(node, scope, isWrappedModule, filename); const isPlatformSelectNode = ( node: CallExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => - isPlatformSelect(node, scope, isWrappedModule) || - isReactPlatformSelect(node, scope, isWrappedModule); + isPlatformSelect(node, scope, isWrappedModule, filename) || + isReactPlatformSelect(node, scope, isWrappedModule, filename); const isPlatformOS = ( node: MemberExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => isIdentifier(node.property, {name: 'OS'}) && - isImportOrGlobal(node.object, scope, [{name: 'Platform'}], isWrappedModule); + isImportOrGlobal( + node.object, + scope, + [{name: 'Platform'}], + isWrappedModule, + filename, + ); const isReactPlatformOS = ( node: MemberExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => isIdentifier(node.property, {name: 'OS'}) && isMemberExpression(node.object) && @@ -79,12 +96,14 @@ export default function createInlinePlatformChecks( scope, [{name: 'React'}, {name: 'ReactNative'}], isWrappedModule, + filename, ); const isPlatformSelect = ( node: CallExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => isMemberExpression(node.callee) && isIdentifier(node.callee.property, {name: 'select'}) && @@ -94,12 +113,14 @@ export default function createInlinePlatformChecks( scope, [{name: 'Platform'}], isWrappedModule, + filename, ); const isReactPlatformSelect = ( node: CallExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ): boolean => isMemberExpression(node.callee) && isIdentifier(node.callee.property, {name: 'select'}) && @@ -112,6 +133,7 @@ export default function createInlinePlatformChecks( scope, [{name: 'React'}, {name: 'ReactNative'}], isWrappedModule, + filename, ); const isRequireCall = ( @@ -138,6 +160,7 @@ export default function createInlinePlatformChecks( scope: Scope, patterns: Array<{name: string}>, isWrappedModule: boolean, + filename?: string, ): boolean => { const identifier = patterns.find((pattern: {name: string}) => isIdentifier(node, pattern), @@ -148,6 +171,38 @@ export default function createInlinePlatformChecks( ) { return true; } + // Special case for handling transformed relative ES imports: + // Works only for RN files: `*/node_modules/react-native/**/*` + // + // ```tsx + // 1. Source code + // import Platform from '../../Utilities/Platform'; + // const test = Platform.OS === 'ios' ? 1 : 2; + // + // 2. After `@react-native/babel-preset` + // var _Platform = _interopRequireDefault(require("../../Utilities/Platform")); + // var test = _Platform.default.OS === 'ios' ? 1 : 2; + // ``` + if ( + isReactNativeFile(filename) && + !identifier && + isMemberExpression(node) + ) { + const objIdentifier = patterns.find((pattern: {name: string}) => + isIdentifier(node.object, {name: `_${pattern.name}`}), + ); + + if ( + objIdentifier && + isToplevelBinding( + scope.getBinding(`_${objIdentifier.name}`), + isWrappedModule, + ) && + isIdentifier(node.property, {name: 'default'}) + ) { + return true; + } + } if (isImport(node, scope, patterns)) { return true; } diff --git a/packages/metro-transform-plugins/types/inline-plugin.d.ts b/packages/metro-transform-plugins/types/inline-plugin.d.ts index 30caa0ae52..87246c97d2 100644 --- a/packages/metro-transform-plugins/types/inline-plugin.d.ts +++ b/packages/metro-transform-plugins/types/inline-plugin.d.ts @@ -6,7 +6,7 @@ * * @noformat * @oncall react_native - * @generated SignedSource<<0a0f52c4e23d8cd25d04b2d46a09e480>> + * @generated SignedSource<> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-transform-plugins/src/inline-plugin.js @@ -26,7 +26,7 @@ export type Options = Readonly<{ requireName?: string; platform: string; }>; -type State = {opts: Options}; +type State = {opts: Options; filename?: string}; declare function inlinePlugin( $$PARAM_0$$: {types: Types}, options: Options, diff --git a/packages/metro-transform-plugins/types/utils/createInlinePlatformChecks.d.ts b/packages/metro-transform-plugins/types/utils/createInlinePlatformChecks.d.ts index 819cc274c5..33ac44b64a 100644 --- a/packages/metro-transform-plugins/types/utils/createInlinePlatformChecks.d.ts +++ b/packages/metro-transform-plugins/types/utils/createInlinePlatformChecks.d.ts @@ -6,7 +6,7 @@ * * @noformat * @oncall react_native - * @generated SignedSource<<13269e5dcf93e0b31428517812e3bb88>> + * @generated SignedSource<> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-transform-plugins/src/utils/createInlinePlatformChecks.js @@ -25,11 +25,13 @@ type PlatformChecks = { node: MemberExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ) => boolean; isPlatformSelectNode: ( node: CallExpression, scope: Scope, isWrappedModule: boolean, + filename?: string, ) => boolean; }; declare function createInlinePlatformChecks(