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