From 17d52b1cd1be6e37e4f0ffe8aa594f55c49935f1 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 17 Jan 2025 06:27:15 -0800 Subject: [PATCH] Bootstrap experimental types build for main package (#48661) Summary: Changelog: [Internal] Reviewed By: iwoplaza Differential Revision: D68102875 --- scripts/build/build.js | 37 +++++++++++++++-- scripts/build/buildRNTypes.js | 77 +++++++++++++++++++++++++++++++++++ scripts/build/config.js | 12 +++++- 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 scripts/build/buildRNTypes.js diff --git a/scripts/build/build.js b/scripts/build/build.js index 4cb9de68a0a29c..bcbce6a8909b02 100644 --- a/scripts/build/build.js +++ b/scripts/build/build.js @@ -10,6 +10,7 @@ */ const {PACKAGES_DIR, REPO_ROOT} = require('../consts'); +const buildRNTypes = require('./buildRNTypes'); const { buildConfig, getBabelConfig, @@ -35,15 +36,16 @@ const IGNORE_PATTERN = '**/__{tests,mocks,fixtures}__/**'; const config = { allowPositionals: true, options: { - help: {type: 'boolean'}, check: {type: 'boolean'}, + experimentalBuildRNTypes: {type: 'boolean'}, + help: {type: 'boolean'}, }, }; async function build() { const { positionals: packageNames, - values: {help, check}, + values: {check, experimentalBuildRNTypes, help}, } = parseArgs(config); if (help) { @@ -54,6 +56,13 @@ async function build() { By default, builds all packages defined in ./scripts/build/config.js. If a a package list is provided, builds only those specified. + + Options: + --check Validate that no build artifacts have been accidentally + committed. + --experimentalBuildRNTypes + [Experimental] Enable source code -> type translation + output for the react-native package. `); process.exitCode = 0; return; @@ -63,10 +72,26 @@ async function build() { console.log('\n' + chalk.bold.inverse('Building packages') + '\n'); } - const packagesToBuild = packageNames.length + let packagesToBuild = packageNames.length ? packageNames.filter(packageName => packageName in buildConfig.packages) : Object.keys(buildConfig.packages); + if (packagesToBuild.includes('react-native')) { + // Remove react-native from the list of packages to build, only type generation is implemented + packagesToBuild = packagesToBuild.filter( + packageName => packageName !== 'react-native', + ); + + if (experimentalBuildRNTypes) { + await emitReactNativeTypes(); + } else if (packagesToBuild.length === 0) { + console.warn( + 'Building the react-native package must be enabled using ' + + '--experimentalBuildRNTypes.', + ); + } + } + let ok = true; for (const packageName of packagesToBuild) { if (check) { @@ -79,11 +104,15 @@ async function build() { process.exitCode = ok ? 0 : 1; } +async function emitReactNativeTypes() { + await buildRNTypes(); +} + async function checkPackage(packageName /*: string */) /*: Promise */ { const artifacts = await exportedBuildArtifacts(packageName); if (artifacts.length > 0) { console.log( - `${chalk.bgRed(packageName)}: has been build and the ${chalk.bold('build artifacts')} committed to the repository. This will break Flow checks.`, + `${chalk.bgRed(packageName)}: has been built and the ${chalk.bold('build artifacts')} committed to the repository. This will break Flow checks.`, ); return false; } diff --git a/scripts/build/buildRNTypes.js b/scripts/build/buildRNTypes.js new file mode 100644 index 00000000000000..544b6fd3b8f882 --- /dev/null +++ b/scripts/build/buildRNTypes.js @@ -0,0 +1,77 @@ +/** + * 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} = require('../consts'); +const translate = require('flow-api-translator'); +const {promises: fs} = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const TYPES_DIR = 'new-types'; +const PACKAGE_NAME = 'react-native'; + +// Start with Animated only as it using the export syntax. +const PATHS = ['Libraries/Animated']; + +async function buildRNTypes() { + const files = PATHS.flatMap(src_path => + glob.sync(path.resolve(PACKAGES_DIR, PACKAGE_NAME, src_path, '**/*.js'), { + nodir: true, + }), + ); + + console.log('Building RN types...'); + for (const file of files) { + // Don't build tests + if (/\/__tests__\//.test(file)) { + continue; + } + + const buildPath = getBuildPath(file); + const source = await fs.readFile(file, 'utf-8'); + const prettierConfig = {parser: 'babel'}; + + await fs.mkdir(path.dirname(buildPath), {recursive: true}); + + try { + const typescriptDef = await translate.translateFlowToTSDef( + source, + prettierConfig, + ); + + if ( + /Unsupported feature: Translating ".*" is currently not supported/.test( + typescriptDef, + ) + ) { + throw new Error('Syntax unsupported by flow-api-translator used in ' + file); + } + + await fs.writeFile(buildPath, typescriptDef); + } catch (e) { + console.error(`Failed to build ${file.replace(PACKAGES_DIR, '')}`); + } + } +} + +function getBuildPath(file /*: string */) /*: string */ { + const packageDir = path.join(PACKAGES_DIR, PACKAGE_NAME); + + return path.join( + packageDir, + file + .replace(packageDir, TYPES_DIR) + .replace(/\.flow\.js$/, '.js') + .replace(/\.js$/, '.d.ts'), + ); +} + +module.exports = buildRNTypes; diff --git a/scripts/build/config.js b/scripts/build/config.js index e889eb4d1b7986..cf40abddbeffdb 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -18,7 +18,12 @@ const {ModuleResolutionKind} = require('typescript'); /*:: export type BuildOptions = $ReadOnly<{ // The target runtime to compile for. - target: 'node', + target: + // Node.js + 'node' | + // [Experimental] react-native package only: Skip build, emit translated + // TypeScript types for 3P use. + 'react-native-emit-types', // Whether to emit Flow definition files (.js.flow) (default: true). emitFlowDefs?: boolean, @@ -58,6 +63,9 @@ const buildConfig /*: BuildConfig */ = { emitTypeScriptDefs: true, target: 'node', }, + 'react-native': { + target: 'react-native-emit-types', + }, }, }; @@ -83,6 +91,8 @@ function getBabelConfig( switch (target) { case 'node': return require('./babel/node.config.js'); + case 'react-native-emit-types': + return {}; // stub } }