Skip to content

Commit 3e14d16

Browse files
coadofacebook-github-bot
authored andcommitted
Add babel plugin to @react-native/babel-preset for console.warn injection under deep react native imports (#50802)
Summary: Pull Request resolved: #50802 The plugin analyses the source of all `import`, `require`, and `export` statements and injects the `console.warn` statement for each path targeting deep react-native source code. It runs only on a dev mode so there is no need to keep that in the `if (__DEV__) ` block. It is possible to disable this plugin by setting `disableDeepImportWarnings: true` and **resetting** the Metro cache: ```js module.exports = { presets: [['module:react-native/babel-preset', { "disableDeepImportWarnings": true }]], }; ``` Changelog: [General][Internal] - Added plugin to react-native/babel-preset injecting `console.warn` for each react native deep import in dev mode. For a given code: ```js import { Image } from 'react-native'; import View from 'react-native/Libraries/Components/View/View'; const Text = require('react-native/Libraries/Text/Text'); export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug'; ``` The transformed output should look like: ```js import { Image } from 'react-native'; import View from 'react-native/Libraries/Components/View/View'; const Text = require('react-native/Libraries/Text/Text'); export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug'; console.warn("Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Components/View/View')."); console.warn("Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Text/Text')."); console.warn("Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Pressability/PressabilityDebug')."); ``` For more information about why this plugin was needed, please check [RFC](react-native-community/discussions-and-proposals#894). Reviewed By: huntie Differential Revision: D70783145 fbshipit-source-id: ae145db6471d861099566a8faf2fbd93bd136450
1 parent bef5cc1 commit 3e14d16

File tree

4 files changed

+309
-0
lines changed

4 files changed

+309
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
* @format
8+
* @flow strict
9+
* @oncall react_native
10+
*/
11+
12+
'use strict';
13+
14+
import type {BabelCoreOptions, EntryOptions, PluginEntry} from '@babel/core';
15+
16+
const {transformSync} = require('@babel/core');
17+
const generate = require('@babel/generator').default;
18+
const t = require('@babel/types');
19+
const nullthrows = require('nullthrows');
20+
21+
function makeTransformOptions<OptionsT: ?EntryOptions>(
22+
plugins: $ReadOnlyArray<PluginEntry>,
23+
options: OptionsT,
24+
): BabelCoreOptions {
25+
return {
26+
ast: true,
27+
babelrc: false,
28+
browserslistConfigFile: false,
29+
code: false,
30+
compact: true,
31+
configFile: false,
32+
plugins: plugins.length
33+
? plugins.map(plugin => [plugin, options])
34+
: [() => ({visitor: {}})],
35+
sourceType: 'module',
36+
filename: 'foo.js',
37+
cwd: 'path/to/project',
38+
};
39+
}
40+
41+
function validateOutputAst(ast: BabelNode) {
42+
const seenNodes = new Set<BabelNode>();
43+
t.traverseFast(nullthrows(ast), function enter(node) {
44+
if (seenNodes.has(node)) {
45+
throw new Error(
46+
'Found a duplicate ' +
47+
node.type +
48+
' node in the output, which can cause' +
49+
' undefined behavior in Babel.',
50+
);
51+
}
52+
seenNodes.add(node);
53+
});
54+
}
55+
56+
function transformToAst<T: ?EntryOptions>(
57+
plugins: $ReadOnlyArray<PluginEntry>,
58+
code: string,
59+
options: T,
60+
): BabelNodeFile {
61+
const transformResult = transformSync(
62+
code,
63+
makeTransformOptions(plugins, options),
64+
);
65+
const ast = nullthrows(transformResult.ast);
66+
validateOutputAst(ast);
67+
return ast;
68+
}
69+
70+
function transform(
71+
code: string,
72+
plugins: $ReadOnlyArray<PluginEntry>,
73+
options: ?EntryOptions,
74+
): string {
75+
return generate(transformToAst(plugins, code, options)).code;
76+
}
77+
78+
exports.transform = transform;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
* @format
8+
* @oncall react_native
9+
*/
10+
11+
'use strict';
12+
13+
const {transform} = require('../__mocks__/test-helpers');
14+
const rnDeepImportsWarningPlugin = require('../plugin-warn-on-deep-imports');
15+
16+
jest.mock('path', () => ({
17+
...jest.requireActual('path'),
18+
resolve: jest.fn((...args) => args.join('/')),
19+
}));
20+
21+
test('deep esm import', () => {
22+
const code = `
23+
import View from 'react-native/Libraries/Components/View/View';
24+
import {Text} from 'react-native';
25+
`;
26+
27+
expect(transform(code, [rnDeepImportsWarningPlugin])).toMatchInlineSnapshot(`
28+
"import View from 'react-native/Libraries/Components/View/View';
29+
import { Text } from 'react-native';
30+
console.warn(\\"Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Components/View/View'). Source: path/to/project/foo.js 2:4\\");"
31+
`);
32+
});
33+
34+
test('deep cjs import', () => {
35+
const code = `
36+
const View = require('react-native/Libraries/Components/View/View');
37+
const {Text} = require('react-native');
38+
`;
39+
40+
expect(transform(code, [rnDeepImportsWarningPlugin])).toMatchInlineSnapshot(`
41+
"const View = require('react-native/Libraries/Components/View/View');
42+
const {
43+
Text
44+
} = require('react-native');
45+
console.warn(\\"Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Components/View/View'). Source: path/to/project/foo.js 2:17\\");"
46+
`);
47+
});
48+
49+
test('multiple deep imports', () => {
50+
const code = `
51+
import View from 'react-native/Libraries/Components/View/View';
52+
import Text from 'react-native/Libraries/Text/Text';
53+
import {Image} from 'react-native';
54+
`;
55+
56+
expect(transform(code, [rnDeepImportsWarningPlugin])).toMatchInlineSnapshot(`
57+
"import View from 'react-native/Libraries/Components/View/View';
58+
import Text from 'react-native/Libraries/Text/Text';
59+
import { Image } from 'react-native';
60+
console.warn(\\"Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Components/View/View'). Source: path/to/project/foo.js 2:4\\");
61+
console.warn(\\"Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Text/Text'). Source: path/to/project/foo.js 3:4\\");"
62+
`);
63+
});
64+
65+
test('deep reexport', () => {
66+
const code = `
67+
export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug';
68+
`;
69+
70+
expect(transform(code, [rnDeepImportsWarningPlugin])).toMatchInlineSnapshot(`
71+
"export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug';
72+
console.warn(\\"Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Pressability/PressabilityDebug'). Source: path/to/project/foo.js 2:4\\");"
73+
`);
74+
});
75+
76+
test('import from other package', () => {
77+
const code = `
78+
import {foo} from 'react-native-foo';
79+
`;
80+
81+
expect(transform(code, [rnDeepImportsWarningPlugin])).toMatchInlineSnapshot(
82+
`"import { foo } from 'react-native-foo';"`,
83+
);
84+
});

packages/react-native-babel-preset/src/configs/main.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
const passthroughSyntaxPlugins = require('../passthrough-syntax-plugins');
1414
const lazyImports = require('./lazy-imports');
1515

16+
const EXCLUDED_FIRST_PARTY_PATHS = [
17+
/[/\\]node_modules[/\\]/,
18+
/[/\\]packages[/\\]react-native(?:-fantom)?[/\\]/,
19+
/[/\\]packages[/\\]virtualized-lists[/\\]/,
20+
];
21+
1622
function isTypeScriptSource(fileName) {
1723
return !!fileName && fileName.endsWith('.ts');
1824
}
@@ -21,6 +27,13 @@ function isTSXSource(fileName) {
2127
return !!fileName && fileName.endsWith('.tsx');
2228
}
2329

30+
function isFirstParty(fileName) {
31+
return (
32+
!!fileName &&
33+
!EXCLUDED_FIRST_PARTY_PATHS.some(regex => regex.test(fileName))
34+
);
35+
}
36+
2437
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
2538
const loose = true;
2639

@@ -51,6 +64,8 @@ const getPreset = (src, options) => {
5164
const hasClass = isNull || src.indexOf('class') !== -1;
5265

5366
const extraPlugins = [];
67+
const firstPartyPlugins = [];
68+
5469
if (!options.useTransformReactJSXExperimental) {
5570
extraPlugins.push([
5671
require('@babel/plugin-transform-react-jsx'),
@@ -171,6 +186,10 @@ const getPreset = (src, options) => {
171186
]);
172187
}
173188

189+
if (options && options.dev && !options.disableDeepImportWarnings) {
190+
firstPartyPlugins.push([require('../plugin-warn-on-deep-imports.js')]);
191+
}
192+
174193
if (options && options.dev && !options.useTransformReactJSXExperimental) {
175194
extraPlugins.push([require('@babel/plugin-transform-react-jsx-source')]);
176195
extraPlugins.push([require('@babel/plugin-transform-react-jsx-self')]);
@@ -240,6 +259,10 @@ const getPreset = (src, options) => {
240259
],
241260
],
242261
},
262+
{
263+
test: isFirstParty,
264+
plugins: firstPartyPlugins,
265+
},
243266
{
244267
plugins: extraPlugins,
245268
},
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
* @format
8+
* @oncall react_native
9+
*/
10+
11+
'use strict';
12+
13+
function getWarningMessage(importPath, loc, source) {
14+
const message = `Deep imports from the 'react-native' package are deprecated ('${importPath}').`;
15+
16+
if (source !== undefined) {
17+
return `${message} Source: ${source} ${loc ? `${loc.start.line}:${loc.start.column}` : ''}`;
18+
}
19+
20+
return message;
21+
}
22+
23+
function createWarning(t, importPath, loc, source) {
24+
const warningMessage = getWarningMessage(importPath, loc, source);
25+
26+
const warning = t.expressionStatement(
27+
t.callExpression(
28+
t.memberExpression(t.identifier('console'), t.identifier('warn')),
29+
[t.stringLiteral(warningMessage)],
30+
),
31+
);
32+
33+
return warning;
34+
}
35+
36+
function isDeepReactNativeImport(source) {
37+
const parts = source.split('/');
38+
return parts.length > 1 && parts[0] === 'react-native';
39+
}
40+
41+
function withLocation(node, loc) {
42+
if (!node.loc) {
43+
return {...node, loc};
44+
}
45+
return node;
46+
}
47+
48+
module.exports = ({types: t}) => ({
49+
name: 'warn-on-deep-imports',
50+
visitor: {
51+
ImportDeclaration(path, state) {
52+
const source = path.node.source.value;
53+
54+
if (isDeepReactNativeImport(source)) {
55+
const loc = path.node.loc;
56+
state.import.push({source, loc});
57+
}
58+
},
59+
CallExpression(path, state) {
60+
const callee = path.get('callee');
61+
const args = path.get('arguments');
62+
63+
if (
64+
callee.isIdentifier({name: 'require'}) &&
65+
args.length === 1 &&
66+
args[0].isStringLiteral()
67+
) {
68+
const source =
69+
args[0].node.type === 'StringLiteral' ? args[0].node.value : '';
70+
if (isDeepReactNativeImport(source)) {
71+
const loc = path.node.loc;
72+
state.require.push({source, loc});
73+
}
74+
}
75+
},
76+
ExportNamedDeclaration(path, state) {
77+
const source = path.node.source;
78+
79+
if (source && isDeepReactNativeImport(source.value)) {
80+
const loc = path.node.loc;
81+
state.export.push({source: source.value, loc});
82+
}
83+
},
84+
Program: {
85+
enter(path, state) {
86+
state.require = [];
87+
state.import = [];
88+
state.export = [];
89+
},
90+
exit(path, state) {
91+
const {body} = path.node;
92+
93+
const requireWarnings = state.require.map(value =>
94+
withLocation(
95+
createWarning(t, value.source, value.loc, state.filename),
96+
value.loc,
97+
),
98+
);
99+
100+
const importWarnings = state.import.map(value =>
101+
withLocation(
102+
createWarning(t, value.source, value.loc, state.filename),
103+
value.loc,
104+
),
105+
);
106+
107+
const exportWarnings = state.export.map(value =>
108+
withLocation(
109+
createWarning(t, value.source, value.loc, state.filename),
110+
value.loc,
111+
),
112+
);
113+
114+
const warnings = [
115+
...requireWarnings,
116+
...importWarnings,
117+
...exportWarnings,
118+
];
119+
120+
body.push(...warnings);
121+
},
122+
},
123+
},
124+
});

0 commit comments

Comments
 (0)