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