From c1a650def41f5c770744a7d61369c63e19fd9f32 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 19 May 2026 03:54:02 -0700 Subject: [PATCH] Add "noflow" entry point mode to react-native Summary: Reference impl for https://github.com/react-native-community/discussions-and-proposals/pull/949 (exporting, then abandoning). Differential Revision: D82653038 --- .gitignore | 9 +- packages/react-native/package.json | 3 + packages/virtualized-lists/package.json | 3 + scripts/build/babel/noflow.config.js | 26 ++++++ scripts/build/build.js | 110 ++++++++++++++++++++++-- scripts/build/config.js | 24 +++++- 6 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 scripts/build/babel/noflow.config.js diff --git a/.gitignore b/.gitignore index d1e403fa6aa7..599e6f0d4804 100644 --- a/.gitignore +++ b/.gitignore @@ -174,8 +174,13 @@ fix_*.patch /private/react-native-fantom/.out/ /private/react-native-fantom/tester/build/ -# [Experimental] Generated TS type definitions -/packages/**/types_generated/ +# JS build output +/packages/*/dist/ +/packages/*/dist_noflow/ +/packages/debugger-shell/build/ + +# Generated TS type definitions +/packages/*/types_generated/ # Python __pycache__/ diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 97b081c2702b..bac5ff7041ba 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -31,11 +31,13 @@ "exports": { ".": { "react-native-strict-api": "./types_generated/index.d.ts", + "react-native-noflow": "./dist_noflow/index.js", "types": "./types/index.d.ts", "default": "./index.js" }, "./*": { "react-native-strict-api": null, + "react-native-noflow": "./dist_noflow/*.js", "types": "./*.d.ts", "default": "./*.js" }, @@ -75,6 +77,7 @@ "files": [ "build.gradle.kts", "cli.js", + "dist_noflow", "flow", "gradle.properties", "gradle/libs.versions.toml", diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json index b7b3302778c4..923d919f6d2a 100644 --- a/packages/virtualized-lists/package.json +++ b/packages/virtualized-lists/package.json @@ -22,16 +22,19 @@ "exports": { ".": { "react-native-strict-api": "./types_generated/index.d.ts", + "react-native-noflow": "./dist_noflow/index.js", "types": "./index.d.ts", "default": "./index.js" }, "./*": { "types": null, + "react-native-noflow": "./dist_noflow/*.js", "default": "./*.js" }, "./package.json": "./package.json" }, "files": [ + "dist_noflow", "index.js", "index.d.ts", "Lists", diff --git a/scripts/build/babel/noflow.config.js b/scripts/build/babel/noflow.config.js new file mode 100644 index 000000000000..e60e0a60ae85 --- /dev/null +++ b/scripts/build/babel/noflow.config.js @@ -0,0 +1,26 @@ +/** + * 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 strict-local + * @format + */ + +// A config which strips and transforms Flow features, to be received by either +// Flow/JS compatible Babel presets. + +// IMPORTANT: We've given no public guarantee that we will preserve this +// transform. The app/frameworks requirement is still to apply all of +// @react-native/babel-preset. + +import type {BabelCoreOptions} from '@babel/core'; + +const config: BabelCoreOptions = { + sourceMaps: true, + presets: [require.resolve('@babel/preset-flow')], + plugins: [require.resolve('babel-plugin-syntax-hermes-parser')], +}; + +module.exports = config; diff --git a/scripts/build/build.js b/scripts/build/build.js index da2d642659c8..48e425b0beec 100644 --- a/scripts/build/build.js +++ b/scripts/build/build.js @@ -10,6 +10,10 @@ require('../shared/babelRegister').registerForScript(); +/*:: +import type {BuildOptions} from './config'; +*/ + const {PACKAGES_DIR, REPO_ROOT} = require('../shared/consts'); const { buildConfig, @@ -30,8 +34,9 @@ const {parseArgs, styleText} = require('util'); const SRC_DIR = 'src'; const BUILD_DIR = 'dist'; +const NOFLOW_BUILD_DIR = 'dist_noflow'; const JS_FILES_PATTERN = '**/*.js'; -const IGNORE_PATTERN = '**/__{tests,mocks,fixtures}__/**'; +const IGNORE_PATTERN = '**/__{tests,mocks,fixtures,flowtests}__/**'; const config = { allowPositionals: true, @@ -77,6 +82,13 @@ async function build() { let ok = true; for (const packageName of packagesToBuild) { + const {target} = getBuildOptions(packageName); + + if (target === 'noflow') { + await buildNoFlowTarget(packageName); + continue; + } + await buildPackage(packageName, prepack); } @@ -163,8 +175,9 @@ async function buildFile( ) { const {silent = false} = options; const packageName = getPackageName(file); - const buildPath = getBuildPath(file); - const {emitFlowDefs, emitTypeScriptDefs} = getBuildOptions(packageName); + const buildOptions = getBuildOptions(packageName); + const {emitFlowDefs, emitTypeScriptDefs} = buildOptions; + const buildPath = getBuildPath(file, buildOptions); const logResult = ({copied, desc} /*: {copied: boolean, desc?: string} */) => silent || @@ -226,6 +239,87 @@ async function buildFile( logResult({copied: true}); } +async function buildNoFlowTarget(packageName /*: string */) { + try { + const buildOptions = getBuildOptions(packageName); + const {srcOverride} = buildOptions; + + process.stdout.write( + `${packageName} ${styleText('yellow', '(noflow)')} ${styleText('dim', '.').repeat(63 - packageName.length)} `, + ); + + const files = glob.sync( + path.resolve( + PACKAGES_DIR, + packageName, + ...(srcOverride != null + ? ['{', srcOverride, '}'] + : [SRC_DIR, '**/*.js']), + ), + { + nodir: true, + ignore: [IGNORE_PATTERN], + }, + ); + + for (const file of files) { + const filePath = path.normalize(file); + const buildPath = getBuildPath(filePath, buildOptions); + const prettierConfig = {parser: 'babel'}; + const source = await fs.readFile(file, 'utf-8'); + + await fs.mkdir(path.dirname(buildPath), {recursive: true}); + + // If file contains `@noflow`, copy only + if (/@noflow/.test(source)) { + await fs.copyFile(file, buildPath); + continue; + } + + // Apply Flow transforms + const babelResult = await babel.transformFileAsync( + filePath, + getBabelConfig(packageName), + ); + const transformed = await prettier.format( + babelResult.code, + /* $FlowFixMe[incompatible-type] Natural Inference rollout. See + * https://fburl.com/workplace/6291gfvu */ + prettierConfig, + ); + + // Write transformed file with source map comment + const relativeSourcePath = path.relative( + path.dirname(buildPath), + filePath, + ); + const sourceMapComment = `\n//# sourceMappingURL=${path.basename(buildPath)}.map`; + await fs.writeFile(buildPath, transformed + sourceMapComment); + + // Write source map file + if (babelResult.map) { + const sourceMapPath = buildPath + '.map'; + const sourceMap = { + ...babelResult.map, + sources: [relativeSourcePath], + }; + await fs.writeFile(sourceMapPath, JSON.stringify(sourceMap, null, 2)); + } + } + + process.stdout.write( + styleText(['reset', 'inverse', 'bold', 'green'], ' DONE '), + ); + } catch (e) { + process.stdout.write( + styleText(['reset', 'inverse', 'bold', 'red'], ' FAIL ') + '\n', + ); + throw e; + } finally { + process.stdout.write('\n'); + } +} + /*:: type PackageJson = { name: string, @@ -356,13 +450,19 @@ function getPackageName(file /*: string */) /*: string */ { return path.relative(PACKAGES_DIR, file).split(path.sep)[0]; } -function getBuildPath(file /*: string */) /*: string */ { +function getBuildPath( + file /*: string */, + {srcOverride, target} /*: BuildOptions */, +) /*: string */ { const packageDir = path.join(PACKAGES_DIR, getPackageName(file)); return path.join( packageDir, file - .replace(path.join(packageDir, SRC_DIR), BUILD_DIR) + .replace( + path.join(packageDir, srcOverride ? '' : SRC_DIR), + target === 'noflow' ? NOFLOW_BUILD_DIR : BUILD_DIR, + ) .replace('.flow.js', '.js'), ); } diff --git a/scripts/build/config.js b/scripts/build/config.js index 499da73c399b..0bced1935e12 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -14,13 +14,24 @@ const {ModuleResolutionKind} = require('typescript'); export type BuildOptions = Readonly<{ // The target runtime to compile for. - target: 'node', + target: + | 'node' + // A special compile target aligning with the "react-native-noflow" exports + // condition. This entry point allows compatible native parsers (e.g. Bun + // and SWC) to consume React Native without Flow types. Output will be stored + // in `dist_noflow/`. + | 'noflow', // Whether to emit Flow definition files (.js.flow) (default: true). emitFlowDefs?: boolean, // Whether to emit TypeScript definition files (.d.ts) (default: false). emitTypeScriptDefs?: boolean, + + // Source dir glob override (default: 'src/**/*'). This is intended to provide + // compatibility for the react-native package only. This setting is ignored + // unless using the 'noflow' compile target. + srcOverride?: string | null, }>; export type BuildConfig = Readonly<{ @@ -57,16 +68,25 @@ const buildConfig: BuildConfig = { emitTypeScriptDefs: true, target: 'node', }, + 'react-native': { + srcOverride: 'Libraries/**/*.js,src/**/*.js,index.js', + target: 'noflow', + }, 'react-native-compatibility-check': { emitTypeScriptDefs: true, target: 'node', }, + 'virtualized-lists': { + srcOverride: 'Lists/**/*.js,Utilities/**/*.js,index.js', + target: 'noflow', + }, }, }; const defaultBuildOptions = { emitFlowDefs: true, emitTypeScriptDefs: false, + srcOverride: null, }; function getBuildOptions( @@ -86,6 +106,8 @@ function getBabelConfig( switch (target) { case 'node': return require('./babel/node.config.js'); + case 'noflow': + return require('./babel/noflow.config.js'); } }