mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add no-deep-imports rule to eslint-plugin-react-native (#50542)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50542 After TS types generation is completed, react native deep imports will be deprecated. This rule produces warnings to let users know to use root imports instead. For more information about why this rule was added, please check [RFC](https://github.com/react-native-community/discussions-and-proposals/pull/894). Changelog: [General][Added] - Added no-deep-imports rule to eslint-plugin-react-native. Reviewed By: robhogan Differential Revision: D71398004 fbshipit-source-id: 69104f69b1b1c59b5b0f115dcdd708a46d8d614d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
70cdf12c4d
commit
87809d9326
@@ -35,6 +35,12 @@ module.exports = {
|
||||
'no-undef': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@react-native/no-deep-imports': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'./packages/react-native/**/*.{js,flow}',
|
||||
|
||||
@@ -53,6 +53,12 @@ module.exports = {
|
||||
files: ['*.jsx'],
|
||||
parser: '@babel/eslint-parser',
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@react-native/no-deep-imports': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const rule = require('../no-deep-imports.js');
|
||||
const {publicAPIMapping} = require('../utils.js');
|
||||
const ESLintTester = require('./eslint-tester.js');
|
||||
const path = require('path');
|
||||
|
||||
const eslintTester = new ESLintTester();
|
||||
|
||||
test('resolve all public API paths', () => {
|
||||
for (const subpath of Object.keys(publicAPIMapping)) {
|
||||
require.resolve(path.join('react-native', subpath));
|
||||
}
|
||||
});
|
||||
|
||||
eslintTester.run('../no-deep-imports', rule, {
|
||||
valid: [
|
||||
"import {View} from 'react-native';",
|
||||
"const {View} = require('react-native');",
|
||||
"import Foo from 'react-native-foo';",
|
||||
"import Foo from 'react-native-foo/Foo';",
|
||||
"import Foo from 'react/native/Foo';",
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "import View from 'react-native/Libraries/Components/View/View';",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Components/View/View'},
|
||||
},
|
||||
],
|
||||
output: "import {View} from 'react-native';",
|
||||
},
|
||||
{
|
||||
code: "const View = require('react-native/Libraries/Components/View/View');",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Components/View/View'},
|
||||
},
|
||||
],
|
||||
output: "const {View} = require('react-native');",
|
||||
},
|
||||
{
|
||||
code: "var View = require('react-native/Libraries/Components/View/View');",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Components/View/View'},
|
||||
},
|
||||
],
|
||||
output: "var {View} = require('react-native');",
|
||||
},
|
||||
{
|
||||
code: "import Foo from 'react-native/Libraries/Components/Foo';",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Components/Foo'},
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
},
|
||||
{
|
||||
code: "import {Foo} from 'react-native/Libraries/Components/Foo';",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Components/Foo'},
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
},
|
||||
{
|
||||
code: "const {Foo} = require('react-native/Libraries/Foo');",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Foo'},
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
},
|
||||
{
|
||||
code: "if(require('react-native/Libraries/Foo')) {};",
|
||||
errors: [
|
||||
{
|
||||
messageId: 'deepImport',
|
||||
data: {importPath: 'react-native/Libraries/Foo'},
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -9,4 +9,5 @@
|
||||
|
||||
exports.rules = {
|
||||
'platform-colors': require('./platform-colors'),
|
||||
'no-deep-imports': require('./no-deep-imports'),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 {publicAPIMapping} = require('./utils.js');
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow deep imports from react native',
|
||||
},
|
||||
messages: {
|
||||
deepImport:
|
||||
"'{{importPath}}' React Native deep imports are deprecated. Please use the top level import instead.",
|
||||
},
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
},
|
||||
|
||||
create: function (context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (!isDeepReactNativeImport(node.source)) {
|
||||
return;
|
||||
}
|
||||
if (isDefaultImport(node)) {
|
||||
const reactNativeSource = node.source.value.slice(
|
||||
'react-native/'.length,
|
||||
);
|
||||
const publicAPIDefaultComponent = publicAPIMapping[reactNativeSource];
|
||||
if (publicAPIDefaultComponent) {
|
||||
context.report({
|
||||
...getStandardReport(node.source),
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(
|
||||
node,
|
||||
`import {${publicAPIDefaultComponent}} from 'react-native';`,
|
||||
);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report(getStandardReport(node.source));
|
||||
}
|
||||
} else {
|
||||
context.report(getStandardReport(node.source));
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isDeepRequire(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = node.parent;
|
||||
const importPath = node.arguments[0].value;
|
||||
|
||||
if (
|
||||
parent.type === 'VariableDeclarator' &&
|
||||
parent.id.type === 'Identifier'
|
||||
) {
|
||||
const reactNativeSource = importPath.slice('react-native/'.length);
|
||||
const publicAPIDefaultComponent = publicAPIMapping[reactNativeSource];
|
||||
if (publicAPIDefaultComponent) {
|
||||
context.report({
|
||||
...getStandardReport(node.arguments[0]),
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(
|
||||
parent,
|
||||
`{${publicAPIDefaultComponent}} = require('react-native')`,
|
||||
);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report(getStandardReport(node.arguments[0]));
|
||||
}
|
||||
} else {
|
||||
context.report(getStandardReport(node.arguments[0]));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getStandardReport(source) {
|
||||
return {
|
||||
node: source,
|
||||
messageId: 'deepImport',
|
||||
data: {
|
||||
importPath: source.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isDefaultImport(node) {
|
||||
return (
|
||||
node.specifiers.length === 1 &&
|
||||
node.specifiers.some(
|
||||
specifier => specifier.type === 'ImportDefaultSpecifier',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isDeepRequire(node) {
|
||||
return (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'require' &&
|
||||
node.arguments.length === 1 &&
|
||||
node.arguments[0].type === 'Literal' &&
|
||||
typeof node.arguments[0].value === 'string' &&
|
||||
isDeepReactNativeImport(node.arguments[0])
|
||||
);
|
||||
}
|
||||
|
||||
function isDeepReactNativeImport(source) {
|
||||
if (source.type !== 'Literal' || typeof source.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const importPath = source.value;
|
||||
const parts = importPath.split('/');
|
||||
return parts.length > 1 && parts[0] === 'react-native';
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* The correctness of paths is checked in the test file.
|
||||
* The assumption is that renaming/removing components shouldn't happen too often.
|
||||
* If a new component is added, it should be imported from the root.
|
||||
* If the path is not matched, the auto-fix won't be suggested.
|
||||
*/
|
||||
const publicAPIMapping = {
|
||||
'Libraries/Components/AccessibilityInfo/AccessibilityInfo':
|
||||
'AccessibilityInfo',
|
||||
'Libraries/Components/ActivityIndicator/ActivityIndicator':
|
||||
'ActivityIndicator',
|
||||
'Libraries/Components/Button': 'Button',
|
||||
'Libraries/Components/DrawerAndroid/DrawerLayoutAndroid':
|
||||
'DrawerLayoutAndroid',
|
||||
'Libraries/Components/LayoutConformance/LayoutConformance':
|
||||
'experimental_LayoutConformance',
|
||||
'Libraries/Lists/FlatList': 'FlatList',
|
||||
'Libraries/Image/Image': 'Image',
|
||||
'Libraries/Image/ImageBackground': 'ImageBackground',
|
||||
'Libraries/Components/TextInput/InputAccessoryView': 'InputAccessoryView',
|
||||
'Libraries/Components/Keyboard/KeyboardAvoidingView': 'KeyboardAvoidingView',
|
||||
'Libraries/Modal/Modal': 'Modal',
|
||||
'Libraries/Components/Pressable/Pressable': 'Pressable',
|
||||
'Libraries/Components/ProgressBarAndroid/ProgressBarAndroid':
|
||||
'ProgressBarAndroid',
|
||||
'Libraries/Components/RefreshControl/RefreshControl': 'RefreshControl',
|
||||
'Libraries/Components/SafeAreaView/SafeAreaView': 'SafeAreaView',
|
||||
'Libraries/Components/ScrollView/ScrollView': 'ScrollView',
|
||||
'Libraries/Lists/SectionList': 'SectionList',
|
||||
'Libraries/Components/StatusBar/StatusBar': 'StatusBar',
|
||||
'Libraries/Components/Switch/Switch': 'Switch',
|
||||
'Libraries/Text/Text': 'Text',
|
||||
'Libraries/Components/TextInput/TextInput': 'TextInput',
|
||||
'Libraries/Components/Touchable/Touchable': 'Touchable',
|
||||
'Libraries/Components/Touchable/TouchableHighlight': 'TouchableHighlight',
|
||||
'Libraries/Components/Touchable/TouchableNativeFeedback':
|
||||
'TouchableNativeFeedback',
|
||||
'Libraries/Components/Touchable/TouchableOpacity': 'TouchableOpacity',
|
||||
'Libraries/Components/Touchable/TouchableWithoutFeedback':
|
||||
'TouchableWithoutFeedback',
|
||||
'Libraries/Components/View/View': 'View',
|
||||
'Libraries/Lists/VirtualizedList': 'VirtualizedList',
|
||||
'Libraries/Lists/VirtualizedSectionList': 'VirtualizedSectionList',
|
||||
'Libraries/ActionSheetIOS/ActionSheetIOS': 'ActionSheetIOS',
|
||||
'Libraries/Alert/Alert': 'Alert',
|
||||
'Libraries/Animated/Animated': 'Animated',
|
||||
'Libraries/Utilities/Appearance': 'Appearance',
|
||||
'Libraries/ReactNative/AppRegistry': 'AppRegistry',
|
||||
'Libraries/AppState/AppState': 'AppState',
|
||||
'Libraries/Utilities/BackHandler': 'BackHandler',
|
||||
'Libraries/Components/Clipboard/Clipboard': 'Clipboard',
|
||||
'Libraries/Utilities/DeviceInfo': 'DeviceInfo',
|
||||
'src/private/devmenu/DevMenu': 'DevMenu',
|
||||
'Libraries/Utilities/DevSettings': 'DevSettings',
|
||||
'Libraries/Utilities/Dimensions': 'Dimensions',
|
||||
'Libraries/Animated/Easing': 'Easing',
|
||||
'Libraries/ReactNative/I18nManager': 'I18nManager',
|
||||
'Libraries/Interaction/InteractionManager': 'InteractionManager',
|
||||
'Libraries/Components/Keyboard/Keyboard': 'Keyboard',
|
||||
'Libraries/LayoutAnimation/LayoutAnimation': 'LayoutAnimation',
|
||||
'Libraries/Linking/Linking': 'Linking',
|
||||
'Libraries/LogBox/LogBox': 'LogBox',
|
||||
'Libraries/NativeModules/specs/NativeDialogManagerAndroid':
|
||||
'NativeDialogManagerAndroid',
|
||||
'Libraries/EventEmitter/NativeEventEmitter': 'NativeEventEmitter',
|
||||
'Libraries/Network/RCTNetworking': 'Networking',
|
||||
'Libraries/Interaction/PanResponder': 'PanResponder',
|
||||
'Libraries/PermissionsAndroid/PermissionsAndroid': 'PermissionsAndroid',
|
||||
'Libraries/Utilities/PixelRatio': 'PixelRatio',
|
||||
'Libraries/PushNotificationIOS/PushNotificationIOS': 'PushNotificationIOS',
|
||||
'Libraries/Settings/Settings': 'Settings',
|
||||
'Libraries/Share/Share': 'Share',
|
||||
'Libraries/StyleSheet/StyleSheet': 'StyleSheet',
|
||||
'Libraries/Performance/Systrace': 'Systrace',
|
||||
'Libraries/Components/ToastAndroid/ToastAndroid': 'ToastAndroid',
|
||||
'Libraries/TurboModule/TurboModuleRegistry': 'TurboModuleRegistry',
|
||||
'Libraries/ReactNative/UIManager': 'UIManager',
|
||||
'Libraries/Animated/useAnimatedValue': 'useAnimatedValue',
|
||||
'Libraries/Utilities/useColorScheme': 'useColorScheme',
|
||||
'Libraries/Utilities/useWindowDimensions': 'useWindowDimensions',
|
||||
'Libraries/UTFSequence': 'UTFSequence',
|
||||
'Libraries/Vibration/Vibration': 'Vibration',
|
||||
'Libraries/Utilities/codegenNativeComponent': 'codegenNativeComponent',
|
||||
'Libraries/Utilities/codegenNativeCommands': 'codegenNativeCommands',
|
||||
'Libraries/EventEmitter/RCTDeviceEventEmitter': 'DeviceEventEmitter',
|
||||
'Libraries/StyleSheet/PlatformColorValueTypesIOS': 'DynamicColorIOS',
|
||||
'Libraries/EventEmitter/RCTNativeAppEventEmitter': 'NativeAppEventEmitter',
|
||||
'Libraries/BatchedBridge/NativeModules': 'NativeModules',
|
||||
'Libraries/Utilities/Platform': 'Platform',
|
||||
'Libraries/StyleSheet/PlatformColorValueTypes': 'PlatformColor',
|
||||
'Libraries/StyleSheet/processColor': 'processColor',
|
||||
'Libraries/ReactNative/requireNativeComponent': 'requireNativeComponent',
|
||||
'Libraries/ReactNative/RootTag': 'RootTagContext',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
publicAPIMapping,
|
||||
};
|
||||
Reference in New Issue
Block a user