expo-modules-autolinking
Version:
Scripts that autolink Expo modules.
173 lines (157 loc) • 5.69 kB
text/typescript
import chalk from 'chalk';
import commander from 'commander';
import fs from 'fs';
import path from 'path';
import {
AutolinkingCommonArguments,
createAutolinkingOptionsLoader,
registerAutolinkingArguments,
} from './autolinkingOptions';
import {
type BaseDependencyResolution,
type DependencyResolution,
type ResolutionResult,
DependencyResolutionSource,
makeCachedDependenciesLinker,
mergeResolutionResults,
scanDependencyResolutionsForPlatform,
} from '../dependencies';
interface VerifyArguments extends AutolinkingCommonArguments {
verbose?: boolean | null;
json?: boolean | null;
}
export function verifyCommand(cli: commander.CommanderStatic) {
return registerAutolinkingArguments(cli.command('verify'))
.option('-v, --verbose', 'Output all results instead of just warnings.', () => true, false)
.option('-j, --json', 'Output results in the plain JSON format.', () => true, false)
.option(
'-p, --platform [platform]',
'The platform to validate native modules for. Available options: "android", "ios", "both"',
'both'
)
.action(async (commandArguments: VerifyArguments) => {
const platforms =
commandArguments.platform === 'both' ? ['android', 'ios'] : [commandArguments.platform!];
const autolinkingOptionsLoader = createAutolinkingOptionsLoader(commandArguments);
const appRoot = await autolinkingOptionsLoader.getAppRoot();
const linker = makeCachedDependenciesLinker({ projectRoot: appRoot });
const results = mergeResolutionResults(
await Promise.all(
platforms.map((platform) => scanDependencyResolutionsForPlatform(linker, platform))
)
);
await verifySearchResults(results, {
appRoot,
verbose: !!commandArguments.verbose,
json: !!commandArguments.json,
});
});
}
interface VerifyOptions {
appRoot: string;
verbose?: boolean;
json?: boolean;
}
interface VerifyGroups {
reactNativeProjectConfig: DependencyResolution[];
searchPaths: DependencyResolution[];
dependencies: DependencyResolution[];
duplicates: DependencyResolution[];
}
/**
* Verifies the search results by checking whether there are no duplicates.
*/
export async function verifySearchResults(
results: ResolutionResult,
options: VerifyOptions
): Promise<void> {
const { appRoot } = options;
async function getHumanReadableDependency(dependency: BaseDependencyResolution): Promise<string> {
let version = dependency.version || null;
if (!version) {
try {
const pkgContents = await fs.promises.readFile(
path.join(dependency.path, 'package.json'),
'utf8'
);
const pkg: unknown = JSON.parse(pkgContents);
if (pkg && typeof pkg === 'object' && 'version' in pkg && typeof pkg.version === 'string') {
version = pkg.version;
}
} catch (error) {
version = null;
}
}
const relative = path.relative(appRoot, dependency.originPath);
return version
? `${dependency.name}@${version} (at: ${relative})`
: `${dependency.name} at: ${relative}`;
}
const groups: VerifyGroups = {
reactNativeProjectConfig: [],
searchPaths: [],
dependencies: [],
duplicates: [],
};
for (const moduleName in results) {
const revision = results[moduleName];
if (!revision) {
continue;
} else if (revision.duplicates?.length) {
groups.duplicates.push(revision);
} else {
switch (revision.source) {
case DependencyResolutionSource.RN_CLI_LOCAL:
groups.reactNativeProjectConfig.push(revision);
break;
case DependencyResolutionSource.SEARCH_PATH:
groups.searchPaths.push(revision);
break;
case DependencyResolutionSource.RECURSIVE_RESOLUTION:
groups.dependencies.push(revision);
break;
}
}
}
if (options.json) {
console.log(JSON.stringify(groups));
return;
}
if (options.verbose) {
if (groups.reactNativeProjectConfig.length) {
console.log(
`🔎 Found ${groups.reactNativeProjectConfig.length} modules from React Native project config`
);
for (const revision of groups.reactNativeProjectConfig) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
if (groups.searchPaths.length) {
console.log(`🔎 Found ${groups.searchPaths.length} modules in search paths`);
for (const revision of groups.searchPaths) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
console.log(`🔎 Found ${groups.dependencies.length} modules in dependencies`);
for (const revision of groups.dependencies) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
if (groups.duplicates.length) {
for (const revision of groups.duplicates) {
console.warn(`⚠️ Found duplicate installations for ${chalk.green(revision.name)}`);
const revisions = [revision, ...(revision.duplicates ?? [])];
for (let idx = 0; idx < revisions.length; idx++) {
const prefix = idx !== revisions.length - 1 ? '├─' : '└─';
const duplicate = revisions[idx];
console.log(` ${prefix} ${await getHumanReadableDependency(duplicate)}`);
}
}
console.warn(
'⚠️ Multiple versions of the same module may introduce some side effects or compatibility issues.\n' +
`Resolve your dependency issues and deduplicate your dependencies. Learn more: https://expo.fyi/resolving-dependency-issues`
);
} else {
console.log('✅ Everything is fine!');
}
}