Extract the buildSchema function in the parsers-commons.js (#35158)

Summary:
This PR aims to extract  the buildSchema function into parsers-commons that is shared between typescript and flow. It is a task of https://github.com/facebook/react-native/issues/34872:
> Extract the buildSchema function ([Flow](https://github.com/facebook/react-native/blob/main/packages/react-native-codegen/src/parsers/flow/index.js#L66), [TypeScript](https://github.com/facebook/react-native/blob/main/packages/react-native-codegen/src/parsers/typescript/index.js#L72)) in the parsers-commons.js file to a top level buildSchema function which takes additional parameters to properly parse the content, get the config type and to build the schema, based on the language used.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->
[Internal] [Changed] - Extract the buildSchema function in the parsers-commons.js

Pull Request resolved: https://github.com/facebook/react-native/pull/35158

Test Plan:
yarn test:
<img width="380" alt="image" src="https://user-images.githubusercontent.com/40902940/209584411-40f66047-e25d-43d4-975d-af10cd202f24.png">

yarn flow:
<img width="150" alt="image" src="https://user-images.githubusercontent.com/40902940/209584423-4cf2cb5a-a300-40a6-962c-e57934f19ad2.png">

yarn lint:
<img width="510" alt="image" src="https://user-images.githubusercontent.com/40902940/209584440-2d2b2658-73d8-47e2-bb8c-64d4633369a2.png">

Reviewed By: cortinico

Differential Revision: D42386804

Pulled By: cipolleschi

fbshipit-source-id: 2a238f7cec982d8ef3fd57a34dc9f58171e32b53
This commit is contained in:
MaeIg 2023-01-12 04:23:42 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 91397d4247
Коммит dc4d73c954
15 изменённых файлов: 721 добавлений и 539 удалений

Просмотреть файл

@ -133,12 +133,11 @@ function rule(context) {
const {buildModuleSchema, createParserErrorCapturer, parser} =
requireModuleParser();
const flowParser = require('flow-parser');
const [parsingErrors, tryParse] = createParserErrorCapturer();
const sourceCode = context.getSourceCode().getText();
const ast = flowParser.parse(sourceCode, {enums: true});
const ast = parser.getAst(sourceCode);
tryParse(() => {
buildModuleSchema(hasteModuleName, ast, tryParse, parser);

Просмотреть файл

@ -17,22 +17,32 @@ import {
parseObjectProperty,
wrapNullable,
unwrapNullable,
buildSchemaFromConfigType,
buildSchema,
} from '../parsers-commons';
import type {ParserType} from '../errors';
const {Visitor} = require('../flow/Visitor');
const {wrapComponentSchema} = require('../flow/components/schema');
const {buildComponentSchema} = require('../flow/components');
const {buildModuleSchema} = require('../flow/modules');
const {isModuleRegistryCall} = require('../utils.js');
const {
ParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
} = require('../errors');
import {MockedParser} from '../parserMock';
import {TypeScriptParser} from '../typescript/parser';
const parser = new MockedParser();
const typeScriptParser = new TypeScriptParser();
const flowTranslateTypeAnnotation = require('../flow/modules/index');
const typeScriptTranslateTypeAnnotation = require('../typescript/modules/index');
beforeEach(() => {
jest.clearAllMocks();
});
describe('wrapNullable', () => {
describe('when nullable is true', () => {
it('returns nullable type annotation', () => {
@ -338,3 +348,428 @@ describe('parseObjectProperty', () => {
});
});
});
describe('buildSchemaFromConfigType', () => {
const astMock = {
type: 'Program',
loc: {
source: null,
start: {line: 2, column: 10},
end: {line: 16, column: 62},
},
range: [11, 373],
body: [],
comments: [],
errors: [],
};
const componentSchemaMock = {
filename: 'filename',
componentName: 'componentName',
extendsProps: [],
events: [],
props: [],
commands: [],
};
const moduleSchemaMock = {
type: 'NativeModule',
aliases: {},
spec: {properties: []},
moduleName: '',
};
const wrapComponentSchemaMock = jest.fn();
const buildComponentSchemaMock = jest.fn(_ => componentSchemaMock);
const buildModuleSchemaMock = jest.fn((_0, _1, _2, _3) => moduleSchemaMock);
const buildSchemaFromConfigTypeHelper = (
configType: 'module' | 'component' | 'none',
filename: ?string,
) =>
buildSchemaFromConfigType(
configType,
filename,
astMock,
wrapComponentSchemaMock,
buildComponentSchemaMock,
buildModuleSchemaMock,
parser,
);
describe('when configType is none', () => {
it('returns an empty schema', () => {
const schema = buildSchemaFromConfigTypeHelper('none');
expect(schema).toEqual({modules: {}});
});
});
describe('when configType is component', () => {
it('calls buildComponentSchema with ast and wrapComponentSchema with the result', () => {
buildSchemaFromConfigTypeHelper('component');
expect(buildComponentSchemaMock).toHaveBeenCalledTimes(1);
expect(buildComponentSchemaMock).toHaveBeenCalledWith(astMock);
expect(wrapComponentSchemaMock).toHaveBeenCalledTimes(1);
expect(wrapComponentSchemaMock).toHaveBeenCalledWith(componentSchemaMock);
expect(buildModuleSchemaMock).not.toHaveBeenCalled();
});
});
describe('when configType is module', () => {
describe('when filename is undefined', () => {
it('throws an error', () => {
expect(() => buildSchemaFromConfigTypeHelper('module')).toThrow(
'Filepath expected while parasing a module',
);
});
});
describe('when filename is null', () => {
it('throws an error', () => {
expect(() => buildSchemaFromConfigTypeHelper('module', null)).toThrow(
'Filepath expected while parasing a module',
);
});
});
describe('when filename is defined and not null', () => {
describe('when buildModuleSchema throws', () => {
it('throws the error', () => {
const parserError = new ParserError(
'moduleName',
astMock,
'Something went wrong',
);
buildModuleSchemaMock.mockImplementationOnce(() => {
throw parserError;
});
expect(() =>
buildSchemaFromConfigTypeHelper('module', 'filename'),
).toThrow(parserError);
});
});
describe('when buildModuleSchema returns null', () => {
it('throws an error', () => {
// $FlowIgnore[incompatible-call] - This is to test an invariant
buildModuleSchemaMock.mockReturnValueOnce(null);
expect(() =>
buildSchemaFromConfigTypeHelper('module', 'filename'),
).toThrow(
'When there are no parsing errors, the schema should not be null',
);
});
});
describe('when buildModuleSchema returns a schema', () => {
it('calls buildModuleSchema with ast and wrapModuleSchema with the result', () => {
buildSchemaFromConfigTypeHelper('module', 'filename');
expect(buildModuleSchemaMock).toHaveBeenCalledTimes(1);
expect(buildModuleSchemaMock).toHaveBeenCalledWith(
'filename',
astMock,
expect.any(Function),
parser,
);
expect(buildComponentSchemaMock).not.toHaveBeenCalled();
expect(wrapComponentSchemaMock).not.toHaveBeenCalled();
});
});
});
});
describe('isModuleRegistryCall', () => {
describe('when node is not of CallExpression type', () => {
it('returns false', () => {
const node = {
type: 'NotCallExpression',
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when node is of CallExpressionType', () => {
describe('when callee type is not of MemberExpression type', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'NotMemberExpression',
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when callee type is of MemberExpression type', () => {
describe('when memberExpression has an object of type different than "Identifier"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'NotIdentifier',
name: 'TurboModuleRegistry',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has an object of name different than "TurboModuleRegistry"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'NotTurboModuleRegistry',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has an object of type "Identifier" and name "TurboModuleRegistry', () => {
describe('when memberExpression has a property of type different than "Identifier"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'NotIdentifier',
name: 'get',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has a property of name different than "get" or "getEnforcing', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'NotGet',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has a property of type "Identifier" and of name "get" or "getEnforcing', () => {
describe('when memberExpression is computed', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'get',
},
computed: true,
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression is not computed', () => {
it('returns true', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'get',
},
computed: false,
},
};
expect(isModuleRegistryCall(node)).toBe(true);
});
});
});
});
});
});
});
});
describe('buildSchema', () => {
const getConfigTypeSpy = jest.spyOn(require('../utils'), 'getConfigType');
describe('when there is no codegenNativeComponent and no TurboModule', () => {
const contents = '';
it('returns an empty module', () => {
const schema = buildSchema(
contents,
'fileName',
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
parser,
);
expect(getConfigTypeSpy).not.toHaveBeenCalled();
expect(schema).toEqual({modules: {}});
});
});
describe('when there is a codegenNativeComponent', () => {
const contents = `
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
const codegenNativeComponent = require('codegenNativeComponent');
export type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
it('returns a module with good properties', () => {
const schema = buildSchema(
contents,
'fileName',
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
parser,
);
expect(getConfigTypeSpy).toHaveBeenCalledTimes(1);
expect(getConfigTypeSpy).toHaveBeenCalledWith(
parser.getAst(contents),
Visitor,
);
expect(schema).toEqual({
modules: {
Module: {
type: 'Component',
components: {
Module: {
extendsProps: [
{
type: 'ReactNativeBuiltInType',
knownTypeName: 'ReactNativeCoreViewProps',
},
],
events: [],
props: [],
commands: [],
},
},
},
},
});
});
});
describe('when there is a TurboModule', () => {
const contents = `
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
export interface Spec extends TurboModule {
+getArray: (a: Array<any>) => Array<string>;
}
export default (TurboModuleRegistry.getEnforcing<Spec>(
'SampleTurboModule',
): Spec);
`;
it('returns a module with good properties', () => {
const schema = buildSchema(
contents,
'fileName',
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
parser,
);
expect(getConfigTypeSpy).toHaveBeenCalledTimes(1);
expect(getConfigTypeSpy).toHaveBeenCalledWith(
parser.getAst(contents),
Visitor,
);
expect(schema).toEqual({
modules: {
fileName: {
type: 'NativeModule',
aliases: {},
spec: {
properties: [
{
name: 'getArray',
optional: false,
typeAnnotation: {
type: 'FunctionTypeAnnotation',
returnTypeAnnotation: {
type: 'ArrayTypeAnnotation',
elementType: {type: 'StringTypeAnnotation'},
},
params: [
{
name: 'a',
optional: false,
typeAnnotation: {type: 'ArrayTypeAnnotation'},
},
],
},
},
],
},
moduleName: 'SampleTurboModule',
excludedPlatforms: undefined,
},
},
});
});
});
});

Просмотреть файл

@ -11,15 +11,11 @@
'use strict';
const {MockedParser} = require('../parserMock');
const {
extractNativeModuleName,
createParserErrorCapturer,
verifyPlatforms,
visit,
buildSchemaFromConfigType,
isModuleRegistryCall,
} = require('../utils.js');
const {ParserError} = require('../errors');
@ -297,297 +293,3 @@ describe('visit', () => {
});
});
});
describe('buildSchemaFromConfigType', () => {
const parser = new MockedParser();
const astMock = {
type: 'Program',
loc: {
source: null,
start: {line: 2, column: 10},
end: {line: 16, column: 62},
},
range: [11, 373],
body: [],
comments: [],
errors: [],
};
const componentSchemaMock = {
filename: 'filename',
componentName: 'componentName',
extendsProps: [],
events: [],
props: [],
commands: [],
};
const moduleSchemaMock = {
type: 'NativeModule',
aliases: {},
spec: {properties: []},
moduleName: '',
};
const wrapComponentSchemaMock = jest.fn();
const buildComponentSchemaMock = jest.fn(_ => componentSchemaMock);
const wrapModuleSchemaMock = jest.spyOn(
require('../parsers-commons'),
'wrapModuleSchema',
);
const buildModuleSchemaMock = jest.fn((_0, _1, _2, _3) => moduleSchemaMock);
const buildSchemaFromConfigTypeHelper = (
configType: 'module' | 'component' | 'none',
filename: ?string,
) =>
buildSchemaFromConfigType(
configType,
filename,
astMock,
wrapComponentSchemaMock,
buildComponentSchemaMock,
buildModuleSchemaMock,
parser,
);
describe('when configType is none', () => {
it('returns an empty schema', () => {
const schema = buildSchemaFromConfigTypeHelper('none');
expect(schema).toEqual({modules: {}});
});
});
describe('when configType is component', () => {
it('calls buildComponentSchema with ast and wrapComponentSchema with the result', () => {
buildSchemaFromConfigTypeHelper('component');
expect(buildComponentSchemaMock).toHaveBeenCalledTimes(1);
expect(buildComponentSchemaMock).toHaveBeenCalledWith(astMock);
expect(wrapComponentSchemaMock).toHaveBeenCalledTimes(1);
expect(wrapComponentSchemaMock).toHaveBeenCalledWith(componentSchemaMock);
expect(buildModuleSchemaMock).not.toHaveBeenCalled();
expect(wrapModuleSchemaMock).not.toHaveBeenCalled();
});
});
describe('when configType is module', () => {
describe('when filename is undefined', () => {
it('throws an error', () => {
expect(() => buildSchemaFromConfigTypeHelper('module')).toThrow(
'Filepath expected while parasing a module',
);
});
});
describe('when filename is null', () => {
it('throws an error', () => {
expect(() => buildSchemaFromConfigTypeHelper('module', null)).toThrow(
'Filepath expected while parasing a module',
);
});
});
describe('when filename is defined and not null', () => {
describe('when buildModuleSchema throws', () => {
it('throws the error', () => {
const parserError = new ParserError(
'moduleName',
astMock,
'Something went wrong',
);
buildModuleSchemaMock.mockImplementationOnce(() => {
throw parserError;
});
expect(() =>
buildSchemaFromConfigTypeHelper('module', 'filename'),
).toThrow(parserError);
});
});
describe('when buildModuleSchema returns null', () => {
it('throws an error', () => {
// $FlowIgnore[incompatible-call] - This is to test an invariant
buildModuleSchemaMock.mockReturnValueOnce(null);
expect(() =>
buildSchemaFromConfigTypeHelper('module', 'filename'),
).toThrow(
'When there are no parsing errors, the schema should not be null',
);
});
});
describe('when buildModuleSchema returns a schema', () => {
it('calls buildModuleSchema with ast and wrapModuleSchema with the result', () => {
buildSchemaFromConfigTypeHelper('module', 'filename');
expect(buildModuleSchemaMock).toHaveBeenCalledTimes(1);
expect(buildModuleSchemaMock).toHaveBeenCalledWith(
'filename',
astMock,
expect.any(Function),
parser,
);
expect(wrapModuleSchemaMock).toHaveBeenCalledTimes(1);
expect(wrapModuleSchemaMock).toHaveBeenCalledWith(
moduleSchemaMock,
'filename',
);
expect(buildComponentSchemaMock).not.toHaveBeenCalled();
expect(wrapComponentSchemaMock).not.toHaveBeenCalled();
});
});
});
});
describe('isModuleRegistryCall', () => {
describe('when node is not of CallExpression type', () => {
it('returns false', () => {
const node = {
type: 'NotCallExpression',
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when node is of CallExpressionType', () => {
describe('when callee type is not of MemberExpression type', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'NotMemberExpression',
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when callee type is of MemberExpression type', () => {
describe('when memberExpression has an object of type different than "Identifier"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'NotIdentifier',
name: 'TurboModuleRegistry',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has an object of name different than "TurboModuleRegistry"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'NotTurboModuleRegistry',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has an object of type "Identifier" and name "TurboModuleRegistry', () => {
describe('when memberExpression has a property of type different than "Identifier"', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'NotIdentifier',
name: 'get',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has a property of name different than "get" or "getEnforcing', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'NotGet',
},
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression has a property of type "Identifier" and of name "get" or "getEnforcing', () => {
describe('when memberExpression is computed', () => {
it('returns false', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'get',
},
computed: true,
},
};
expect(isModuleRegistryCall(node)).toBe(false);
});
});
describe('when memberExpression is not computed', () => {
it('returns true', () => {
const node = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'TurboModuleRegistry',
},
property: {
type: 'Identifier',
name: 'get',
},
computed: false,
},
};
expect(isModuleRegistryCall(node)).toBe(true);
});
});
});
});
});
});
});
});

41
packages/react-native-codegen/src/parsers/flow/Visitor.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,41 @@
/**
* 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
* @format
*/
'use strict';
const {isModuleRegistryCall} = require('../utils');
function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): {
[type: string]: (node: $FlowFixMe) => void,
} {
return {
CallExpression(node: $FlowFixMe) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'codegenNativeComponent'
) {
infoMap.isComponent = true;
}
if (isModuleRegistryCall(node)) {
infoMap.isModule = true;
}
},
InterfaceExtends(node: $FlowFixMe) {
if (node.id.name === 'TurboModule') {
infoMap.isModule = true;
}
},
};
}
module.exports = {
Visitor,
};

Просмотреть файл

@ -1,78 +0,0 @@
/**
* 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
* @format
*/
'use strict';
import type {SchemaType} from '../../CodegenSchema';
import type {Parser} from '../parser';
// $FlowFixMe[untyped-import] there's no flowtype flow-parser
const flowParser = require('flow-parser');
const {
getConfigType,
buildSchemaFromConfigType,
isModuleRegistryCall,
} = require('../utils');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
function Visitor(infoMap: {isComponent: boolean, isModule: boolean}) {
return {
CallExpression(node: $FlowFixMe) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'codegenNativeComponent'
) {
infoMap.isComponent = true;
}
if (isModuleRegistryCall(node)) {
infoMap.isModule = true;
}
},
InterfaceExtends(node: $FlowFixMe) {
if (node.id.name === 'TurboModule') {
infoMap.isModule = true;
}
},
};
}
function buildSchema(
contents: string,
filename: ?string,
parser: Parser,
): SchemaType {
// Early return for non-Spec JavaScript files
if (
!contents.includes('codegenNativeComponent') &&
!contents.includes('TurboModule')
) {
return {modules: {}};
}
const ast = flowParser.parse(contents, {enums: true});
const configType = getConfigType(ast, Visitor);
return buildSchemaFromConfigType(
configType,
filename,
ast,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
parser,
);
}
module.exports = {
buildSchema,
};

Просмотреть файл

@ -13,19 +13,32 @@
import type {SchemaType} from '../../CodegenSchema.js';
const fs = require('fs');
const {buildSchema} = require('./buildSchema');
const {buildSchema} = require('../parsers-commons');
const {Visitor} = require('./Visitor');
const {FlowParser} = require('./parser');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
const parser = new FlowParser();
function parseModuleFixture(filename: string): SchemaType {
const contents = fs.readFileSync(filename, 'utf8');
return buildSchema(contents, 'path/NativeSampleTurboModule.js', parser);
return parseString(contents, 'path/NativeSampleTurboModule.js');
}
function parseString(contents: string, filename: ?string): SchemaType {
return buildSchema(contents, filename, parser);
return buildSchema(
contents,
filename,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
parser,
);
}
module.exports = {

Просмотреть файл

@ -20,7 +20,14 @@ import type {
import type {ParserType} from '../errors';
import type {Parser} from '../parser';
const {buildSchema} = require('./buildSchema');
// $FlowFixMe[untyped-import] there's no flowtype flow-parser
const flowParser = require('flow-parser');
const {buildSchema} = require('../parsers-commons');
const {Visitor} = require('./Visitor');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
const fs = require('fs');
@ -89,7 +96,21 @@ class FlowParser implements Parser {
parseFile(filename: string): SchemaType {
const contents = fs.readFileSync(filename, 'utf8');
return buildSchema(contents, filename, this);
return buildSchema(
contents,
filename,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
this,
);
}
getAst(contents: string): $FlowFixMe {
return flowParser.parse(contents, {
enums: true,
});
}
getFunctionTypeAnnotationParameters(

Просмотреть файл

@ -84,6 +84,13 @@ export interface Parser {
*/
parseFile(filename: string): SchemaType;
/**
* Given the content of a file, it returns an AST.
* @parameter contents: the content of the file.
* @returns: the AST of the file.
*/
getAst(contents: string): $FlowFixMe;
/**
* Given a FunctionTypeAnnotation, it returns an array of its parameters.
* @parameter functionTypeAnnotation: a FunctionTypeAnnotation

Просмотреть файл

@ -20,6 +20,8 @@ import type {
NativeModuleParamTypeAnnotation,
} from '../CodegenSchema';
// $FlowFixMe[untyped-import] there's no flowtype flow-parser
const flowParser = require('flow-parser');
const {
UnsupportedObjectPropertyTypeAnnotationParserError,
} = require('./errors');
@ -89,6 +91,12 @@ export class MockedParser implements Parser {
};
}
getAst(contents: string): $FlowFixMe {
return flowParser.parse(contents, {
enums: true,
});
}
getFunctionTypeAnnotationParameters(
functionTypeAnnotation: $FlowFixMe,
): $ReadOnlyArray<$FlowFixMe> {

Просмотреть файл

@ -27,6 +27,13 @@ import type {
import type {Parser} from './parser';
import type {ParserType} from './errors';
import type {ParserErrorCapturer, TypeDeclarationMap} from './utils';
import type {ComponentSchemaBuilderConfig} from './flow/components/schema';
const {
getConfigType,
extractNativeModuleName,
createParserErrorCapturer,
} = require('./utils');
const {
throwIfPropertyValueTypeIsUnsupported,
@ -365,6 +372,98 @@ function buildPropertySchema(
};
}
function buildSchemaFromConfigType(
configType: 'module' | 'component' | 'none',
filename: ?string,
ast: $FlowFixMe,
wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType,
buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig,
buildModuleSchema: (
hasteModuleName: string,
ast: $FlowFixMe,
tryParse: ParserErrorCapturer,
parser: Parser,
) => NativeModuleSchema,
parser: Parser,
): SchemaType {
switch (configType) {
case 'component': {
return wrapComponentSchema(buildComponentSchema(ast));
}
case 'module': {
if (filename === undefined || filename === null) {
throw new Error('Filepath expected while parasing a module');
}
const nativeModuleName = extractNativeModuleName(filename);
const [parsingErrors, tryParse] = createParserErrorCapturer();
const schema = tryParse(() =>
buildModuleSchema(nativeModuleName, ast, tryParse, parser),
);
if (parsingErrors.length > 0) {
/**
* TODO(T77968131): We have two options:
* - Throw the first error, but indicate there are more then one errors.
* - Display all errors, nicely formatted.
*
* For the time being, we're just throw the first error.
**/
throw parsingErrors[0];
}
invariant(
schema != null,
'When there are no parsing errors, the schema should not be null',
);
return wrapModuleSchema(schema, nativeModuleName);
}
default:
return {modules: {}};
}
}
function buildSchema(
contents: string,
filename: ?string,
wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType,
buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig,
buildModuleSchema: (
hasteModuleName: string,
ast: $FlowFixMe,
tryParse: ParserErrorCapturer,
parser: Parser,
) => NativeModuleSchema,
Visitor: ({isComponent: boolean, isModule: boolean}) => {
[type: string]: (node: $FlowFixMe) => void,
},
parser: Parser,
): SchemaType {
// Early return for non-Spec JavaScript files
if (
!contents.includes('codegenNativeComponent') &&
!contents.includes('TurboModule')
) {
return {modules: {}};
}
const ast = parser.getAst(contents);
const configType = getConfigType(ast, Visitor);
return buildSchemaFromConfigType(
configType,
filename,
ast,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
parser,
);
}
module.exports = {
wrapModuleSchema,
unwrapNullable,
@ -375,4 +474,6 @@ module.exports = {
translateDefault,
translateFunctionTypeAnnotation,
buildPropertySchema,
buildSchemaFromConfigType,
buildSchema,
};

47
packages/react-native-codegen/src/parsers/typescript/Visitor.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,47 @@
/**
* 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
* @format
*/
'use strict';
const {isModuleRegistryCall} = require('../utils');
function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): {
[type: string]: (node: $FlowFixMe) => void,
} {
return {
CallExpression(node: $FlowFixMe) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'codegenNativeComponent'
) {
infoMap.isComponent = true;
}
if (isModuleRegistryCall(node)) {
infoMap.isModule = true;
}
},
TSInterfaceDeclaration(node: $FlowFixMe) {
if (
Array.isArray(node.extends) &&
node.extends.some(
extension => extension.expression.name === 'TurboModule',
)
) {
infoMap.isModule = true;
}
},
};
}
module.exports = {
Visitor,
};

Просмотреть файл

@ -1,88 +0,0 @@
/**
* 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
* @format
*/
'use strict';
import type {SchemaType} from '../../CodegenSchema';
import type {Parser} from '../parser';
// $FlowFixMe[untyped-import] Use flow-types for @babel/parser
const babelParser = require('@babel/parser');
const {
buildSchemaFromConfigType,
getConfigType,
isModuleRegistryCall,
} = require('../utils');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
function Visitor(infoMap: {isComponent: boolean, isModule: boolean}) {
return {
CallExpression(node: $FlowFixMe) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'codegenNativeComponent'
) {
infoMap.isComponent = true;
}
if (isModuleRegistryCall(node)) {
infoMap.isModule = true;
}
},
TSInterfaceDeclaration(node: $FlowFixMe) {
if (
Array.isArray(node.extends) &&
node.extends.some(
extension => extension.expression.name === 'TurboModule',
)
) {
infoMap.isModule = true;
}
},
};
}
function buildSchema(
contents: string,
filename: ?string,
parser: Parser,
): SchemaType {
// Early return for non-Spec JavaScript files
if (
!contents.includes('codegenNativeComponent') &&
!contents.includes('TurboModule')
) {
return {modules: {}};
}
const ast = babelParser.parse(contents, {
sourceType: 'module',
plugins: ['typescript'],
}).program;
const configType = getConfigType(ast, Visitor);
return buildSchemaFromConfigType(
configType,
filename,
ast,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
parser,
);
}
module.exports = {
buildSchema,
};

Просмотреть файл

@ -13,19 +13,32 @@
import type {SchemaType} from '../../CodegenSchema.js';
const fs = require('fs');
const {buildSchema} = require('./buildSchema');
const {buildSchema} = require('../parsers-commons');
const {Visitor} = require('./Visitor');
const {TypeScriptParser} = require('./parser');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
const parser = new TypeScriptParser();
function parseModuleFixture(filename: string): SchemaType {
const contents = fs.readFileSync(filename, 'utf8');
return buildSchema(contents, 'path/NativeSampleTurboModule.ts', parser);
return parseString(contents, 'path/NativeSampleTurboModule.ts');
}
function parseString(contents: string, filename: ?string): SchemaType {
return buildSchema(contents, filename, parser);
return buildSchema(
contents,
filename,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
parser,
);
}
module.exports = {

Просмотреть файл

@ -20,7 +20,14 @@ import type {
import type {ParserType} from '../errors';
import type {Parser} from '../parser';
const {buildSchema} = require('./buildSchema');
// $FlowFixMe[untyped-import] Use flow-types for @babel/parser
const babelParser = require('@babel/parser');
const {buildSchema} = require('../parsers-commons');
const {Visitor} = require('./Visitor');
const {buildComponentSchema} = require('./components');
const {wrapComponentSchema} = require('./components/schema');
const {buildModuleSchema} = require('./modules');
const fs = require('fs');
@ -95,7 +102,22 @@ class TypeScriptParser implements Parser {
parseFile(filename: string): SchemaType {
const contents = fs.readFileSync(filename, 'utf8');
return buildSchema(contents, filename, this);
return buildSchema(
contents,
filename,
wrapComponentSchema,
buildComponentSchema,
buildModuleSchema,
Visitor,
this,
);
}
getAst(contents: string): $FlowFixMe {
return babelParser.parse(contents, {
sourceType: 'module',
plugins: ['typescript'],
}).program;
}
getFunctionTypeAnnotationParameters(

Просмотреть файл

@ -10,15 +10,9 @@
'use strict';
import type {ComponentSchemaBuilderConfig} from './flow/components/schema';
import type {NativeModuleSchema, SchemaType} from '../CodegenSchema';
import type {Parser} from './parser';
const {ParserError} = require('./errors');
const {wrapModuleSchema} = require('./parsers-commons');
const path = require('path');
const invariant = require('invariant');
export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe};
@ -125,60 +119,6 @@ function visit(
}
}
function buildSchemaFromConfigType(
configType: 'module' | 'component' | 'none',
filename: ?string,
ast: $FlowFixMe,
wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType,
buildComponentSchema: (ast: $FlowFixMe) => ComponentSchemaBuilderConfig,
buildModuleSchema: (
hasteModuleName: string,
ast: $FlowFixMe,
tryParse: ParserErrorCapturer,
parser: Parser,
) => NativeModuleSchema,
parser: Parser,
): SchemaType {
switch (configType) {
case 'component': {
return wrapComponentSchema(buildComponentSchema(ast));
}
case 'module': {
if (filename === undefined || filename === null) {
throw new Error('Filepath expected while parasing a module');
}
const nativeModuleName = extractNativeModuleName(filename);
const [parsingErrors, tryParse] = createParserErrorCapturer();
const schema = tryParse(() =>
buildModuleSchema(nativeModuleName, ast, tryParse, parser),
);
if (parsingErrors.length > 0) {
/**
* TODO(T77968131): We have two options:
* - Throw the first error, but indicate there are more then one errors.
* - Display all errors, nicely formatted.
*
* For the time being, we're just throw the first error.
**/
throw parsingErrors[0];
}
invariant(
schema != null,
'When there are no parsing errors, the schema should not be null',
);
return wrapModuleSchema(schema, nativeModuleName);
}
default:
return {modules: {}};
}
}
function getConfigType(
// TODO(T71778680): Flow-type this node.
ast: $FlowFixMe,
@ -254,6 +194,5 @@ module.exports = {
createParserErrorCapturer,
verifyPlatforms,
visit,
buildSchemaFromConfigType,
isModuleRegistryCall,
};