From b92fbc6b9b30f08486b75a9f40d78d364249ccb8 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Fri, 8 May 2020 00:47:20 -0700 Subject: [PATCH] Allow out of tree platforms to work without custom metro configs (#1115) * Allow out of tree platforms to work without custom metro configs * Need to add the platofrm specifc InitializeCore to getModulesRunBeforeMainModule * lock * Add npmPackageName to schema * Some documentation * Use finally to restore resolveRequest --- docs/platforms.md | 8 +++ packages/cli-types/src/index.ts | 1 + packages/cli/package.json | 1 + packages/cli/src/tools/config/schema.ts | 2 + packages/cli/src/tools/loadMetroConfig.ts | 36 ++++++++++++-- .../cli/src/tools/metroPlatformResolver.ts | 49 +++++++++++++++++++ yarn.lock | 2 +- 7 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 packages/cli/src/tools/metroPlatformResolver.ts diff --git a/docs/platforms.md b/docs/platforms.md index 8156e3680..58b54f2d2 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -40,6 +40,7 @@ At the end, a map of available platforms is passed to the bundler (Metro) to mak ```ts type PlatformConfig = { + npmPackageName?: string; projectConfig: (string, ProjectParams) => ?ProjectConfig, dependencyConfig: (string, ProjectParams) => ?DependencyConfig, linkConfig: () => { @@ -57,6 +58,13 @@ type PlatformConfig = { }; ``` +### npmPackageName + +Returns the name of the npm package that should be used as the source for react-native JS code for platforms that provide platform specific overrides to core JS files. This causes the default metro config to redirect imports of react-native to another package based when bundling for that platform. The package specified should provide a complete react-native implementation for that platform. + +If this property is not specified, it is assumed that the code in core `react-native` works for the platform. + + ### projectConfig Returns a project configuration for a given platform or `null`, when no project found. This is later used inside `linkConfig` to perform linking and unlinking. diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index 0d8ea11a9..5acbad060 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -63,6 +63,7 @@ interface PlatformConfig< DependencyConfig, DependencyParams > { + npmPackageName?: string; projectConfig: ( projectRoot: string, projectParams: ProjectParams | void, diff --git a/packages/cli/package.json b/packages/cli/package.json index 9aef3b5d9..31d29623c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -53,6 +53,7 @@ "metro-config": "^0.58.0", "metro-core": "^0.58.0", "metro-react-native-babel-transformer": "^0.58.0", + "metro-resolver": "^0.58.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "node-stream-zip": "^1.9.1", diff --git a/packages/cli/src/tools/config/schema.ts b/packages/cli/src/tools/config/schema.ts index fa962c9db..6ce6d3537 100644 --- a/packages/cli/src/tools/config/schema.ts +++ b/packages/cli/src/tools/config/schema.ts @@ -83,6 +83,7 @@ export const dependencyConfig = t platforms: map( t.string(), t.object({ + npmPackageName: t.string().optional(), dependencyConfig: t.func(), projectConfig: t.func(), linkConfig: t.func(), @@ -178,6 +179,7 @@ export const projectConfig = t platforms: map( t.string(), t.object({ + npmPackageName: t.string().optional(), dependencyConfig: t.func(), projectConfig: t.func(), linkConfig: t.func(), diff --git a/packages/cli/src/tools/loadMetroConfig.ts b/packages/cli/src/tools/loadMetroConfig.ts index 0a511d68b..78547dbfe 100644 --- a/packages/cli/src/tools/loadMetroConfig.ts +++ b/packages/cli/src/tools/loadMetroConfig.ts @@ -5,6 +5,7 @@ import path from 'path'; // @ts-ignore - no typed definition for the package import {loadConfig} from 'metro-config'; import {Config} from '@react-native-community/cli-types'; +import {reactNativePlatformResolver} from './metroPlatformResolver'; const INTERNAL_CALLSITES_REGEX = new RegExp( [ @@ -21,6 +22,12 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( export interface MetroConfig { resolver: { + resolveRequest?: ( + context: any, + realModuleName: string, + platform: string, + moduleName: string, + ) => any; resolverMainFields: string[]; platforms: string[]; }; @@ -48,16 +55,40 @@ export interface MetroConfig { * Default configuration */ export const getDefaultConfig = (ctx: Config): MetroConfig => { + const outOfTreePlatforms = Object.keys(ctx.platforms).filter( + platform => ctx.platforms[platform].npmPackageName, + ); + return { resolver: { + resolveRequest: + outOfTreePlatforms.length === 0 + ? undefined + : reactNativePlatformResolver( + outOfTreePlatforms.reduce<{[platform: string]: string}>( + (result, platform) => { + result[platform] = ctx.platforms[platform].npmPackageName!; + return result; + }, + {}, + ), + ), resolverMainFields: ['react-native', 'browser', 'main'], platforms: [...Object.keys(ctx.platforms), 'native'], }, serializer: { + // We can include multiple copies of InitializeCore here because metro will + // only add ones that are already part of the bundle getModulesRunBeforeMainModule: () => [ require.resolve( path.join(ctx.reactNativePath, 'Libraries/Core/InitializeCore'), ), + ...outOfTreePlatforms.map(platform => + require.resolve( + `${ctx.platforms[platform] + .npmPackageName!}/Libraries/Core/InitializeCore`, + ), + ), ], getPolyfills: () => require(path.join(ctx.reactNativePath, 'rn-get-polyfills'))(), @@ -77,10 +108,7 @@ export const getDefaultConfig = (ctx: Config): MetroConfig => { babelTransformerPath: require.resolve( 'metro-react-native-babel-transformer', ), - assetRegistryPath: path.join( - ctx.reactNativePath, - 'Libraries/Image/AssetRegistry', - ), + assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry', }, watchFolders: [], }; diff --git a/packages/cli/src/tools/metroPlatformResolver.ts b/packages/cli/src/tools/metroPlatformResolver.ts new file mode 100644 index 000000000..fbaa8946a --- /dev/null +++ b/packages/cli/src/tools/metroPlatformResolver.ts @@ -0,0 +1,49 @@ +/** + * This is an implementation of a metro resolveRequest option which will remap react-native imports + * to different npm packages based on the platform requested. This allows a single metro instance/config + * to produce bundles for multiple out of tree platforms at a time. + * + * @param platformImplementations + * A map of platform to npm package that implements that platform + * + * Ex: + * { + * windows: 'react-native-windows' + * macos: 'react-native-macos' + * } + */ +// @ts-ignore - no typed definition for the package +import {resolve} from 'metro-resolver'; + +export function reactNativePlatformResolver(platformImplementations: { + [platform: string]: string; +}) { + return ( + context: any, + _realModuleName: string, + platform: string, + moduleName: string, + ) => { + let backupResolveRequest = context.resolveRequest; + delete context.resolveRequest; + + try { + let modifiedModuleName = moduleName; + if (platformImplementations[platform]) { + if (moduleName === 'react-native') { + modifiedModuleName = platformImplementations[platform]; + } else if (moduleName.startsWith('react-native/')) { + modifiedModuleName = `${ + platformImplementations[platform] + }/${modifiedModuleName.slice('react-native/'.length)}`; + } + } + let result = resolve(context, modifiedModuleName, platform); + return result; + } catch (e) { + throw e; + } finally { + context.resolveRequest = backupResolveRequest; + } + }; +} diff --git a/yarn.lock b/yarn.lock index e62618995..f948ca7f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7750,7 +7750,7 @@ metro-react-native-babel-transformer@^0.58.0: metro-react-native-babel-preset "0.58.0" metro-source-map "0.58.0" -metro-resolver@0.58.0: +metro-resolver@0.58.0, metro-resolver@^0.58.0: version "0.58.0" resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.58.0.tgz#4d03edc52e2e25d45f16688adf3b3f268ea60df9" integrity sha512-XFbAKvCHN2iWqKeiRARzEXn69eTDdJVJC7lu16S4dPQJ+Dy82dZBr5Es12iN+NmbJuFgrAuIHbpWrdnA9tOf6Q==