Skip to content
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

More robust loading of CJS plugins using require() #141

Merged
merged 1 commit into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 46 additions & 55 deletions lib/package-json.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { join, resolve, extname, basename, dirname } from 'node:path';
import { join, resolve, basename, dirname } from 'node:path';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { importAbs } from './import.js';
import { createRequire } from 'node:module';
import type { Plugin } from './types.js';
import type { PackageJson } from 'type-fest';

const require = createRequire(import.meta.url);

export function getPluginRoot(path: string) {
return join(process.cwd(), path);
}
Expand All @@ -23,66 +26,54 @@ function loadPackageJson(path: string): PackageJson {

export async function loadPlugin(path: string): Promise<Plugin> {
const pluginRoot = getPluginRoot(path);
const pluginPackageJson = loadPackageJson(path);

// Check for the entry point on the `exports` or `main` field in package.json.
let pluginEntryPoint;
const exports = pluginPackageJson.exports;
if (typeof exports === 'string') {
pluginEntryPoint = exports;
} else if (
typeof exports === 'object' &&
exports !== null &&
!Array.isArray(exports)
) {
// Check various properties on the `exports` object.
// https://nodejs.org/api/packages.html#conditional-exports
const propertiesToCheck: (keyof PackageJson.ExportConditions)[] = [
'.',
'node',
'import',
'require',
'default',
];
for (const prop of propertiesToCheck) {
// @ts-expect-error -- The union type for the object is causing trouble.
const value = exports[prop];
if (typeof value === 'string') {
pluginEntryPoint = value;
break;
try {
// Try require first which should work for CJS plugins.
return require(pluginRoot); // eslint-disable-line import/no-dynamic-require
} catch {
// Otherwise, for ESM plugins, we'll have to try to resolve the exact plugin entry point and import it.
const pluginPackageJson = loadPackageJson(path);
let pluginEntryPoint;
const exports = pluginPackageJson.exports;
if (typeof exports === 'string') {
pluginEntryPoint = exports;
} else if (
typeof exports === 'object' &&
exports !== null &&
!Array.isArray(exports)
) {
// Check various properties on the `exports` object.
// https://nodejs.org/api/packages.html#conditional-exports
const propertiesToCheck: (keyof PackageJson.ExportConditions)[] = [
'.',
'node',
'import',
'require',
'default',
];
for (const prop of propertiesToCheck) {
// @ts-expect-error -- The union type for the object is causing trouble.
const value = exports[prop];
if (typeof value === 'string') {
pluginEntryPoint = value;
break;
}
}
}
} else if (typeof pluginPackageJson.main === 'string') {
pluginEntryPoint = pluginPackageJson.main;
}

if (!pluginEntryPoint) {
pluginEntryPoint = 'index.js'; // This is the default value for the `main` field: https://docs.npmjs.com/cli/v8/configuring-npm/package-json#main
}

if (pluginEntryPoint.endsWith('/')) {
// The `main` field is allowed to specify a directory.
pluginEntryPoint = `${pluginEntryPoint}/index.js`;
}
if (!pluginEntryPoint) {
throw new Error('Unable to determine plugin entry point.');
}

const SUPPORTED_FILE_TYPES = ['.js', '.cjs', '.mjs'];
if (!SUPPORTED_FILE_TYPES.includes(extname(pluginEntryPoint))) {
throw new Error(
`Unsupported file type for plugin entry point. Current types supported: ${SUPPORTED_FILE_TYPES.join(
', '
)}. Entry point detected: ${pluginEntryPoint}`
);
}
const pluginEntryPointAbs = join(pluginRoot, pluginEntryPoint);
if (!existsSync(pluginEntryPointAbs)) {
throw new Error(
`ESLint plugin entry point does not exist. Tried: ${pluginEntryPoint}`
);
}

const pluginEntryPointAbs = join(pluginRoot, pluginEntryPoint);
if (!existsSync(pluginEntryPointAbs)) {
throw new Error(
`Could not find entry point for ESLint plugin. Tried: ${pluginEntryPoint}`
);
const { default: plugin } = await importAbs(pluginEntryPointAbs);
return plugin;
}

const { default: plugin } = await importAbs(pluginEntryPointAbs);
return plugin;
}

export function getPluginPrefix(path: string): string {
Expand Down
17 changes: 17 additions & 0 deletions test/fixtures/cjs-config-extends/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# eslint-plugin-test

## Rules

<!-- begin auto-generated rules list -->

💼 Configurations enabled in.\
✅ Set in the `recommended` configuration.

| Name | Description | 💼 |
| :----------------------------- | :--------------------- | :- |
| [no-bar](docs/rules/no-bar.md) | Description of no-bar. | ✅ |
| [no-baz](docs/rules/no-baz.md) | Description of no-baz. | ✅ |
| [no-biz](docs/rules/no-biz.md) | Description of no-biz. | ✅ |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. | ✅ |

<!-- end auto-generated rules list -->
1 change: 1 addition & 0 deletions test/fixtures/cjs-config-extends/base-base-base-config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { rules: { 'test/no-bar': 'error' } };
1 change: 1 addition & 0 deletions test/fixtures/cjs-config-extends/base-base-config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { extends: require.resolve('./base-base-base-config.cjs') };
11 changes: 11 additions & 0 deletions test/fixtures/cjs-config-extends/base-config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
extends: [require.resolve('./base-base-config.cjs')],
rules: { 'test/no-foo': 'error' },
overrides: [
{
extends: [require.resolve('./override-config.cjs')],
files: ['*.js'],
rules: { 'test/no-baz': 'error' },
},
],
};
5 changes: 5 additions & 0 deletions test/fixtures/cjs-config-extends/docs/rules/no-bar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Description of no-bar (`test/no-bar`)

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->
5 changes: 5 additions & 0 deletions test/fixtures/cjs-config-extends/docs/rules/no-baz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Description of no-baz (`test/no-baz`)

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->
5 changes: 5 additions & 0 deletions test/fixtures/cjs-config-extends/docs/rules/no-biz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Description of no-biz (`test/no-biz`)

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->
5 changes: 5 additions & 0 deletions test/fixtures/cjs-config-extends/docs/rules/no-foo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Description of no-foo (`test/no-foo`)

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->
31 changes: 31 additions & 0 deletions test/fixtures/cjs-config-extends/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
rules: {
'no-foo': {
meta: { docs: { description: 'Description of no-foo.' } },
create() {},
},
'no-bar': {
meta: { docs: { description: 'Description of no-bar.' } },
create() {},
},
'no-baz': {
meta: { docs: { description: 'Description of no-baz.' } },
create() {},
},
'no-biz': {
meta: { docs: { description: 'Description of no-biz.' } },
create() {},
},
},
configs: {
recommended: {
extends: [
require.resolve('./base-config.cjs'),
// Should ignore these since they're external:
'eslint:recommended',
'plugin:some-plugin/recommended',
'prettier',
],
},
},
};
1 change: 1 addition & 0 deletions test/fixtures/cjs-config-extends/override-config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { rules: { 'test/no-biz': 'error' } };
4 changes: 4 additions & 0 deletions test/fixtures/cjs-config-extends/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "eslint-plugin-test",
"type": "commonjs"
}
8 changes: 8 additions & 0 deletions test/fixtures/cjs-main-directory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# eslint-plugin-test

<!-- begin auto-generated rules list -->

| Name |
| :--- |

<!-- end auto-generated rules list -->
1 change: 1 addition & 0 deletions test/fixtures/cjs-main-directory/lib/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { rules: {} };
5 changes: 5 additions & 0 deletions test/fixtures/cjs-main-directory/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "eslint-plugin-test",
"type": "commonjs",
"main": "lib/index.cjs"
}
2 changes: 2 additions & 0 deletions test/fixtures/cjs-main-file-does-not-exist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- begin auto-generated rules list -->
<!-- end auto-generated rules list -->
5 changes: 5 additions & 0 deletions test/fixtures/cjs-main-file-does-not-exist/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "eslint-plugin-test",
"type": "commonjs",
"main": "index.js"
}
8 changes: 8 additions & 0 deletions test/fixtures/cjs-missing-main/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# eslint-plugin-test

<!-- begin auto-generated rules list -->

| Name |
| :--- |

<!-- end auto-generated rules list -->
1 change: 1 addition & 0 deletions test/fixtures/cjs-missing-main/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { rules: {} };
4 changes: 4 additions & 0 deletions test/fixtures/cjs-missing-main/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "eslint-plugin-test",
"type": "commonjs"
}
9 changes: 9 additions & 0 deletions test/fixtures/cjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# eslint-plugin-test

<!-- begin auto-generated rules list -->

| Name | Description |
| :----------------------------- | :------------ |
| [no-foo](docs/rules/no-foo.md) | disallow foo. |

<!-- end auto-generated rules list -->
3 changes: 3 additions & 0 deletions test/fixtures/cjs/docs/rules/no-foo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Disallow foo (`test/no-foo`)

<!-- end auto-generated rule header -->
8 changes: 8 additions & 0 deletions test/fixtures/cjs/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
rules: {
'no-foo': {
meta: { docs: { description: 'disallow foo.' } },
create() {},
},
},
};
4 changes: 4 additions & 0 deletions test/fixtures/cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "eslint-plugin-test",
"type": "commonjs"
}
78 changes: 78 additions & 0 deletions test/lib/__snapshots__/generate-cjs-test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generate (cjs) basic generates the documentation 1`] = `
"# eslint-plugin-test

<!-- begin auto-generated rules list -->

| Name | Description |
| :----------------------------- | :------------ |
| [no-foo](docs/rules/no-foo.md) | disallow foo. |

<!-- end auto-generated rules list -->
"
`;

exports[`generate (cjs) basic generates the documentation 2`] = `
"# Disallow foo (\`test/no-foo\`)

<!-- end auto-generated rule header -->
"
`;

exports[`generate (cjs) config that extends another config generates the documentation 1`] = `
"# eslint-plugin-test

## Rules

<!-- begin auto-generated rules list -->

💼 Configurations enabled in.\\
✅ Set in the \`recommended\` configuration.

| Name | Description | 💼 |
| :----------------------------- | :--------------------- | :- |
| [no-bar](docs/rules/no-bar.md) | Description of no-bar. | ✅ |
| [no-baz](docs/rules/no-baz.md) | Description of no-baz. | ✅ |
| [no-biz](docs/rules/no-biz.md) | Description of no-biz. | ✅ |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. | ✅ |

<!-- end auto-generated rules list -->
"
`;

exports[`generate (cjs) config that extends another config generates the documentation 2`] = `
"# Description of no-foo (\`test/no-foo\`)

💼 This rule is enabled in the ✅ \`recommended\` config.

<!-- end auto-generated rule header -->
"
`;

exports[`generate (cjs) config that extends another config generates the documentation 3`] = `
"# Description of no-bar (\`test/no-bar\`)

💼 This rule is enabled in the ✅ \`recommended\` config.

<!-- end auto-generated rule header -->
"
`;

exports[`generate (cjs) config that extends another config generates the documentation 4`] = `
"# Description of no-baz (\`test/no-baz\`)

💼 This rule is enabled in the ✅ \`recommended\` config.

<!-- end auto-generated rule header -->
"
`;

exports[`generate (cjs) config that extends another config generates the documentation 5`] = `
"# Description of no-biz (\`test/no-biz\`)

💼 This rule is enabled in the ✅ \`recommended\` config.

<!-- end auto-generated rule header -->
"
`;