Skip to content

Commit d243be5

Browse files
cursoragenthuntie
andcommitted
Respect react-private-interface as runtime-only in js-api typegen
Hardcode support for the react-native/react-private-interface package export ("types": null) so build-types does not expand src/private/* dependencies into types_generated. This unblocks exposing ReactNativeFeatureFlags through react-private-interface (#57940). - Skip the import in simpleResolve - Do not follow src/private deps from react-private-interface.js.flow - Strip ./private/* re-exports from the generated .d.ts - Give api-extractor a minimal temp exports map for this subpath Co-authored-by: Alex Hunt <huntie@users.noreply.github.com>
1 parent 091ac61 commit d243be5

7 files changed

Lines changed: 171 additions & 11 deletions

File tree

‎scripts/js-api/build-types/buildApiSnapshot.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,13 @@ async function generateConfigFiles(tempDirectory: string) {
303303
adjustedApiExtractorConfig,
304304
);
305305

306-
// generate basic package.json
307-
const packageJSON = {name: 'react-native'};
306+
const {
307+
API_EXTRACTOR_TEMP_EXPORTS,
308+
} = require('./resolution/reactPrivateInterface');
309+
const packageJSON = {
310+
name: 'react-native',
311+
exports: API_EXTRACTOR_TEMP_EXPORTS,
312+
};
308313
await fs.writeFile(
309314
path.join(tempDirectory, 'package.json'),
310315
JSON.stringify(packageJSON, null, 2),

‎scripts/js-api/build-types/resolution/getDependencies.js‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {DependencyContext} from './simpleResolve';
1212
import type {ParseResult} from 'flow-transform/dist/transform/parse';
1313

1414
const resolveTypeInputFile = require('./resolveTypeInputFile');
15+
const {shouldExpandDependency} = require('./reactPrivateInterface');
1516
const simpleResolve = require('./simpleResolve');
1617
const debug = require('debug')('build-types:resolution');
1718
const {traverse} = require('flow-transform/dist/traverse/traverse');
@@ -82,7 +83,14 @@ async function getDependencies(
8283
);
8384

8485
if (resolved != null) {
85-
dependencies.add(resolveTypeInputFile(resolved) ?? resolved);
86+
const dependency = resolveTypeInputFile(resolved) ?? resolved;
87+
if (!shouldExpandDependency(filePath, dependency)) {
88+
debug(
89+
`Skipping private dependency from react-private-interface: '${importPath}' in ${filePath}`,
90+
);
91+
return;
92+
}
93+
dependencies.add(dependency);
8694
}
8795
}),
8896
);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
const {REACT_NATIVE_PACKAGE_DIR} = require('../../../shared/consts');
12+
const path = require('node:path');
13+
14+
const REACT_PRIVATE_INTERFACE_IMPORT = 'react-native/react-private-interface';
15+
16+
const REACT_PRIVATE_INTERFACE_FILES: Set<string> = new Set([
17+
path.join(REACT_NATIVE_PACKAGE_DIR, 'src/react-private-interface.js'),
18+
path.join(REACT_NATIVE_PACKAGE_DIR, 'src/react-private-interface.js.flow'),
19+
]);
20+
21+
function isReactPrivateInterfaceImport(importPath: string): boolean {
22+
return importPath === REACT_PRIVATE_INTERFACE_IMPORT;
23+
}
24+
25+
function isReactPrivateInterfaceFile(filePath: string): boolean {
26+
return REACT_PRIVATE_INTERFACE_FILES.has(path.resolve(filePath));
27+
}
28+
29+
function isPrivateSourceDependency(depFile: string): boolean {
30+
const relativeFromPackage = path.relative(
31+
REACT_NATIVE_PACKAGE_DIR,
32+
path.resolve(depFile),
33+
);
34+
35+
if (relativeFromPackage === '' || relativeFromPackage.startsWith('..')) {
36+
return false;
37+
}
38+
39+
const privatePrefix = 'src' + path.sep + 'private' + path.sep;
40+
return (
41+
relativeFromPackage === 'src' + path.sep + 'private' ||
42+
relativeFromPackage.startsWith(privatePrefix)
43+
);
44+
}
45+
46+
/**
47+
* `react-private-interface` is a runtime-only package export (`"types": null`).
48+
* Do not expand its `src/private/*` dependencies into generated types.
49+
*/
50+
function shouldExpandDependency(fromFile: string, depFile: string): boolean {
51+
if (!isReactPrivateInterfaceFile(fromFile)) {
52+
return true;
53+
}
54+
return !isPrivateSourceDependency(depFile);
55+
}
56+
57+
function isPrivateModuleSpecifier(specifier: string): boolean {
58+
return (
59+
specifier.startsWith('./private/') || specifier.startsWith('../private/')
60+
);
61+
}
62+
63+
/**
64+
* Minimal exports map for the api-extractor temp layout so TypeScript does not
65+
* treat `react-private-interface` as a typed entry point.
66+
*/
67+
const API_EXTRACTOR_TEMP_EXPORTS = {
68+
'.': {
69+
types: './react-native/index.d.ts',
70+
},
71+
'./react-private-interface': {
72+
types: null,
73+
default: './react-native/src/react-private-interface.js',
74+
},
75+
};
76+
77+
module.exports = {
78+
API_EXTRACTOR_TEMP_EXPORTS,
79+
isPrivateModuleSpecifier,
80+
isReactPrivateInterfaceFile,
81+
isReactPrivateInterfaceImport,
82+
shouldExpandDependency,
83+
};

‎scripts/js-api/build-types/resolution/simpleResolve.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
const {PACKAGES_DIR} = require('../../../shared/consts');
1212
const {getPackages} = require('../../../shared/monorepoUtils');
13+
const {
14+
isReactPrivateInterfaceImport,
15+
} = require('./reactPrivateInterface');
1316
const {existsSync} = require('node:fs');
1417
const path = require('node:path');
1518

@@ -38,6 +41,11 @@ async function simpleResolve(
3841
});
3942
}
4043

44+
// Runtime-only package export — not part of the generated type graph.
45+
if (isReactPrivateInterfaceImport(importPath)) {
46+
return null;
47+
}
48+
4149
// Resolve exact '@react-native/<package>' import
4250
if (importPath in cachedProjectInfo) {
4351
const packageJson = cachedProjectInfo[importPath].packageJson;

‎scripts/js-api/build-types/templates/api-extractor.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
"compilerOptions": {
99
"lib": ["es2020"],
1010
"types": [],
11-
"moduleResolution": "bundler",
12-
"customConditions": ["react-native-strict-api"]
11+
"moduleResolution": "bundler"
1312
}
1413
}
1514
},
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {PluginObj} from '@babel/core';
12+
13+
const {
14+
isPrivateModuleSpecifier,
15+
} = require('../../resolution/reactPrivateInterface');
16+
17+
/**
18+
* `react-private-interface` is exported at runtime only (`"types": null`).
19+
* Strip `./private/*` re-exports from its generated `.d.ts`.
20+
*/
21+
const stripReactPrivateInterfacePrivateExports: PluginObj<unknown> = {
22+
visitor: {
23+
ImportDeclaration(nodePath) {
24+
if (isPrivateModuleSpecifier(nodePath.node.source.value)) {
25+
nodePath.remove();
26+
}
27+
},
28+
ExportNamedDeclaration(nodePath) {
29+
if (
30+
nodePath.node.source != null &&
31+
isPrivateModuleSpecifier(nodePath.node.source.value)
32+
) {
33+
nodePath.remove();
34+
}
35+
},
36+
ExportAllDeclaration(nodePath) {
37+
if (isPrivateModuleSpecifier(nodePath.node.source.value)) {
38+
nodePath.remove();
39+
}
40+
},
41+
},
42+
};
43+
44+
module.exports = stripReactPrivateInterfacePrivateExports;

‎scripts/js-api/build-types/translateSourceFile.js‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import type {ParseResult} from 'flow-transform/dist/transform/parse';
1313
import type {TransformASTResult} from 'flow-transform/dist/transform/transformAST';
1414

1515
const getDependencies = require('./resolution/getDependencies');
16+
const {
17+
isReactPrivateInterfaceFile,
18+
} = require('./resolution/reactPrivateInterface');
1619
const applyBabelTransformsSeq = require('./utils/applyBabelTransformsSeq');
1720
const translate = require('flow-api-translator');
1821
const {parse, print} = require('flow-transform');
@@ -28,12 +31,22 @@ const preTransforms: Array<PreTransformFn> = [
2831
require('./transforms/flow/reattachDocComments'),
2932
require('./transforms/flow/ensureNoUnprefixedProps'),
3033
];
31-
const postTransforms = (filePath: string): Array<PluginObj<unknown>> => [
32-
require('./transforms/typescript/convertTypeAliasesToInterfaces'),
33-
require('./transforms/typescript/ensureUndefinedOnOptionalMembers'),
34-
require('./transforms/typescript/replaceProtectedConstructors'),
35-
require('./transforms/typescript/replaceDefaultExportName')(filePath),
36-
];
34+
const postTransforms = (filePath: string): Array<PluginObj<unknown>> => {
35+
const transforms: Array<PluginObj<unknown>> = [
36+
require('./transforms/typescript/convertTypeAliasesToInterfaces'),
37+
require('./transforms/typescript/ensureUndefinedOnOptionalMembers'),
38+
require('./transforms/typescript/replaceProtectedConstructors'),
39+
require('./transforms/typescript/replaceDefaultExportName')(filePath),
40+
];
41+
42+
if (isReactPrivateInterfaceFile(filePath)) {
43+
transforms.push(
44+
require('./transforms/typescript/stripReactPrivateInterfacePrivateExports'),
45+
);
46+
}
47+
48+
return transforms;
49+
};
3750
const prettierOptions = {parser: 'babel'};
3851
const unsupportedFeatureRegex =
3952
/Unsupported feature: Translating ".*" is currently not supported/;

0 commit comments

Comments
 (0)