1import { Command } from '@expo/commander';
2import chalk from 'chalk';
3import { Application, TSConfigReader, TypeDocReader } from 'typedoc';
4import fs from 'fs-extra';
5import recursiveOmitBy from 'recursive-omit-by';
6
7import { EXPO_DIR, PACKAGES_DIR } from '../Constants';
8import logger from '../Logger';
9
10type ActionOptions = {
11  packageName: string;
12};
13
14type EntryPoint = string | string[];
15
16type CommandAdditionalParams = [
17  entryPoint: EntryPoint,
18  jsonFileName?: string
19];
20
21const DATA_PATH = `${EXPO_DIR}/docs/public/static/data/unversioned`;
22const MINIFY_JSON = true;
23
24const executeCommand = async (
25  packageName: string,
26  entryPoint: EntryPoint = 'index.ts',
27  jsonFileName: string = packageName
28) => {
29  const app = new Application();
30
31  app.options.addReader(new TSConfigReader());
32  app.options.addReader(new TypeDocReader());
33
34  const entriesPath = `${PACKAGES_DIR}/${packageName}/src`;
35  const tsConfigPath = `${PACKAGES_DIR}/${packageName}/tsconfig.json`;
36  const jsonOutputPath = `${DATA_PATH}/${jsonFileName}.json`;
37
38  const entryPoints = Array.isArray(entryPoint) ? (
39    entryPoint.map(entry => `${entriesPath}/${entry}`)
40  ) : (
41    [`${entriesPath}/${entryPoint}`]
42  );
43
44  app.bootstrap({
45    entryPoints,
46    tsconfig: tsConfigPath,
47    disableSources: true,
48    hideGenerator: true,
49    excludePrivate: true,
50    excludeProtected: true,
51  });
52
53  const project = app.convert();
54
55  if (project) {
56    await app.generateJson(project, jsonOutputPath);
57    const output = await fs.readJson(jsonOutputPath);
58
59    if (Array.isArray(entryPoint)) {
60      const filterEntries = entryPoint.map(entry => entry.substring(0, entry.lastIndexOf('.')));
61      output.children = output.children
62        .filter(entry => filterEntries.includes(entry.name))
63        .map(entry => entry.children)
64        .flat()
65        .sort((a, b) => a.name.localeCompare(b.name));
66    }
67
68    if (MINIFY_JSON) {
69      const minifiedJson = recursiveOmitBy(
70        output,
71        ({ key }) => key === 'id' || key === 'groups'
72      );
73      await fs.writeFile(jsonOutputPath, JSON.stringify(minifiedJson, null, 0));
74    } else {
75      await fs.writeFile(jsonOutputPath, JSON.stringify(output));
76    }
77  } else {
78    throw new Error(`�� Failed to extract API data from source code for '${packageName}' package.`);
79  }
80};
81
82async function action({ packageName }: ActionOptions) {
83  const packagesMapping: Record<string, CommandAdditionalParams> = {
84    'expo-apple-authentication': [['AppleAuthentication.ts', 'AppleAuthentication.types.ts']],
85    'expo-application': ['Application.ts'],
86    'expo-battery': ['Battery.ts'],
87    'expo-blur': ['index.ts'],
88    'expo-cellular': ['Cellular.ts'],
89    'expo-clipboard': ['Clipboard.ts'],
90    'expo-keep-awake': ['index.ts'],
91    'expo-linear-gradient': ['LinearGradient.tsx'],
92    'expo-mail-composer': ['MailComposer.ts'],
93    'expo-print': ['Print.ts'],
94    'expo-random': ['Random.ts'],
95    'expo-sensors': ['Pedometer.ts', 'expo-pedometer'],
96    'expo-sharing': ['Sharing.ts'],
97    'expo-splash-screen': ['SplashScreen.ts'],
98    'expo-store-review': ['StoreReview.ts'],
99    'expo-task-manager': ['TaskManager.ts'],
100    'expo-tracking-transparency': ['TrackingTransparency.ts'],
101  };
102
103  try {
104    if (packageName) {
105      const packagesEntries = Object.entries(packagesMapping)
106        .filter(([key, value]) => key == packageName || value.includes(packageName))
107        .map(([key, value]) => executeCommand(key, ...value));
108      if (packagesEntries.length) {
109        await Promise.all(packagesEntries);
110        logger.log(chalk.green(`\n�� Successful extraction of docs API data for the selected package!`));
111      } else {
112        logger.warn(`�� Package '${packageName}' API data generation is not supported yet!`);
113      }
114    } else {
115      const packagesEntries = Object.entries(packagesMapping)
116        .map(([key, value]) => executeCommand(key, ...value));
117      await Promise.all(packagesEntries);
118      logger.log(chalk.green(`\n�� Successful extraction of docs API data for all available packages!`));
119    }
120  } catch (error) {
121    logger.error(error);
122  }
123}
124
125export default (program: Command) => {
126  program
127    .command('generate-docs-api-data')
128    .alias('gdad')
129    .description(`Extract API data for docs using TypeDoc.`)
130    .option('-p, --packageName <packageName>', 'Extract API data only for the specific package.')
131    .asyncAction(action);
132};
133