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