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

feat: support new config system #1245

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 93 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ yarn add --dev eslint eslint-plugin-jest
**Note:** If you installed ESLint globally then you must also install
`eslint-plugin-jest` globally.

## Usage
## Usage (legacy: `.eslintrc*`)

Add `jest` to the plugins section of your `.eslintrc` configuration file. You
can omit the `eslint-plugin-` prefix:
Expand Down Expand Up @@ -59,7 +59,7 @@ doing:
This is included in all configs shared by this plugin, so can be omitted if
extending them.

#### Aliased Jest globals
### Aliased Jest globals

You can tell this plugin about any global Jests you have aliased using the
`globalAliases` setting:
Expand Down Expand Up @@ -143,7 +143,64 @@ module.exports = {
};
```

## Shareable configurations
## Usage (new: `eslint.config.js`)

From [`v8.21.0`](https://github.com/eslint/eslint/releases/tag/v8.21.0), eslint
announced a new config system. In the new system, `.eslintrc*` is no longer
used. `eslint.config.js` would be the default config file name.

And from [`v8.23.0`](https://github.com/eslint/eslint/releases/tag/v8.23.0),
eslint CLI starts to look up `eslint.config.js`. **So, if your eslint is
`>=8.23.0`, you're 100% ready to use the new config system.**

You might want to check out the official blog posts,

- <https://eslint.org/blog/2022/08/new-config-system-part-1/>
- <https://eslint.org/blog/2022/08/new-config-system-part-2/>
- <https://eslint.org/blog/2022/08/new-config-system-part-3/>

and the
[official docs](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new).

The default export of `eslint-plugin-jest` is a plugin object.

```js
import jest from 'eslint-plugin-jest';
import jestGlobals from 'eslint-plugin-jest/globals';

export default [
{
// A config for source code (non-test files)
files: ['**/*.test.{js,jsx}'],
// ...
},
// --- snip ---
{
// The config in case you use jest snapshot test
files: ['**/*.snap'],
plugins: {
jest,
},
processor: 'jest/.snap',
},
{
// A config for test files (non-source-code)
files: ['**/*.test.{js,jsx}'],
languageOptions: {
globals: jestGlobals,
},
plugins: {
jest,
},
rules: {
// rules you want
'jest/better-regex': 'warn',
},
},
];
```

## Shareable configurations (legacy: `.eslintrc*`)

### Recommended

Expand Down Expand Up @@ -193,6 +250,39 @@ While the `recommended` and `style` configurations only change in major versions
the `all` configuration may change in any release and is thus unsuited for
installations requiring long-term consistency.

## Shareable configurations (new: `eslint.config.js`)

If you use the new config system (`eslint.config.js`), there're 3 shareable
configs.

- `eslint-plugin-jest/all`
- `eslint-plugin-jest/recommended`
- `eslint-plugin-jest/style`

**Note**: Shareable configs will enable the
[`languageOptions.globals`](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects).

In the new config system, `plugin:` protocol(e.g. `plugin:jest/recommended`) is
no longer valid. As eslint does not automatically import the preset config
(shareable config), you explicitly do it by yourself.

**Note**: The new plugin object does not have `configs` property as well.

```js
import jest from 'eslint-plugin-jest/all';

export default [
{
// A config for source code (non-test files)
files: ['**/*.test.{js,jsx}'],
// ...
},
// --- snip ---
...jest, // This is not a plugin object, but a shareable config object(array)
// --- snip ---
];
```

## Rules

<!-- begin base rules list -->
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@
"email": "hello@jkimbo.com",
"url": "jkimbo.com"
},
"main": "lib/",
"main": "lib/legacy.js",
"exports": {
".": {
"import": "./lib/index.js",
Copy link
Member

Choose a reason for hiding this comment

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

hmm, I don't think we produce ESM?

Copy link
Author

Choose a reason for hiding this comment

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

exports can benefit CJS projects, like eslint-plugin-jest.

There's a difference in module resolution between the legacy and new config system.

In the legacy config system, as we know,

  • A shareable config or plugin is not loaded(require or import) in eslintrc.js.
  • Instead, a user just specifies the name as a string (e.g. plugin:jest/recommended or plugin: [ 'jest' ]), and eslint internally require() (not import()) them.

In the new config system,

  • A shareable config or plugin is explicitly loaded by a user, directly or transitively, following the native module resolution by nodejs. eslint does not load by itself anymore.
  • A shareable config or plugin can be required or imported, as eslint support both CJS and ESM for eslint.config.js.

Thus,

if ( the plugin is require()d ){
  the user is either using legacy or new config system. 
} else if( the plugin is import()ed ) {
  the user is using new config system
}

Therefore, by this PR,

  • A user who uses the legacy config system can let eslint to do require('eslint-plugin-jest'). This is not breaking change.
  • A user who uses the new config system in ESM can import jest from 'eslint-plugin-jest'. This feels natural. This can be possible by the conditional export.
  • A user who uses the new config system in CJS can require( 'eslint-plugin-jest/new').

"require": "./lib/legacy.js"
},
"./new": "./lib/index.js",
"./all": "./lib/configs/all.js",
"./recommended": "./lib/configs/recommended.js",
"./style": "./lib/configs/style.js",
"./globals": "./lib/globals.json",
"./globals.json": "./lib/globals.json"
},
"files": [
"docs/",
"lib/"
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync } from 'fs';
import { resolve } from 'path';
import plugin from '../';
import plugin from '../legacy';

const numberOfRules = 50;
const ruleNames = Object.keys(plugin.rules);
Expand Down
26 changes: 26 additions & 0 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import globals from '../globals.json';
import jest from '../index';
import legacy from '../legacy';

export default [
{
files: ['**/*.snap'],
plugins: {
jest,
},
processor: 'jest/.snap',
},
{
files: [
'**/*.{test,spec}.{js,cjs,mjs,jsx,ts,tsx}',
'**/__tests__/*.{js,cjs,mjs,jsx,ts,tsx}',
],
languageOptions: {
globals,
},
plugins: {
jest,
},
rules: legacy.configs.all.rules,
},
];
12 changes: 12 additions & 0 deletions src/configs/recommended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import legacy from '../legacy';
import all from './all';

const [snap, test] = all;

export default [
snap,
{
...test,
rules: legacy.configs.recommended.rules,
},
];
12 changes: 12 additions & 0 deletions src/configs/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import legacy from '../legacy';
import all from './all';

const [snap, test] = all;

export default [
snap,
{
...test,
rules: legacy.configs.style.rules,
},
];
89 changes: 4 additions & 85 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,6 @@
import { readdirSync } from 'fs';
import { join, parse } from 'path';
import type { TSESLint } from '@typescript-eslint/utils';
import globals from './globals.json';
import * as snapshotProcessor from './processors/snapshot-processor';
import legacy from './legacy';

type RuleModule = TSESLint.RuleModule<string, unknown[]> & {
meta: Required<Pick<TSESLint.RuleMetaData<string>, 'docs'>>;
};

// v5 of `@typescript-eslint/experimental-utils` removed this
declare module '@typescript-eslint/utils/dist/ts-eslint/Rule' {
export interface RuleMetaDataDocs {
category: 'Best Practices' | 'Possible Errors';
}
}

// copied from https://github.com/babel/babel/blob/d8da63c929f2d28c401571e2a43166678c555bc4/packages/babel-helpers/src/helpers.js#L602-L606
/* istanbul ignore next */
const interopRequireDefault = (obj: any): { default: any } =>
obj && obj.__esModule ? obj : { default: obj };

const importDefault = (moduleName: string) =>
// eslint-disable-next-line @typescript-eslint/no-require-imports
interopRequireDefault(require(moduleName)).default;

const rulesDir = join(__dirname, 'rules');
const excludedFiles = ['__tests__', 'detectJestVersion', 'utils'];

const rules = readdirSync(rulesDir)
.map(rule => parse(rule).name)
.filter(rule => !excludedFiles.includes(rule))
.reduce<Record<string, RuleModule>>(
(acc, curr) => ({
...acc,
[curr]: importDefault(join(rulesDir, curr)) as RuleModule,
}),
{},
);

const recommendedRules = Object.entries(rules)
.filter(([, rule]) => rule.meta.docs.recommended)
.reduce(
(acc, [name, rule]) => ({
...acc,
[`jest/${name}`]: rule.meta.docs.recommended,
}),
{},
);

const allRules = Object.entries(rules)
.filter(([, rule]) => !rule.meta.deprecated)
.reduce(
(acc, [name]) => ({
...acc,
[`jest/${name}`]: 'error',
}),
{},
);

const createConfig = (rules: Record<string, TSESLint.Linter.RuleLevel>) => ({
plugins: ['jest'],
env: { 'jest/globals': true },
rules,
});

export = {
configs: {
all: createConfig(allRules),
recommended: createConfig(recommendedRules),
style: createConfig({
'jest/no-alias-methods': 'warn',
'jest/prefer-to-be': 'error',
'jest/prefer-to-contain': 'error',
'jest/prefer-to-have-length': 'error',
}),
},
environments: {
globals: {
globals,
},
},
processors: {
'.snap': snapshotProcessor,
},
rules,
export default {
rules: legacy.rules,
processors: legacy.processors,
};
87 changes: 87 additions & 0 deletions src/legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { readdirSync } from 'fs';
import { join, parse } from 'path';
import type { TSESLint } from '@typescript-eslint/utils';
import globals from './globals.json';
import * as snapshotProcessor from './processors/snapshot-processor';

type RuleModule = TSESLint.RuleModule<string, unknown[]> & {
meta: Required<Pick<TSESLint.RuleMetaData<string>, 'docs'>>;
};

// v5 of `@typescript-eslint/experimental-utils` removed this
declare module '@typescript-eslint/utils/dist/ts-eslint/Rule' {
export interface RuleMetaDataDocs {
category: 'Best Practices' | 'Possible Errors';
}
}

// copied from https://github.com/babel/babel/blob/d8da63c929f2d28c401571e2a43166678c555bc4/packages/babel-helpers/src/helpers.js#L602-L606
/* istanbul ignore next */
const interopRequireDefault = (obj: any): { default: any } =>
obj && obj.__esModule ? obj : { default: obj };

const importDefault = (moduleName: string) =>
// eslint-disable-next-line @typescript-eslint/no-require-imports
interopRequireDefault(require(moduleName)).default;

const rulesDir = join(__dirname, 'rules');
const excludedFiles = ['__tests__', 'detectJestVersion', 'utils'];

const rules = readdirSync(rulesDir)
.map(rule => parse(rule).name)
.filter(rule => !excludedFiles.includes(rule))
.reduce<Record<string, RuleModule>>(
(acc, curr) => ({
...acc,
[curr]: importDefault(join(rulesDir, curr)) as RuleModule,
}),
{},
);

const recommendedRules = Object.entries(rules)
.filter(([, rule]) => rule.meta.docs.recommended)
.reduce(
(acc, [name, rule]) => ({
...acc,
[`jest/${name}`]: rule.meta.docs.recommended,
}),
{},
);

const allRules = Object.entries(rules)
.filter(([, rule]) => !rule.meta.deprecated)
.reduce(
(acc, [name]) => ({
...acc,
[`jest/${name}`]: 'error',
}),
{},
);

const createConfig = (rules: Record<string, TSESLint.Linter.RuleLevel>) => ({
plugins: ['jest'],
env: { 'jest/globals': true },
rules,
});

export = {
configs: {
all: createConfig(allRules),
recommended: createConfig(recommendedRules),
style: createConfig({
'jest/no-alias-methods': 'warn',
'jest/prefer-to-be': 'error',
'jest/prefer-to-contain': 'error',
'jest/prefer-to-have-length': 'error',
}),
},
environments: {
globals: {
globals,
},
},
processors: {
'.snap': snapshotProcessor,
},
rules,
};
2 changes: 1 addition & 1 deletion tools/regenerate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'path';
import type { JSONSchema, TSESLint } from '@typescript-eslint/utils';
import prettier, { Options } from 'prettier';
import { prettier as prettierRC } from '../package.json';
import plugin from '../src/index';
import plugin from '../src/legacy';
import { getRuleNoticeLines } from './rule-notices';

// Marker so that rule doc header (title/notices) can be automatically updated.
Expand Down