Skip to content
Closed
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
221 changes: 103 additions & 118 deletions packages/react-native/ReactNativeApi.d.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> {
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);
}"
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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/;

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -156,33 +135,10 @@ function makePropertiesReadonly(members: Array<t.TSTypeElement>): 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<unknown> = {
visitor: {
TSTypeAliasDeclaration(path) {
if (!hasEmitAsInterfaceAnnotation(path)) {
if (!hasAnnotation(path, ANNOTATION_PATTERN)) {
return;
}
convertToInterface(path);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<unknown> = {
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;
Original file line number Diff line number Diff line change
@@ -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};
1 change: 1 addition & 0 deletions scripts/js-api/build-types/translateSourceFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const preTransforms: Array<PreTransformFn> = [
];
const postTransforms = (filePath: string): Array<PluginObj<unknown>> => [
require('./transforms/typescript/convertTypeAliasesToInterfaces'),
require('./transforms/typescript/replaceProtectedConstructors'),
require('./transforms/typescript/replaceDefaultExportName')(filePath),
];
const prettierOptions = {parser: 'babel'};
Expand Down
Loading