Skip to content

Commit

Permalink
Merge pull request #141 from bmish/require-cjs
Browse files Browse the repository at this point in the history
More robust loading of CJS plugins using `require()`
  • Loading branch information
bmish committed Nov 17, 2022
2 parents f3aaabc + 26cb836 commit ae3e411
Show file tree
Hide file tree
Showing 28 changed files with 502 additions and 436 deletions.
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 -->
"
`;

0 comments on commit ae3e411

Please sign in to comment.