-
Notifications
You must be signed in to change notification settings - Fork 626
[eslint-patch] Allow for extended shareable ESLint configurations to use any name #3020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some concept here |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the switch to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, same question |
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does calling |
||
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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// 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; | ||
|
||
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; | ||
} | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be more efficient to verify
currentModule.filename
ends withdist/eslintrc.cjs
before callingrequire.resolve
, then verify that the output of path.dirname matches the remainder ofcurrentModule.filename
?