expo-modules-autolinking
Version:
Scripts that autolink Expo modules.
206 lines (190 loc) • 7.52 kB
text/typescript
import fs from 'fs';
import type { PackageRevision, SupportedPlatform } from '../types';
import { scanDependenciesRecursively } from './resolution';
import { scanDependenciesFromRNProjectConfig } from './rncliLocal';
import { scanDependenciesInSearchPath } from './scanning';
import {
type DependencyResolution,
type ResolutionResult,
DependencyResolutionSource,
} from './types';
import { filterMapResolutionResult, mergeResolutionResults } from './utils';
import { resolveExpoModule } from '../autolinking/findModules';
import type { AutolinkingOptions } from '../commands/autolinkingOptions';
import { createAutolinkingOptionsLoader } from '../commands/autolinkingOptions';
import { createMemoizer, type Memoizer } from '../memoize';
import type { RNConfigReactNativeProjectConfig } from '../reactNativeConfig';
import { resolveReactNativeModule } from '../reactNativeConfig';
import { loadConfigAsync } from '../reactNativeConfig/config';
export interface CachedDependenciesSearchOptions {
includeNames: Set<string>;
excludeNames: Set<string>;
searchPaths: string[];
}
export interface CachedDependenciesLinker {
memoizer: Memoizer;
getOptionsForPlatform(
platform: SupportedPlatform,
extraInclude?: string[]
): Promise<CachedDependenciesSearchOptions>;
loadReactNativeProjectConfig(): Promise<RNConfigReactNativeProjectConfig | null>;
scanDependenciesFromRNProjectConfig(): Promise<ResolutionResult>;
scanDependenciesRecursively(): Promise<ResolutionResult>;
scanDependenciesInSearchPath(searchPath: string): Promise<ResolutionResult>;
}
export function makeCachedDependenciesLinker(params: {
projectRoot: string;
}): CachedDependenciesLinker {
const memoizer = createMemoizer();
const autolinkingOptionsLoader = createAutolinkingOptionsLoader({
projectRoot: params.projectRoot,
});
let appRoot: Promise<string> | undefined;
const getAppRoot = () => appRoot || (appRoot = autolinkingOptionsLoader.getAppRoot());
const dependenciesResultBySearchPath = new Map<string, Promise<ResolutionResult>>();
let reactNativeProjectConfig: Promise<RNConfigReactNativeProjectConfig | null> | undefined;
let reactNativeProjectConfigDependencies: Promise<ResolutionResult> | undefined;
let recursiveDependencies: Promise<ResolutionResult> | undefined;
return {
memoizer,
async getOptionsForPlatform(platform, extraInclude) {
const options = await autolinkingOptionsLoader.getPlatformOptions(platform);
return makeCachedDependenciesSearchOptions(options, extraInclude);
},
async loadReactNativeProjectConfig() {
if (reactNativeProjectConfig === undefined) {
reactNativeProjectConfig = memoizer.call(
loadConfigAsync,
await getAppRoot()
) as Promise<RNConfigReactNativeProjectConfig | null>;
}
return reactNativeProjectConfig;
},
async scanDependenciesFromRNProjectConfig() {
if (reactNativeProjectConfigDependencies === undefined) {
reactNativeProjectConfigDependencies = memoizer.withMemoizer(async () => {
return await scanDependenciesFromRNProjectConfig(
await getAppRoot(),
await this.loadReactNativeProjectConfig()
);
});
}
return reactNativeProjectConfigDependencies;
},
async scanDependenciesRecursively() {
if (recursiveDependencies === undefined) {
recursiveDependencies = memoizer.withMemoizer(async () => {
return scanDependenciesRecursively(await getAppRoot());
});
}
return recursiveDependencies;
},
async scanDependenciesInSearchPath(searchPath: string) {
let result = dependenciesResultBySearchPath.get(searchPath);
if (!result) {
dependenciesResultBySearchPath.set(
searchPath,
(result = memoizer.withMemoizer(scanDependenciesInSearchPath, searchPath))
);
}
return result;
},
};
}
export async function isNativeModuleAsync(
resolution: DependencyResolution,
reactNativeProjectConfig: RNConfigReactNativeProjectConfig | null,
platform: SupportedPlatform,
excludeNames: Set<string>
) {
const [reactNativeModule, expoModule] = await Promise.all([
resolveReactNativeModule(resolution, reactNativeProjectConfig, platform, excludeNames),
resolveExpoModule(resolution, platform, excludeNames),
]);
return !!reactNativeModule || !!expoModule;
}
export async function scanDependencyResolutionsForPlatform(
linker: CachedDependenciesLinker,
platform: SupportedPlatform,
extraInclude?: string[]
): Promise<ResolutionResult> {
const opts = await linker.getOptionsForPlatform(platform, extraInclude);
const reactNativeProjectConfig = await linker.loadReactNativeProjectConfig();
const resolutions = mergeResolutionResults(
await Promise.all([
linker.scanDependenciesFromRNProjectConfig(),
...opts.searchPaths.map((searchPath) => {
return linker.scanDependenciesInSearchPath(searchPath);
}),
linker.scanDependenciesRecursively(),
])
);
return await linker.memoizer.withMemoizer(async () => {
const dependencies = await filterMapResolutionResult(resolutions, async (resolution) => {
if (opts.excludeNames.has(resolution.name)) {
return null;
} else if (opts.includeNames.has(resolution.name)) {
return resolution;
} else if (resolution.source === DependencyResolutionSource.RN_CLI_LOCAL) {
// If the dependency was resolved frpom the React Native project config, we'll only
// attempt to resolve it as a React Native module
const reactNativeModuleDesc = await resolveReactNativeModule(
resolution,
reactNativeProjectConfig,
platform,
opts.excludeNames
);
if (!reactNativeModuleDesc) {
return null;
}
} else {
const isNativeModule = await isNativeModuleAsync(
resolution,
reactNativeProjectConfig,
platform,
opts.excludeNames
);
if (!isNativeModule) {
return null;
}
}
return resolution;
});
return dependencies;
});
}
export async function scanExpoModuleResolutionsForPlatform(
linker: CachedDependenciesLinker,
platform: SupportedPlatform,
extraInclude?: string[]
): Promise<Record<string, PackageRevision>> {
const { excludeNames, searchPaths } = await linker.getOptionsForPlatform(platform, extraInclude);
const resolutions = mergeResolutionResults(
await Promise.all(
[
...searchPaths.map((searchPath) => {
return linker.scanDependenciesInSearchPath(searchPath);
}),
linker.scanDependenciesRecursively(),
].filter((x) => x != null)
)
);
return await linker.memoizer.withMemoizer(async () => {
return await filterMapResolutionResult(resolutions, async (resolution) => {
return !excludeNames.has(resolution.name)
? await resolveExpoModule(resolution, platform, excludeNames)
: null;
});
});
}
const makeCachedDependenciesSearchOptions = (
options: AutolinkingOptions,
extraInclude?: string[]
) => ({
excludeNames: new Set(options.exclude),
includeNames: new Set(extraInclude ? [...options.include, ...extraInclude] : options.include),
searchPaths:
options.nativeModulesDir && fs.existsSync(options.nativeModulesDir)
? [options.nativeModulesDir, ...(options.searchPaths ?? [])]
: (options.searchPaths ?? []),
});