Add lint rule preventing "Node" imports from react and migrate "Node" types to use React namespace in react-native (#50030)

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

In Flow importing Node from react is equivalent importing ReactNode in Typescript. During translation Node is not translated to ReactNode but React.Node is translated to React.ReactNode. The easiest solution is to migrate all "Node" types to "React.Node"  so that the translation is correct. To make sure that everyone follow the lint rule is added that checks for Node imports from react.

Changelog:
[Internal] - Added lint rule preventing "Node" imports from react and migrated "Node" types to use React namespace in react-native

Reviewed By: javache, huntie

Differential Revision: D71189533

fbshipit-source-id: baea8feb46be3dc30b6e58bcefe140655ec0530a
This commit is contained in:
Dawid Małecki
2025-03-17 06:47:23 -07:00
committed by Facebook GitHub Bot
parent 8ae4bbc9a5
commit 321efa9448
12 changed files with 102 additions and 25 deletions
+1
View File
@@ -74,6 +74,7 @@ module.exports = {
'lint/no-haste-imports': 2,
'lint/no-react-native-imports': 2,
'lint/require-extends-error': 2,
'lint/no-react-node-imports': 2,
},
},
{
@@ -15,17 +15,18 @@ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {
BlurEvent,
FocusEvent,
GestureResponderEvent,
LayoutChangeEvent,
LayoutRectangle,
MouseEvent,
PointerEvent,
GestureResponderEvent,
} from '../../Types/CoreEventTypes';
import type {
AccessibilityActionEvent,
AccessibilityProps,
} from './ViewAccessibility';
import type {Node} from 'react';
import React from 'react';
export type ViewLayout = LayoutRectangle;
export type ViewLayoutEvent = LayoutChangeEvent;
@@ -357,7 +358,7 @@ export type ViewPropsIOS = $ReadOnly<{
}>;
type ViewBaseProps = $ReadOnly<{
children?: Node,
children?: React.Node,
style?: ?ViewStyleProp,
/**
+3 -2
View File
@@ -24,7 +24,8 @@ import type {
import typeof Image from './Image';
import type {ImageResizeMode} from './ImageResizeMode';
import type {ImageSource, ImageURISource} from './ImageSource';
import type {ElementRef, Node, RefSetter} from 'react';
import type React from 'react';
import type {ElementRef, RefSetter} from 'react';
export type ImageSourcePropType = ImageSource;
@@ -341,7 +342,7 @@ export type ImageProps = $ReadOnly<{
export type ImageBackgroundProps = $ReadOnly<{
...ImageProps,
children?: Node,
children?: React.Node,
/**
* Style applied to the outer View component
@@ -8,8 +8,6 @@
* @format
*/
import type {Node} from 'react';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
@@ -21,7 +19,7 @@ const styles = StyleSheet.create({
},
});
const DebugInstructions: () => Node = Platform.select({
const DebugInstructions: () => React.Node = Platform.select({
ios: () => (
<Text>
Press <Text style={styles.highlight}>Cmd + D</Text> in the simulator or{' '}
@@ -8,8 +8,6 @@
* @format
*/
import type {Node} from 'react';
import ImageBackground from '../../Image/ImageBackground';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
@@ -18,7 +16,7 @@ import Colors from './Colors';
import HermesBadge from './HermesBadge';
import React from 'react';
const Header = (): Node => {
const Header = (): React.Node => {
const isDarkMode = useColorScheme() === 'dark';
return (
<ImageBackground
@@ -8,8 +8,6 @@
* @format
*/
import type {Node} from 'react';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
@@ -17,7 +15,7 @@ import useColorScheme from '../../Utilities/useColorScheme';
import Colors from './Colors';
import React from 'react';
const HermesBadge = (): Node => {
const HermesBadge = (): React.Node => {
const isDarkMode = useColorScheme() === 'dark';
const version =
global.HermesInternal?.getRuntimeProperties?.()['OSS Release Version'] ??
@@ -8,8 +8,6 @@
* @format
*/
import type {Node} from 'react';
import TouchableOpacity from '../../Components/Touchable/TouchableOpacity';
import View from '../../Components/View/View';
import openURLInBrowser from '../../Core/Devtools/openURLInBrowser';
@@ -81,7 +79,7 @@ const links = [
},
];
const LinkList = (): Node => {
const LinkList = (): React.Node => {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.container}>
@@ -8,8 +8,6 @@
* @format
*/
import type {Node} from 'react';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
@@ -21,7 +19,7 @@ const styles = StyleSheet.create({
},
});
const ReloadInstructions: () => Node = Platform.select({
const ReloadInstructions: () => React.Node = Platform.select({
ios: () => (
<Text>
Press <Text style={styles.highlight}>Cmd + R</Text> in the simulator to
+3 -2
View File
@@ -24,7 +24,8 @@ import type {
PointerEvent,
TextLayoutEvent,
} from '../Types/CoreEventTypes';
import type {Node} from 'react';
import React from 'react';
export type PressRetentionOffset = $ReadOnly<{
top: number,
@@ -169,7 +170,7 @@ type TextBaseProps = $ReadOnly<{
*/
'aria-labelledby'?: ?string,
children?: ?Node,
children?: ?React.Node,
/**
* When `numberOfLines` is set, this prop defines how text will be
@@ -4035,7 +4035,7 @@ export type ViewPropsIOS = $ReadOnly<{
shouldRasterizeIOS?: ?boolean,
}>;
type ViewBaseProps = $ReadOnly<{
children?: Node,
children?: React.Node,
style?: ?ViewStyleProp,
collapsable?: ?boolean,
collapsableChildren?: ?boolean,
@@ -4679,7 +4679,7 @@ export type ImageProps = $ReadOnly<{
}>;
export type ImageBackgroundProps = $ReadOnly<{
...ImageProps,
children?: Node,
children?: React.Node,
style?: ?ViewStyleProp,
imageStyle?: ?ImageStyleProp,
imageRef?: RefSetter<ElementRef<Image>>,
@@ -8136,7 +8136,7 @@ type TextBaseProps = $ReadOnly<{
\\"aria-expanded\\"?: ?boolean,
\\"aria-selected\\"?: ?boolean,
\\"aria-labelledby\\"?: ?string,
children?: ?Node,
children?: ?React.Node,
ellipsizeMode?: ?(\\"clip\\" | \\"head\\" | \\"middle\\" | \\"tail\\"),
id?: string,
maxFontSizeMultiplier?: ?number,
@@ -0,0 +1,39 @@
/**
* 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.
*
* @format
*/
'use strict';
const rule = require('../no-react-node-imports');
const {RuleTester} = require('eslint');
const ruleTester = new RuleTester({
parser: require.resolve('hermes-eslint'),
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
},
});
ruleTester.run('import(...)', rule, {
valid: [
{
code: `import React from "react";`,
},
{
code: `import { Foo } from "react";`,
},
],
invalid: [
{
code: `import { Node } from "react";`,
errors: [{messageId: 'nodeImport'}],
output: null,
},
],
});
@@ -0,0 +1,44 @@
/**
* 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.
*
* @format
*/
'use strict';
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow named Node imports from react',
},
messages: {
nodeImport:
'React.Node is React.ReactNode in TypeScript. To help us replace these correctly during type translation, prefer `import * as React` and using `React.Node`.',
},
schema: [],
},
create(context) {
return {
ImportDeclaration(node) {
if (node.source.value === 'react') {
node.specifiers.forEach(specifier => {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.name === 'Node'
) {
context.report({
node: specifier,
messageId: 'nodeImport',
});
}
});
}
},
};
},
};