1import { Command } from '@expo/commander'; 2import chalk from 'chalk'; 3import { PromisyClass, TaskQueue } from 'cwait'; 4import fs from 'fs-extra'; 5import os from 'os'; 6import path from 'path'; 7import recursiveOmitBy from 'recursive-omit-by'; 8import { Application, TSConfigReader, TypeDocReader } from 'typedoc'; 9 10import { EXPO_DIR, PACKAGES_DIR } from '../Constants'; 11import logger from '../Logger'; 12 13type ActionOptions = { 14 packageName?: string; 15 sdk?: string; 16}; 17 18type EntryPoint = string | string[]; 19 20type CommandAdditionalParams = [entryPoint: EntryPoint, packageName?: string]; 21 22const MINIFY_JSON = true; 23 24const PACKAGES_MAPPING: Record<string, CommandAdditionalParams> = { 25 'expo-accelerometer': [['Accelerometer.ts', 'DeviceSensor.ts'], 'expo-sensors'], 26 'expo-app-loading': ['index.ts'], 27 'expo-apple-authentication': ['index.ts'], 28 'expo-application': ['Application.ts'], 29 'expo-audio': [['Audio.ts', 'Audio.types.ts'], 'expo-av'], 30 'expo-auth-session': ['AuthSession.ts'], 31 'expo-av': [['AV.ts', 'AV.types.ts'], 'expo-av'], 32 'expo-asset': [['Asset.ts', 'AssetHooks.ts']], 33 'expo-background-fetch': ['BackgroundFetch.ts'], 34 'expo-battery': ['Battery.ts'], 35 'expo-barometer': [['Barometer.ts', 'DeviceSensor.ts'], 'expo-sensors'], 36 'expo-barcode-scanner': ['BarCodeScanner.tsx'], 37 'expo-blur': ['index.ts'], 38 'expo-brightness': ['Brightness.ts'], 39 'expo-build-properties': [['withBuildProperties.ts', 'pluginConfig.ts']], 40 'expo-calendar': ['Calendar.ts'], 41 'expo-camera': ['index.ts'], 42 'expo-cellular': ['Cellular.ts'], 43 'expo-checkbox': ['Checkbox.ts'], 44 'expo-clipboard': [['Clipboard.ts', 'Clipboard.types.ts']], 45 'expo-constants': [['Constants.ts', 'Constants.types.ts']], 46 'expo-contacts': ['Contacts.ts'], 47 'expo-crypto': ['Crypto.ts'], 48 'expo-device': ['Device.ts'], 49 'expo-device-motion': [['DeviceMotion.ts', 'DeviceSensor.ts'], 'expo-sensors'], 50 'expo-document-picker': ['index.ts'], 51 'expo-error-recovery': ['ErrorRecovery.ts'], 52 'expo-face-detector': ['FaceDetector.ts'], 53 'expo-file-system': ['index.ts'], 54 'expo-firebase-analytics': ['Analytics.ts'], 55 'expo-firebase-core': ['FirebaseCore.ts'], 56 'expo-font': ['index.ts'], 57 'expo-gl': ['index.ts'], 58 'expo-gyroscope': [['Gyroscope.ts', 'DeviceSensor.ts'], 'expo-sensors'], 59 'expo-haptics': ['Haptics.ts'], 60 'expo-image': [['Image.tsx', 'Image.types.ts']], 61 'expo-image-manipulator': ['ImageManipulator.ts'], 62 'expo-image-picker': ['ImagePicker.ts'], 63 'expo-in-app-purchases': ['InAppPurchases.ts'], 64 'expo-intent-launcher': ['IntentLauncher.ts'], 65 'expo-keep-awake': ['index.ts'], 66 'expo-light-sensor': [['LightSensor.ts', 'DeviceSensor.ts'], 'expo-sensors'], 67 'expo-linking': ['Linking.ts'], 68 'expo-linear-gradient': ['LinearGradient.tsx'], 69 'expo-local-authentication': ['LocalAuthentication.ts'], 70 'expo-localization': ['Localization.ts'], 71 'expo-location': ['Location.ts'], 72 'expo-magnetometer': [['Magnetometer.ts', 'DeviceSensor.ts'], 'expo-sensors'], 73 'expo-mail-composer': ['MailComposer.ts'], 74 'expo-media-library': ['MediaLibrary.ts'], 75 'expo-navigation-bar': ['NavigationBar.ts'], 76 'expo-network': ['Network.ts'], 77 'expo-pedometer': ['Pedometer.ts', 'expo-sensors'], 78 'expo-print': ['Print.ts'], 79 'expo-random': ['Random.ts'], 80 'expo-screen-capture': ['ScreenCapture.ts'], 81 'expo-screen-orientation': ['ScreenOrientation.ts'], 82 'expo-secure-store': ['SecureStore.ts'], 83 'expo-sharing': ['Sharing.ts'], 84 'expo-sms': ['SMS.ts'], 85 'expo-speech': ['Speech/Speech.ts'], 86 'expo-splash-screen': ['SplashScreen.ts'], 87 'expo-sqlite': ['index.ts'], 88 'expo-status-bar': ['StatusBar.ts'], 89 'expo-store-review': ['StoreReview.ts'], 90 'expo-system-ui': ['SystemUI.ts'], 91 'expo-task-manager': ['TaskManager.ts'], 92 'expo-tracking-transparency': ['TrackingTransparency.ts'], 93 'expo-updates': ['index.ts'], 94 'expo-video': [['Video.tsx', 'Video.types.ts'], 'expo-av'], 95 'expo-video-thumbnails': ['VideoThumbnails.ts'], 96 'expo-web-browser': ['WebBrowser.ts'], 97}; 98 99const executeCommand = async ( 100 jsonFileName: string, 101 sdk?: string, 102 entryPoint: EntryPoint = 'index.ts', 103 packageName: string = jsonFileName 104) => { 105 const app = new Application(); 106 107 app.options.addReader(new TSConfigReader()); 108 app.options.addReader(new TypeDocReader()); 109 110 const dataPath = path.join( 111 EXPO_DIR, 112 'docs', 113 'public', 114 'static', 115 'data', 116 sdk ? `v${sdk}.0.0` : `unversioned` 117 ); 118 119 if (!fs.existsSync(dataPath)) { 120 throw new Error( 121 ` The path for given SDK version do not exist! 122 Check if you have provided the correct major SDK version to the '--sdk' parameter. 123 Path: '${dataPath}'` 124 ); 125 } 126 127 const basePath = path.join(PACKAGES_DIR, packageName); 128 const entriesPath = path.join(basePath, 'src'); 129 const tsConfigPath = path.join(basePath, 'tsconfig.json'); 130 const jsonOutputPath = path.join(dataPath, `${jsonFileName}.json`); 131 132 const entryPoints = Array.isArray(entryPoint) 133 ? entryPoint.map((entry) => path.join(entriesPath, entry)) 134 : [path.join(entriesPath, entryPoint)]; 135 136 app.bootstrap({ 137 entryPoints, 138 tsconfig: tsConfigPath, 139 disableSources: true, 140 hideGenerator: true, 141 excludePrivate: true, 142 excludeProtected: true, 143 }); 144 145 const project = app.convert(); 146 147 if (project) { 148 await app.generateJson(project, jsonOutputPath); 149 const output = await fs.readJson(jsonOutputPath); 150 output.name = jsonFileName; 151 152 if (Array.isArray(entryPoint)) { 153 const filterEntries = entryPoint.map((entry) => entry.substring(0, entry.lastIndexOf('.'))); 154 output.children = output.children 155 .filter((entry) => filterEntries.includes(entry.name)) 156 .map((entry) => entry.children) 157 .flat() 158 .sort((a, b) => a.name.localeCompare(b.name)); 159 } 160 161 if (MINIFY_JSON) { 162 const minifiedJson = recursiveOmitBy( 163 output, 164 ({ key, node }) => 165 ['id', 'groups', 'target'].includes(key) || (key === 'flags' && !Object.keys(node).length) 166 ); 167 await fs.writeFile(jsonOutputPath, JSON.stringify(minifiedJson, null, 0)); 168 } else { 169 await fs.writeFile(jsonOutputPath, JSON.stringify(output)); 170 } 171 } else { 172 throw new Error(` Failed to extract API data from source code for '${packageName}' package.`); 173 } 174}; 175 176async function action({ packageName, sdk }: ActionOptions) { 177 const taskQueue = new TaskQueue(Promise as PromisyClass, os.cpus().length); 178 179 try { 180 if (packageName) { 181 const packagesEntries = Object.entries(PACKAGES_MAPPING) 182 .filter(([key, value]) => key === packageName || value.includes(packageName)) 183 .map(([key, value]) => taskQueue.add(() => executeCommand(key, sdk, ...value))); 184 if (packagesEntries.length) { 185 await Promise.all(packagesEntries); 186 logger.log( 187 chalk.green(`\n Successful extraction of docs API data for the selected package!`) 188 ); 189 } else { 190 logger.warn(` Package '${packageName}' API data generation is not supported yet!`); 191 } 192 } else { 193 const packagesEntries = Object.entries(PACKAGES_MAPPING).map(([key, value]) => 194 taskQueue.add(() => executeCommand(key, sdk, ...value)) 195 ); 196 await Promise.all(packagesEntries); 197 logger.log( 198 chalk.green(`\n Successful extraction of docs API data for all available packages!`) 199 ); 200 } 201 } catch (error) { 202 logger.error(error); 203 } 204} 205 206export default (program: Command) => { 207 program 208 .command('generate-docs-api-data') 209 .alias('gdad') 210 .description(`Extract API data JSON files for docs using TypeDoc.`) 211 .option('-p, --packageName <packageName>', 'Extract API data only for the specific package.') 212 .option('-s, --sdk <version>', 'Set the data output path to the specific SDK version.') 213 .asyncAction(action); 214}; 215