mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
17466a07a5
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51864 Changelog: [Internal] Reviewed By: huntie Differential Revision: D76109534 fbshipit-source-id: d1b5d6016d16dab36c1872e85e8a1efcb2c89c7c
253 lines
7.1 KiB
JavaScript
253 lines
7.1 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
|
|
*/
|
|
|
|
import type {PluginObj} from '@babel/core';
|
|
|
|
const {PACKAGES_DIR, REACT_NATIVE_PACKAGE_DIR} = require('../consts');
|
|
const {API_EXTRACTOR_CONFIG_FILE, TYPES_OUTPUT_DIR} = require('./config');
|
|
const apiSnapshotTemplate = require('./templates/ReactNativeApi.d.ts-template.js');
|
|
const resolveCyclicImportsInDefinition = require('./transforms/resolveCyclicImportsInDefinition');
|
|
const babel = require('@babel/core');
|
|
const {
|
|
Extractor,
|
|
ExtractorConfig,
|
|
// $FlowFixMe[cannot-resolve-module]
|
|
} = require('@microsoft/api-extractor');
|
|
const {promises: fs} = require('fs');
|
|
const glob = require('glob');
|
|
const path = require('path');
|
|
const prettier = require('prettier');
|
|
const osTempDir = require('temp-dir');
|
|
|
|
const inputFilesPostTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
|
|
require('./transforms/renameDefaultExportedIdentifiers'),
|
|
];
|
|
|
|
const postTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
|
|
require('./transforms/sortTypeDefinitions'),
|
|
require('./transforms/sortProperties'),
|
|
require('./transforms/sortUnions'),
|
|
];
|
|
|
|
async function buildAPISnapshot() {
|
|
const tempDirectory = await createTempDir('react-native-js-api-snapshot');
|
|
const packages = await findPackagesWithTypedef();
|
|
|
|
await preparePackagesInTempDir(tempDirectory, packages);
|
|
await rewriteLocalImports(tempDirectory, packages);
|
|
|
|
const extractorConfig = ExtractorConfig.loadFileAndPrepare(
|
|
path.join(tempDirectory, API_EXTRACTOR_CONFIG_FILE),
|
|
);
|
|
|
|
const extractorResult = Extractor.invoke(extractorConfig, {
|
|
localBuild: true,
|
|
showVerboseMessages: true,
|
|
});
|
|
|
|
if (extractorResult.succeeded) {
|
|
const apiSnapshot = apiSnapshotTemplate(
|
|
await getCleanedUpRollup(tempDirectory),
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(REACT_NATIVE_PACKAGE_DIR, 'ReactNativeApi.d.ts'),
|
|
apiSnapshot,
|
|
);
|
|
} else {
|
|
process.exitCode = 1;
|
|
console.error(
|
|
`API Extractor failed with ${extractorResult.errorCount} errors` +
|
|
` and ${extractorResult.warningCount} warnings`,
|
|
);
|
|
}
|
|
|
|
await fs.rm(tempDirectory, {recursive: true});
|
|
}
|
|
|
|
async function findPackagesWithTypedef() {
|
|
const packagesWithGeneratedTypes = glob
|
|
.sync(`${PACKAGES_DIR}/**/types_generated`, {nodir: false})
|
|
.map(typesPath =>
|
|
path.relative(PACKAGES_DIR, typesPath).split('/').slice(0, -1).join('/'),
|
|
);
|
|
|
|
const packagesWithNames = await Promise.all(
|
|
packagesWithGeneratedTypes.map(async pkg => {
|
|
const packageJsonContent = await fs.readFile(
|
|
path.join(PACKAGES_DIR, pkg, 'package.json'),
|
|
'utf-8',
|
|
);
|
|
const packageJson = JSON.parse(packageJsonContent);
|
|
|
|
return {
|
|
directory: pkg,
|
|
name: packageJson.name as string,
|
|
};
|
|
}),
|
|
);
|
|
|
|
return packagesWithNames;
|
|
}
|
|
|
|
async function preparePackagesInTempDir(
|
|
tempDirectory: string,
|
|
packages: $ReadOnlyArray<{directory: string, name: string}>,
|
|
) {
|
|
await generateConfigFiles(tempDirectory);
|
|
|
|
await Promise.all(
|
|
packages.map(async pkg => {
|
|
await copyDirectory(
|
|
path.join(PACKAGES_DIR, pkg.directory, TYPES_OUTPUT_DIR),
|
|
path.join(tempDirectory, pkg.directory),
|
|
);
|
|
}),
|
|
);
|
|
|
|
const typeDefs = glob.sync(`${tempDirectory}/**/*.d.ts`);
|
|
await Promise.all(
|
|
typeDefs.map(async file => {
|
|
const source = await fs.readFile(file, 'utf-8');
|
|
const transformed = await applyPostTransforms(
|
|
source,
|
|
inputFilesPostTransforms,
|
|
);
|
|
await fs.writeFile(file, transformed);
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rewrite imports to local packages in the temp directory. We do this to
|
|
* avoid cyclic references, which API Extractor cannot process.
|
|
*/
|
|
async function rewriteLocalImports(
|
|
tempDirectory: string,
|
|
packages: $ReadOnlyArray<{directory: string, name: string}>,
|
|
) {
|
|
const definitions = glob.sync(`${tempDirectory}/**/*.d.ts`);
|
|
|
|
await Promise.all(
|
|
definitions.map(async file => {
|
|
const source = await fs.readFile(file, 'utf-8');
|
|
const fixedImports = await resolveCyclicImportsInDefinition({
|
|
packages: packages,
|
|
rootPath: tempDirectory,
|
|
sourcePath: file,
|
|
source: source,
|
|
});
|
|
await fs.writeFile(file, fixedImports);
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function getCleanedUpRollup(tempDirectory: string) {
|
|
const rollupPath = path.join(
|
|
tempDirectory,
|
|
'react-native',
|
|
'dist',
|
|
'api-rollup.d.ts',
|
|
);
|
|
const sourceRollup = await fs.readFile(rollupPath, 'utf-8');
|
|
|
|
const cleanedRollup = sourceRollup
|
|
.replace(/\/\*[\s\S]*?\*\//gm, '') // Remove block comments
|
|
.replace(/\\\\.*$/gm, '') // Remove inline comments
|
|
.replace(/^\s+$/gm, '') // Clear whitespace-only lines
|
|
.replace(/\n+/gm, '\n'); // Collapse empty lines
|
|
|
|
const transformedRollup = await applyPostTransforms(
|
|
cleanedRollup,
|
|
postTransforms,
|
|
);
|
|
|
|
const formattedRollup = prettier.format(transformedRollup, {
|
|
parser: 'typescript',
|
|
});
|
|
|
|
return formattedRollup;
|
|
}
|
|
|
|
async function applyPostTransforms(
|
|
inSrc: string,
|
|
transforms: $ReadOnlyArray<PluginObj<mixed>>,
|
|
): Promise<string> {
|
|
const result = await babel.transformAsync(inSrc, {
|
|
plugins: ['@babel/plugin-syntax-typescript', ...transforms],
|
|
});
|
|
|
|
return result.code;
|
|
}
|
|
|
|
async function generateConfigFiles(tempDirectory: string) {
|
|
// generate tsconfig
|
|
const tsConfig = {
|
|
$schema: 'http://json.schemastore.org/tsconfig',
|
|
};
|
|
|
|
const outPath = path.join(tempDirectory, 'tsconfig.json');
|
|
|
|
await fs.mkdir(path.dirname(outPath), {recursive: true});
|
|
await fs.writeFile(outPath, JSON.stringify(tsConfig, null, 2));
|
|
|
|
// generate api extractor config
|
|
const apiExtractorConfig = await fs.readFile(
|
|
path.join(__dirname, 'templates', API_EXTRACTOR_CONFIG_FILE),
|
|
'utf-8',
|
|
);
|
|
const adjustedApiExtractorConfig = apiExtractorConfig.replaceAll(
|
|
'${typegen_directory}',
|
|
tempDirectory,
|
|
);
|
|
await fs.writeFile(
|
|
path.join(tempDirectory, API_EXTRACTOR_CONFIG_FILE),
|
|
adjustedApiExtractorConfig,
|
|
);
|
|
|
|
// generate basic package.json
|
|
const packageJSON = {name: 'react-native'};
|
|
await fs.writeFile(
|
|
path.join(tempDirectory, 'package.json'),
|
|
JSON.stringify(packageJSON, null, 2),
|
|
);
|
|
}
|
|
|
|
async function copyDirectory(src: string, dest: string) {
|
|
await fs.mkdir(dest, {recursive: true});
|
|
|
|
const entries = await fs.readdir(src, {withFileTypes: true});
|
|
|
|
for (let entry of entries) {
|
|
// name can only be a buffer when explicitly set as the encoding option
|
|
const fileName: string = entry.name as any;
|
|
const srcPath = path.join(src, fileName);
|
|
const destPath = path.join(dest, fileName);
|
|
|
|
if (entry.isDirectory()) {
|
|
await copyDirectory(srcPath, destPath);
|
|
} else {
|
|
await fs.copyFile(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createTempDir(dirName: string): Promise<string> {
|
|
// $FlowExpectedError[incompatible-call] temp-dir is typed as a default export
|
|
const tempDir = path.join(osTempDir, dirName);
|
|
|
|
await fs.mkdir(tempDir, {recursive: true});
|
|
|
|
return tempDir;
|
|
}
|
|
|
|
module.exports = buildAPISnapshot;
|