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