From cb2949609a6c52d6dc7f0e20b24ea7139a2908ad Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:46:06 -0700 Subject: [PATCH] Update based on PR feedback --- eslint/eslint-patch/src/_patch-base.ts | 31 +++++--- .../src/custom-config-package-names.ts | 45 +++++++++++ .../src/modern-module-resolution.ts | 74 +++++++++++++++++++ 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 eslint/eslint-patch/src/custom-config-package-names.ts create mode 100644 eslint/eslint-patch/src/modern-module-resolution.ts diff --git a/eslint/eslint-patch/src/_patch-base.ts b/eslint/eslint-patch/src/_patch-base.ts index 1165eb2c8ad..9524e1cee76 100644 --- a/eslint/eslint-patch/src/_patch-base.ts +++ b/eslint/eslint-patch/src/_patch-base.ts @@ -13,10 +13,6 @@ const fs = require('fs'); const isModuleResolutionError: (ex: unknown) => boolean = (ex) => typeof ex === 'object' && !!ex && 'code' in ex && (ex as { code: unknown }).code === 'MODULE_NOT_FOUND'; -// error: "The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received ''" -const isInvalidImporterPath: (ex: unknown) => boolean = (ex) => - (ex as { code: unknown } | undefined)?.code === 'ERR_INVALID_ARG_VALUE'; - // Module path for eslintrc.cjs // Example: ".../@eslint/eslintrc/dist/eslintrc.cjs" let eslintrcBundlePath: string | undefined = undefined; @@ -39,7 +35,7 @@ let eslintFolder: string | undefined = undefined; // Probe for the ESLint >=8.0.0 layout: for (let currentModule = module; ; ) { - if (!eslintrcBundlePath) { + if (!eslintrcBundlePath && currentModule.filename.endsWith('eslintrc.cjs')) { // For ESLint >=8.0.0, all @eslint/eslintrc code is bundled at this path: // .../@eslint/eslintrc/dist/eslintrc.cjs try { @@ -49,8 +45,9 @@ for (let currentModule = module; ; ) { // Make sure we actually resolved the module in our call path // and not some other spurious dependency. - if (path.join(eslintrcFolder, 'dist/eslintrc.cjs') === currentModule.filename) { - eslintrcBundlePath = path.join(eslintrcFolder, 'dist/eslintrc.cjs'); + const resolvedEslintrcBundlePath: string = path.join(eslintrcFolder, 'dist/eslintrc.cjs'); + if (resolvedEslintrcBundlePath === currentModule.filename) { + eslintrcBundlePath = resolvedEslintrcBundlePath; } } catch (ex: unknown) { // Module resolution failures are expected, as we're walking @@ -95,7 +92,7 @@ for (let currentModule = module; ; ) { if (!eslintFolder) { // Probe for the ESLint >=7.8.0 layout: for (let currentModule = module; ; ) { - if (!configArrayFactoryPath) { + if (!configArrayFactoryPath && currentModule.filename.endsWith('config-array-factory.js')) { // For ESLint >=7.8.0, config-array-factory.js is at this path: // .../@eslint/eslintrc/lib/config-array-factory.js try { @@ -105,8 +102,12 @@ if (!eslintFolder) { }) ); - if (path.join(eslintrcFolder, '/lib/config-array-factory.js') == currentModule.filename) { - configArrayFactoryPath = path.join(eslintrcFolder, 'lib/config-array-factory.js'); + const resolvedConfigArrayFactoryPath: string = path.join( + eslintrcFolder, + '/lib/config-array-factory.js' + ); + if (resolvedConfigArrayFactoryPath === currentModule.filename) { + configArrayFactoryPath = resolvedConfigArrayFactoryPath; moduleResolverPath = path.join(eslintrcFolder, 'lib/shared/relative-module-resolver'); namingPath = path.join(eslintrcFolder, 'lib/shared/naming'); } @@ -118,7 +119,7 @@ if (!eslintFolder) { throw ex; } } - } else { + } else if (currentModule.filename.endsWith('cli-engine.js')) { // Next look for a file in ESLint's folder // .../eslint/lib/cli-engine/cli-engine.js try { @@ -209,4 +210,10 @@ if (eslintMajorVersion === 8) { Naming = require(namingPath!); } -export { ConfigArrayFactory, ModuleResolver, Naming, eslintMajorVersion as EslintMajorVersion }; +export { + ConfigArrayFactory, + ModuleResolver, + Naming, + eslintMajorVersion as EslintMajorVersion, + isModuleResolutionError +}; diff --git a/eslint/eslint-patch/src/custom-config-package-names.ts b/eslint/eslint-patch/src/custom-config-package-names.ts new file mode 100644 index 00000000000..740b9c66ba4 --- /dev/null +++ b/eslint/eslint-patch/src/custom-config-package-names.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// This is a workaround for ESLint's requirement to consume shareable configurations from package names prefixed +// with "eslint-config". +// +// To remove this requirement, add this line to the top of your project's .eslintrc.js file: +// +// require("@rushstack/eslint-patch/custom-config-package-names"); +// +import { ConfigArrayFactory, ModuleResolver, Naming } from './_patch-base'; + +if (!ConfigArrayFactory.__loadExtendedShareableConfigPatched) { + ConfigArrayFactory.__loadExtendedShareableConfigPatched = true; + const originalLoadExtendedShareableConfig = ConfigArrayFactory.prototype._loadExtendedShareableConfig; + + // Common between ESLint versions + // https://github.com/eslint/eslintrc/blob/242d569020dfe4f561e4503787b99ec016337457/lib/config-array-factory.js#L910 + ConfigArrayFactory.prototype._loadExtendedShareableConfig = function (extendName: string) { + const originalResolve = ModuleResolver.resolve; + try { + ModuleResolver.resolve = function (moduleName: string, relativeToPath: string) { + try { + return originalResolve.call(this, moduleName, relativeToPath); + } catch (e) { + // Only change the name we resolve if we cannot find the normalized module, since it is + // valid to rely on the normalized package name. Use the originally provided module path + // instead of the normalized module path. + if ( + (e as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND' && + moduleName !== extendName && + moduleName === Naming.normalizePackageName(extendName, 'eslint-config') + ) { + return originalResolve.call(this, extendName, relativeToPath); + } else { + throw e; + } + } + }; + return originalLoadExtendedShareableConfig.apply(this, arguments); + } finally { + ModuleResolver.resolve = originalResolve; + } + }; +} diff --git a/eslint/eslint-patch/src/modern-module-resolution.ts b/eslint/eslint-patch/src/modern-module-resolution.ts new file mode 100644 index 00000000000..f1c3d8b9905 --- /dev/null +++ b/eslint/eslint-patch/src/modern-module-resolution.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +// +// To correct how ESLint searches for plugin packages, add this line to the top of your project's .eslintrc.js file: +// +// require("@rushstack/eslint-patch/modern-module-resolution"); +// + +import { + EslintMajorVersion, + ConfigArrayFactory, + ModuleResolver, + isModuleResolutionError +} from './_patch-base'; + +// error: "The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received ''" +const isInvalidImporterPath: (ex: unknown) => boolean = (ex) => + (ex as { code: unknown } | undefined)?.code === 'ERR_INVALID_ARG_VALUE'; + +if (!ConfigArrayFactory.__loadPluginPatched) { + ConfigArrayFactory.__loadPluginPatched = true; + const originalLoadPlugin = ConfigArrayFactory.prototype._loadPlugin; + + if (EslintMajorVersion === 6) { + // ESLint 6.x + // https://github.com/eslint/eslint/blob/9738f8cc864d769988ccf42bb70f524444df1349/lib/cli-engine/config-array-factory.js#L915 + ConfigArrayFactory.prototype._loadPlugin = function ( + name: string, + importerPath: string, + importerName: string + ) { + const originalResolve = ModuleResolver.resolve; + try { + ModuleResolver.resolve = function (moduleName: string, relativeToPath: string) { + try { + // resolve using importerPath instead of relativeToPath + return originalResolve.call(this, moduleName, importerPath); + } catch (e) { + if (isModuleResolutionError(e) || isInvalidImporterPath(e)) { + return originalResolve.call(this, moduleName, relativeToPath); + } + throw e; + } + }; + return originalLoadPlugin.apply(this, arguments); + } finally { + ModuleResolver.resolve = originalResolve; + } + }; + } else { + // ESLint 7.x || 8.x + // https://github.com/eslint/eslintrc/blob/242d569020dfe4f561e4503787b99ec016337457/lib/config-array-factory.js#L1023 + ConfigArrayFactory.prototype._loadPlugin = function (name: string, ctx: Record) { + const originalResolve = ModuleResolver.resolve; + try { + ModuleResolver.resolve = function (moduleName: string, relativeToPath: string) { + try { + // resolve using ctx.filePath instead of relativeToPath + return originalResolve.call(this, moduleName, ctx.filePath); + } catch (e) { + if (isModuleResolutionError(e) || isInvalidImporterPath(e)) { + return originalResolve.call(this, moduleName, relativeToPath); + } + throw e; + } + }; + return originalLoadPlugin.apply(this, arguments); + } finally { + ModuleResolver.resolve = originalResolve; + } + }; + } +}