@automattic/eslint-plugin-wpvip 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,13 +10,15 @@ Install `eslint` and `@automattic/eslint-plugin-wpvip` to your project.
10
10
  npm install --save-dev eslint @automattic/eslint-plugin-wpvip
11
11
  ```
12
12
 
13
+ Optional integrations are auto-detected when your project also installs `typescript`, `jest`, `react`, or `prettier`. These packages are declared as optional peer dependencies so consumers can opt in to the stacks they actually use.
14
+
13
15
  ## Contributing
14
16
 
15
17
  See [CONTRIBUTING.md](https://github.com/Automattic/eslint-config-wpvip/blob/trunk/CONTRIBUTING.md) for details on development, testing, publishing, etc.
16
18
 
17
19
  ## Configuration
18
20
 
19
- Create an `.eslintrc.js` file. **Note:** The `init` file allows you to avoid installing peer dependencies (available from `v0.5.0`).
21
+ Create an `.eslintrc.js` file. If you use the `recommended` preset, you can preload the optional integrations with `init`.
20
22
 
21
23
  ```js
22
24
  require( '@automattic/eslint-plugin-wpvip/init' );
@@ -29,6 +31,8 @@ module.exports = {
29
31
 
30
32
  And that's it! It works automatically with most Babel and TypeScript projects. Code editors that are configured to work with ESLint will automatically pick up the rules and flag any errors or warnings.
31
33
 
34
+ If your project uses only JavaScript, you do not need to install the optional peers. If you use the modular `typescript`, `testing`, `react`, or `prettier` configs directly, install the corresponding package in your project first.
35
+
32
36
  You may also wish to define an `.eslintignore` file if there are files or paths that you do not want to lint.
33
37
 
34
38
  Package scripts can be useful to run linting and formatting commands automatically. Here are some suggested scripts for your project's `package.json`—only copy the ones that are useful to you. The `cmd:` scripts help you compose commands without repeating verbose CLI arguments.
@@ -59,8 +63,8 @@ Of course, this recommended config may not be ideal for every project, so feel f
59
63
  module.exports = {
60
64
  extends: [
61
65
  'plugin:@automattic/wpvip/javascript',
62
- 'plugin:@automattic/wpvip/typescript', // when "typescript" is installed
63
66
  'plugin:@automattic/wpvip/formatting',
67
+ 'plugin:@automattic/wpvip/typescript', // when "typescript" is installed
64
68
  'plugin:@automattic/wpvip/testing', // when "jest" is installed
65
69
  'plugin:@automattic/wpvip/react', // when "react" is installed
66
70
  'plugin:@automattic/wpvip/prettier', // when "prettier" is installed
package/configs/index.js CHANGED
@@ -5,14 +5,41 @@ const configs = {
5
5
  formatting: require( './formatting' ),
6
6
  javascript: require( './javascript' ),
7
7
  jsdoc: require( './jsdoc' ),
8
- prettier: require( './prettier' ),
9
- react: require( './react' ),
10
8
  recommended: require( './recommended' ),
11
- testing: require( './testing' ),
12
- typescript: require( './typescript' ),
13
9
  'weak-javascript': require( './weak-javascript' ),
14
10
  'weak-testing': require( './weak-testing' ),
15
11
  'weak-typescript': require( './weak-typescript' ),
16
12
  };
17
13
 
14
+ // Optional configs are lazy-loaded to avoid MODULE_NOT_FOUND crashes when
15
+ // optional peers (typescript, react, jest, prettier) are not installed.
16
+ // These are only accessed when explicitly requested (e.g., via direct extends)
17
+ // or via recommended config, which guards their loading via loadOptionalConfig.
18
+ const optionalConfigs = [ 'prettier', 'react', 'testing', 'typescript' ];
19
+
20
+ function requireOptionalConfig( name ) {
21
+ switch ( name ) {
22
+ case 'prettier':
23
+ return require( './prettier' );
24
+ case 'react':
25
+ return require( './react' );
26
+ case 'testing':
27
+ return require( './testing' );
28
+ case 'typescript':
29
+ return require( './typescript' );
30
+ default:
31
+ throw new Error( `Unknown optional config: ${ name }` );
32
+ }
33
+ }
34
+
35
+ for ( const name of optionalConfigs ) {
36
+ Object.defineProperty( configs, name, {
37
+ enumerable: true,
38
+ configurable: true,
39
+ get() {
40
+ return requireOptionalConfig( name );
41
+ },
42
+ } );
43
+ }
44
+
18
45
  module.exports = configs;
@@ -14,6 +14,15 @@ const SecurityPluginConfigs = require( 'eslint-plugin-security' );
14
14
  const UnusedImportsPlugin = require( 'eslint-plugin-unused-imports' );
15
15
  const globals = require( 'globals' );
16
16
 
17
+ // Resolve the TypeScript resolver from this package's perspective so that
18
+ // eslint-plugin-import can load it regardless of how the consuming project
19
+ // hoists its dependencies. Passing the absolute path as the resolver name
20
+ // bypasses `eslint-plugin-import`'s name-based lookup, which otherwise tries
21
+ // to resolve `eslint-import-resolver-typescript` from each linted source
22
+ // file's path and silently falls back to requiring `typescript` (the
23
+ // compiler) when the resolver is nested under this plugin's node_modules.
24
+ const typescriptResolverPath = require.resolve( 'eslint-import-resolver-typescript' );
25
+
17
26
  /** @type import('eslint').Linter.Config[] */
18
27
  module.exports = [
19
28
  JsonPlugin.configs.recommended,
@@ -273,8 +282,10 @@ module.exports = [
273
282
  node: {
274
283
  extensions: [ '.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs', '.cts', '.mts' ],
275
284
  },
276
- typescript: 'eslint-import-resolver-typescript',
285
+ [ typescriptResolverPath ]: {},
277
286
  },
278
287
  },
279
288
  },
280
289
  ];
290
+
291
+ module.exports.typescriptResolverPath = typescriptResolverPath;
@@ -1,22 +1,13 @@
1
- const isPackageInstalled = require( '../utils/is-package-installed' );
1
+ const loadOptionalConfig = require( '../utils/load-optional-config' );
2
2
 
3
3
  /** @type import('eslint').Linter.Config[] */
4
4
  const configs = [ ...require( './javascript' ), ...require( './formatting' ) ];
5
5
 
6
- if ( isPackageInstalled( 'typescript' ) ) {
7
- configs.push( ...require( './typescript' ) );
8
- }
6
+ configs.push( ...loadOptionalConfig( 'typescript', () => require( './typescript' ) ) );
7
+ configs.push( ...loadOptionalConfig( 'jest', () => require( './testing' ) ) );
8
+ configs.push( ...loadOptionalConfig( 'react', () => require( './react' ) ) );
9
+ configs.push( ...loadOptionalConfig( 'prettier', () => require( './prettier' ) ) );
9
10
 
10
- if ( isPackageInstalled( 'jest' ) ) {
11
- configs.push( ...require( './testing' ) );
12
- }
13
-
14
- if ( isPackageInstalled( 'react' ) ) {
15
- configs.push( ...require( './react' ) );
16
- }
17
-
18
- if ( isPackageInstalled( 'prettier' ) ) {
19
- configs.push( ...require( './prettier' ) );
20
- }
11
+ configs.typescriptResolverPath = require( './javascript' ).typescriptResolverPath;
21
12
 
22
13
  module.exports = configs;
@@ -16,14 +16,47 @@ module.exports = [
16
16
  * to "off"). If a rule is already set to a warning, do not disable it.
17
17
  */
18
18
  rules: {
19
+ '@typescript-eslint/await-thenable': 'warn',
20
+ '@typescript-eslint/ban-ts-comment': 'warn',
21
+ '@typescript-eslint/default-param-last': 'warn',
22
+ '@typescript-eslint/explicit-member-accessibility': [
23
+ 'warn',
24
+ { overrides: { constructors: 'off' } },
25
+ ],
26
+ '@typescript-eslint/no-base-to-string': 'warn',
27
+ '@typescript-eslint/no-duplicate-type-constituents': 'warn',
28
+ '@typescript-eslint/no-dynamic-delete': 'warn',
29
+ '@typescript-eslint/no-empty-object-type': 'warn',
19
30
  '@typescript-eslint/no-explicit-any': 'warn',
20
31
  '@typescript-eslint/no-floating-promises': 'warn',
32
+ '@typescript-eslint/no-namespace': 'warn',
33
+ '@typescript-eslint/no-non-null-assertion': 'warn',
21
34
  '@typescript-eslint/no-misused-promises': 'warn',
35
+ '@typescript-eslint/no-invalid-void-type': [ 'warn', { allowAsThisParameter: true } ],
36
+ '@typescript-eslint/no-redundant-type-constituents': 'warn',
37
+ '@typescript-eslint/no-require-imports': 'warn',
38
+ '@typescript-eslint/no-this-alias': 'warn',
22
39
  '@typescript-eslint/no-unsafe-argument': 'warn',
23
40
  '@typescript-eslint/no-unsafe-assignment': 'warn',
24
41
  '@typescript-eslint/no-unsafe-call': 'warn',
42
+ '@typescript-eslint/no-unsafe-enum-comparison': 'warn',
43
+ '@typescript-eslint/no-unsafe-function-type': 'warn',
25
44
  '@typescript-eslint/no-unsafe-member-access': 'warn',
26
45
  '@typescript-eslint/no-unsafe-return': 'warn',
46
+ 'no-unused-vars': 'off',
47
+ '@typescript-eslint/no-unused-vars': [
48
+ 'warn',
49
+ {
50
+ argsIgnorePattern: '^_',
51
+ caughtErrorsIgnorePattern: '^_',
52
+ destructuredArrayIgnorePattern: '^_',
53
+ ignoreRestSiblings: true,
54
+ varsIgnorePattern: '^_',
55
+ },
56
+ ],
57
+ '@typescript-eslint/no-wrapper-object-types': 'warn',
58
+ '@typescript-eslint/only-throw-error': 'warn',
59
+ '@typescript-eslint/prefer-promise-reject-errors': 'warn',
27
60
  '@typescript-eslint/require-await': 'warn',
28
61
  '@typescript-eslint/restrict-plus-operands': 'warn',
29
62
  '@typescript-eslint/restrict-template-expressions': 'warn',
package/index.js CHANGED
@@ -2,5 +2,6 @@ const configs = require( './configs' );
2
2
  const plugin = require( './plugin' );
3
3
 
4
4
  plugin.configs = configs;
5
+ plugin.typescriptResolverPath = require( './configs/javascript' ).typescriptResolverPath;
5
6
 
6
7
  module.exports = plugin;
package/init.js ADDED
@@ -0,0 +1,6 @@
1
+ const loadOptionalConfig = require( './utils/load-optional-config' );
2
+
3
+ loadOptionalConfig( 'typescript', () => require( './configs/typescript' ) );
4
+ loadOptionalConfig( 'jest', () => require( './configs/testing' ) );
5
+ loadOptionalConfig( 'react', () => require( './configs/react' ) );
6
+ loadOptionalConfig( 'prettier', () => require( './configs/prettier' ) );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/eslint-plugin-wpvip",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "ESLint plugin for internal WordPress VIP projects",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -38,32 +38,50 @@
38
38
  "eslint-import-resolver-typescript": "^4.4.4",
39
39
  "eslint-plugin-import": "^2.32.0",
40
40
  "eslint-plugin-jest": "^29.12.2",
41
- "eslint-plugin-jsdoc": "^62.5.4",
41
+ "eslint-plugin-jsdoc": "^63.0.0",
42
42
  "eslint-plugin-json": "^4.0.1",
43
43
  "eslint-plugin-jsx-a11y": "^6.10.2",
44
44
  "eslint-plugin-prettier": "^5.5.5",
45
45
  "eslint-plugin-promise": "^7.2.1",
46
46
  "eslint-plugin-react": "^7.37.5",
47
47
  "eslint-plugin-react-hooks": "^7.0.1",
48
- "eslint-plugin-security": "^3.0.1",
48
+ "eslint-plugin-security": "^4.0.0",
49
49
  "eslint-plugin-unused-imports": "^4.3.0",
50
50
  "find-package-json": "^1.2.0",
51
51
  "globals": "^16.0.0 || ^17.0.0",
52
52
  "typescript-eslint": "^8.48.1"
53
53
  },
54
54
  "peerDependencies": {
55
- "eslint": "^9.7.0"
55
+ "eslint": "^9.7.0",
56
+ "jest": "^29.0.0 || ^30.0.0",
57
+ "prettier": "^3.0.0",
58
+ "react": "^18.0.0 || ^19.0.0",
59
+ "typescript": "^5.0.0 || ^6.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "jest": {
63
+ "optional": true
64
+ },
65
+ "prettier": {
66
+ "optional": true
67
+ },
68
+ "react": {
69
+ "optional": true
70
+ },
71
+ "typescript": {
72
+ "optional": true
73
+ }
56
74
  },
57
75
  "devDependencies": {
58
76
  "@tsconfig/node20": "^20.1.8",
59
- "@types/eslint": "9.6.1",
60
- "@types/jest": "30.0.0",
61
- "@types/react": "19.2.7",
77
+ "@types/eslint": "^9.6.1",
78
+ "@types/jest": "^30.0.0",
79
+ "@types/react": "~19.2.7",
62
80
  "eslint": "^9.39.1",
63
- "jest": "30.2.0",
81
+ "jest": "^30.4.2",
64
82
  "prettier": "npm:wp-prettier@3.0.3",
65
- "react": "19.2.3",
66
- "ts-jest": "29.4.6",
67
- "typescript": "5.9.3"
83
+ "react": "~19.2.3",
84
+ "ts-jest": "^29.4.6",
85
+ "typescript": "^6.0.3"
68
86
  }
69
87
  }
@@ -0,0 +1,30 @@
1
+ const debugLog = require( './debug-log' );
2
+ const isPackageInstalled = require( './is-package-installed' );
3
+
4
+ module.exports = function loadOptionalConfig( packageName, loadConfig ) {
5
+ if ( ! isPackageInstalled( packageName ) ) {
6
+ return [];
7
+ }
8
+
9
+ try {
10
+ return loadConfig();
11
+ } catch ( error ) {
12
+ if ( 'MODULE_NOT_FOUND' !== error?.code ) {
13
+ throw error;
14
+ }
15
+
16
+ // Only swallow the error if the missing module is the optional peer itself.
17
+ // This prevents masking errors from transitive dependencies or unrelated modules.
18
+ const missingModule = error.message?.match( /Cannot find module '([^']+)'/ )?.[ 1 ] || '';
19
+ const isOptionalPeerMissing =
20
+ missingModule === packageName || missingModule.startsWith( `${ packageName }/` );
21
+
22
+ if ( ! isOptionalPeerMissing ) {
23
+ throw error;
24
+ }
25
+
26
+ debugLog( `Skipping optional config for ${ packageName }: ${ error.message }` );
27
+
28
+ return [];
29
+ }
30
+ };