Skip to content

Commit e665d75

Browse files
robhoganfacebook-github-bot
authored andcommitted
Add private-deep-imports lint (disallow metro*/src/...) (#1528)
Summary: Pull Request resolved: #1528 Lint against `metro*/src/`-style deep imports, as a step towards disallowing them. Deep imports will still be allowed, but will be required to use `metro*/private` and hence explicitly come without semver stability guarantees. Changelog: Internal Reviewed By: vzaidman Differential Revision: D77832800
1 parent 5e3a6bb commit e665d75

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

scripts/eslint/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module.exports = {
3030
'consistent-return': 'error',
3131
'import/no-extraneous-dependencies': 'error',
3232
'fb-www/extra-arrow-initializer': 'off',
33+
'lint/metro-deep-imports': 'warn',
3334
'lint/sort-imports': 'warn',
3435
'lint/strictly-null': 'warn',
3536
'max-len': 'off',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
'use strict';
13+
14+
const rule = require('../metro-deep-imports.js');
15+
const ESLintTester = require('eslint').RuleTester;
16+
17+
ESLintTester.setDefaultConfig({
18+
parser: require.resolve('hermes-eslint'),
19+
parserOptions: {
20+
ecmaVersion: 6,
21+
sourceType: 'module',
22+
},
23+
});
24+
25+
const eslintTester = new ESLintTester();
26+
27+
eslintTester.run('../metro-deep-imports', rule, {
28+
valid: [
29+
'require("metro")',
30+
'const Foo = require("metro-subpkg")',
31+
'require("metro/private/Bar")',
32+
'import Baz from "metro-baz/private/Baz"',
33+
'import NotMetro from "foo/src/bar"',
34+
35+
// metro-runtime allows subpath imports. We can't rely on package#exports
36+
// redirections as they may be disabled under Metro, and we must be able
37+
// to import single modules as polyfills are side-effectful.
38+
'import Polyfill from "metro-runtime/src/polyfills/foo"',
39+
'const Polyfill = require("metro-runtime/src/polyfills/foo")',
40+
],
41+
invalid: [
42+
{
43+
code: 'const myLib = require("metro/src/lib")',
44+
output: "const myLib = require('metro/private/lib')",
45+
},
46+
{
47+
code: "import MetroInternal from 'metro-pkg/src/internal'",
48+
output: "import MetroInternal from 'metro-pkg/private/internal'",
49+
},
50+
{
51+
code: "import type {Bar} from 'metro-types/src/bar'",
52+
output: "import type {Bar} from 'metro-types/private/bar'",
53+
},
54+
].map(obj => ({...obj, errors: [{messageId: 'METRO_DEEP_IMPORT'}]})),
55+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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-local
9+
*/
10+
11+
'use strict';
12+
13+
/*::
14+
// $FlowExpectedError[untyped-type-import] - eslint not typed in OSS
15+
import type {RuleModule, SuggestionReportDescriptor} from 'eslint';
16+
import type {StringLiteral} from 'hermes-estree';
17+
*/
18+
19+
/**
20+
* Lint against imports from the `src` directory of Metro packages. These are
21+
* deprecated in favour of package root (semver public) exports, and explicitly
22+
* /private/ deep imports.
23+
*
24+
* We make an exception for `metro-runtime`, because:
25+
* 1) Runtime modules and polyfills must be imported as single files, so they
26+
* may be selectively bundled and so unwanted side-effects are not
27+
* evaluated - so some kind of subpath import is essential.
28+
* 2) While we do have a `package.json#exports` map in metro-runtime, we can't
29+
* currently enforce the use of it because `exports` resolution may be
30+
* opted-out in Metro resolver.
31+
*/
32+
33+
const METRO_DEEP_IMPORT_RE = /^(metro(?!-runtime)(?:-[a-z\-]+)?)\/src\//;
34+
const messageId = 'METRO_DEEP_IMPORT';
35+
36+
module.exports = {
37+
meta: {
38+
type: 'problem',
39+
docs: {
40+
description:
41+
'Deep imports from Metro must use explicitly-private subpaths',
42+
},
43+
messages: {
44+
METRO_DEEP_IMPORT:
45+
"Metro deep imports from src ('{{originalImport}}') are deprecated. Prefer top level imports, or replace '/src/' with '/private/'.",
46+
},
47+
schema: [],
48+
fixable: 'code',
49+
},
50+
51+
create(context) {
52+
return {
53+
ImportDeclaration(node) {
54+
if (
55+
typeof node.source.value !== 'string' ||
56+
!METRO_DEEP_IMPORT_RE.test(node.source.value)
57+
) {
58+
return;
59+
}
60+
const stringNode = node.source;
61+
context.report({
62+
node: node.source,
63+
messageId,
64+
data: {originalImport: stringNode.value},
65+
fix: getFix(stringNode),
66+
});
67+
},
68+
CallExpression(node) {
69+
if (
70+
node.callee.type !== 'Identifier' ||
71+
node.callee.name !== 'require' ||
72+
node.arguments.length < 1 ||
73+
node.arguments[0].type !== 'Literal' ||
74+
node.arguments[0].literalType !== 'string' ||
75+
!METRO_DEEP_IMPORT_RE.test(node.arguments[0].value)
76+
) {
77+
return;
78+
}
79+
const stringNode = node.arguments[0];
80+
context.report({
81+
node,
82+
messageId,
83+
data: {originalImport: stringNode.value},
84+
fix: getFix(stringNode),
85+
});
86+
},
87+
};
88+
},
89+
} /*:: as RuleModule */;
90+
91+
function getFix(
92+
nodeToReplace /*: StringLiteral */,
93+
// $FlowExpectedError[value-as-type] - eslint not typed in OSS
94+
) /*: SuggestionReportDescriptor['fix'] */ {
95+
return fixer =>
96+
fixer.replaceText(
97+
nodeToReplace,
98+
`'${nodeToReplace.value.replace(METRO_DEEP_IMPORT_RE, '$1/private/')}'`,
99+
);
100+
}

0 commit comments

Comments
 (0)