From 2b77af6fd18d7a7504f9715cb993131f98413142 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 18:54:46 +0000 Subject: [PATCH] Skip ReactNativeFeatureFlags in react-private-interface typegen Minimum fix for react/react-native#57940: when react-private-interface gains a ReactNativeFeatureFlags export, do not translate src/private/featureflags/* into types_generated. - Skip react-native/react-private-interface in simpleResolve - Block feature-flags deps from react-private-interface.js.flow - Strip the feature-flags re-export from the generated .d.ts Co-authored-by: Alex Hunt --- .../build-types/resolution/getDependencies.js | 10 ++- .../resolution/reactPrivateInterface.js | 64 +++++++++++++++++++ .../build-types/resolution/simpleResolve.js | 8 +++ .../build-types/templates/api-extractor.json | 3 +- ...eactPrivateInterfaceFeatureFlagsExports.js | 48 ++++++++++++++ .../js-api/build-types/translateSourceFile.js | 25 ++++++-- 6 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 scripts/js-api/build-types/resolution/reactPrivateInterface.js create mode 100644 scripts/js-api/build-types/transforms/typescript/stripReactPrivateInterfaceFeatureFlagsExports.js diff --git a/scripts/js-api/build-types/resolution/getDependencies.js b/scripts/js-api/build-types/resolution/getDependencies.js index 42fb16ecf177..ef95220c2277 100644 --- a/scripts/js-api/build-types/resolution/getDependencies.js +++ b/scripts/js-api/build-types/resolution/getDependencies.js @@ -12,6 +12,7 @@ import type {DependencyContext} from './simpleResolve'; import type {ParseResult} from 'flow-transform/dist/transform/parse'; const resolveTypeInputFile = require('./resolveTypeInputFile'); +const {shouldExpandDependency} = require('./reactPrivateInterface'); const simpleResolve = require('./simpleResolve'); const debug = require('debug')('build-types:resolution'); const {traverse} = require('flow-transform/dist/traverse/traverse'); @@ -82,7 +83,14 @@ async function getDependencies( ); if (resolved != null) { - dependencies.add(resolveTypeInputFile(resolved) ?? resolved); + const dependency = resolveTypeInputFile(resolved) ?? resolved; + if (!shouldExpandDependency(filePath, dependency)) { + debug( + `Skipping feature-flags dependency from react-private-interface: '${importPath}' in ${filePath}`, + ); + return; + } + dependencies.add(dependency); } }), ); diff --git a/scripts/js-api/build-types/resolution/reactPrivateInterface.js b/scripts/js-api/build-types/resolution/reactPrivateInterface.js new file mode 100644 index 000000000000..19e16200bd47 --- /dev/null +++ b/scripts/js-api/build-types/resolution/reactPrivateInterface.js @@ -0,0 +1,64 @@ +/** + * 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 + */ + +const {REACT_NATIVE_PACKAGE_DIR} = require('../../../shared/consts'); +const path = require('node:path'); + +const REACT_PRIVATE_INTERFACE_IMPORT = 'react-native/react-private-interface'; + +const REACT_PRIVATE_INTERFACE_FILES: Set = new Set([ + path.join(REACT_NATIVE_PACKAGE_DIR, 'src/react-private-interface.js'), + path.join(REACT_NATIVE_PACKAGE_DIR, 'src/react-private-interface.js.flow'), +]); + +const FEATURE_FLAGS_PREFIX = + 'src' + path.sep + 'private' + path.sep + 'featureflags' + path.sep; + +function isReactPrivateInterfaceImport(importPath: string): boolean { + return importPath === REACT_PRIVATE_INTERFACE_IMPORT; +} + +function isReactPrivateInterfaceFile(filePath: string): boolean { + return REACT_PRIVATE_INTERFACE_FILES.has(path.resolve(filePath)); +} + +function isFeatureFlagsSourceDependency(depFile: string): boolean { + const relativeFromPackage = path.relative( + REACT_NATIVE_PACKAGE_DIR, + path.resolve(depFile), + ); + + if (relativeFromPackage === '' || relativeFromPackage.startsWith('..')) { + return false; + } + + return ( + relativeFromPackage === + 'src' + path.sep + 'private' + path.sep + 'featureflags' || + relativeFromPackage.startsWith(FEATURE_FLAGS_PREFIX) + ); +} + +/** + * `react-private-interface` exposes `ReactNativeFeatureFlags` at runtime only. + * Do not expand its feature-flags dependencies into generated types. + */ +function shouldExpandDependency(fromFile: string, depFile: string): boolean { + if (!isReactPrivateInterfaceFile(fromFile)) { + return true; + } + return !isFeatureFlagsSourceDependency(depFile); +} + +module.exports = { + isReactPrivateInterfaceFile, + isReactPrivateInterfaceImport, + shouldExpandDependency, +}; diff --git a/scripts/js-api/build-types/resolution/simpleResolve.js b/scripts/js-api/build-types/resolution/simpleResolve.js index 8bdb14383ca5..317a0fb51a41 100644 --- a/scripts/js-api/build-types/resolution/simpleResolve.js +++ b/scripts/js-api/build-types/resolution/simpleResolve.js @@ -10,6 +10,9 @@ const {PACKAGES_DIR} = require('../../../shared/consts'); const {getPackages} = require('../../../shared/monorepoUtils'); +const { + isReactPrivateInterfaceImport, +} = require('./reactPrivateInterface'); const {existsSync} = require('node:fs'); const path = require('node:path'); @@ -38,6 +41,11 @@ async function simpleResolve( }); } + // Runtime-only package export — not part of the generated type graph. + if (isReactPrivateInterfaceImport(importPath)) { + return null; + } + // Resolve exact '@react-native/' import if (importPath in cachedProjectInfo) { const packageJson = cachedProjectInfo[importPath].packageJson; diff --git a/scripts/js-api/build-types/templates/api-extractor.json b/scripts/js-api/build-types/templates/api-extractor.json index d9215e1691ee..1e0b8c15bf41 100644 --- a/scripts/js-api/build-types/templates/api-extractor.json +++ b/scripts/js-api/build-types/templates/api-extractor.json @@ -8,8 +8,7 @@ "compilerOptions": { "lib": ["es2020"], "types": [], - "moduleResolution": "bundler", - "customConditions": ["react-native-strict-api"] + "moduleResolution": "bundler" } } }, diff --git a/scripts/js-api/build-types/transforms/typescript/stripReactPrivateInterfaceFeatureFlagsExports.js b/scripts/js-api/build-types/transforms/typescript/stripReactPrivateInterfaceFeatureFlagsExports.js new file mode 100644 index 000000000000..b9a9683fc59e --- /dev/null +++ b/scripts/js-api/build-types/transforms/typescript/stripReactPrivateInterfaceFeatureFlagsExports.js @@ -0,0 +1,48 @@ +/** + * 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 + */ + +import type {PluginObj} from '@babel/core'; + +function isFeatureFlagsModuleSpecifier(specifier: string): boolean { + return ( + specifier.startsWith('./private/featureflags/') || + specifier.startsWith('../private/featureflags/') + ); +} + +/** + * `ReactNativeFeatureFlags` is exposed from `react-private-interface` at + * runtime only. Strip its re-export from the generated `.d.ts` when we skip + * translating `src/private/featureflags/*`. + */ +const stripReactPrivateInterfaceFeatureFlagsExports: PluginObj = { + visitor: { + ImportDeclaration(nodePath) { + if (isFeatureFlagsModuleSpecifier(nodePath.node.source.value)) { + nodePath.remove(); + } + }, + ExportNamedDeclaration(nodePath) { + if ( + nodePath.node.source != null && + isFeatureFlagsModuleSpecifier(nodePath.node.source.value) + ) { + nodePath.remove(); + } + }, + ExportAllDeclaration(nodePath) { + if (isFeatureFlagsModuleSpecifier(nodePath.node.source.value)) { + nodePath.remove(); + } + }, + }, +}; + +module.exports = stripReactPrivateInterfaceFeatureFlagsExports; diff --git a/scripts/js-api/build-types/translateSourceFile.js b/scripts/js-api/build-types/translateSourceFile.js index 842d89bc7430..6848a1f45719 100644 --- a/scripts/js-api/build-types/translateSourceFile.js +++ b/scripts/js-api/build-types/translateSourceFile.js @@ -13,6 +13,9 @@ import type {ParseResult} from 'flow-transform/dist/transform/parse'; import type {TransformASTResult} from 'flow-transform/dist/transform/transformAST'; const getDependencies = require('./resolution/getDependencies'); +const { + isReactPrivateInterfaceFile, +} = require('./resolution/reactPrivateInterface'); const applyBabelTransformsSeq = require('./utils/applyBabelTransformsSeq'); const translate = require('flow-api-translator'); const {parse, print} = require('flow-transform'); @@ -28,12 +31,22 @@ const preTransforms: Array = [ require('./transforms/flow/reattachDocComments'), require('./transforms/flow/ensureNoUnprefixedProps'), ]; -const postTransforms = (filePath: string): Array> => [ - require('./transforms/typescript/convertTypeAliasesToInterfaces'), - require('./transforms/typescript/ensureUndefinedOnOptionalMembers'), - require('./transforms/typescript/replaceProtectedConstructors'), - require('./transforms/typescript/replaceDefaultExportName')(filePath), -]; +const postTransforms = (filePath: string): Array> => { + const transforms: Array> = [ + require('./transforms/typescript/convertTypeAliasesToInterfaces'), + require('./transforms/typescript/ensureUndefinedOnOptionalMembers'), + require('./transforms/typescript/replaceProtectedConstructors'), + require('./transforms/typescript/replaceDefaultExportName')(filePath), + ]; + + if (isReactPrivateInterfaceFile(filePath)) { + transforms.push( + require('./transforms/typescript/stripReactPrivateInterfaceFeatureFlagsExports'), + ); + } + + return transforms; +}; const prettierOptions = {parser: 'babel'}; const unsupportedFeatureRegex = /Unsupported feature: Translating ".*" is currently not supported/;