Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion scripts/js-api/build-types/resolution/getDependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}
}),
);
Expand Down
64 changes: 64 additions & 0 deletions scripts/js-api/build-types/resolution/reactPrivateInterface.js
Original file line number Diff line number Diff line change
@@ -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<string> = 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,
};
8 changes: 8 additions & 0 deletions scripts/js-api/build-types/resolution/simpleResolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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/<package>' import
if (importPath in cachedProjectInfo) {
const packageJson = cachedProjectInfo[importPath].packageJson;
Expand Down
3 changes: 1 addition & 2 deletions scripts/js-api/build-types/templates/api-extractor.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"compilerOptions": {
"lib": ["es2020"],
"types": [],
"moduleResolution": "bundler",
"customConditions": ["react-native-strict-api"]
"moduleResolution": "bundler"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<unknown> = {
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;
25 changes: 19 additions & 6 deletions scripts/js-api/build-types/translateSourceFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -28,12 +31,22 @@ const preTransforms: Array<PreTransformFn> = [
require('./transforms/flow/reattachDocComments'),
require('./transforms/flow/ensureNoUnprefixedProps'),
];
const postTransforms = (filePath: string): Array<PluginObj<unknown>> => [
require('./transforms/typescript/convertTypeAliasesToInterfaces'),
require('./transforms/typescript/ensureUndefinedOnOptionalMembers'),
require('./transforms/typescript/replaceProtectedConstructors'),
require('./transforms/typescript/replaceDefaultExportName')(filePath),
];
const postTransforms = (filePath: string): Array<PluginObj<unknown>> => {
const transforms: Array<PluginObj<unknown>> = [
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/;
Expand Down