From dc2e12c21f7fe107c39c90852ac6763ffb9d6bf0 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 7 Jan 2022 14:37:07 -0800 Subject: [PATCH 1/3] Move patch to new eslint package folder --- eslint/eslint-patch/src/_patch-base.ts | 202 ++++++++++++++++++ .../src/custom-config-package-names.ts | 38 ++++ .../src/modern-module-resolution.ts | 193 +---------------- 3 files changed, 244 insertions(+), 189 deletions(-) create mode 100644 eslint/eslint-patch/src/_patch-base.ts create mode 100644 eslint/eslint-patch/src/custom-config-package-names.ts diff --git a/eslint/eslint-patch/src/_patch-base.ts b/eslint/eslint-patch/src/_patch-base.ts new file mode 100644 index 00000000000..d0b527c9721 --- /dev/null +++ b/eslint/eslint-patch/src/_patch-base.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const path = require('path'); +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'; + +// Module path for eslintrc.cjs +// Example: ".../@eslint/eslintrc/dist/eslintrc.cjs" +let eslintrcBundlePath: string | undefined = undefined; + +// Module path for config-array-factory.js +// Example: ".../@eslint/eslintrc/lib/config-array-factory" +let configArrayFactoryPath: string | undefined = undefined; + +// Module path for naming.js +// Example: ".../@eslint/eslintrc/lib/shared/naming" +let namingPath: string | undefined = undefined; + +// Module path for relative-module-resolver.js +// Example: ".../@eslint/eslintrc/lib/shared/relative-module-resolver" +let moduleResolverPath: string | undefined = undefined; + +// Folder path where ESLint's package.json can be found +// Example: ".../node_modules/eslint" +let eslintFolder: string | undefined = undefined; + +// Probe for the ESLint >=8.0.0 layout: +for (let currentModule = module; ; ) { + if (!eslintrcBundlePath) { + // For ESLint >=8.0.0, all @eslint/eslintrc code is bundled at this path: + // .../@eslint/eslintrc/dist/eslintrc.cjs + try { + const eslintrcFolder = path.dirname( + require.resolve('@eslint/eslintrc/package.json', { paths: [currentModule.path] }) + ); + + // 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'); + } + } catch (ex: unknown) { + // Module resolution failures are expected, as we're walking + // up our require stack to look for eslint. All other errors + // are rethrown. + if (!isModuleResolutionError(ex)) { + throw ex; + } + } + } else { + // Next look for a file in ESLint's folder + // .../eslint/lib/cli-engine/cli-engine.js + try { + const eslintCandidateFolder = path.dirname( + require.resolve('eslint/package.json', { + paths: [currentModule.path] + }) + ); + + // Make sure we actually resolved the module in our call path + // and not some other spurious dependency. + if (path.join(eslintCandidateFolder, 'lib/cli-engine/cli-engine.js') === currentModule.filename) { + eslintFolder = eslintCandidateFolder; + break; + } + } catch (ex: unknown) { + // Module resolution failures are expected, as we're walking + // up our require stack to look for eslint. All other errors + // are rethrown. + if (!isModuleResolutionError(ex)) { + throw ex; + } + } + } + + if (!currentModule.parent) { + break; + } + currentModule = currentModule.parent; +} + +if (!eslintFolder) { + // Probe for the ESLint >=7.8.0 layout: + for (let currentModule = module; ; ) { + if (!configArrayFactoryPath) { + // For ESLint >=7.8.0, config-array-factory.js is at this path: + // .../@eslint/eslintrc/lib/config-array-factory.js + try { + const eslintrcFolder = path.dirname( + require.resolve('@eslint/eslintrc/package.json', { + paths: [currentModule.path] + }) + ); + + if (path.join(eslintrcFolder, '/lib/config-array-factory.js') == currentModule.filename) { + configArrayFactoryPath = path.join(eslintrcFolder, 'lib/config-array-factory.js'); + moduleResolverPath = path.join(eslintrcFolder, 'lib/shared/relative-module-resolver'); + namingPath = path.join(eslintrcFolder, 'lib/shared/naming'); + } + } catch (ex: unknown) { + // Module resolution failures are expected, as we're walking + // up our require stack to look for eslint. All other errors + // are rethrown. + if (!isModuleResolutionError(ex)) { + throw ex; + } + } + } else { + // Next look for a file in ESLint's folder + // .../eslint/lib/cli-engine/cli-engine.js + try { + const eslintCandidateFolder = path.dirname( + require.resolve('eslint/package.json', { + paths: [currentModule.path] + }) + ); + + if (path.join(eslintCandidateFolder, 'lib/cli-engine/cli-engine.js') == currentModule.filename) { + eslintFolder = eslintCandidateFolder; + break; + } + } catch (ex: unknown) { + // Module resolution failures are expected, as we're walking + // up our require stack to look for eslint. All other errors + // are rethrown. + if (!isModuleResolutionError(ex)) { + throw ex; + } + } + } + + if (!currentModule.parent) { + break; + } + currentModule = currentModule.parent; + } +} + +if (!eslintFolder) { + // Probe for the <7.8.0 layout: + for (let currentModule = module; ; ) { + // For ESLint <7.8.0, config-array-factory.js was at this path: + // .../eslint/lib/cli-engine/config-array-factory.js + if (/[\\/]eslint[\\/]lib[\\/]cli-engine[\\/]config-array-factory\.js$/i.test(currentModule.filename)) { + eslintFolder = path.join(path.dirname(currentModule.filename), '../..'); + configArrayFactoryPath = path.join(eslintFolder, 'lib/cli-engine/config-array-factory'); + moduleResolverPath = path.join(eslintFolder, 'lib/shared/relative-module-resolver'); + namingPath = path.join(eslintFolder, 'lib/shared/naming'); + break; + } + + if (!currentModule.parent) { + // This was tested with ESLint 6.1.0 .. 7.12.1. + throw new Error( + 'Failed to patch ESLint because the calling module was not recognized.\n' + + 'If you are using a newer ESLint version that may be unsupported, please create a GitHub issue:\n' + + 'https://github.com/microsoft/rushstack/issues' + ); + } + currentModule = currentModule.parent; + } +} + +// Detect the ESLint package version +const eslintPackageJson = fs.readFileSync(path.join(eslintFolder, 'package.json')).toString(); +const eslintPackageObject = JSON.parse(eslintPackageJson); +const eslintPackageVersion = eslintPackageObject.version; +const versionMatch = /^([0-9]+)\./.exec(eslintPackageVersion); // parse the SemVer MAJOR part +if (!versionMatch) { + throw new Error('Unable to parse ESLint version: ' + eslintPackageVersion); +} +const eslintMajorVersion = Number(versionMatch[1]); +if (!(eslintMajorVersion >= 6 && eslintMajorVersion <= 8)) { + throw new Error( + 'The patch-eslint.js script has only been tested with ESLint version 6.x, 7.x, and 8.x.' + + ` (Your version: ${eslintPackageVersion})\n` + + 'Consider reporting a GitHub issue:\n' + + 'https://github.com/microsoft/rushstack/issues' + ); +} + +let ConfigArrayFactory: any; +if (eslintMajorVersion === 8) { + ConfigArrayFactory = require(eslintrcBundlePath!).Legacy.ConfigArrayFactory; +} else { + ConfigArrayFactory = require(configArrayFactoryPath!).ConfigArrayFactory; +} + +let ModuleResolver: { resolve: any }; +let Naming: { normalizePackageName: any }; +if (eslintMajorVersion === 8) { + ModuleResolver = require(eslintrcBundlePath!).Legacy.ModuleResolver; + Naming = require(eslintrcBundlePath!).Legacy.naming; +} else { + ModuleResolver = require(moduleResolverPath!); + Naming = require(namingPath!); +} + +export { ConfigArrayFactory, ModuleResolver, Naming, eslintMajorVersion as EslintMajorVersion }; 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..3a532292d1c --- /dev/null +++ b/eslint/eslint-patch/src/custom-config-package-names.ts @@ -0,0 +1,38 @@ +// 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, Naming } from './_patch-base'; + +if (!ConfigArrayFactory.__loadExtendedSharableConfigPatched) { + ConfigArrayFactory.__loadExtendedSharableConfigPatched = true; + const originalLoadExtendedSharableConfig = ConfigArrayFactory.prototype._loadExtendedSharableConfig; + + ConfigArrayFactory.prototype._loadExtendedSharableConfig = function () { + try { + return originalLoadExtendedSharableConfig.apply(this, arguments); + } catch (e) { + // We only care about the case where extended configs are not found. + const error: Error & { messageTemplate?: string } = e as Error & { messageTemplate?: string }; + if (!error.messageTemplate || error.messageTemplate !== 'extend-config-missing') { + throw e; + } + } + + // Extend config could not be resolved from the original location. Override the normalization + // logic to use the extended config as it was specified. + const originalNormalize = Naming.normalizePackageName; + try { + Naming.normalizePackageName = (name: string, prefix: string) => name; + return originalLoadExtendedSharableConfig.apply(this, arguments); + } finally { + Naming.normalizePackageName = originalNormalize; + } + }; +} diff --git a/eslint/eslint-patch/src/modern-module-resolution.ts b/eslint/eslint-patch/src/modern-module-resolution.ts index 41e96143f18..694bc684fa4 100644 --- a/eslint/eslint-patch/src/modern-module-resolution.ts +++ b/eslint/eslint-patch/src/modern-module-resolution.ts @@ -7,199 +7,14 @@ // // require("@rushstack/eslint-patch/modern-module-resolution"); // -const path = require('path'); -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'; +import { EslintMajorVersion, ConfigArrayFactory, ModuleResolver } from './_patch-base'; -// Module path for eslintrc.cjs -// Example: ".../@eslint/eslintrc/dist/eslintrc.cjs" -let eslintrcBundlePath: string | undefined = undefined; - -// Module path for config-array-factory.js -// Example: ".../@eslint/eslintrc/lib/config-array-factory" -let configArrayFactoryPath: string | undefined = undefined; - -// Module path for relative-module-resolver.js -// Example: ".../@eslint/eslintrc/lib/shared/relative-module-resolver" -let moduleResolverPath: string | undefined = undefined; - -// Folder path where ESLint's package.json can be found -// Example: ".../node_modules/eslint" -let eslintFolder: string | undefined = undefined; - -// Probe for the ESLint >=8.0.0 layout: -for (let currentModule = module; ; ) { - if (!eslintrcBundlePath) { - // For ESLint >=8.0.0, all @eslint/eslintrc code is bundled at this path: - // .../@eslint/eslintrc/dist/eslintrc.cjs - try { - const eslintrcFolder = path.dirname( - require.resolve('@eslint/eslintrc/package.json', { paths: [currentModule.path] }) - ); - - // 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'); - } - } catch (ex: unknown) { - // Module resolution failures are expected, as we're walking - // up our require stack to look for eslint. All other errors - // are rethrown. - if (!isModuleResolutionError(ex)) { - throw ex; - } - } - } else { - // Next look for a file in ESLint's folder - // .../eslint/lib/cli-engine/cli-engine.js - try { - const eslintCandidateFolder = path.dirname( - require.resolve('eslint/package.json', { - paths: [currentModule.path] - }) - ); - - // Make sure we actually resolved the module in our call path - // and not some other spurious dependency. - if (path.join(eslintCandidateFolder, 'lib/cli-engine/cli-engine.js') === currentModule.filename) { - eslintFolder = eslintCandidateFolder; - break; - } - } catch (ex: unknown) { - // Module resolution failures are expected, as we're walking - // up our require stack to look for eslint. All other errors - // are rethrown. - if (!isModuleResolutionError(ex)) { - throw ex; - } - } - } - - if (!currentModule.parent) { - break; - } - currentModule = currentModule.parent; -} - -if (!eslintFolder) { - // Probe for the ESLint >=7.8.0 layout: - for (let currentModule = module; ; ) { - if (!configArrayFactoryPath) { - // For ESLint >=7.8.0, config-array-factory.js is at this path: - // .../@eslint/eslintrc/lib/config-array-factory.js - try { - const eslintrcFolder = path.dirname( - require.resolve('@eslint/eslintrc/package.json', { - paths: [currentModule.path] - }) - ); - - if (path.join(eslintrcFolder, '/lib/config-array-factory.js') == currentModule.filename) { - configArrayFactoryPath = path.join(eslintrcFolder, 'lib/config-array-factory.js'); - moduleResolverPath = path.join(eslintrcFolder, 'lib/shared/relative-module-resolver'); - } - } catch (ex: unknown) { - // Module resolution failures are expected, as we're walking - // up our require stack to look for eslint. All other errors - // are rethrown. - if (!isModuleResolutionError(ex)) { - throw ex; - } - } - } else { - // Next look for a file in ESLint's folder - // .../eslint/lib/cli-engine/cli-engine.js - try { - const eslintCandidateFolder = path.dirname( - require.resolve('eslint/package.json', { - paths: [currentModule.path] - }) - ); - - if (path.join(eslintCandidateFolder, 'lib/cli-engine/cli-engine.js') == currentModule.filename) { - eslintFolder = eslintCandidateFolder; - break; - } - } catch (ex: unknown) { - // Module resolution failures are expected, as we're walking - // up our require stack to look for eslint. All other errors - // are rethrown. - if (!isModuleResolutionError(ex)) { - throw ex; - } - } - } - - if (!currentModule.parent) { - break; - } - currentModule = currentModule.parent; - } -} - -if (!eslintFolder) { - // Probe for the <7.8.0 layout: - for (let currentModule = module; ; ) { - // For ESLint <7.8.0, config-array-factory.js was at this path: - // .../eslint/lib/cli-engine/config-array-factory.js - if (/[\\/]eslint[\\/]lib[\\/]cli-engine[\\/]config-array-factory\.js$/i.test(currentModule.filename)) { - eslintFolder = path.join(path.dirname(currentModule.filename), '../..'); - configArrayFactoryPath = path.join(eslintFolder, 'lib/cli-engine/config-array-factory'); - moduleResolverPath = path.join(eslintFolder, 'lib/shared/relative-module-resolver'); - break; - } - - if (!currentModule.parent) { - // This was tested with ESLint 6.1.0 .. 7.12.1. - throw new Error( - 'Failed to patch ESLint because the calling module was not recognized.\n' + - 'If you are using a newer ESLint version that may be unsupported, please create a GitHub issue:\n' + - 'https://github.com/microsoft/rushstack/issues' - ); - } - currentModule = currentModule.parent; - } -} - -// Detect the ESLint package version -const eslintPackageJson = fs.readFileSync(path.join(eslintFolder, 'package.json')).toString(); -const eslintPackageObject = JSON.parse(eslintPackageJson); -const eslintPackageVersion = eslintPackageObject.version; -const versionMatch = /^([0-9]+)\./.exec(eslintPackageVersion); // parse the SemVer MAJOR part -if (!versionMatch) { - throw new Error('Unable to parse ESLint version: ' + eslintPackageVersion); -} -const eslintMajorVersion = Number(versionMatch[1]); -if (!(eslintMajorVersion >= 6 && eslintMajorVersion <= 8)) { - throw new Error( - 'The patch-eslint.js script has only been tested with ESLint version 6.x, 7.x, and 8.x.' + - ` (Your version: ${eslintPackageVersion})\n` + - 'Consider reporting a GitHub issue:\n' + - 'https://github.com/microsoft/rushstack/issues' - ); -} - -let ConfigArrayFactory; -if (eslintMajorVersion === 8) { - ConfigArrayFactory = require(eslintrcBundlePath!).Legacy.ConfigArrayFactory; -} else { - ConfigArrayFactory = require(configArrayFactoryPath!).ConfigArrayFactory; -} -if (!ConfigArrayFactory.__patched) { - ConfigArrayFactory.__patched = true; - - let ModuleResolver: { resolve: any }; - if (eslintMajorVersion === 8) { - ModuleResolver = require(eslintrcBundlePath!).Legacy.ModuleResolver; - } else { - ModuleResolver = require(moduleResolverPath!); - } +if (!ConfigArrayFactory.__loadPluginPatched) { + ConfigArrayFactory.__loadPluginPatched = true; const originalLoadPlugin = ConfigArrayFactory.prototype._loadPlugin; - if (eslintMajorVersion === 6) { + if (EslintMajorVersion === 6) { // ESLint 6.x ConfigArrayFactory.prototype._loadPlugin = function ( name: string, From 6f3037968c2ec5e2a0913324f37e9c46120f9c7c Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 7 Jan 2022 14:38:02 -0800 Subject: [PATCH 2/3] Rush change --- ...r-danade-EslintResolverTweaks_2022-01-07-22-37.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/eslint-patch/user-danade-EslintResolverTweaks_2022-01-07-22-37.json diff --git a/common/changes/@rushstack/eslint-patch/user-danade-EslintResolverTweaks_2022-01-07-22-37.json b/common/changes/@rushstack/eslint-patch/user-danade-EslintResolverTweaks_2022-01-07-22-37.json new file mode 100644 index 00000000000..ffca34b8d48 --- /dev/null +++ b/common/changes/@rushstack/eslint-patch/user-danade-EslintResolverTweaks_2022-01-07-22-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-patch", + "comment": "Allow consumption of non-\"eslint-config-\" prefixed shareable configs", + "type": "minor" + } + ], + "packageName": "@rushstack/eslint-patch" +} \ No newline at end of file From 4de86d2036e21c8b9bfebb157498d8847c267b4d Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Sat, 8 Jan 2022 00:31:43 -0800 Subject: [PATCH 3/3] Use a more stable method to ensure that custom config package names can be consumed --- .../src/custom-config-package-names.ts | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/eslint/eslint-patch/src/custom-config-package-names.ts b/eslint/eslint-patch/src/custom-config-package-names.ts index 3a532292d1c..ffabaacacd6 100644 --- a/eslint/eslint-patch/src/custom-config-package-names.ts +++ b/eslint/eslint-patch/src/custom-config-package-names.ts @@ -8,31 +8,36 @@ // // require("@rushstack/eslint-patch/custom-config-package-names"); // -import { ConfigArrayFactory, Naming } from './_patch-base'; +import { ConfigArrayFactory, ModuleResolver, Naming } from './_patch-base'; -if (!ConfigArrayFactory.__loadExtendedSharableConfigPatched) { - ConfigArrayFactory.__loadExtendedSharableConfigPatched = true; - const originalLoadExtendedSharableConfig = ConfigArrayFactory.prototype._loadExtendedSharableConfig; +if (!ConfigArrayFactory.__loadExtendedShareableConfigPatched) { + ConfigArrayFactory.__loadExtendedShareableConfigPatched = true; + const originalLoadExtendedShareableConfig = ConfigArrayFactory.prototype._loadExtendedShareableConfig; - ConfigArrayFactory.prototype._loadExtendedSharableConfig = function () { + ConfigArrayFactory.prototype._loadExtendedShareableConfig = function (extendName: string) { + const originalResolve = ModuleResolver.resolve; try { - return originalLoadExtendedSharableConfig.apply(this, arguments); - } catch (e) { - // We only care about the case where extended configs are not found. - const error: Error & { messageTemplate?: string } = e as Error & { messageTemplate?: string }; - if (!error.messageTemplate || error.messageTemplate !== 'extend-config-missing') { - throw e; - } - } - - // Extend config could not be resolved from the original location. Override the normalization - // logic to use the extended config as it was specified. - const originalNormalize = Naming.normalizePackageName; - try { - Naming.normalizePackageName = (name: string, prefix: string) => name; - return originalLoadExtendedSharableConfig.apply(this, arguments); + 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 { - Naming.normalizePackageName = originalNormalize; + ModuleResolver.resolve = originalResolve; } }; }