expo-modules-autolinking
Version:
Scripts that autolink Expo modules.
221 lines (201 loc) • 7.41 kB
text/typescript
import fs from 'fs';
import path from 'path';
import type { SupportedPlatform } from '../types';
import {
findGradleAndManifestAsync,
parsePackageNameAsync,
resolveDependencyConfigImplAndroidAsync,
} from './androidResolver';
import { loadConfigAsync } from './config';
import { resolveDependencyConfigImplIosAsync } from './iosResolver';
import type {
RNConfigDependency,
RNConfigDependencyAndroid,
RNConfigDependencyIos,
RNConfigDependencyWeb,
RNConfigReactNativeAppProjectConfig,
RNConfigReactNativeLibraryConfig,
RNConfigReactNativeProjectConfig,
RNConfigResult,
} from './reactNativeConfig.types';
import { discoverExpoModuleConfigAsync, ExpoModuleConfig } from '../ExpoModuleConfig';
import { AutolinkingOptions } from '../commands/autolinkingOptions';
import {
DependencyResolution,
filterMapResolutionResult,
mergeResolutionResults,
scanDependenciesFromRNProjectConfig,
scanDependenciesInSearchPath,
scanDependenciesRecursively,
} from '../dependencies';
import { checkDependencyWebAsync } from './webResolver';
const isMissingFBReactNativeSpecCodegenOutput = async (reactNativePath: string) => {
const generatedDir = path.resolve(reactNativePath, 'React/FBReactNativeSpec');
try {
const stat = await fs.promises.lstat(generatedDir);
return !stat.isDirectory();
} catch {
return true;
}
};
export async function resolveReactNativeModule(
resolution: DependencyResolution,
projectConfig: RNConfigReactNativeProjectConfig | null,
platform: SupportedPlatform,
excludeNames: Set<string>
): Promise<RNConfigDependency | null> {
if (excludeNames.has(resolution.name)) {
return null;
}
const libraryConfig = await loadConfigAsync<RNConfigReactNativeLibraryConfig>(resolution.path);
const reactNativeConfig = {
...libraryConfig?.dependency,
...projectConfig?.dependencies?.[resolution.name],
};
if (Object.keys(libraryConfig?.platforms ?? {}).length > 0) {
// Package defines platforms would be a platform host package.
// The rnc-cli will skip this package.
return null;
} else if (resolution.name === 'react-native' || resolution.name === 'react-native-macos') {
// Starting from version 0.76, the `react-native` package only defines platforms
// when @react-native-community/cli-platform-android/ios is installed.
// Therefore, we need to manually filter it out.
return null;
}
let maybeExpoModuleConfig: ExpoModuleConfig | null | undefined;
if (!libraryConfig) {
// NOTE(@kitten): If we don't have an explicit react-native.config.{js,ts} file,
// we should pass the Expo Module config (if it exists) to the resolvers below,
// which can then decide if the React Native inferred config and Expo Module
// configs conflict
try {
maybeExpoModuleConfig = await discoverExpoModuleConfigAsync(resolution.path);
} catch {
// We ignore invalid Expo Modules for the purpose of auto-linking and
// pretend the config doesn't exist, if it isn't valid JSON
}
}
let platformData:
| RNConfigDependencyAndroid
| RNConfigDependencyIos
| RNConfigDependencyWeb
| null = null;
if (platform === 'android') {
platformData = await resolveDependencyConfigImplAndroidAsync(
resolution.path,
reactNativeConfig.platforms?.android,
maybeExpoModuleConfig
);
} else if (platform === 'ios') {
platformData = await resolveDependencyConfigImplIosAsync(
resolution,
reactNativeConfig.platforms?.ios,
maybeExpoModuleConfig
);
} else if (platform === 'web') {
platformData = await checkDependencyWebAsync(
resolution,
reactNativeConfig,
maybeExpoModuleConfig
);
}
return (
platformData && {
root: resolution.path,
name: resolution.name,
platforms: {
[platform]: platformData,
},
}
);
}
interface CreateRNConfigParams {
appRoot: string;
sourceDir: string | undefined;
autolinkingOptions: AutolinkingOptions & { platform: SupportedPlatform };
}
/**
* Create config for react-native core autolinking.
*/
export async function createReactNativeConfigAsync({
appRoot,
sourceDir,
autolinkingOptions,
}: CreateRNConfigParams): Promise<RNConfigResult> {
const excludeNames = new Set(autolinkingOptions.exclude);
const projectConfig = await loadConfigAsync<RNConfigReactNativeProjectConfig>(appRoot);
// custom native modules should be resolved first so that they can override other modules
const searchPaths = autolinkingOptions.nativeModulesDir
? [autolinkingOptions.nativeModulesDir, ...autolinkingOptions.searchPaths]
: autolinkingOptions.searchPaths;
const limitDepth = autolinkingOptions.legacy_shallowReactNativeLinking ? 1 : undefined;
const resolutions = mergeResolutionResults(
await Promise.all([
scanDependenciesFromRNProjectConfig(appRoot, projectConfig),
...searchPaths.map((searchPath) => scanDependenciesInSearchPath(searchPath)),
scanDependenciesRecursively(appRoot, { limitDepth }),
])
);
const dependencies = await filterMapResolutionResult(resolutions, (resolution) =>
resolveReactNativeModule(resolution, projectConfig, autolinkingOptions.platform, excludeNames)
);
// See: https://github.com/facebook/react-native/pull/53690
// When we're building react-native from source without these generated files, we need to force them to be generated
// Every published react-native version (or out-of-tree version) should have these files, but building from the raw repo won't (e.g. Expo Go)
const reactNativeResolution = resolutions['react-native'];
if (
reactNativeResolution &&
autolinkingOptions.platform === 'ios' &&
(await isMissingFBReactNativeSpecCodegenOutput(reactNativeResolution.path))
) {
dependencies['react-native'] = {
root: reactNativeResolution.path,
name: 'react-native',
platforms: {
ios: {
// This will trigger a warning in list_native_modules but will trigger the artifacts
// codegen codepath as expected
podspecPath: '',
version: reactNativeResolution.version,
configurations: [],
scriptPhases: [],
},
},
};
}
return {
root: appRoot,
reactNativePath: resolutions['react-native']?.path!,
dependencies,
project: await resolveAppProjectConfigAsync(appRoot, autolinkingOptions.platform, sourceDir),
};
}
export async function resolveAppProjectConfigAsync(
projectRoot: string,
platform: SupportedPlatform,
sourceDir?: string
): Promise<RNConfigReactNativeAppProjectConfig> {
// TODO(@kitten): use the commandRoot here to find these files in non <projectRoot>/<platform> folders
if (platform === 'android') {
const androidDir = sourceDir ?? path.join(projectRoot, 'android');
const { gradle, manifest } = await findGradleAndManifestAsync({ androidDir, isLibrary: false });
if (gradle == null || manifest == null) {
return {};
}
const packageName = await parsePackageNameAsync(androidDir, manifest, gradle);
return {
android: {
packageName: packageName ?? '',
sourceDir: sourceDir ?? path.join(projectRoot, 'android'),
},
};
}
if (platform === 'ios') {
return {
ios: {
sourceDir: sourceDir ?? path.join(projectRoot, 'ios'),
},
};
}
return {};
}