Skip to content

[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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
202 changes: 202 additions & 0 deletions eslint/eslint-patch/src/_patch-base.ts
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) {
Copy link
Contributor

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 with dist/eslintrc.cjs before calling require.resolve, then verify that the output of path.dirname matches the remainder of 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the switch to == from ===?

Copy link
Contributor

Choose a reason for hiding this comment

The 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does calling toString() provide some advantage over specifying the encoding in the readFileSync call? I'd expect the latter to potentially optimize internally for different buffering behavior.

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 };
43 changes: 43 additions & 0 deletions eslint/eslint-patch/src/custom-config-package-names.ts
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;
}
};
}
Loading