Skip to content

Commit 17c47e5

Browse files
committed
Fix codegen discovery of formatted native specs
1 parent 024b474 commit 17c47e5

2 files changed

Lines changed: 166 additions & 2 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
'use strict';
12+
13+
const {FlowParser} = require('../../../parsers/flow/parser');
14+
const {TypeScriptParser} = require('../../../parsers/typescript/parser');
15+
const {combineSchemas} = require('../combine-js-to-schema');
16+
const fs = require('node:fs');
17+
18+
jest.mock('node:fs', () => ({
19+
...jest.requireActual('node:fs'),
20+
readFileSync: jest.fn(),
21+
}));
22+
23+
const readFileSync = jest.spyOn(fs, 'readFileSync');
24+
25+
describe.each([
26+
['Flow', 'js', new FlowParser()],
27+
['TypeScript', 'ts', new TypeScriptParser()],
28+
['TypeScript (.tsx)', 'tsx', new TypeScriptParser()],
29+
])('combineSchemas with %s', (language, extension, parser) => {
30+
const filename = `SampleNativeComponent.${extension}`;
31+
const componentSource = `
32+
import type {ViewProps} from 'react-native';
33+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
34+
${
35+
language === 'Flow'
36+
? 'type NativeProps = $ReadOnly<{...ViewProps}>;'
37+
: 'interface NativeProps extends ViewProps {}'
38+
}
39+
export default codegenNativeComponent<NativeProps>('Sample');
40+
`;
41+
const moduleFilename = `NativeSample.${extension}`;
42+
const moduleSource = `
43+
import type {TurboModule} from 'react-native';
44+
import {TurboModuleRegistry} from 'react-native';
45+
export interface Spec extends TurboModule {}
46+
export default TurboModuleRegistry.getEnforcing<Spec>('Sample');
47+
`;
48+
49+
afterEach(() => {
50+
jest.clearAllMocks();
51+
});
52+
53+
it.each([
54+
'codegenNativeComponent<',
55+
'codegenNativeComponent <',
56+
'codegenNativeComponent\n<',
57+
'codegenNativeComponent /* comment */ <',
58+
'codegenNativeComponent // comment\n<',
59+
'codegenNativeComponent // comment\r\n<',
60+
'codegenNativeComponent // comment\u2028<',
61+
'codegenNativeComponent // comment\u2029<',
62+
])('combines components using %s', call => {
63+
readFileSync.mockReturnValue(
64+
componentSource.replace('codegenNativeComponent<', call),
65+
);
66+
67+
const schema = parser.parseString(componentSource, filename);
68+
expect(Object.keys(schema.modules)).toHaveLength(1);
69+
expect(combineSchemas([filename], 'SampleLibrary')).toEqual({
70+
libraryName: 'SampleLibrary',
71+
modules: schema.modules,
72+
});
73+
});
74+
75+
it.each([
76+
'export default (\n',
77+
'export default(\n',
78+
'export /* comment */ default (\n',
79+
'export default ((\n',
80+
])('combines components with %s', declaration => {
81+
readFileSync.mockReturnValue(
82+
componentSource.replace(
83+
"export default codegenNativeComponent<NativeProps>('Sample');",
84+
`${declaration}
85+
codegenNativeComponent<NativeProps>('Sample')
86+
${declaration.includes('((') ? '))' : ')'};`,
87+
),
88+
);
89+
90+
expect(combineSchemas([filename]).modules).toEqual(
91+
parser.parseString(componentSource, filename).modules,
92+
);
93+
});
94+
95+
it.each([
96+
'extends TurboModule',
97+
'extends TurboModule',
98+
'extends\tTurboModule',
99+
'extends\nTurboModule',
100+
'extends /* comment */ TurboModule',
101+
'extends // comment\nTurboModule',
102+
'extends // comment\r\nTurboModule',
103+
'extends // comment\u2028TurboModule',
104+
'extends // comment\u2029TurboModule',
105+
])('combines modules using %s', declaration => {
106+
readFileSync.mockReturnValue(
107+
moduleSource.replace('extends TurboModule', declaration),
108+
);
109+
110+
const schema = parser.parseString(moduleSource, moduleFilename);
111+
expect(Object.keys(schema.modules)).toHaveLength(1);
112+
expect(combineSchemas([moduleFilename]).modules).toEqual(schema.modules);
113+
});
114+
115+
it.each([
116+
'',
117+
'not valid JavaScript',
118+
"export default 'codegenNativeComponent';",
119+
"export default 'TurboModule';",
120+
'/* codegenNativeComponent */',
121+
'/* TurboModule */',
122+
"import {TurboModuleRegistry} from 'react-native'; export default TurboModuleRegistry.get('Sample');",
123+
'export default // codegenNativeComponent<NativeProps>("Sample");',
124+
'interface Other extends // TurboModule {}',
125+
'export default /* first */ undefined; /* second */ codegenNativeComponent<NativeProps>("Sample");',
126+
'interface Other extends /* first */ Base {} /* second */ TurboModule;',
127+
])('ignores non-spec contents: %s', contents => {
128+
readFileSync.mockReturnValue(contents);
129+
130+
expect(combineSchemas([filename])).toEqual({libraryName: '', modules: {}});
131+
expect(readFileSync).toHaveBeenCalledTimes(1);
132+
});
133+
134+
it('ignores JSX files that only mention codegen identifiers in comments', () => {
135+
readFileSync.mockReturnValue(
136+
`/* codegenNativeComponent TurboModule */
137+
export default () => <View />;`,
138+
);
139+
140+
expect(combineSchemas(['SampleNativeComponent.tsx'])).toEqual({
141+
libraryName: '',
142+
modules: {},
143+
});
144+
expect(readFileSync).toHaveBeenCalledTimes(1);
145+
});
146+
147+
it('reports parser errors in formatted component specs', () => {
148+
readFileSync.mockReturnValue(
149+
componentSource.replace(
150+
"codegenNativeComponent<NativeProps>('Sample')",
151+
"codegenNativeComponent /* comment */ <MissingProps>('Sample')",
152+
),
153+
);
154+
155+
expect(() => combineSchemas([filename])).toThrow();
156+
});
157+
});

‎packages/react-native-codegen/src/cli/combine/combine-js-to-schema.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const {globSync} = require('tinyglobby');
2020

2121
const flowParser = new FlowParser();
2222
const typescriptParser = new TypeScriptParser();
23+
const trivia = String.raw`(?:\s|/\*(?:[^*]|\*(?!/))*\*/|//[^\r\n\u2028\u2029]*[\r\n\u2028\u2029])`;
24+
const componentDeclaration = new RegExp(
25+
String.raw`\bexport${trivia}+default\b${trivia}*(?:\(${trivia}*)*codegenNativeComponent${trivia}*<`,
26+
);
27+
const moduleDeclaration = new RegExp(
28+
String.raw`\bextends${trivia}+TurboModule\b`,
29+
);
2330

2431
function combineSchemas(
2532
files: Array<string>,
@@ -31,8 +38,8 @@ function combineSchemas(
3138

3239
if (
3340
contents &&
34-
(/export\s+default\s+\(?codegenNativeComponent</.test(contents) ||
35-
/extends TurboModule/.test(contents))
41+
(componentDeclaration.test(contents) ||
42+
moduleDeclaration.test(contents))
3643
) {
3744
const isTypeScript =
3845
path.extname(filename) === '.ts' || path.extname(filename) === '.tsx';

0 commit comments

Comments
 (0)