UNPKG

expo-modules-autolinking

Version:
290 lines (250 loc) 9.38 kB
import spawnAsync from '@expo/spawn-async'; import glob from 'fast-glob'; import fs from 'fs-extra'; import path from 'path'; import { fileExistsAsync } from '../fileUtils'; import type { AppleCodeSignEntitlements, ExtraDependencies, ModuleDescriptorIos, ModuleIosPodspecInfo, PackageRevision, SearchOptions, } from '../types'; const APPLE_PROPERTIES_FILE = 'Podfile.properties.json'; const APPLE_EXTRA_BUILD_DEPS_KEY = 'apple.extraPods'; const indent = ' '; async function findPodspecFiles(revision: PackageRevision): Promise<string[]> { const configPodspecPaths = revision.config?.applePodspecPaths(); if (configPodspecPaths && configPodspecPaths.length) { return configPodspecPaths; } const podspecFiles = await glob('*/*.podspec', { cwd: revision.path, ignore: ['**/node_modules/**'], }); return podspecFiles; } export function getSwiftModuleNames( pods: ModuleIosPodspecInfo[], swiftModuleNames: string[] | undefined ): string[] { if (swiftModuleNames && swiftModuleNames.length) { return swiftModuleNames; } // by default, non-alphanumeric characters in the pod name are replaced by _ in the module name return pods.map((pod) => pod.podName.replace(/[^a-zA-Z0-9]/g, '_')); } /** * Resolves module search result with additional details required for iOS platform. */ export async function resolveModuleAsync( packageName: string, revision: PackageRevision, options: SearchOptions ): Promise<ModuleDescriptorIos | null> { const podspecFiles = await findPodspecFiles(revision); if (!podspecFiles.length) { return null; } const pods = podspecFiles.map((podspecFile) => ({ podName: path.basename(podspecFile, path.extname(podspecFile)), podspecDir: path.dirname(path.join(revision.path, podspecFile)), })); const swiftModuleNames = getSwiftModuleNames(pods, revision.config?.appleSwiftModuleNames()); return { packageName, pods, swiftModuleNames, flags: options.flags, modules: revision.config?.appleModules() ?? [], appDelegateSubscribers: revision.config?.appleAppDelegateSubscribers() ?? [], reactDelegateHandlers: revision.config?.appleReactDelegateHandlers() ?? [], debugOnly: revision.config?.appleDebugOnly() ?? false, }; } export async function resolveExtraBuildDependenciesAsync( projectNativeRoot: string ): Promise<ExtraDependencies | null> { const propsFile = path.join(projectNativeRoot, APPLE_PROPERTIES_FILE); try { const contents = await fs.readFile(propsFile, 'utf8'); const podfileJson = JSON.parse(contents); if (podfileJson[APPLE_EXTRA_BUILD_DEPS_KEY]) { // expo-build-properties would serialize the extraPods as JSON string, we should parse it again. const extraPods = JSON.parse(podfileJson[APPLE_EXTRA_BUILD_DEPS_KEY]); return extraPods; } } catch {} return null; } /** * Generates Swift file that contains all autolinked Swift packages. */ export async function generateModulesProviderAsync( modules: ModuleDescriptorIos[], targetPath: string, entitlementPath: string ): Promise<void> { const className = path.basename(targetPath, path.extname(targetPath)); const entitlements = await parseEntitlementsAsync(entitlementPath); const generatedFileContent = await generatePackageListFileContentAsync( modules, className, entitlements ); await fs.outputFile(targetPath, generatedFileContent); } /** * Generates the string to put into the generated package list. */ async function generatePackageListFileContentAsync( modules: ModuleDescriptorIos[], className: string, entitlements: AppleCodeSignEntitlements ): Promise<string> { const iosModules = modules.filter( (module) => module.modules.length || module.appDelegateSubscribers.length || module.reactDelegateHandlers.length ); const modulesToImport = iosModules.filter((module) => !module.debugOnly); const debugOnlyModules = iosModules.filter((module) => module.debugOnly); const swiftModules = ([] as string[]) .concat(...modulesToImport.map((module) => module.swiftModuleNames)) .filter(Boolean); const debugOnlySwiftModules = ([] as string[]) .concat(...debugOnlyModules.map((module) => module.swiftModuleNames)) .filter(Boolean); const modulesClassNames = ([] as string[]) .concat(...modulesToImport.map((module) => module.modules)) .filter(Boolean); const debugOnlyModulesClassNames = ([] as string[]) .concat(...debugOnlyModules.map((module) => module.modules)) .filter(Boolean); const appDelegateSubscribers = ([] as string[]).concat( ...modulesToImport.map((module) => module.appDelegateSubscribers) ); const debugOnlyAppDelegateSubscribers = ([] as string[]).concat( ...debugOnlyModules.map((module) => module.appDelegateSubscribers) ); const reactDelegateHandlerModules = modulesToImport.filter( (module) => !!module.reactDelegateHandlers.length ); const debugOnlyReactDelegateHandlerModules = debugOnlyModules.filter( (module) => !!module.reactDelegateHandlers.length ); return `/** * Automatically generated by expo-modules-autolinking. * * This autogenerated class provides a list of classes of native Expo modules, * but only these that are written in Swift and use the new API for creating Expo modules. */ import ExpoModulesCore ${generateCommonImportList(swiftModules)} ${generateDebugOnlyImportList(debugOnlySwiftModules)} @objc(${className}) public class ${className}: ModulesProvider { public override func getModuleClasses() -> [AnyModule.Type] { ${generateModuleClasses(modulesClassNames, debugOnlyModulesClassNames)} } public override func getAppDelegateSubscribers() -> [ExpoAppDelegateSubscriber.Type] { ${generateModuleClasses(appDelegateSubscribers, debugOnlyAppDelegateSubscribers)} } public override func getReactDelegateHandlers() -> [ExpoReactDelegateHandlerTupleType] { ${generateReactDelegateHandlers(reactDelegateHandlerModules, debugOnlyReactDelegateHandlerModules)} } public override func getAppCodeSignEntitlements() -> AppCodeSignEntitlements { return AppCodeSignEntitlements.from(json: #"${JSON.stringify(entitlements)}"#) } } `; } function generateCommonImportList(swiftModules: string[]): string { return swiftModules.map((moduleName) => `import ${moduleName}`).join('\n'); } function generateDebugOnlyImportList(swiftModules: string[]): string { if (!swiftModules.length) { return ''; } return ( wrapInDebugConfigurationCheck( 0, swiftModules.map((moduleName) => `import ${moduleName}`).join('\n') ) + '\n' ); } function generateModuleClasses(classNames: string[], debugOnlyClassName: string[]): string { const commonClassNames = formatArrayOfClassNames(classNames); if (debugOnlyClassName.length > 0) { return wrapInDebugConfigurationCheck( 2, `return ${formatArrayOfClassNames(classNames.concat(debugOnlyClassName))}`, `return ${commonClassNames}` ); } else { return `${indent.repeat(2)}return ${commonClassNames}`; } } /** * Formats an array of class names to Swift's array containing these classes. */ function formatArrayOfClassNames(classNames: string[]): string { return `[${classNames.map((className) => `\n${indent.repeat(3)}${className}.self`).join(',')} ${indent.repeat(2)}]`; } function generateReactDelegateHandlers( module: ModuleDescriptorIos[], debugOnlyModules: ModuleDescriptorIos[] ): string { const commonModules = formatArrayOfReactDelegateHandler(module); if (debugOnlyModules.length > 0) { return wrapInDebugConfigurationCheck( 2, `return ${formatArrayOfReactDelegateHandler(module.concat(debugOnlyModules))}`, `return ${commonModules}` ); } else { return `${indent.repeat(2)}return ${commonModules}`; } } /** * Formats an array of modules to Swift's array containing ReactDelegateHandlers */ export function formatArrayOfReactDelegateHandler(modules: ModuleDescriptorIos[]): string { const values: string[] = []; for (const module of modules) { for (const handler of module.reactDelegateHandlers) { values.push(`(packageName: "${module.packageName}", handler: ${handler}.self)`); } } return `[${values.map((value) => `\n${indent.repeat(3)}${value}`).join(',')} ${indent.repeat(2)}]`; } function wrapInDebugConfigurationCheck( indentationLevel: number, debugBlock: string, releaseBlock: string | null = null ) { if (releaseBlock) { return `${indent.repeat(indentationLevel)}#if EXPO_CONFIGURATION_DEBUG\n${indent.repeat( indentationLevel )}${debugBlock}\n${indent.repeat(indentationLevel)}#else\n${indent.repeat( indentationLevel )}${releaseBlock}\n${indent.repeat(indentationLevel)}#endif`; } return `${indent.repeat(indentationLevel)}#if EXPO_CONFIGURATION_DEBUG\n${indent.repeat( indentationLevel )}${debugBlock}\n${indent.repeat(indentationLevel)}#endif`; } async function parseEntitlementsAsync(entitlementPath: string): Promise<AppleCodeSignEntitlements> { if (!(await fileExistsAsync(entitlementPath))) { return {}; } const { stdout } = await spawnAsync('plutil', ['-convert', 'json', '-o', '-', entitlementPath]); const entitlementsJson = JSON.parse(stdout); return { appGroups: entitlementsJson['com.apple.security.application-groups'] || undefined, }; }