1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @format
8 */
9
10'use strict';
11
12/**
13 * This script crawls through a React Native application's dependencies and invokes the codegen
14 * for any libraries that require it.
15 * To enable codegen support, the library should include a config in the codegenConfigKey key
16 * in a codegenConfigFilename file.
17 */
18
19const {execSync} = require('child_process');
20const fs = require('fs');
21const os = require('os');
22const path = require('path');
23
24const RN_ROOT = path.join(__dirname, '../..');
25
26const CODEGEN_REPO_PATH = `${RN_ROOT}/packages/react-native-codegen`;
27const CODEGEN_NPM_PATH = `${RN_ROOT}/../react-native-codegen`;
28const CORE_LIBRARIES = new Set(['rncore', 'FBReactNativeSpec']);
29const REACT_NATIVE_DEPENDENCY_NAME = 'react-native';
30
31// HELPERS
32
33function isReactNativeCoreLibrary(libraryName) {
34  return CORE_LIBRARIES.has(libraryName);
35}
36
37function executeNodeScript(node, script) {
38  execSync(`${node} ${script}`);
39}
40
41function isAppRootValid(appRootDir) {
42  if (appRootDir == null) {
43    console.error('Missing path to React Native application');
44    process.exitCode = 1;
45    return false;
46  }
47  return true;
48}
49
50function readPackageJSON(appRootDir) {
51  return JSON.parse(fs.readFileSync(path.join(appRootDir, 'package.json')));
52}
53
54function printDeprecationWarningIfNeeded(dependency) {
55  if (dependency === REACT_NATIVE_DEPENDENCY_NAME) {
56    return;
57  }
58  console.log(`[Codegen] CodegenConfig Deprecated Setup for ${dependency}.
59    The configuration file still contains the codegen in the libraries array.
60    If possible, replace it with a single object.
61  `);
62  console.debug(`BEFORE:
63    {
64      // ...
65      "codegenConfig": {
66        "libraries": [
67          {
68            "name": "libName1",
69            "type": "all|components|modules",
70            "jsSrcsRoot": "libName1/js"
71          },
72          {
73            "name": "libName2",
74            "type": "all|components|modules",
75            "jsSrcsRoot": "libName2/src"
76          }
77        ]
78      }
79    }
80
81    AFTER:
82    {
83      "codegenConfig": {
84        "name": "libraries",
85        "type": "all",
86        "jsSrcsRoot": "."
87      }
88    }
89  `);
90}
91
92// Reading Libraries
93function extractLibrariesFromConfigurationArray(
94  configFile,
95  codegenConfigKey,
96  libraries,
97  dependency,
98  dependencyPath,
99) {
100  console.log(`[Codegen] Found ${dependency}`);
101  configFile[codegenConfigKey].libraries.forEach(config => {
102    const libraryConfig = {
103      library: dependency,
104      config,
105      libraryPath: dependencyPath,
106    };
107    libraries.push(libraryConfig);
108  });
109}
110
111function extractLibrariesFromJSON(
112  configFile,
113  libraries,
114  codegenConfigKey,
115  dependency,
116  dependencyPath,
117) {
118  var isBlocking = false;
119  if (dependency == null) {
120    dependency = REACT_NATIVE_DEPENDENCY_NAME;
121    dependencyPath = RN_ROOT;
122    // If we are exploring the ReactNative libraries, we want to raise an error
123    // if the codegen is not properly configured.
124    isBlocking = true;
125  }
126
127  if (configFile[codegenConfigKey] == null) {
128    if (isBlocking) {
129      throw `[Codegen] Error: Could not find codegen config for ${dependency} .`;
130    }
131    return;
132  }
133
134  if (configFile[codegenConfigKey].libraries == null) {
135    console.log(`[Codegen] Found ${dependency}`);
136    var config = configFile[codegenConfigKey];
137    libraries.push({
138      library: dependency,
139      config,
140      libraryPath: dependencyPath,
141    });
142  } else {
143    printDeprecationWarningIfNeeded(dependency);
144    extractLibrariesFromConfigurationArray(
145      configFile,
146      codegenConfigKey,
147      libraries,
148      dependency,
149      dependencyPath,
150    );
151  }
152}
153
154function handleReactNativeCodeLibraries(
155  libraries,
156  codegenConfigFilename,
157  codegenConfigKey,
158) {
159  // Handle react-native core libraries.
160  // This is required when react-native is outside of node_modules.
161  console.log('[Codegen] Processing react-native core libraries');
162  const reactNativePkgJson = path.join(RN_ROOT, codegenConfigFilename);
163  if (!fs.existsSync(reactNativePkgJson)) {
164    throw '[Codegen] Error: Could not find config file for react-native.';
165  }
166  const reactNativeConfigFile = JSON.parse(fs.readFileSync(reactNativePkgJson));
167  extractLibrariesFromJSON(reactNativeConfigFile, libraries, codegenConfigKey);
168}
169
170function handleThirdPartyLibraries(
171  libraries,
172  baseCodegenConfigFileDir,
173  dependencies,
174  codegenConfigFilename,
175  codegenConfigKey,
176) {
177  // Determine which of these are codegen-enabled libraries
178  const configDir = baseCodegenConfigFileDir || path.join(RN_ROOT, '..');
179  console.log(
180    `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${configDir}`,
181  );
182
183  // Handle third-party libraries
184  Object.keys(dependencies).forEach(dependency => {
185    if (dependency === REACT_NATIVE_DEPENDENCY_NAME) {
186      // react-native should already be added.
187      return;
188    }
189    const codegenConfigFileDir = path.join(configDir, dependency);
190    const configFilePath = path.join(
191      codegenConfigFileDir,
192      codegenConfigFilename,
193    );
194    if (fs.existsSync(configFilePath)) {
195      const configFile = JSON.parse(fs.readFileSync(configFilePath));
196      extractLibrariesFromJSON(
197        configFile,
198        libraries,
199        codegenConfigKey,
200        dependency,
201        codegenConfigFileDir,
202      );
203    }
204  });
205}
206
207function handleLibrariesFromReactNativeConfig(
208  libraries,
209  codegenConfigKey,
210  codegenConfigFilename,
211  appRootDir,
212) {
213  const rnConfigFileName = 'react-native.config.js';
214
215  console.log(
216    `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${rnConfigFileName}`,
217  );
218
219  const rnConfigFilePath = path.join(appRootDir, rnConfigFileName);
220
221  if (fs.existsSync(rnConfigFilePath)) {
222    const rnConfig = require(rnConfigFilePath);
223
224    if (rnConfig.dependencies != null) {
225      Object.keys(rnConfig.dependencies).forEach(name => {
226        const dependencyConfig = rnConfig.dependencies[name];
227
228        if (dependencyConfig.root) {
229          const codegenConfigFileDir = path.resolve(
230            appRootDir,
231            dependencyConfig.root,
232          );
233          const configFilePath = path.join(
234            codegenConfigFileDir,
235            codegenConfigFilename,
236          );
237          const pkgJsonPath = path.join(codegenConfigFileDir, 'package.json');
238
239          if (fs.existsSync(configFilePath)) {
240            const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath));
241            const configFile = JSON.parse(fs.readFileSync(configFilePath));
242            extractLibrariesFromJSON(
243              configFile,
244              libraries,
245              codegenConfigKey,
246              pkgJson.name,
247              codegenConfigFileDir,
248            );
249          }
250        }
251      });
252    }
253  }
254}
255
256function handleInAppLibraries(
257  libraries,
258  pkgJson,
259  codegenConfigKey,
260  appRootDir,
261) {
262  console.log(
263    '\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in the app',
264  );
265
266  extractLibrariesFromJSON(
267    pkgJson,
268    libraries,
269    codegenConfigKey,
270    pkgJson.name,
271    appRootDir,
272  );
273}
274
275// CodeGen
276
277function getCodeGenCliPath() {
278  let codegenCliPath;
279  if (fs.existsSync(CODEGEN_REPO_PATH)) {
280    codegenCliPath = CODEGEN_REPO_PATH;
281
282    if (!fs.existsSync(path.join(CODEGEN_REPO_PATH, 'lib'))) {
283      console.log('\n\n[Codegen] >>>>> Building react-native-codegen package');
284      execSync('yarn install', {
285        cwd: codegenCliPath,
286        stdio: 'inherit',
287      });
288      execSync('yarn build', {
289        cwd: codegenCliPath,
290        stdio: 'inherit',
291      });
292    }
293  } else if (fs.existsSync(CODEGEN_NPM_PATH)) {
294    codegenCliPath = CODEGEN_NPM_PATH;
295  } else {
296    throw "error: Could not determine react-native-codegen location. Try running 'yarn install' or 'npm install' in your project root.";
297  }
298  return codegenCliPath;
299}
300
301function computeIOSOutputDir(outputPath, appRootDir) {
302  return path.join(outputPath ? outputPath : appRootDir, 'build/generated/ios');
303}
304
305function generateSchema(tmpDir, library, node, codegenCliPath) {
306  const pathToSchema = path.join(tmpDir, 'schema.json');
307  const pathToJavaScriptSources = path.join(
308    library.libraryPath,
309    library.config.jsSrcsDir,
310  );
311
312  console.log(`\n\n[Codegen] >>>>> Processing ${library.config.name}`);
313  // Generate one schema for the entire library...
314  executeNodeScript(
315    node,
316    `${path.join(
317      codegenCliPath,
318      'lib',
319      'cli',
320      'combine',
321      'combine-js-to-schema-cli.js',
322    )} ${pathToSchema} ${pathToJavaScriptSources}`,
323  );
324  console.log(`[Codegen] Generated schema: ${pathToSchema}`);
325  return pathToSchema;
326}
327
328function generateCode(iosOutputDir, library, tmpDir, node, pathToSchema) {
329  // ...then generate native code artifacts.
330  const libraryTypeArg = library.config.type
331    ? `--libraryType ${library.config.type}`
332    : '';
333
334  const tmpOutputDir = path.join(tmpDir, 'out');
335  fs.mkdirSync(tmpOutputDir, {recursive: true});
336
337  executeNodeScript(
338    node,
339    `${path.join(RN_ROOT, 'scripts', 'generate-specs-cli.js')} \
340        --platform ios \
341        --schemaPath ${pathToSchema} \
342        --outputDir ${tmpOutputDir} \
343        --libraryName ${library.config.name} \
344        ${libraryTypeArg}`,
345  );
346
347  // Finally, copy artifacts to the final output directory.
348  fs.mkdirSync(iosOutputDir, {recursive: true});
349  execSync(`cp -R ${tmpOutputDir}/* ${iosOutputDir}`);
350  console.log(`[Codegen] Generated artifacts: ${iosOutputDir}`);
351}
352
353function generateNativeCodegenFiles(
354  libraries,
355  fabricEnabled,
356  iosOutputDir,
357  node,
358  codegenCliPath,
359  schemaPaths,
360) {
361  let fabricEnabledTypes = ['components', 'all'];
362  libraries.forEach(library => {
363    if (
364      !fabricEnabled &&
365      fabricEnabledTypes.indexOf(library.config.type) >= 0
366    ) {
367      console.log(
368        `[Codegen] ${library.config.name} skipped because fabric is not enabled.`,
369      );
370      return;
371    }
372    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), library.config.name));
373    const pathToSchema = generateSchema(tmpDir, library, node, codegenCliPath);
374    generateCode(iosOutputDir, library, tmpDir, node, pathToSchema);
375
376    // Filter the react native core library out.
377    // In the future, core library and third party library should
378    // use the same way to generate/register the fabric components.
379    if (!isReactNativeCoreLibrary(library.config.name)) {
380      schemaPaths[library.config.name] = pathToSchema;
381    }
382  });
383}
384
385function createComponentProvider(
386  fabricEnabled,
387  schemaPaths,
388  node,
389  iosOutputDir,
390) {
391  if (fabricEnabled) {
392    console.log('\n\n>>>>> Creating component provider');
393    // Save the list of spec paths to a temp file.
394    const schemaListTmpPath = `${os.tmpdir()}/rn-tmp-schema-list.json`;
395    const fd = fs.openSync(schemaListTmpPath, 'w');
396    fs.writeSync(fd, JSON.stringify(schemaPaths));
397    fs.closeSync(fd);
398    console.log(`Generated schema list: ${schemaListTmpPath}`);
399
400    // Generate FabricComponentProvider.
401    // Only for iOS at this moment.
402    executeNodeScript(
403      node,
404      `${path.join(
405        RN_ROOT,
406        'scripts',
407        'generate-provider-cli.js',
408      )} --platform ios --schemaListPath "${schemaListTmpPath}" --outputDir ${iosOutputDir}`,
409    );
410    console.log(`Generated provider in: ${iosOutputDir}`);
411  }
412}
413
414function findCodegenEnabledLibraries(
415  appRootDir,
416  baseCodegenConfigFileDir,
417  codegenConfigFilename,
418  codegenConfigKey,
419) {
420  const pkgJson = readPackageJSON(appRootDir);
421  const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies};
422  const libraries = [];
423
424  handleReactNativeCodeLibraries(
425    libraries,
426    codegenConfigFilename,
427    codegenConfigKey,
428  );
429  handleThirdPartyLibraries(
430    libraries,
431    baseCodegenConfigFileDir,
432    dependencies,
433    codegenConfigFilename,
434    codegenConfigKey,
435  );
436  handleLibrariesFromReactNativeConfig(
437    libraries,
438    codegenConfigKey,
439    codegenConfigFilename,
440    appRootDir,
441  );
442  handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir);
443
444  return libraries;
445}
446
447// It removes all the empty files and empty folders
448// it finds, starting from `filepath`, recursively.
449//
450// This function is needed since, after aligning the codegen between
451// iOS and Android, we have to create empty folders in advance and
452// we don't know wheter they will be populated up until the end of the process.
453//
454// @parameter filepath: the root path from which we want to remove the empty files and folders.
455function cleanupEmptyFilesAndFolders(filepath) {
456  const stats = fs.statSync(filepath);
457
458  if (stats.isFile() && stats.size === 0) {
459    fs.rmSync(filepath);
460    return;
461  } else if (stats.isFile()) {
462    return;
463  }
464
465  const dirContent = fs.readdirSync(filepath);
466  dirContent.forEach(contentPath =>
467    cleanupEmptyFilesAndFolders(path.join(filepath, contentPath)),
468  );
469
470  // The original folder may be filled with empty folders
471  // if that the case, we would also like to remove the parent.
472  // Hence, we need to read the folder again.
473  const newContent = fs.readdirSync(filepath);
474  if (newContent.length === 0) {
475    fs.rmdirSync(filepath);
476    return;
477  }
478}
479
480// Execute
481
482/**
483 * This function is the entry point for the codegen. It:
484 * - reads the package json
485 * - extracts the libraries
486 * - setups the CLI to generate the code
487 * - generate the code
488 *
489 * @parameter appRootDir: the directory with the app source code, where the `codegenConfigFilename` lives.
490 * @parameter outputPath: the base output path for the CodeGen.
491 * @parameter node: the path to the node executable, used to run the codegen scripts.
492 * @parameter codegenConfigFilename: the file that contains the codeGen configuration. The default is `package.json`.
493 * @parameter codegenConfigKey: the key in the codegenConfigFile that controls the codegen.
494 * @parameter baseCodegenConfigFileDir: the directory of the codeGenConfigFile.
495 * @parameter fabricEnabled: whether fabric is enabled or not.
496 * @throws If it can't find a config file for react-native.
497 * @throws If it can't find a CodeGen configuration in the file.
498 * @throws If it can't find a cli for the CodeGen.
499 */
500function execute(
501  appRootDir,
502  outputPath,
503  node,
504  codegenConfigFilename,
505  codegenConfigKey,
506  baseCodegenConfigFileDir,
507  fabricEnabled,
508) {
509  if (!isAppRootValid(appRootDir)) {
510    return;
511  }
512
513  try {
514    const libraries = findCodegenEnabledLibraries(
515      appRootDir,
516      baseCodegenConfigFileDir,
517      codegenConfigFilename,
518      codegenConfigKey,
519    );
520
521    if (libraries.length === 0) {
522      console.log('[Codegen] No codegen-enabled libraries found.');
523      return;
524    }
525
526    const codegenCliPath = getCodeGenCliPath();
527
528    const schemaPaths = {};
529
530    const iosOutputDir = computeIOSOutputDir(outputPath, appRootDir);
531
532    generateNativeCodegenFiles(
533      libraries,
534      fabricEnabled,
535      iosOutputDir,
536      node,
537      codegenCliPath,
538      schemaPaths,
539    );
540
541    createComponentProvider(fabricEnabled, schemaPaths, node, iosOutputDir);
542    cleanupEmptyFilesAndFolders(iosOutputDir);
543  } catch (err) {
544    console.error(err);
545    process.exitCode = 1;
546  }
547
548  console.log('\n\n[Codegen] Done.');
549  return;
550}
551
552module.exports = {
553  execute: execute,
554  // exported for testing purposes only:
555  _extractLibrariesFromJSON: extractLibrariesFromJSON,
556  _findCodegenEnabledLibraries: findCodegenEnabledLibraries,
557  _executeNodeScript: executeNodeScript,
558  _generateCode: generateCode,
559  _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders,
560};
561