mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
a978d343a6
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38990 This PR adds auto-generation of Typescript definitions from Flow source code for packages using the shared monorepo build setup (https://github.com/facebook/react-native/pull/38718). Today, these are the following Node.js packages: - `packages/community-cli-plugin` - `packages/dev-middleware` (⬅️ `emitTypeScriptDefs` enabled) This also improves emitted Flow definitions (`.js.flow`), by using [`flow-api-translator`](https://www.npmjs.com/package/flow-api-translator) to strip implementations. **All changes** - Include `flow-api-translator` and configure this to emit type definitions as part of `yarn build`. - Add translation from Flow source to TypeScript definitions (`.d.ts`) adjacent to each built file. - Improve emitted Flow definitions (`.js.flow`), by using `flow-api-translator` to strip implementations (previously, source files were copied). The Flow and TS defs now mirror each other. - Add `emitFlowDefs` and `emitTypeScriptDefs` options to build config to configure the above. - Integrate TypeScript compiler to perform program validation on emitted `.d.ts` files. - This is based on this guide: https://github.com/microsoft/TypeScript-wiki/blob/main/Using-the-Compiler-API.md#a-minimal-compiler. - Throw an exception on the `rewritePackageExports` step if a package does not define an `"exports"` field. - Add minimal `flow-typed` definitions for `typescript` 😄. **Notes on [`flow-api-translator`](https://www.npmjs.com/package/flow-api-translator)** This project is experimental but is in a more mature state than when we evaluated it earlier in 2023. - It's now possible to run this tool on our new Node.js packages, since they are exclusively authored using `import`/`export` syntax (a requirement of the tool). - As a safety net, we run the TypeScript compiler against the generated program, which will fail the build. Changelog: [Internal] Reviewed By: robhogan Differential Revision: D48312463 fbshipit-source-id: 817edb35f911f52fa987946f2d8fc1a319078c9d
277 lines
7.4 KiB
JavaScript
277 lines
7.4 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 babel = require('@babel/core');
|
||
const {parseArgs} = require('@pkgjs/parseargs');
|
||
const chalk = require('chalk');
|
||
const translate = require('flow-api-translator');
|
||
const glob = require('glob');
|
||
const micromatch = require('micromatch');
|
||
const {promises: fs} = require('fs');
|
||
const path = require('path');
|
||
const prettier = require('prettier');
|
||
const ts = require('typescript');
|
||
const {
|
||
buildConfig,
|
||
getBabelConfig,
|
||
getBuildOptions,
|
||
getTypeScriptCompilerOptions,
|
||
} = require('./config');
|
||
|
||
const REPO_ROOT = path.resolve(__dirname, '../..');
|
||
const PACKAGES_DIR /*: string */ = path.join(REPO_ROOT, 'packages');
|
||
const SRC_DIR = 'src';
|
||
const BUILD_DIR = 'dist';
|
||
const JS_FILES_PATTERN = '**/*.js';
|
||
const IGNORE_PATTERN = '**/__{tests,mocks,fixtures}__/**';
|
||
|
||
const config = {
|
||
allowPositionals: true,
|
||
options: {
|
||
help: {type: 'boolean'},
|
||
},
|
||
};
|
||
|
||
async function build() {
|
||
const {
|
||
positionals: packageNames,
|
||
values: {help},
|
||
} = parseArgs(config);
|
||
|
||
if (help) {
|
||
console.log(`
|
||
Usage: node ./scripts/build/build.js <packages>
|
||
|
||
Build packages (shared monorepo build setup).
|
||
|
||
By default, builds all packages defined in ./scripts/build/config.js. If a
|
||
a package list is provided, builds only those specified.
|
||
`);
|
||
process.exitCode = 0;
|
||
return;
|
||
}
|
||
|
||
console.log('\n' + chalk.bold.inverse('Building packages') + '\n');
|
||
|
||
const packagesToBuild = packageNames.length
|
||
? packageNames.filter(packageName => packageName in buildConfig.packages)
|
||
: Object.keys(buildConfig.packages);
|
||
|
||
for (const packageName of packagesToBuild) {
|
||
await buildPackage(packageName);
|
||
}
|
||
|
||
process.exitCode = 0;
|
||
}
|
||
|
||
async function buildPackage(packageName /*: string */) {
|
||
const {emitTypeScriptDefs} = getBuildOptions(packageName);
|
||
const files = glob.sync(
|
||
path.resolve(PACKAGES_DIR, packageName, SRC_DIR, '**/*'),
|
||
{nodir: true},
|
||
);
|
||
|
||
process.stdout.write(
|
||
`${packageName} ${chalk.dim('.').repeat(72 - packageName.length)} `,
|
||
);
|
||
|
||
// Build all files matched for package
|
||
for (const file of files) {
|
||
await buildFile(path.normalize(file), true);
|
||
}
|
||
|
||
// Validate program for emitted .d.ts files
|
||
if (emitTypeScriptDefs) {
|
||
validateTypeScriptDefs(packageName);
|
||
}
|
||
|
||
// Rewrite package.json "exports" field (src -> dist)
|
||
await rewritePackageExports(packageName);
|
||
|
||
process.stdout.write(chalk.reset.inverse.bold.green(' DONE ') + '\n');
|
||
}
|
||
|
||
async function buildFile(file /*: string */, silent /*: boolean */ = false) {
|
||
const packageName = getPackageName(file);
|
||
const buildPath = getBuildPath(file);
|
||
const {emitFlowDefs, emitTypeScriptDefs} = getBuildOptions(packageName);
|
||
|
||
const logResult = ({copied, desc} /*: {copied: boolean, desc?: string} */) =>
|
||
silent ||
|
||
console.log(
|
||
chalk.dim(' - ') +
|
||
path.relative(PACKAGES_DIR, file) +
|
||
(copied ? ' -> ' + path.relative(PACKAGES_DIR, buildPath) : ' ') +
|
||
(desc != null ? ' (' + desc + ')' : ''),
|
||
);
|
||
|
||
if (micromatch.isMatch(file, IGNORE_PATTERN)) {
|
||
logResult({copied: false, desc: 'ignore'});
|
||
return;
|
||
}
|
||
|
||
await fs.mkdir(path.dirname(buildPath), {recursive: true});
|
||
|
||
if (!micromatch.isMatch(file, JS_FILES_PATTERN)) {
|
||
await fs.copyFile(file, buildPath);
|
||
logResult({copied: true, desc: 'copy'});
|
||
return;
|
||
}
|
||
|
||
const source = await fs.readFile(file, 'utf-8');
|
||
const prettierConfig = {parser: 'babel'};
|
||
|
||
// Transform source file using Babel
|
||
const transformed = prettier.format(
|
||
(await babel.transformFileAsync(file, getBabelConfig(packageName))).code,
|
||
prettierConfig,
|
||
);
|
||
await fs.writeFile(buildPath, transformed);
|
||
|
||
// Translate source Flow types for each type definition target
|
||
if (/@flow/.test(source)) {
|
||
await Promise.all([
|
||
emitFlowDefs
|
||
? fs.writeFile(
|
||
buildPath + '.flow',
|
||
await translate.translateFlowToFlowDef(source, prettierConfig),
|
||
)
|
||
: null,
|
||
emitTypeScriptDefs
|
||
? fs.writeFile(
|
||
buildPath.replace(/\.js$/, '') + '.d.ts',
|
||
await translate.translateFlowToTSDef(source, prettierConfig),
|
||
)
|
||
: null,
|
||
]);
|
||
}
|
||
|
||
logResult({copied: true});
|
||
}
|
||
|
||
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(path.join(packageDir, SRC_DIR), BUILD_DIR),
|
||
);
|
||
}
|
||
|
||
async function rewritePackageExports(packageName /*: string */) {
|
||
const packageJsonPath = path.join(PACKAGES_DIR, packageName, 'package.json');
|
||
const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||
|
||
if (pkg.exports == null) {
|
||
throw new Error(
|
||
packageName +
|
||
' does not define an "exports" field in its package.json. As part ' +
|
||
'of the build setup, this field must be used in order to rewrite ' +
|
||
'paths to built files in production.',
|
||
);
|
||
}
|
||
|
||
pkg.exports = rewriteExportsField(pkg.exports);
|
||
|
||
await fs.writeFile(
|
||
packageJsonPath,
|
||
prettier.format(JSON.stringify(pkg), {parser: 'json'}),
|
||
);
|
||
}
|
||
|
||
/*::
|
||
type ExportsField = {
|
||
[subpath: string]: ExportsField | string,
|
||
} | string;
|
||
*/
|
||
|
||
function rewriteExportsField(
|
||
exportsField /*: ExportsField */,
|
||
) /*: ExportsField */ {
|
||
if (typeof exportsField === 'string') {
|
||
return rewriteExportsTarget(exportsField);
|
||
}
|
||
|
||
for (const key in exportsField) {
|
||
if (typeof exportsField[key] === 'string') {
|
||
exportsField[key] = rewriteExportsTarget(exportsField[key]);
|
||
} else if (typeof exportsField[key] === 'object') {
|
||
exportsField[key] = rewriteExportsField(exportsField[key]);
|
||
}
|
||
}
|
||
|
||
return exportsField;
|
||
}
|
||
|
||
function rewriteExportsTarget(target /*: string */) /*: string */ {
|
||
return target.replace('./' + SRC_DIR + '/', './' + BUILD_DIR + '/');
|
||
}
|
||
|
||
function validateTypeScriptDefs(packageName /*: string */) {
|
||
const files = glob.sync(
|
||
path.resolve(PACKAGES_DIR, packageName, BUILD_DIR, '**/*.d.ts'),
|
||
);
|
||
const compilerOptions = {
|
||
...getTypeScriptCompilerOptions(packageName),
|
||
noEmit: true,
|
||
skipLibCheck: false,
|
||
};
|
||
const program = ts.createProgram(files, compilerOptions);
|
||
const emitResult = program.emit();
|
||
|
||
if (emitResult.diagnostics.length) {
|
||
for (const diagnostic of emitResult.diagnostics) {
|
||
if (diagnostic.file != null) {
|
||
let {line, character} = ts.getLineAndCharacterOfPosition(
|
||
diagnostic.file,
|
||
diagnostic.start,
|
||
);
|
||
let message = ts.flattenDiagnosticMessageText(
|
||
diagnostic.messageText,
|
||
'\n',
|
||
);
|
||
console.log(
|
||
// $FlowIssue[incompatible-use] Type refined above
|
||
`${diagnostic.file.fileName} (${line + 1},${
|
||
character + 1
|
||
}): ${message}`,
|
||
);
|
||
} else {
|
||
console.log(
|
||
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
|
||
);
|
||
}
|
||
}
|
||
|
||
throw new Error(
|
||
'Failing build because TypeScript errors were encountered for ' +
|
||
'generated type definitions.',
|
||
);
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
buildFile,
|
||
getBuildPath,
|
||
BUILD_DIR,
|
||
PACKAGES_DIR,
|
||
SRC_DIR,
|
||
};
|
||
|
||
if (require.main === module) {
|
||
// eslint-disable-next-line no-void
|
||
void build();
|
||
}
|