Skip to content

Commit 75be907

Browse files
j-piaseckifacebook-github-bot
authored andcommitted
Integrate building a type snapshot into typegen script (#51765)
Summary: Pull Request resolved: #51765 Changelog: [Internal] Reviewed By: huntie Differential Revision: D70093373 fbshipit-source-id: 1b536f86b205f71c8a708c4ab6aaaedc3b8e9450
1 parent 48395d3 commit 75be907

15 files changed

+917
-132
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@babel/preset-env": "^7.25.3",
5151
"@babel/preset-flow": "^7.24.7",
5252
"@jest/create-cache-key-function": "^29.7.0",
53+
"@microsoft/api-extractor": "^7.52.2",
5354
"@react-native/metro-babel-transformer": "0.80.0-main",
5455
"@react-native/metro-config": "0.80.0-main",
5556
"@tsconfig/node18": "1.0.1",
@@ -105,6 +106,7 @@
105106
"shelljs": "^0.8.5",
106107
"signedsource": "^1.0.0",
107108
"supports-color": "^7.1.0",
109+
"temp-dir": "^2.0.0",
108110
"tinybench": "^3.1.0",
109111
"typescript": "5.0.4",
110112
"ws": "^6.2.3"
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
* @flow
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import type {PluginObj} from '@babel/core';
13+
14+
const {PACKAGES_DIR, REACT_NATIVE_PACKAGE_DIR} = require('../consts');
15+
const {API_EXTRACTOR_CONFIG_FILE, TYPES_OUTPUT_DIR} = require('./config');
16+
const apiSnapshotTemplate = require('./templates/ReactNativeApi.d.ts-template.js');
17+
const resolveCyclicImportsInDefinition = require('./transforms/resolveCyclicImportsInDefinition');
18+
const babel = require('@babel/core');
19+
const {
20+
Extractor,
21+
ExtractorConfig,
22+
// $FlowFixMe[cannot-resolve-module]
23+
} = require('@microsoft/api-extractor');
24+
const {promises: fs} = require('fs');
25+
const glob = require('glob');
26+
const path = require('path');
27+
const prettier = require('prettier');
28+
const osTempDir = require('temp-dir');
29+
30+
const postTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
31+
require('./transforms/fixReactImportName'),
32+
require('./transforms/sortTypeDefinitions'),
33+
require('./transforms/sortProperties'),
34+
require('./transforms/sortUnions'),
35+
];
36+
37+
async function buildAPISnapshot() {
38+
const tempDirectory = await createTempDir('react-native-js-api-snapshot');
39+
const packages = await findPackagesWithTypedef();
40+
41+
await preparePackagesInTempDir(tempDirectory, packages);
42+
await rewriteLocalImports(tempDirectory, packages);
43+
44+
const extractorConfig = ExtractorConfig.loadFileAndPrepare(
45+
path.join(tempDirectory, API_EXTRACTOR_CONFIG_FILE),
46+
);
47+
48+
const extractorResult = Extractor.invoke(extractorConfig, {
49+
localBuild: true,
50+
showVerboseMessages: true,
51+
});
52+
53+
if (extractorResult.succeeded) {
54+
const apiSnapshot = apiSnapshotTemplate(
55+
await getCleanedUpRollup(tempDirectory),
56+
);
57+
58+
await fs.writeFile(
59+
path.join(REACT_NATIVE_PACKAGE_DIR, 'ReactNativeApi.d.ts'),
60+
apiSnapshot,
61+
);
62+
} else {
63+
process.exitCode = 1;
64+
console.error(
65+
`API Extractor failed with ${extractorResult.errorCount} errors` +
66+
` and ${extractorResult.warningCount} warnings`,
67+
);
68+
}
69+
70+
await fs.rm(tempDirectory, {recursive: true});
71+
}
72+
73+
async function findPackagesWithTypedef() {
74+
const packagesWithGeneratedTypes = glob
75+
.sync(`${PACKAGES_DIR}/**/types_generated`, {nodir: false})
76+
.map(typesPath =>
77+
path.relative(PACKAGES_DIR, typesPath).split('/').slice(0, -1).join('/'),
78+
);
79+
80+
const packagesWithNames = await Promise.all(
81+
packagesWithGeneratedTypes.map(async pkg => {
82+
const packageJsonContent = await fs.readFile(
83+
path.join(PACKAGES_DIR, pkg, 'package.json'),
84+
'utf-8',
85+
);
86+
const packageJson = JSON.parse(packageJsonContent);
87+
88+
return {
89+
directory: pkg,
90+
name: packageJson.name as string,
91+
};
92+
}),
93+
);
94+
95+
return packagesWithNames;
96+
}
97+
98+
async function preparePackagesInTempDir(
99+
tempDirectory: string,
100+
packages: $ReadOnlyArray<{directory: string, name: string}>,
101+
) {
102+
await generateConfigFiles(tempDirectory);
103+
104+
await Promise.all(
105+
packages.map(async pkg => {
106+
await copyDirectory(
107+
path.join(PACKAGES_DIR, pkg.directory, TYPES_OUTPUT_DIR),
108+
path.join(tempDirectory, pkg.directory),
109+
);
110+
}),
111+
);
112+
}
113+
114+
/**
115+
* Rewrite imports to local packages in the temp directory. We do this to
116+
* avoid cyclic references, which API Extractor cannot process.
117+
*/
118+
async function rewriteLocalImports(
119+
tempDirectory: string,
120+
packages: $ReadOnlyArray<{directory: string, name: string}>,
121+
) {
122+
const definitions = glob.sync(`${tempDirectory}/**/*.d.ts`);
123+
124+
await Promise.all(
125+
definitions.map(async file => {
126+
const source = await fs.readFile(file, 'utf-8');
127+
const fixedImports = await resolveCyclicImportsInDefinition({
128+
packages: packages,
129+
rootPath: tempDirectory,
130+
sourcePath: file,
131+
source: source,
132+
});
133+
await fs.writeFile(file, fixedImports);
134+
}),
135+
);
136+
}
137+
138+
async function getCleanedUpRollup(tempDirectory: string) {
139+
const rollupPath = path.join(
140+
tempDirectory,
141+
'react-native',
142+
'dist',
143+
'api-rollup.d.ts',
144+
);
145+
const sourceRollup = await fs.readFile(rollupPath, 'utf-8');
146+
147+
const cleanedRollup = sourceRollup
148+
.replace(/\/\*[\s\S]*?\*\//gm, '') // Remove block comments
149+
.replace(/\\\\.*$/gm, '') // Remove inline comments
150+
.replace(/^\s+$/gm, '') // Clear whitespace-only lines
151+
.replace(/\n+/gm, '\n'); // Collapse empty lines
152+
153+
const transformedRollup = await applyPostTransforms(cleanedRollup);
154+
155+
const formattedRollup = prettier.format(transformedRollup, {
156+
parser: 'typescript',
157+
});
158+
159+
return formattedRollup;
160+
}
161+
162+
async function applyPostTransforms(inSrc: string): Promise<string> {
163+
const result = await babel.transformAsync(inSrc, {
164+
plugins: ['@babel/plugin-syntax-typescript', ...postTransforms],
165+
});
166+
167+
return result.code;
168+
}
169+
170+
async function generateConfigFiles(tempDirectory: string) {
171+
// generate tsconfig
172+
const tsConfig = {
173+
$schema: 'http://json.schemastore.org/tsconfig',
174+
};
175+
176+
const outPath = path.join(tempDirectory, 'tsconfig.json');
177+
178+
await fs.mkdir(path.dirname(outPath), {recursive: true});
179+
await fs.writeFile(outPath, JSON.stringify(tsConfig, null, 2));
180+
181+
// generate api extractor config
182+
const apiExtractorConfig = await fs.readFile(
183+
path.join(__dirname, 'templates', API_EXTRACTOR_CONFIG_FILE),
184+
'utf-8',
185+
);
186+
const adjustedApiExtractorConfig = apiExtractorConfig.replaceAll(
187+
'${typegen_directory}',
188+
tempDirectory,
189+
);
190+
await fs.writeFile(
191+
path.join(tempDirectory, API_EXTRACTOR_CONFIG_FILE),
192+
adjustedApiExtractorConfig,
193+
);
194+
195+
// generate basic package.json
196+
const packageJSON = {name: 'react-native'};
197+
await fs.writeFile(
198+
path.join(tempDirectory, 'package.json'),
199+
JSON.stringify(packageJSON, null, 2),
200+
);
201+
}
202+
203+
async function copyDirectory(src: string, dest: string) {
204+
await fs.mkdir(dest, {recursive: true});
205+
206+
const entries = await fs.readdir(src, {withFileTypes: true});
207+
208+
for (let entry of entries) {
209+
// name can only be a buffer when explicitly set as the encoding option
210+
const fileName: string = entry.name as any;
211+
const srcPath = path.join(src, fileName);
212+
const destPath = path.join(dest, fileName);
213+
214+
if (entry.isDirectory()) {
215+
await copyDirectory(srcPath, destPath);
216+
} else {
217+
await fs.copyFile(srcPath, destPath);
218+
}
219+
}
220+
}
221+
222+
async function createTempDir(dirName: string): Promise<string> {
223+
// $FlowExpectedError[incompatible-call] temp-dir is typed as a default export
224+
const tempDir = path.join(osTempDir, dirName);
225+
226+
await fs.mkdir(tempDir, {recursive: true});
227+
228+
return tempDir;
229+
}
230+
231+
module.exports = buildAPISnapshot;

scripts/build-types/config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ const IGNORE_PATTERNS = [
2929
*/
3030
const TYPES_OUTPUT_DIR = 'types_generated';
3131

32+
/**
33+
* The filename used for the configuration of @microsoft/api-extractor.
34+
*/
35+
const API_EXTRACTOR_CONFIG_FILE = 'api-extractor.json';
36+
3237
module.exports = {
38+
API_EXTRACTOR_CONFIG_FILE,
3339
ENTRY_POINT,
3440
IGNORE_PATTERNS,
3541
TYPES_OUTPUT_DIR,

scripts/build-types/index.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
require('../babel-register').registerForScript();
1212

13+
const buildApiSnapshot = require('./BuildApiSnapshot');
1314
const buildGeneratedTypes = require('./buildGeneratedTypes');
1415
const chalk = require('chalk');
1516
const debug = require('debug');
@@ -19,12 +20,13 @@ const config = {
1920
options: {
2021
debug: {type: 'boolean'},
2122
help: {type: 'boolean'},
23+
withSnapshot: {type: 'boolean'},
2224
},
2325
};
2426

2527
async function main() {
2628
const {
27-
values: {debug: debugEnabled, help},
29+
values: {debug: debugEnabled, help, withSnapshot},
2830
/* $FlowFixMe[incompatible-call] Natural Inference rollout. See
2931
* https://fburl.com/workplace/6291gfvu */
3032
} = parseArgs(config);
@@ -45,13 +47,21 @@ async function main() {
4547

4648
console.log(
4749
'\n' +
48-
chalk.bold.inverse.yellow(
49-
'Building generated react-native package types',
50-
) +
50+
chalk.bold.inverse('Building generated react-native package types') +
5151
'\n',
5252
);
5353

5454
await buildGeneratedTypes();
55+
56+
if (withSnapshot) {
57+
console.log(
58+
'\n' +
59+
chalk.bold.inverse.yellow('EXPERIMENTAL - Building API snapshot') +
60+
'\n',
61+
);
62+
63+
await buildApiSnapshot();
64+
}
5565
}
5666

5767
if (require.main === module) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
const signedsource = require('signedsource');
12+
13+
function apiSnapshotTemplate(source: string): string {
14+
return signedsource.signFile(
15+
`/**
16+
* Copyright (c) Meta Platforms, Inc. and affiliates.
17+
*
18+
* This source code is licensed under the MIT license found in the
19+
* LICENSE file in the root directory of this source tree.
20+
*
21+
* ${signedsource.getSigningToken()}
22+
*
23+
* This file was generated by scripts/build-types/index.js.
24+
*/
25+
26+
// ----------------------------------------------------------------------------
27+
// JavaScript API snapshot for react-native.
28+
//
29+
// This snapshot captures the public JavaScript API of React Native, based on
30+
// types exported by packages/react-native/index.js.flow.
31+
//
32+
// Modifications to this file indicate changes to the shape of JavaScript
33+
// values and types that can be imported from the react-native package.
34+
// ----------------------------------------------------------------------------
35+
36+
${source}
37+
`,
38+
);
39+
}
40+
41+
module.exports = apiSnapshotTemplate;

0 commit comments

Comments
 (0)