Skip to content

Commit

Permalink
fix: load plugins in the CLI in flat config mode (#18185)
Browse files Browse the repository at this point in the history
* fix: load plugins in the CLI in flat config mode

* apply review suggestions

* fix nesting of unit tests

* improve error message
  • Loading branch information
fasttime committed Mar 13, 2024
1 parent 5251327 commit ae8103d
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 11 deletions.
2 changes: 2 additions & 0 deletions docs/src/extend/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export default plugin;
module.exports = plugin;
```

If you plan to distribute your plugin as an npm package, make sure that the module that exports the plugin object is the default export of your package. This will enable ESLint to import the plugin when it is specified in the command line in the [`--plugin` option](../use/command-line-interface#--plugin).

### Meta Data in Plugins

For easier debugging and more effective caching of plugins, it's recommended to provide a name and version in a `meta` object at the root of your plugin, like this:
Expand Down
39 changes: 28 additions & 11 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const debug = require("debug")("eslint:cli");
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
/** @typedef {import("./shared/types").Plugin} Plugin */
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */

//------------------------------------------------------------------------------
Expand All @@ -47,6 +48,32 @@ const mkdir = promisify(fs.mkdir);
const stat = promisify(fs.stat);
const writeFile = promisify(fs.writeFile);

/**
* Loads plugins with the specified names.
* @param {{ "import": (name: string) => Promise<any> }} importer An object with an `import` method called once for each plugin.
* @param {string[]} pluginNames The names of the plugins to be loaded, with or without the "eslint-plugin-" prefix.
* @returns {Promise<Record<string, Plugin>>} A mapping of plugin short names to implementations.
*/
async function loadPlugins(importer, pluginNames) {
const plugins = {};

await Promise.all(pluginNames.map(async pluginName => {

const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const module = await importer.import(longName);

if (!("default" in module)) {
throw new Error(`"${longName}" cannot be used with the \`--plugin\` option because its default module does not provide a \`default\` export`);
}

const shortName = naming.getShorthandName(pluginName, "eslint-plugin");

plugins[shortName] = module.default;
}));

return plugins;
}

/**
* Predicate function for whether or not to apply fixes in quiet mode.
* If a message is a warning, do not apply a fix.
Expand Down Expand Up @@ -152,17 +179,7 @@ async function translateOptions({
}

if (plugin) {
const plugins = {};

for (const pluginName of plugin) {

const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");

plugins[shortName] = await importer.import(longName);
}

overrideConfig[0].plugins = plugins;
overrideConfig[0].plugins = await loadPlugins(importer, plugin);
}

} else {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
99 changes: 99 additions & 0 deletions tests/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1757,4 +1757,103 @@ describe("cli", () => {
});


describe("flat Only", () => {

describe("`--plugin` option", () => {

let originalCwd;

beforeEach(() => {
originalCwd = process.cwd();
process.chdir(getFixturePath("plugins"));
});

afterEach(() => {
process.chdir(originalCwd);
originalCwd = void 0;
});

it("should load a plugin from a CommonJS package", async () => {
const code = "--plugin hello-cjs --rule 'hello-cjs/hello: error' ../files/*.js";

const exitCode = await cli.execute(code, null, true);

assert.strictEqual(exitCode, 1);
assert.ok(log.info.calledOnce);
assert.include(log.info.firstCall.firstArg, "Hello CommonJS!");
});

it("should load a plugin from an ESM package", async () => {
const code = "--plugin hello-esm --rule 'hello-esm/hello: error' ../files/*.js";

const exitCode = await cli.execute(code, null, true);

assert.strictEqual(exitCode, 1);
assert.ok(log.info.calledOnce);
assert.include(log.info.firstCall.firstArg, "Hello ESM!");
});

it("should load multiple plugins", async () => {
const code = "--plugin 'hello-cjs, hello-esm' --rule 'hello-cjs/hello: warn, hello-esm/hello: error' ../files/*.js";

const exitCode = await cli.execute(code, null, true);

assert.strictEqual(exitCode, 1);
assert.ok(log.info.calledOnce);
assert.include(log.info.firstCall.firstArg, "Hello CommonJS!");
assert.include(log.info.firstCall.firstArg, "Hello ESM!");
});

it("should resolve plugins specified with 'eslint-plugin-'", async () => {
const code = "--plugin 'eslint-plugin-schema-array, @scope/eslint-plugin-example' --rule 'schema-array/rule1: warn, @scope/example/test: warn' ../passing.js";

const exitCode = await cli.execute(code, null, true);

assert.strictEqual(exitCode, 0);
});

it("should resolve plugins in the parent directory's node_module subdirectory", async () => {
process.chdir("subdir");
const code = "--plugin 'example, @scope/example' file.js";

const exitCode = await cli.execute(code, null, true);

assert.strictEqual(exitCode, 0);
});

it("should fail if a plugin is not found", async () => {
const code = "--plugin 'example, no-such-plugin' ../passing.js";

await stdAssert.rejects(
cli.execute(code, null, true),
({ message }) => {
assert(
message.startsWith("Cannot find module 'eslint-plugin-no-such-plugin'\n"),
`Unexpected error message:\n${message}`
);
return true;
}
);
});

it("should fail if a plugin throws an error while loading", async () => {
const code = "--plugin 'example, throws-on-load' ../passing.js";

await stdAssert.rejects(
cli.execute(code, null, true),
{ message: "error thrown while loading this module" }
);
});

it("should fail to load a plugin from a package without a default export", async () => {
const code = "--plugin 'example, no-default-export' ../passing.js";

await stdAssert.rejects(
cli.execute(code, null, true),
{ message: '"eslint-plugin-no-default-export" cannot be used with the `--plugin` option because its default module does not provide a `default` export' }
);
});
});
});

});

0 comments on commit ae8103d

Please sign in to comment.