From 494964a4c9b3da234641d3262938c3dbf89b4c0b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 2 Jun 2026 06:49:08 -0700 Subject: [PATCH] Hide runtime-managed class constructors from public API (#57028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Mark `ReactNativeElement` and `ReadOnlyNode` as non-user-constructible in the generated TypeScript definitions by emitting `protected constructor()`. **Motivation** - Correctness — like web DOM elements (`HTMLElement`, etc.), `ReactNativeElement` instances are created by the React Native runtime, not in user space. See also D107227212. - Reduce public API surface (in particular, since this is also aliased to the `HostInstance` type). **Changes** - Adds a new `build-types protected-constructor` directive and corresponding `build-types` post-transform. - Applies directive to `ReactNativeElement` and `ReadOnlyNode`. - Also extracts shared `build-types` directive helpers into `transforms/typescript/utils/buildDirectives.js`. Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D107119545 --- packages/react-native/ReactNativeApi.d.ts | 221 ++++++++---------- .../webapis/dom/nodes/ReactNativeElement.js | 1 + .../private/webapis/dom/nodes/ReadOnlyNode.js | 1 + .../replaceProtectedConstructors-test.js | 107 +++++++++ .../convertTypeAliasesToInterfaces.js | 58 +---- .../replaceProtectedConstructors.js | 51 ++++ .../typescript/utils/buildDirectives.js | 59 +++++ .../js-api/build-types/translateSourceFile.js | 1 + 8 files changed, 330 insertions(+), 169 deletions(-) create mode 100644 scripts/js-api/build-types/transforms/typescript/__tests__/replaceProtectedConstructors-test.js create mode 100644 scripts/js-api/build-types/transforms/typescript/replaceProtectedConstructors.js create mode 100644 scripts/js-api/build-types/transforms/typescript/utils/buildDirectives.js diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 67486c67cf0b..a34c29584d98 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6996c96aed9284cd96a6fa1c64d7df71>> + * @generated SignedSource<<88d962e8bf936576e85b897a4192f39a>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -2727,10 +2727,6 @@ declare type InputValue = | null | undefined declare type Insets = Rect -declare type InstanceHandle = - | InternalInstanceHandle - | ReactNativeDocumentElementInstanceHandle - | ReactNativeDocumentInstanceHandle declare type Int32 = number declare type InternalInstanceHandle = symbol & { __InternalInstanceHandle__: string @@ -4006,9 +4002,6 @@ declare class ReactNativeDocument_default extends ReadOnlyNode_default { get nodeValue(): null get textContent(): null } -declare type ReactNativeDocumentElementInstanceHandle = symbol & { - __ReactNativeDocumentElementInstanceHandle__: string -} declare type ReactNativeDocumentInstanceHandle = symbol & { __ReactNativeDocumentInstanceHandle__: string } @@ -4017,12 +4010,7 @@ declare class ReactNativeElement_default implements NativeMethods { blur(): void - constructor( - tag: number, - viewConfig: ViewConfig, - instanceHandle: InstanceHandle, - ownerDocument: ReactNativeDocument_default, - ) + protected constructor() focus(): void measure(callback: MeasureOnSuccessCallback): void measureInWindow(callback: MeasureInWindowOnSuccessCallback): void @@ -4102,10 +4090,7 @@ declare class ReadOnlyNode_default extends ReadOnlyNodeBase { static TEXT_NODE: number get childNodes(): NodeList_default compareDocumentPosition(otherNode: ReadOnlyNode_default): number - constructor( - instanceHandle: InstanceHandle, - ownerDocument: null | ReactNativeDocument_default, - ) + protected constructor() contains(otherNode: ReadOnlyNode_default): boolean get firstChild(): null | ReadOnlyNode_default getRootNode(): ReadOnlyNode_default @@ -5957,23 +5942,23 @@ declare type WrapperComponentProvider = ( appParameters: Object, ) => React.ComponentType export { - AccessibilityActionEvent, // 5c5928b9 - AccessibilityInfo, // 539eb4b3 + AccessibilityActionEvent, // 9ead30c4 + AccessibilityInfo, // f7cbfa51 AccessibilityProps, // 5a2836fc AccessibilityRole, // f2f2e066 AccessibilityState, // b0c2b3f7 AccessibilityValue, // cf8bcb74 ActionSheetIOS, // b558559e ActionSheetIOSOptions, // 1756eb5a - ActivityIndicator, // f06b0687 - ActivityIndicatorProps, // 5e976856 + ActivityIndicator, // 7c0fa2e8 + ActivityIndicatorProps, // d3357183 Alert, // 5bf12165 AlertButton, // bf1a3b60 AlertButtonStyle, // ec9fb242 AlertOptions, // a0cdac0f AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 1a47480c + Animated, // d3264d4b AppConfig, // ce4209a7 AppRegistry, // 5edf0524 AppState, // 12012be5 @@ -5983,12 +5968,12 @@ export { AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 - BlurEvent, // e6151a1f + BlurEvent, // 4d39aa26 BoxShadowValue, // b679703f - Button, // 869e5a89 - ButtonProps, // 349967e6 + Button, // 2a455f1c + ButtonProps, // b276cb7a Clipboard, // 41addb89 - CodegenTypes, // 0b8108a8 + CodegenTypes, // 51ca21ff ColorSchemeName, // 6615edd6 ColorValue, // 98989a8f ComponentProvider, // b5c60ddd @@ -6004,9 +5989,9 @@ export { DimensionsPayload, // 653bc26c DisplayMetrics, // 1dc35cef DisplayMetricsAndroid, // 872e62eb - DrawerLayoutAndroid, // eb4bcfa5 - DrawerLayoutAndroidProps, // 1ddb208e - DrawerSlideEvent, // 0256d35a + DrawerLayoutAndroid, // 3939f773 + DrawerLayoutAndroidProps, // ec039f19 + DrawerSlideEvent, // 6a679d9c DropShadowValue, // e9df2606 DynamicColorIOS, // d96c228c DynamicColorIOSTuple, // 023ce58e @@ -6020,28 +6005,28 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // e47faa4e - FlatListProps, // 22cacca0 - FocusEvent, // 62fc1eb8 + FlatList, // fda604e6 + FlatListProps, // 4386b761 + FocusEvent, // 4fab86b8 FontVariant, // 7c7558bb - GestureResponderEvent, // f693e9a5 - GestureResponderHandlers, // cc70e4cb - HostComponent, // 16fccab5 - HostInstance, // 9b5a9ec2 + GestureResponderEvent, // 30249124 + GestureResponderHandlers, // 23b0d45f + HostComponent, // 277fe52e + HostInstance, // 3a2a75ad I18nManager, // f9870e00 IEventEmitter, // fbef6131 IOSKeyboardEvent, // e67bfe3a IgnorePattern, // ec6f6ece - Image, // 1891541a - ImageBackground, // cf95f60b - ImageBackgroundProps, // c68c896d - ImageErrorEvent, // d3ee606e - ImageLoadEvent, // 6b547ea5 - ImageProgressEventIOS, // 4c866a82 - ImageProps, // 801d1e1c + Image, // 10e30790 + ImageBackground, // ab05f7c3 + ImageBackgroundProps, // a35908a1 + ImageErrorEvent, // 3c2e70cc + ImageLoadEvent, // 6d3e7731 + ImageProgressEventIOS, // fb9bbc86 + ImageProps, // 196f0d32 ImagePropsAndroid, // 9fd9bcbb - ImagePropsBase, // 372b652b - ImagePropsIOS, // c4ae0c06 + ImagePropsBase, // fdbd3b49 + ImagePropsIOS, // 32e6747c ImageRequireSource, // 681d683b ImageResolvedAssetSource, // f3060931 ImageSize, // 1c47cf88 @@ -6053,12 +6038,12 @@ export { InputAccessoryViewProps, // ac36060b InputModeOptions, // 4e8581b9 Insets, // e7fe432a - KeyDownEvent, // d4971b72 + KeyDownEvent, // e446406b KeyEvent, // 20fa4267 - KeyUpEvent, // bc6bd87b + KeyUpEvent, // d4b54d8e Keyboard, // 49414c97 - KeyboardAvoidingView, // c8c29c8d - KeyboardAvoidingViewProps, // 4114f649 + KeyboardAvoidingView, // 6192aeef + KeyboardAvoidingViewProps, // 7c8e6d80 KeyboardEvent, // c3f895d4 KeyboardEventEasing, // af4091c8 KeyboardEventName, // 59299ad6 @@ -6071,7 +6056,7 @@ export { LayoutAnimationProperty, // 52995f01 LayoutAnimationType, // 2da0a29b LayoutAnimationTypes, // 081b3bde - LayoutChangeEvent, // c388ba2b + LayoutChangeEvent, // b0cb1b07 LayoutConformanceProps, // 055f03b8 LayoutRectangle, // 6601b294 Linking, // 9a6a174d @@ -6083,34 +6068,34 @@ export { MeasureInWindowOnSuccessCallback, // a285f598 MeasureLayoutOnSuccessCallback, // 3592502a MeasureOnSuccessCallback, // 82824e59 - Modal, // bdd9bb3f - ModalBaseProps, // 3cca97dc - ModalProps, // aa4211df + Modal, // 48ace2d7 + ModalBaseProps, // ad1ae814 + ModalProps, // 04199141 ModalPropsAndroid, // 515fb173 - ModalPropsIOS, // 144bbc95 - ModeChangeEvent, // a5e9864f - MouseEvent, // fdce82bc + ModalPropsIOS, // c16dab61 + ModeChangeEvent, // b030f9be + MouseEvent, // a33f8058 NativeAppEventEmitter, // 08d4c47d NativeColorValue, // d2094c29 - NativeComponentRegistry, // 21481a60 + NativeComponentRegistry, // 6497d2b6 NativeDialogManagerAndroid, // 5be8497e NativeEventEmitter, // 27f97c1a NativeEventSubscription, // de3942e7 - NativeMethods, // a2311987 - NativeMethodsMixin, // a819ef55 + NativeMethods, // ce1a8622 + NativeMethodsMixin, // 6127a27d NativeModules, // 4597cd36 - NativeMouseEvent, // 558d45b0 - NativePointerEvent, // f1763d80 + NativeMouseEvent, // ddcc5836 + NativePointerEvent, // 49e97dbb NativeScrollEvent, // caad7f53 - NativeSyntheticEvent, // 35855c4c + NativeSyntheticEvent, // 99ec1d60 NativeTouchEvent, // 59b676df NativeUIEvent, // 44ac26ac Networking, // bbc5be42 OpaqueColorValue, // 25f3fa5b - PanResponder, // 4320c1ba - PanResponderCallbacks, // ed54b109 + PanResponder, // ff2437d5 + PanResponderCallbacks, // 315230b0 PanResponderGestureState, // 54baf558 - PanResponderInstance, // 46b4629d + PanResponderInstance, // 51c64e49 Permission, // 06473f4f PermissionStatus, // 4b7de97b PermissionsAndroid, // db2a401e @@ -6120,29 +6105,29 @@ export { PlatformOSType, // 0a17561e PlatformSelectSpec, // 09ed7758 PointValue, // 69db075f - PointerEvent, // fe3989a1 - PressabilityConfig, // 6dedcb61 - PressabilityEventHandlers, // 3e6c0f56 - Pressable, // 0241ba70 + PointerEvent, // 8dd8fcfd + PressabilityConfig, // cf525d89 + PressabilityEventHandlers, // 8fcdfdf6 + Pressable, // 6039f73a PressableAndroidRippleConfig, // ee32eaca - PressableProps, // a9048420 + PressableProps, // 13d5a2f1 PressableStateCallbackType, // 9af36561 ProcessedColorValue, // 33f74304 - ProgressBarAndroid, // 00fcd180 - ProgressBarAndroidProps, // f59f8f03 + ProgressBarAndroid, // 3ea5543a + ProgressBarAndroidProps, // 84635506 PublicRootInstance, // 8040afd7 - PublicTextInstance, // cd0d8f8d + PublicTextInstance, // 265237c6 PushNotificationEventName, // 84e7e150 PushNotificationIOS, // b4d1fe78 PushNotificationPermissions, // c2e7ae4f Rationale, // 5df1b1c1 ReactNativeVersion, // abd76827 - RefreshControl, // ed67a3b2 - RefreshControlProps, // bfc35644 + RefreshControl, // 068b1015 + RefreshControlProps, // 1b07a4c7 RefreshControlPropsAndroid, // 99f64c97 RefreshControlPropsIOS, // 72a36381 Registry, // 6c39216d - ResponderSyntheticEvent, // fb10247c + ResponderSyntheticEvent, // 57478720 ReturnKeyTypeOptions, // afd47ba3 Role, // af7b889d RootTag, // 3cd10504 @@ -6150,21 +6135,21 @@ export { RootViewStyleProvider, // d4818465 Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaView, // a7cd92bb + SafeAreaView, // 13e5d7d8 ScaledSize, // 07e417c7 - ScrollEvent, // 5d529218 - ScrollResponderType, // 197f0107 + ScrollEvent, // 68939866 + ScrollResponderType, // f696d6fb ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // 938178e6 - ScrollViewImperativeMethods, // 27f6b917 - ScrollViewProps, // d5d9f043 + ScrollView, // 21775fc8 + ScrollViewImperativeMethods, // 314462c7 + ScrollViewProps, // 3354a492 ScrollViewPropsAndroid, // 44210553 - ScrollViewPropsIOS, // b34b696c + ScrollViewPropsIOS, // 7ca110e7 ScrollViewScrollToOptions, // 3313411e SectionBase, // b376bddc - SectionList, // 6b202d76 + SectionList, // f04d848d SectionListData, // 119baf83 - SectionListProps, // 6bdbf0ef + SectionListProps, // 9c8b2b1e SectionListRenderItem, // 1fad0435 SectionListRenderItemInfo, // 745e1992 Separators, // 6a45f7e3 @@ -6182,66 +6167,66 @@ export { StyleProp, // fa0e9b4a StyleSheet, // e77dd046 SubmitBehavior, // c4ddf490 - Switch, // 015be3f7 - SwitchChangeEvent, // 63e9c50b - SwitchProps, // 0dbf23ea + Switch, // bf145836 + SwitchChangeEvent, // f3013e4e + SwitchProps, // 9b60edbf Systrace, // 626d178c TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 608149e8 + Text, // 0937861d TextContentType, // 239b3ecc - TextInput, // 3b7016bb + TextInput, // ee8f6f5d TextInputAndroidProps, // 3f09ce49 - TextInputChangeEvent, // 3ab11bb4 - TextInputContentSizeChangeEvent, // f71f8571 - TextInputEndEditingEvent, // e5f70633 - TextInputFocusEvent, // 020507e6 + TextInputChangeEvent, // b5264e88 + TextInputContentSizeChangeEvent, // a6612e5e + TextInputEndEditingEvent, // ffeb6ebd + TextInputFocusEvent, // 6ae5be45 TextInputIOSProps, // 0d05a855 - TextInputKeyPressEvent, // 3924ad9b - TextInputProps, // 96c07405 - TextInputSelectionChangeEvent, // d4d10630 - TextInputSubmitEditingEvent, // 22885c31 - TextLayoutEvent, // 73ab173e - TextProps, // 4c29419c + TextInputKeyPressEvent, // fcead0c9 + TextInputProps, // 0fa27aa2 + TextInputSelectionChangeEvent, // 1a6383cf + TextInputSubmitEditingEvent, // e3152e2d + TextLayoutEvent, // c3e8821d + TextProps, // fb3a9124 TextStyle, // bb9b7a58 ToastAndroid, // 88a8969a - Touchable, // da3239ee - TouchableHighlight, // 49bbefe7 - TouchableHighlightProps, // f14a1131 - TouchableNativeFeedback, // d89b59a8 - TouchableNativeFeedbackProps, // a88ade2e - TouchableOpacity, // 8d1b023b - TouchableOpacityProps, // 908e84b9 - TouchableWithoutFeedback, // 71d446ec - TouchableWithoutFeedbackProps, // e0c1c566 + Touchable, // a05e8365 + TouchableHighlight, // 4f247d12 + TouchableHighlightProps, // 384d8d78 + TouchableNativeFeedback, // 855953dc + TouchableNativeFeedbackProps, // 1d2c2871 + TouchableOpacity, // eab90960 + TouchableOpacityProps, // 38265cbb + TouchableWithoutFeedback, // f000a22f + TouchableWithoutFeedbackProps, // e7f63a63 TransformsStyle, // 65e70f18 TurboModule, // dfe29706 TurboModuleRegistry, // 4ace6db2 UIManager, // a1a7cc01 UTFSequence, // ad625158 Vibration, // 31e4bbf8 - View, // 234d8db5 - ViewProps, // 6d644e8b - ViewPropsAndroid, // ac650c5c + View, // 5a1289a3 + ViewProps, // 1c9bc89c + ViewPropsAndroid, // b95e4831 ViewPropsIOS, // 58ee19bf ViewStyle, // 00a0f8fb VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e - VirtualizedListProps, // 9453ce86 + VirtualizedListProps, // cb75c897 VirtualizedSectionList, // 9fd9cd61 - VirtualizedSectionListProps, // 7a993da5 + VirtualizedSectionListProps, // e037ec57 WrapperComponentProvider, // 9cf3844c codegenNativeCommands, // 628a7c0a - codegenNativeComponent, // 2baac257 + codegenNativeComponent, // 65335a0c findNodeHandle, // 93f80214 processColor, // 6e877698 registerCallableModule, // 839c8cfe - requireNativeComponent, // 7f7f105a + requireNativeComponent, // 35636f3c useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 useColorScheme, // d585efdb - usePressability, // b4e21b46 + usePressability, // 581a946a useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index 0cd9e0f18016..01e65cfba575 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -67,6 +67,7 @@ const noop = () => {}; // was slower than this method because the engine has to create an object than // we then discard to create a new one. +/** @build-types protected-constructor */ class ReactNativeElement extends ReadOnlyElement implements NativeMethods { // These need to be accessible from `ReactFabricPublicInstanceUtils`. __nativeTag: number; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js index 03082500c2c8..2c1a2a375183 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js @@ -50,6 +50,7 @@ const ReadOnlyNodeBase: typeof Object = // extend this class so it inherits all the methods and it sets the class // hierarchy correctly. +/** @build-types protected-constructor */ class ReadOnlyNode extends ReadOnlyNodeBase { constructor( instanceHandle: InstanceHandle, diff --git a/scripts/js-api/build-types/transforms/typescript/__tests__/replaceProtectedConstructors-test.js b/scripts/js-api/build-types/transforms/typescript/__tests__/replaceProtectedConstructors-test.js new file mode 100644 index 000000000000..3ab7a5186b7a --- /dev/null +++ b/scripts/js-api/build-types/transforms/typescript/__tests__/replaceProtectedConstructors-test.js @@ -0,0 +1,107 @@ +/** + * 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 replaceProtectedConstructors = require('../replaceProtectedConstructors'); +const babel = require('@babel/core'); + +async function transform(code: string): Promise { + const result = await babel.transformAsync(code, { + plugins: ['@babel/plugin-syntax-typescript', replaceProtectedConstructors], + }); + + return result?.code ?? ''; +} + +describe('replaceProtectedConstructors', () => { + test('should not modify class without annotation', async () => { + const result = await transform( + `declare class Foo { + constructor(x: number); +}`, + ); + expect(result).toMatchInlineSnapshot(` +"declare class Foo { + constructor(x: number); +}" +`); + }); + + test('should replace constructor with empty protected constructor()', async () => { + const result = await transform( + `/** @build-types protected-constructor */ +declare class Foo { + constructor(x: number, y: string); + blur(): void; +}`, + ); + expect(result).toMatchInlineSnapshot(` +"declare class Foo { + protected constructor(); + blur(): void; +}" +`); + }); + + test('should handle exported class', async () => { + const result = await transform( + `/** @build-types protected-constructor */ +export declare class Foo { + constructor(x: number); +}`, + ); + expect(result).toMatchInlineSnapshot(` +"export declare class Foo { + protected constructor(); +}" +`); + }); + + test('should handle class with extends and implements', async () => { + const result = await transform( + `/** @build-types protected-constructor */ +declare class Foo extends Bar implements Baz { + constructor(tag: number, config: Config); + focus(): void; +}`, + ); + expect(result).toMatchInlineSnapshot(` +"declare class Foo extends Bar implements Baz { + protected constructor(); + focus(): void; +}" +`); + }); + + test('should preserve surrounding declarations', async () => { + const result = await transform( + `declare class Other { + constructor(x: number); +} +/** @build-types protected-constructor */ +declare class Foo { + constructor(x: number); +} +declare class Another { + constructor(y: string); +}`, + ); + expect(result).toMatchInlineSnapshot(` +"declare class Other { + constructor(x: number); +} +declare class Foo { + protected constructor(); +} +declare class Another { + constructor(y: string); +}" +`); + }); +}); diff --git a/scripts/js-api/build-types/transforms/typescript/convertTypeAliasesToInterfaces.js b/scripts/js-api/build-types/transforms/typescript/convertTypeAliasesToInterfaces.js index a9eb7c2375e9..c33238180983 100644 --- a/scripts/js-api/build-types/transforms/typescript/convertTypeAliasesToInterfaces.js +++ b/scripts/js-api/build-types/transforms/typescript/convertTypeAliasesToInterfaces.js @@ -13,6 +13,11 @@ import type {PluginObj} from '@babel/core'; import * as t from '@babel/types'; +const { + hasAnnotation, + stripAnnotationComments, +} = require('./utils/buildDirectives'); + const ANNOTATION_PATTERN = /@build-types\s+emit-as-interface\b/; /** @@ -24,7 +29,7 @@ const ANNOTATION_PATTERN = /@build-types\s+emit-as-interface\b/; * only possible on `interface` declarations (open), not `type` (closed). */ function convertToInterface(path: $FlowFixMe): void { - stripAnnotationComments(path); + stripAnnotationComments(path, ANNOTATION_PATTERN); const {typeAnnotation} = path.node; let innerType = typeAnnotation; @@ -98,32 +103,6 @@ function convertToInterface(path: $FlowFixMe): void { path.replaceWith(interfaceNode); } -function hasAnnotationInComments( - comments: ?ReadonlyArray<{type: string, value: string}>, -): boolean { - return ( - Array.isArray(comments) && - comments.some( - comment => - comment.type === 'CommentBlock' && - ANNOTATION_PATTERN.test(comment.value), - ) - ); -} - -function hasEmitAsInterfaceAnnotation(path: $FlowFixMe): boolean { - if (hasAnnotationInComments(path.node.leadingComments)) { - return true; - } - if ( - path.parentPath?.isExportNamedDeclaration() && - hasAnnotationInComments(path.parentPath.node.leadingComments) - ) { - return true; - } - return false; -} - function typeToExtendsClause( tsType: t.TSType, wrapInReadonly: boolean, @@ -156,33 +135,10 @@ function makePropertiesReadonly(members: Array): void { } } -function stripAnnotationComments(path: $FlowFixMe): void { - const filter = (comments: $FlowFixMe) => - comments?.filter( - (c: $FlowFixMe) => - !(c.type === 'CommentBlock' && ANNOTATION_PATTERN.test(c.value)), - ) ?? []; - path.node.leadingComments = filter(path.node.leadingComments); - if (path.parentPath?.isExportNamedDeclaration()) { - path.parentPath.node.leadingComments = filter( - path.parentPath.node.leadingComments, - ); - } - const target = path.parentPath?.isExportNamedDeclaration() - ? path.parentPath - : path; - const prevSibling = target.getPrevSibling(); - if (prevSibling?.node) { - prevSibling.node.trailingComments = filter( - prevSibling.node.trailingComments, - ); - } -} - const visitor: PluginObj = { visitor: { TSTypeAliasDeclaration(path) { - if (!hasEmitAsInterfaceAnnotation(path)) { + if (!hasAnnotation(path, ANNOTATION_PATTERN)) { return; } convertToInterface(path); diff --git a/scripts/js-api/build-types/transforms/typescript/replaceProtectedConstructors.js b/scripts/js-api/build-types/transforms/typescript/replaceProtectedConstructors.js new file mode 100644 index 000000000000..356430201cae --- /dev/null +++ b/scripts/js-api/build-types/transforms/typescript/replaceProtectedConstructors.js @@ -0,0 +1,51 @@ +/** + * 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 + * @oncall react_native + */ + +import type {PluginObj} from '@babel/core'; + +const { + hasAnnotation, + stripAnnotationComments, +} = require('./utils/buildDirectives'); + +const ANNOTATION_PATTERN = /@build-types\s+protected-constructor\b/; + +/** + * Replace the constructor of a class annotated with + * `@build-types protected-constructor` with `protected constructor()`. + * + * This is used to hide constructor signatures from the public API, indicating + * that instances are not user-constructible. + */ +const visitor: PluginObj = { + visitor: { + ClassDeclaration(path) { + if (!hasAnnotation(path, ANNOTATION_PATTERN)) { + return; + } + stripAnnotationComments(path, ANNOTATION_PATTERN); + + for (const member of path.node.body.body) { + if (member.kind === 'constructor') { + // $FlowFixMe[prop-missing] + member.accessibility = 'protected'; + // $FlowFixMe[prop-missing] + member.params = []; + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-type] + member.returnType = null; + } + } + }, + }, +}; + +module.exports = visitor; diff --git a/scripts/js-api/build-types/transforms/typescript/utils/buildDirectives.js b/scripts/js-api/build-types/transforms/typescript/utils/buildDirectives.js new file mode 100644 index 000000000000..859f009b90f3 --- /dev/null +++ b/scripts/js-api/build-types/transforms/typescript/utils/buildDirectives.js @@ -0,0 +1,59 @@ +/** + * 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 + * @oncall react_native + */ + +function hasAnnotationInComments( + comments: ?ReadonlyArray<{type: string, value: string}>, + pattern: RegExp, +): boolean { + return ( + Array.isArray(comments) && + comments.some( + comment => comment.type === 'CommentBlock' && pattern.test(comment.value), + ) + ); +} + +function hasAnnotation(path: $FlowFixMe, pattern: RegExp): boolean { + if (hasAnnotationInComments(path.node.leadingComments, pattern)) { + return true; + } + if ( + path.parentPath?.isExportNamedDeclaration() && + hasAnnotationInComments(path.parentPath.node.leadingComments, pattern) + ) { + return true; + } + return false; +} + +function stripAnnotationComments(path: $FlowFixMe, pattern: RegExp): void { + const filter = (comments: $FlowFixMe) => + comments?.filter( + (c: $FlowFixMe) => !(c.type === 'CommentBlock' && pattern.test(c.value)), + ) ?? []; + path.node.leadingComments = filter(path.node.leadingComments); + if (path.parentPath?.isExportNamedDeclaration()) { + path.parentPath.node.leadingComments = filter( + path.parentPath.node.leadingComments, + ); + } + const target = path.parentPath?.isExportNamedDeclaration() + ? path.parentPath + : path; + const prevSibling = target.getPrevSibling(); + if (prevSibling?.node) { + prevSibling.node.trailingComments = filter( + prevSibling.node.trailingComments, + ); + } +} + +module.exports = {hasAnnotation, stripAnnotationComments}; diff --git a/scripts/js-api/build-types/translateSourceFile.js b/scripts/js-api/build-types/translateSourceFile.js index 1987f84b00b7..44bfc2e7f07a 100644 --- a/scripts/js-api/build-types/translateSourceFile.js +++ b/scripts/js-api/build-types/translateSourceFile.js @@ -30,6 +30,7 @@ const preTransforms: Array = [ ]; const postTransforms = (filePath: string): Array> => [ require('./transforms/typescript/convertTypeAliasesToInterfaces'), + require('./transforms/typescript/replaceProtectedConstructors'), require('./transforms/typescript/replaceDefaultExportName')(filePath), ]; const prettierOptions = {parser: 'babel'};