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 key === 'id' || 76 key === 'groups' || 77 (key === 'flags' && !Object.keys(node).length) 78 ); 79 await fs.writeFile(jsonOutputPath, JSON.stringify(minifiedJson, null, 0)); 80 } else { 81 await fs.writeFile(jsonOutputPath, JSON.stringify(output)); 82 } 83 } else { 84 throw new Error(` Failed to extract API data from source code for '${packageName}' package.`); 85 } 86}; 87 88async function action({packageName, version = 'unversioned'}: ActionOptions) { 89 const packagesMapping: Record<string, CommandAdditionalParams> = { 90 'expo-analytics-segment': ['Segment.ts'], 91 'expo-app-loading': ['index.ts'], 92 'expo-apple-authentication': [['AppleAuthentication.ts', 'AppleAuthentication.types.ts']], 93 'expo-application': ['Application.ts'], 94 'expo-background-fetch': ['BackgroundFetch.ts'], 95 'expo-battery': ['Battery.ts'], 96 'expo-blur': ['index.ts'], 97 'expo-cellular': ['Cellular.ts'], 98 'expo-clipboard': ['Clipboard.ts'], 99 'expo-haptics': ['Haptics.ts'], 100 'expo-keep-awake': ['index.ts'], 101 'expo-linear-gradient': ['LinearGradient.tsx'], 102 'expo-localization': ['Localization.ts'], 103 'expo-mail-composer': ['MailComposer.ts'], 104 'expo-network': ['Network.ts'], 105 'expo-pedometer': ['Pedometer.ts', 'expo-sensors'], 106 'expo-print': ['Print.ts'], 107 'expo-random': ['Random.ts'], 108 'expo-secure-store': ['SecureStore.ts'], 109 'expo-sharing': ['Sharing.ts'], 110 'expo-speech': ['Speech/Speech.ts'], 111 'expo-screen-capture': ['ScreenCapture.ts'], 112 'expo-splash-screen': ['SplashScreen.ts'], 113 'expo-sms': ['SMS.ts'], 114 'expo-store-review': ['StoreReview.ts'], 115 'expo-task-manager': ['TaskManager.ts'], 116 'expo-tracking-transparency': ['TrackingTransparency.ts'], 117 'expo-video-thumbnails': ['VideoThumbnails.ts'], 118 'expo-web-browser': ['WebBrowser.ts'], 119 }; 120 121 try { 122 if (packageName) { 123 const packagesEntries = Object.entries(packagesMapping) 124 .filter(([key, value]) => key == packageName || value.includes(packageName)) 125 .map(([key, value]) => executeCommand(key, version, ...value)); 126 if (packagesEntries.length) { 127 await Promise.all(packagesEntries); 128 logger.log(chalk.green(`\n Successful extraction of docs API data for the selected package!`)); 129 } else { 130 logger.warn(` Package '${packageName}' API data generation is not supported yet!`); 131 } 132 } else { 133 const packagesEntries = Object.entries(packagesMapping) 134 .map(([key, value]) => executeCommand(key, version, ...value)); 135 await Promise.all(packagesEntries); 136 logger.log(chalk.green(`\n Successful extraction of docs API data for all available packages!`)); 137 } 138 } catch (error) { 139 logger.error(error); 140 } 141} 142 143export default (program: Command) => { 144 program 145 .command('generate-docs-api-data') 146 .alias('gdad') 147 .description(`Extract API data JSON files for docs using TypeDoc.`) 148 .option('-p, --packageName <packageName>', 'Extract API data only for the specific package.') 149 .option('-v, --version <version>', 'Set the data output path to the specific version.', 'unversioned') 150 .asyncAction(action); 151}; 152