1import { Command } from '@expo/commander'; 2import chalk from 'chalk'; 3import { Application, TSConfigReader, TypeDocReader } from 'typedoc'; 4import fs from 'fs-extra'; 5import path from 'path'; 6import recursiveOmitBy from 'recursive-omit-by'; 7 8import { EXPO_DIR, PACKAGES_DIR } from '../Constants'; 9import logger from '../Logger'; 10 11type ActionOptions = { 12 packageName?: string; 13 version?: string; 14}; 15 16type EntryPoint = string | string[]; 17 18type CommandAdditionalParams = [ 19 entryPoint: EntryPoint, 20 packageName?: string 21]; 22 23const MINIFY_JSON = true; 24 25const executeCommand = async ( 26 jsonFileName: string, 27 version: string, 28 entryPoint: EntryPoint = 'index.ts', 29 packageName: string = jsonFileName 30) => { 31 const app = new Application(); 32 33 app.options.addReader(new TSConfigReader()); 34 app.options.addReader(new TypeDocReader()); 35 36 const dataPath = path.join(EXPO_DIR, 'docs', 'public', 'static', 'data', version); 37 const basePath = path.join(PACKAGES_DIR, packageName); 38 const entriesPath = path.join(basePath, 'src'); 39 const tsConfigPath = path.join(basePath, 'tsconfig.json'); 40 const jsonOutputPath = path.join(dataPath, `${jsonFileName}.json`); 41 42 const entryPoints = Array.isArray(entryPoint) ? ( 43 entryPoint.map(entry => path.join(entriesPath, entry)) 44 ) : ( 45 [path.join(entriesPath, entryPoint)] 46 ); 47 48 app.bootstrap({ 49 entryPoints, 50 tsconfig: tsConfigPath, 51 disableSources: true, 52 hideGenerator: true, 53 excludePrivate: true, 54 excludeProtected: true, 55 }); 56 57 const project = app.convert(); 58 59 if (project) { 60 await app.generateJson(project, jsonOutputPath); 61 const output = await fs.readJson(jsonOutputPath); 62 output.name = jsonFileName; 63 64 if (Array.isArray(entryPoint)) { 65 const filterEntries = entryPoint.map(entry => entry.substring(0, entry.lastIndexOf('.'))); 66 output.children = output.children 67 .filter(entry => filterEntries.includes(entry.name)) 68 .map(entry => entry.children) 69 .flat() 70 .sort((a, b) => a.name.localeCompare(b.name)); 71 } 72 73 if (MINIFY_JSON) { 74 const minifiedJson = recursiveOmitBy(output, ({key, node}) => 75 ['id', 'groups', 'target'].includes(key) || 76 (key === 'flags' && !Object.keys(node).length) 77 ); 78 await fs.writeFile(jsonOutputPath, JSON.stringify(minifiedJson, null, 0)); 79 } else { 80 await fs.writeFile(jsonOutputPath, JSON.stringify(output)); 81 } 82 } else { 83 throw new Error(` Failed to extract API data from source code for '${packageName}' package.`); 84 } 85}; 86 87async function action({packageName, version = 'unversioned'}: ActionOptions) { 88 const packagesMapping: Record<string, CommandAdditionalParams> = { 89 'expo-analytics-amplitude': ['Amplitude.ts'], 90 'expo-analytics-segment': ['Segment.ts'], 91 'expo-app-loading': ['index.ts'], 92 'expo-apple-authentication': ['index.ts'], 93 'expo-application': ['Application.ts'], 94 'expo-background-fetch': ['BackgroundFetch.ts'], 95 'expo-battery': ['Battery.ts'], 96 'expo-blur': ['index.ts'], 97 'expo-brightness': ['Brightness.ts'], 98 'expo-cellular': ['Cellular.ts'], 99 'expo-checkbox': ['Checkbox.ts'], 100 'expo-clipboard': ['Clipboard.ts'], 101 'expo-document-picker': ['index.ts'], 102 'expo-error-recovery': ['ErrorRecovery.ts'], 103 'expo-facebook': [['Facebook.ts', 'Facebook.types.ts']], 104 'expo-font': ['index.ts'], 105 'expo-haptics': ['Haptics.ts'], 106 'expo-image-manipulator': ['ImageManipulator.ts'], 107 'expo-keep-awake': ['index.ts'], 108 'expo-linear-gradient': ['LinearGradient.tsx'], 109 'expo-local-authentication': ['LocalAuthentication.ts'], 110 'expo-localization': ['Localization.ts'], 111 'expo-mail-composer': ['MailComposer.ts'], 112 'expo-media-library': ['MediaLibrary.ts'], 113 'expo-network': ['Network.ts'], 114 'expo-pedometer': ['Pedometer.ts', 'expo-sensors'], 115 'expo-print': ['Print.ts'], 116 'expo-random': ['Random.ts'], 117 'expo-secure-store': ['SecureStore.ts'], 118 'expo-sharing': ['Sharing.ts'], 119 'expo-speech': ['Speech/Speech.ts'], 120 'expo-screen-capture': ['ScreenCapture.ts'], 121 'expo-splash-screen': ['SplashScreen.ts'], 122 'expo-sqlite': ['index.ts'], 123 'expo-sms': ['SMS.ts'], 124 'expo-store-review': ['StoreReview.ts'], 125 'expo-task-manager': ['TaskManager.ts'], 126 'expo-tracking-transparency': ['TrackingTransparency.ts'], 127 'expo-updates': ['index.ts'], 128 'expo-video-thumbnails': ['VideoThumbnails.ts'], 129 'expo-web-browser': ['WebBrowser.ts'], 130 }; 131 132 try { 133 if (packageName) { 134 const packagesEntries = Object.entries(packagesMapping) 135 .filter(([key, value]) => key == packageName || value.includes(packageName)) 136 .map(([key, value]) => executeCommand(key, version, ...value)); 137 if (packagesEntries.length) { 138 await Promise.all(packagesEntries); 139 logger.log(chalk.green(`\n Successful extraction of docs API data for the selected package!`)); 140 } else { 141 logger.warn(` Package '${packageName}' API data generation is not supported yet!`); 142 } 143 } else { 144 const packagesEntries = Object.entries(packagesMapping) 145 .map(([key, value]) => executeCommand(key, version, ...value)); 146 await Promise.all(packagesEntries); 147 logger.log(chalk.green(`\n Successful extraction of docs API data for all available packages!`)); 148 } 149 } catch (error) { 150 logger.error(error); 151 } 152} 153 154export default (program: Command) => { 155 program 156 .command('generate-docs-api-data') 157 .alias('gdad') 158 .description(`Extract API data JSON files for docs using TypeDoc.`) 159 .option('-p, --packageName <packageName>', 'Extract API data only for the specific package.') 160 .option('-v, --version <version>', 'Set the data output path to the specific version.', 'unversioned') 161 .asyncAction(action); 162}; 163