Files
react-native/scripts/build-types/buildGeneratedTypes.js
Alex Hunt d32ca8e899 Move build-types config into own module (#50450)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50450

Intends to be reused by the incoming `buildApiSnapshot` behaviour.

Changelog: [Internal]

Reviewed By: iwoplaza

Differential Revision: D72306509

fbshipit-source-id: 9e03c95f2469e10c05bdd0be39d4e217a6cc197f
2025-04-03 08:31:23 -07:00

175 lines
4.9 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
* @oncall react_native
*/
const {PACKAGES_DIR, REPO_ROOT} = require('../consts');
const {ENTRY_POINT, IGNORE_PATTERNS, TYPES_OUTPUT_DIR} = require('./config');
const getRequireStack = require('./resolution/getRequireStack');
const translatedModuleTemplate = require('./templates/translatedModule.d.ts-template');
const translateSourceFile = require('./translateSourceFile');
const {promises: fs} = require('fs');
const micromatch = require('micromatch');
const path = require('path');
/**
* Build generated TypeScript types for react-native.
*
* Bundles the react-native package and its type dependencies into an output
* directory of TypeScript definition files which will be published to npm.
*
* We translate Flow source code to TypeScript using
* [flow-api-translator](https://www.npmjs.com/package/flow-api-translator)
* along with our own pre and post-processing.
*/
async function buildGeneratedTypes(): Promise<void> {
const files = new Set<string>([path.join(REPO_ROOT, ENTRY_POINT)]);
const translatedFiles = new Set<string>();
const dependencyEdges: DependencyEdges = [];
const allErrors: Array<ModuleTranslationError> = [];
while (files.size > 0) {
const {dependencies, errors} = await translateSourceFiles(
dependencyEdges,
files,
);
dependencyEdges.push(...dependencies);
allErrors.push(...errors);
files.forEach(file => translatedFiles.add(file));
files.clear();
for (const [, dep] of dependencies) {
if (
!translatedFiles.has(dep) &&
!IGNORE_PATTERNS.some(pattern => micromatch.isMatch(dep, pattern))
) {
files.add(dep);
}
}
}
await fs.copyFile(
path.join(__dirname, 'templates/tsconfig.json'),
path.join(PACKAGES_DIR, 'react-native', TYPES_OUTPUT_DIR, 'tsconfig.json'),
);
if (allErrors.length > 0) {
process.exitCode = 1;
}
}
type DependencyEdges = Array<[string, string]>;
type TranslateFilesResult = {
dependencies: DependencyEdges,
errors: Array<ModuleTranslationError>,
};
async function translateSourceFiles(
dependencyEdges: DependencyEdges,
inputFiles: Iterable<string>,
): Promise<TranslateFilesResult> {
const files = new Set<string>([...inputFiles]);
const dependencies: DependencyEdges = [];
const errors: Array<ModuleTranslationError> = [];
await Promise.all(
Array.from(files).map(async file => {
const buildPath = getBuildPath(file);
const source = await fs.readFile(file, 'utf-8');
try {
const {result: typescriptDef, dependencies: fileDeps} =
await translateSourceFile(source, file);
for (const dep of fileDeps) {
dependencies.push([file, dep]);
}
await fs.mkdir(path.dirname(buildPath), {recursive: true});
await fs.writeFile(
buildPath,
translatedModuleTemplate({
originalFileName: path.relative(REPO_ROOT, file),
source: stripDocblock(typescriptDef),
tripleSlashDirectives: extractTripleSlashDirectives(source),
}),
);
} catch (e) {
const error = new ModuleTranslationError(file, e);
error.requireStack = getRequireStack(dependencyEdges, file);
errors.push(error);
console.error(error.formatError());
}
}),
);
return {dependencies, errors};
}
function getPackageName(file: string): string {
return path.relative(PACKAGES_DIR, file).split(path.sep)[0];
}
function getBuildPath(file: string): string {
const packageDir = path.join(PACKAGES_DIR, getPackageName(file));
return path.join(
packageDir,
file
.replace(packageDir, TYPES_OUTPUT_DIR)
.replace(/\.js\.flow$/, '.js')
.replace(/\.js$/, '.d.ts'),
);
}
function stripDocblock(source: string): string {
return source.replace(/\/\*\*[\s\S]*?\*\/\n/, '');
}
function extractTripleSlashDirectives(source: string): Array<string> {
const directives = source.match(/^\/\/\/.*$/gm);
if (directives == null) {
return [];
}
return directives.map(directive => directive.replace(/^\/\/\//g, '').trim());
}
class ModuleTranslationError extends Error {
requireStack: Array<string> = [];
originalError: Error;
constructor(filePath: string, originalError: Error) {
super(`Failed to build ${path.relative(REPO_ROOT, filePath)}`);
this.name = 'ModuleTranslationError';
this.originalError = originalError;
}
formatError(): string {
let output = `${this.name}: ${this.message}`;
if (this.originalError?.stack != null) {
output += `\n ${this.originalError.stack}`;
}
if (this.requireStack.length > 0) {
output += '\n Require stack:';
for (const stackEntry of this.requireStack) {
output += `\n - ${stackEntry}`;
}
}
return output;
}
}
module.exports = buildGeneratedTypes;