From e665d7567dc72f23b08f00960cb5d2bed892fe65 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 9 Jul 2025 03:02:24 -0700 Subject: [PATCH] Add private-deep-imports lint (disallow `metro*/src/...`) (#1528) Summary: Pull Request resolved: https://github.com/facebook/metro/pull/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 --- scripts/eslint/base.js | 1 + .../__tests__/metro-deep-imports-test.js | 55 ++++++++++ scripts/eslint/rules/metro-deep-imports.js | 100 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 scripts/eslint/rules/__tests__/metro-deep-imports-test.js create mode 100644 scripts/eslint/rules/metro-deep-imports.js diff --git a/scripts/eslint/base.js b/scripts/eslint/base.js index aee8888a8d..eee3f394a4 100644 --- a/scripts/eslint/base.js +++ b/scripts/eslint/base.js @@ -30,6 +30,7 @@ module.exports = { 'consistent-return': 'error', 'import/no-extraneous-dependencies': 'error', 'fb-www/extra-arrow-initializer': 'off', + 'lint/metro-deep-imports': 'warn', 'lint/sort-imports': 'warn', 'lint/strictly-null': 'warn', 'max-len': 'off', diff --git a/scripts/eslint/rules/__tests__/metro-deep-imports-test.js b/scripts/eslint/rules/__tests__/metro-deep-imports-test.js new file mode 100644 index 0000000000..1fe9ecc0a2 --- /dev/null +++ b/scripts/eslint/rules/__tests__/metro-deep-imports-test.js @@ -0,0 +1,55 @@ +/** + * 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. + * + * @flow + * @format + * @oncall react_native + */ + +'use strict'; + +const rule = require('../metro-deep-imports.js'); +const ESLintTester = require('eslint').RuleTester; + +ESLintTester.setDefaultConfig({ + parser: require.resolve('hermes-eslint'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}); + +const eslintTester = new ESLintTester(); + +eslintTester.run('../metro-deep-imports', rule, { + valid: [ + 'require("metro")', + 'const Foo = require("metro-subpkg")', + 'require("metro/private/Bar")', + 'import Baz from "metro-baz/private/Baz"', + 'import NotMetro from "foo/src/bar"', + + // metro-runtime allows subpath imports. We can't rely on package#exports + // redirections as they may be disabled under Metro, and we must be able + // to import single modules as polyfills are side-effectful. + 'import Polyfill from "metro-runtime/src/polyfills/foo"', + 'const Polyfill = require("metro-runtime/src/polyfills/foo")', + ], + invalid: [ + { + code: 'const myLib = require("metro/src/lib")', + output: "const myLib = require('metro/private/lib')", + }, + { + code: "import MetroInternal from 'metro-pkg/src/internal'", + output: "import MetroInternal from 'metro-pkg/private/internal'", + }, + { + code: "import type {Bar} from 'metro-types/src/bar'", + output: "import type {Bar} from 'metro-types/private/bar'", + }, + ].map(obj => ({...obj, errors: [{messageId: 'METRO_DEEP_IMPORT'}]})), +}); diff --git a/scripts/eslint/rules/metro-deep-imports.js b/scripts/eslint/rules/metro-deep-imports.js new file mode 100644 index 0000000000..19e2a3f313 --- /dev/null +++ b/scripts/eslint/rules/metro-deep-imports.js @@ -0,0 +1,100 @@ +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +/*:: +// $FlowExpectedError[untyped-type-import] - eslint not typed in OSS +import type {RuleModule, SuggestionReportDescriptor} from 'eslint'; +import type {StringLiteral} from 'hermes-estree'; +*/ + +/** + * Lint against imports from the `src` directory of Metro packages. These are + * deprecated in favour of package root (semver public) exports, and explicitly + * /private/ deep imports. + * + * We make an exception for `metro-runtime`, because: + * 1) Runtime modules and polyfills must be imported as single files, so they + * may be selectively bundled and so unwanted side-effects are not + * evaluated - so some kind of subpath import is essential. + * 2) While we do have a `package.json#exports` map in metro-runtime, we can't + * currently enforce the use of it because `exports` resolution may be + * opted-out in Metro resolver. + */ + +const METRO_DEEP_IMPORT_RE = /^(metro(?!-runtime)(?:-[a-z\-]+)?)\/src\//; +const messageId = 'METRO_DEEP_IMPORT'; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'Deep imports from Metro must use explicitly-private subpaths', + }, + messages: { + METRO_DEEP_IMPORT: + "Metro deep imports from src ('{{originalImport}}') are deprecated. Prefer top level imports, or replace '/src/' with '/private/'.", + }, + schema: [], + fixable: 'code', + }, + + create(context) { + return { + ImportDeclaration(node) { + if ( + typeof node.source.value !== 'string' || + !METRO_DEEP_IMPORT_RE.test(node.source.value) + ) { + return; + } + const stringNode = node.source; + context.report({ + node: node.source, + messageId, + data: {originalImport: stringNode.value}, + fix: getFix(stringNode), + }); + }, + CallExpression(node) { + if ( + node.callee.type !== 'Identifier' || + node.callee.name !== 'require' || + node.arguments.length < 1 || + node.arguments[0].type !== 'Literal' || + node.arguments[0].literalType !== 'string' || + !METRO_DEEP_IMPORT_RE.test(node.arguments[0].value) + ) { + return; + } + const stringNode = node.arguments[0]; + context.report({ + node, + messageId, + data: {originalImport: stringNode.value}, + fix: getFix(stringNode), + }); + }, + }; + }, +} /*:: as RuleModule */; + +function getFix( + nodeToReplace /*: StringLiteral */, + // $FlowExpectedError[value-as-type] - eslint not typed in OSS +) /*: SuggestionReportDescriptor['fix'] */ { + return fixer => + fixer.replaceText( + nodeToReplace, + `'${nodeToReplace.value.replace(METRO_DEEP_IMPORT_RE, '$1/private/')}'`, + ); +}