@forthtilliath/eslint-config 0.7.0 → 0.9.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
@@ -24,10 +24,51 @@ Or, from within this monorepo, as a workspace dependency:
24
24
  ```
25
25
 
26
26
  `eslint` and `typescript` are peer dependencies — install them yourself (any
27
- recent flat-config-era ESLint 9+/10+, and TypeScript 5+). Every ESLint plugin
28
- each variant actually uses (`typescript-eslint`, `eslint-plugin-react`,
29
- `angular-eslint`, etc.) ships as a regular dependency of this package, so
30
- there's nothing else to install per variant.
27
+ recent flat-config-era ESLint 9+/10+, and TypeScript 5+). Only what the base
28
+ config needs (`typescript-eslint`, `@eslint/js`, `globals`, import sorting,
29
+ Prettier conflict-resolution) ships as a regular dependency: every other
30
+ plugin is an **optional peer dependency**, installed by the project only for
31
+ the variant and options it actually uses. A React Native app doesn't get
32
+ `angular-eslint` (and the Angular CLI it pulls in), nor a TypeScript library
33
+ every React plugin.
34
+
35
+ | Variant / option | Also install |
36
+ | ----------------------- | --------------------------------------------------------------------------------------------------------- |
37
+ | `.` (base) | — |
38
+ | `/react` | `@eslint-react/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh` |
39
+ | `/react-native` | the `/react` list + `eslint-plugin-react-native` |
40
+ | `/nextjs` | the `/react` list + `@next/eslint-plugin-next` |
41
+ | `/storybook` | the `/react` list + `eslint-plugin-storybook` |
42
+ | `/angular` | `angular-eslint` |
43
+ | `turbo: true` (default) | `eslint-plugin-turbo` — or pass `turbo: false` outside a Turborepo |
44
+ | `a11y: true`\* | `eslint-plugin-jsx-a11y` — \*default for `/react`, `/nextjs`, `/storybook`; pass `a11y: false` to skip it |
45
+ | `i18n: true` | `eslint-plugin-i18next` |
46
+ | `testingLibrary: true` | `eslint-plugin-testing-library eslint-plugin-jest-dom` |
47
+
48
+ ```bash
49
+ # e.g. a standalone React Native / Expo app, with createReactNativeConfig({ turbo: false })
50
+ npm install --save-dev @forthtilliath/eslint-config eslint typescript \
51
+ @eslint-react/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks \
52
+ eslint-plugin-react-refresh eslint-plugin-react-native
53
+ ```
54
+
55
+ A missing plugin fails with an explicit error naming the package to install
56
+ (or the option to turn off) as soon as ESLint loads the config.
57
+
58
+ **ESLint 10:** `eslint-plugin-react` and `eslint-plugin-react-native` still
59
+ declare `eslint` up to 9 only, so npm refuses to install them next to ESLint
60
+ 10 (`ERESOLVE`). They work under ESLint 10 (see the `react-native` rules
61
+ below for the exceptions), so tell npm to accept it in the app's
62
+ `package.json`:
63
+
64
+ ```json
65
+ {
66
+ "overrides": {
67
+ "eslint-plugin-react": { "eslint": "$eslint" },
68
+ "eslint-plugin-react-native": { "eslint": "$eslint" }
69
+ }
70
+ }
71
+ ```
31
72
 
32
73
  ## Usage
33
74
 
@@ -173,6 +214,10 @@ consumer here yet, so `src/angular.test.js` lints small fixture files under
173
214
  for that. Extend it (or add a sibling `<variant>.test.js`) whenever a variant
174
215
  changes without a real consumer to catch a regression.
175
216
 
217
+ `src/package.test.js` guards the dependency layout: any plugin imported
218
+ outside the base config must stay an optional peer dependency, never a
219
+ regular one.
220
+
176
221
  ## Scripts
177
222
 
178
223
  ```bash
@@ -1 +1 @@
1
- {"version":3,"file":"angular.d.ts","sourceRoot":"","sources":["../src/angular.js"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,8CAHW,OAAO,WAAW,EAAE,iBAAiB,GACnC,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CA+D5C;AAED;;;;8CAI8C;AAC9C,4BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACS"}
1
+ {"version":3,"file":"angular.d.ts","sourceRoot":"","sources":["../src/angular.js"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,8CAHW,OAAO,WAAW,EAAE,iBAAiB,GACnC,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CA+D5C;AAED;;;;8CAI8C;AAC9C,4BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAC2B"}
package/dist/angular.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import angular from "angular-eslint";
2
2
  import { defineConfig } from "eslint/config";
3
3
  import tseslint from "typescript-eslint";
4
+ import { lazyConfig } from "./internal/lazy-config.js";
4
5
  import { createBaseConfig, NODE_BUILTIN_IMPORT_GROUP } from "./base.js";
5
6
  /**
6
7
  * A custom ESLint configuration for Angular applications/libraries.
@@ -80,4 +81,4 @@ export function createAngularConfig(options) {
80
81
  * default options. Use `createAngularConfig(options)` instead to customize it.
81
82
  *
82
83
  * @type {import("eslint").Linter.Config[]} */
83
- export const angularConfig = createAngularConfig();
84
+ export const angularConfig = lazyConfig(() => createAngularConfig());
package/dist/base.d.ts CHANGED
@@ -9,8 +9,9 @@
9
9
  * fall back to the plain `recommended` preset instead (useful while
10
10
  * migrating an existing codebase that isn't ready for the strict rules).
11
11
  * @property {boolean} [turbo=true] - Enable eslint-plugin-turbo's
12
- * `no-undeclared-env-vars` rule. Only relevant inside a Turborepo; set to
13
- * `false` for a standalone project.
12
+ * `no-undeclared-env-vars` rule (needs `eslint-plugin-turbo` installed).
13
+ * Only relevant inside a Turborepo; set to `false` for a standalone
14
+ * project.
14
15
  * @property {boolean} [snakeCase=false] - Also accept `snake_case` variable
15
16
  * names in `@typescript-eslint/naming-convention` — for code that
16
17
  * destructures rows straight from a SQL database (e.g. Supabase), whose
@@ -70,8 +71,9 @@ export type BaseConfigOptions = {
70
71
  strict?: boolean | undefined;
71
72
  /**
72
73
  * - Enable eslint-plugin-turbo's
73
- * `no-undeclared-env-vars` rule. Only relevant inside a Turborepo; set to
74
- * `false` for a standalone project.
74
+ * `no-undeclared-env-vars` rule (needs `eslint-plugin-turbo` installed).
75
+ * Only relevant inside a Turborepo; set to `false` for a standalone
76
+ * project.
75
77
  */
76
78
  turbo?: boolean | undefined;
77
79
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.js"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;GAUG;AACH,mEALG;IAA0B,SAAS;IACT,KAAK;CAE/B,GAAU,OAAO,QAAQ,EAAE,MAAM,CAAC,SAAS,CA+B7C;AAED;;;;;GAKG;AACH,2EAHW,iBAAiB,GACf,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAiH5C;AAjMD;;;;;;GAMG;AACH,wCAFU,MAAM,EAAE,CAQhB;AAsLF;;;;;KAKK;AACL,yBAFU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAEG"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,mEALG;IAA0B,SAAS;IACT,KAAK;CAE/B,GAAU,OAAO,QAAQ,EAAE,MAAM,CAAC,SAAS,CA+B7C;AAED;;;;;GAKG;AACH,2EAHW,iBAAiB,GACf,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAiH5C;AAlMD;;;;;;GAMG;AACH,wCAFU,MAAM,EAAE,CAQhB;AAuLF;;;;;KAKK;AACL,yBAFU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAEqB"}
package/dist/base.js CHANGED
@@ -2,9 +2,13 @@ import eslint from "@eslint/js";
2
2
  import { defineConfig } from "eslint/config";
3
3
  import eslintConfigPrettier from "eslint-config-prettier";
4
4
  import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
5
- import turboPlugin from "eslint-plugin-turbo";
6
5
  import globals from "globals";
7
6
  import tseslint from "typescript-eslint";
7
+ import { lazyConfig } from "./internal/lazy-config.js";
8
+ import { importOptional, requirePlugin } from "./internal/optional-plugin.js";
9
+ // Optional peer dependency: only a Turborepo needs it (and its own `turbo`
10
+ // peer dependency, the whole Turborepo binary).
11
+ const turboPlugin = await importOptional("eslint-plugin-turbo", () => import("eslint-plugin-turbo"));
8
12
  /**
9
13
  * Node.js builtins without the `node:` prefix, as the first
10
14
  * `simple-import-sort/imports` group. Exported so other variants (e.g.
@@ -30,8 +34,9 @@ export const NODE_BUILTIN_IMPORT_GROUP = [
30
34
  * fall back to the plain `recommended` preset instead (useful while
31
35
  * migrating an existing codebase that isn't ready for the strict rules).
32
36
  * @property {boolean} [turbo=true] - Enable eslint-plugin-turbo's
33
- * `no-undeclared-env-vars` rule. Only relevant inside a Turborepo; set to
34
- * `false` for a standalone project.
37
+ * `no-undeclared-env-vars` rule (needs `eslint-plugin-turbo` installed).
38
+ * Only relevant inside a Turborepo; set to `false` for a standalone
39
+ * project.
35
40
  * @property {boolean} [snakeCase=false] - Also accept `snake_case` variable
36
41
  * names in `@typescript-eslint/naming-convention` — for code that
37
42
  * destructures rows straight from a SQL database (e.g. Supabase), whose
@@ -152,7 +157,7 @@ export function createBaseConfig({ prettier = true, strict = true, turbo = true,
152
157
  turbo
153
158
  ? {
154
159
  plugins: {
155
- turbo: turboPlugin,
160
+ turbo: requirePlugin(turboPlugin, "eslint-plugin-turbo", "turbo"),
156
161
  },
157
162
  rules: {
158
163
  "turbo/no-undeclared-env-vars": "error",
@@ -194,4 +199,4 @@ export function createBaseConfig({ prettier = true, strict = true, turbo = true,
194
199
  *
195
200
  * @type {import("eslint").Linter.Config[]}
196
201
  * */
197
- export const baseConfig = createBaseConfig();
202
+ export const baseConfig = lazyConfig(() => createBaseConfig());
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Wraps a config factory call so it only runs the first time the resulting
3
+ * array is actually read (spread, iterated, indexed...).
4
+ *
5
+ * Every variant module exports a ready-made default (`reactConfig`,
6
+ * `nextJsConfig`...) next to its factory. Built eagerly, that default would
7
+ * load every optional plugin its default options enable (`a11y`, `turbo`) as
8
+ * soon as the module is imported — so a project calling
9
+ * `createReactNativeConfig({ turbo: false })` would still crash on a missing
10
+ * eslint-plugin-turbo, just because `reactConfig` was evaluated on the way.
11
+ *
12
+ * @param {() => import("eslint").Linter.Config[]} create
13
+ * @returns {import("eslint").Linter.Config[]}
14
+ */
15
+ export function lazyConfig(create: () => import("eslint").Linter.Config[]): import("eslint").Linter.Config[];
16
+ //# sourceMappingURL=lazy-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-config.d.ts","sourceRoot":"","sources":["../../src/internal/lazy-config.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,mCAHW,MAAM,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GACpC,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAe5C"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wraps a config factory call so it only runs the first time the resulting
3
+ * array is actually read (spread, iterated, indexed...).
4
+ *
5
+ * Every variant module exports a ready-made default (`reactConfig`,
6
+ * `nextJsConfig`...) next to its factory. Built eagerly, that default would
7
+ * load every optional plugin its default options enable (`a11y`, `turbo`) as
8
+ * soon as the module is imported — so a project calling
9
+ * `createReactNativeConfig({ turbo: false })` would still crash on a missing
10
+ * eslint-plugin-turbo, just because `reactConfig` was evaluated on the way.
11
+ *
12
+ * @param {() => import("eslint").Linter.Config[]} create
13
+ * @returns {import("eslint").Linter.Config[]}
14
+ */
15
+ export function lazyConfig(create) {
16
+ /** @type {import("eslint").Linter.Config[] | undefined} */
17
+ let config;
18
+ const resolve = () => (config ??= create());
19
+ // The target is a real (empty) array so `Array.isArray()` stays true —
20
+ // ESLint's `defineConfig()` relies on it to flatten nested configs.
21
+ return new Proxy(/** @type {import("eslint").Linter.Config[]} */ ([]), {
22
+ get: (_target, key) => Reflect.get(resolve(), key),
23
+ has: (_target, key) => Reflect.has(resolve(), key),
24
+ ownKeys: () => Reflect.ownKeys(resolve()),
25
+ getOwnPropertyDescriptor: (_target, key) => Reflect.getOwnPropertyDescriptor(resolve(), key),
26
+ });
27
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Plugins that only some variants or options use are optional peer
3
+ * dependencies (see package.json's `peerDependenciesMeta`): a React Native app
4
+ * must not end up installing angular-eslint (and the whole Angular CLI it
5
+ * pulls in), nor a plain TypeScript library every React plugin. Plugins a
6
+ * variant always needs are imported statically by that variant's module;
7
+ * plugins behind an option (`a11y`, `i18n`, `testingLibrary`, `turbo`) go
8
+ * through these helpers instead, so a project that leaves the option off
9
+ * doesn't have to install them.
10
+ */
11
+ /**
12
+ * Imports an optional plugin, resolving to `undefined` when the package isn't
13
+ * installed. Any other failure (a broken install, a missing transitive
14
+ * dependency of the plugin itself) is rethrown rather than hidden.
15
+ *
16
+ * @template T
17
+ * @param {string} name - The package name, as passed to `import()`.
18
+ * @param {() => Promise<{ default: T }>} load - `() => import(name)`, written
19
+ * out by the caller so the plugin keeps its type.
20
+ * @returns {Promise<T | undefined>}
21
+ */
22
+ export function importOptional<T>(name: string, load: () => Promise<{
23
+ default: T;
24
+ }>): Promise<T | undefined>;
25
+ /**
26
+ * Returns `plugin`, or throws an error explaining which package to install
27
+ * (or which option to turn off) when it's missing.
28
+ *
29
+ * @template T
30
+ * @param {T | undefined} plugin
31
+ * @param {string} name - The package name.
32
+ * @param {string} option - The option that needs it, e.g. `a11y`.
33
+ * @returns {T}
34
+ */
35
+ export function requirePlugin<T>(plugin: T | undefined, name: string, option: string): T;
36
+ //# sourceMappingURL=optional-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optional-plugin.d.ts","sourceRoot":"","sources":["../../src/internal/optional-plugin.js"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,+BANa,CAAC,QACH,MAAM,QACN,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC,GAE3B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASlC;AAmBD;;;;;;;;;GASG;AACH,8BANa,CAAC,UACH,CAAC,GAAG,SAAS,QACb,MAAM,UACN,MAAM,GACJ,CAAC,CAWb"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Plugins that only some variants or options use are optional peer
3
+ * dependencies (see package.json's `peerDependenciesMeta`): a React Native app
4
+ * must not end up installing angular-eslint (and the whole Angular CLI it
5
+ * pulls in), nor a plain TypeScript library every React plugin. Plugins a
6
+ * variant always needs are imported statically by that variant's module;
7
+ * plugins behind an option (`a11y`, `i18n`, `testingLibrary`, `turbo`) go
8
+ * through these helpers instead, so a project that leaves the option off
9
+ * doesn't have to install them.
10
+ */
11
+ /**
12
+ * Imports an optional plugin, resolving to `undefined` when the package isn't
13
+ * installed. Any other failure (a broken install, a missing transitive
14
+ * dependency of the plugin itself) is rethrown rather than hidden.
15
+ *
16
+ * @template T
17
+ * @param {string} name - The package name, as passed to `import()`.
18
+ * @param {() => Promise<{ default: T }>} load - `() => import(name)`, written
19
+ * out by the caller so the plugin keeps its type.
20
+ * @returns {Promise<T | undefined>}
21
+ */
22
+ export async function importOptional(name, load) {
23
+ try {
24
+ return (await load()).default;
25
+ }
26
+ catch (error) {
27
+ if (isPackageNotFound(error, name))
28
+ return undefined;
29
+ throw error;
30
+ }
31
+ }
32
+ /**
33
+ * @param {unknown} error
34
+ * @param {string} name
35
+ * @returns {boolean}
36
+ */
37
+ function isPackageNotFound(error, name) {
38
+ return (error instanceof Error &&
39
+ "code" in error &&
40
+ error.code === "ERR_MODULE_NOT_FOUND" &&
41
+ // Node names the package it couldn't find: checking it tells "the
42
+ // plugin isn't installed" apart from "the plugin is installed but one of
43
+ // its own dependencies is missing", which must not be swallowed.
44
+ error.message.includes(`'${name}'`));
45
+ }
46
+ /**
47
+ * Returns `plugin`, or throws an error explaining which package to install
48
+ * (or which option to turn off) when it's missing.
49
+ *
50
+ * @template T
51
+ * @param {T | undefined} plugin
52
+ * @param {string} name - The package name.
53
+ * @param {string} option - The option that needs it, e.g. `a11y`.
54
+ * @returns {T}
55
+ */
56
+ export function requirePlugin(plugin, name, option) {
57
+ if (plugin === undefined) {
58
+ throw new Error(`@forthtilliath/eslint-config: the \`${option}\` option needs ${name}, ` +
59
+ `which isn't installed. Install it as a devDependency ` +
60
+ `(npm install --save-dev ${name}), or pass \`${option}: false\`.`);
61
+ }
62
+ return plugin;
63
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../src/nextjs.js"],"names":[],"mappings":"AAKA;;;;;;;KAOK;AACL,6CAHW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAsD5C;AAED;;;;;KAKK;AACL,2BAFU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAEO"}
1
+ {"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../src/nextjs.js"],"names":[],"mappings":"AAMA;;;;;;;KAOK;AACL,6CAHW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAsD5C;AAED;;;;;KAKK;AACL,2BAFU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAEyB"}
package/dist/nextjs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import pluginNext from "@next/eslint-plugin-next";
2
2
  import { defineConfig } from "eslint/config";
3
+ import { lazyConfig } from "./internal/lazy-config.js";
3
4
  import { createReactConfig } from "./react.js";
4
5
  /**
5
6
  * A custom ESLint configuration for Next.js applications, built on top of
@@ -68,4 +69,4 @@ export function createNextJsConfig(options) {
68
69
  *
69
70
  * @type {import("eslint").Linter.Config[]}
70
71
  * */
71
- export const nextJsConfig = createNextJsConfig();
72
+ export const nextJsConfig = lazyConfig(() => createNextJsConfig());
@@ -1 +1 @@
1
- {"version":3,"file":"react-native.d.ts","sourceRoot":"","sources":["../src/react-native.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AAEH;;;;iDAIiD;AACjD,+DAFW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAwC5C;AAED;;;;;8CAK8C;AAC9C,gCADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACiB"}
1
+ {"version":3,"file":"react-native.d.ts","sourceRoot":"","sources":["../src/react-native.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AAEH;;;;iDAIiD;AACjD,+DAFW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAwC5C;AAED;;;;;8CAK8C;AAC9C,gCADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACmC"}
@@ -1,5 +1,6 @@
1
1
  import { defineConfig } from "eslint/config";
2
2
  import pluginReactNative from "eslint-plugin-react-native";
3
+ import { lazyConfig } from "./internal/lazy-config.js";
3
4
  import { createReactConfig } from "./react.js";
4
5
  /**
5
6
  * eslint-plugin-react-native only ships a legacy (eslintrc) `configs.all`
@@ -58,4 +59,4 @@ export function createReactNativeConfig({ a11y = false, ...options } = {}) {
58
59
  * customize it.
59
60
  *
60
61
  * @type {import("eslint").Linter.Config[]} */
61
- export const reactNativeConfig = createReactNativeConfig();
62
+ export const reactNativeConfig = lazyConfig(() => createReactNativeConfig());
package/dist/react.d.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * @typedef {object} ReactConfigOptions
3
3
  * @property {boolean} [a11y=true] - Enable eslint-plugin-jsx-a11y's
4
- * recommended rules.
4
+ * recommended rules (needs `eslint-plugin-jsx-a11y` installed).
5
5
  * @property {boolean} [i18n=false] - Enable eslint-plugin-i18next's
6
- * `no-literal-string` rule, forbidding any hardcoded string in JSX. Only
7
- * makes sense for a project fully committed to i18n (e.g. driven by
8
- * next-intl) — off by default since it flags every literal otherwise.
6
+ * `no-literal-string` rule, forbidding any hardcoded string in JSX (needs
7
+ * `eslint-plugin-i18next` installed). Only makes sense for a project fully
8
+ * committed to i18n (e.g. driven by next-intl) — off by default since it
9
+ * flags every literal otherwise.
9
10
  * @property {boolean} [testingLibrary=false] - Enable
10
11
  * eslint-plugin-testing-library's React rules and eslint-plugin-jest-dom's
11
- * recommended rules, scoped to test files only. Off by default:
12
+ * recommended rules, scoped to test files only (needs both installed). Off
13
+ * by default:
12
14
  * `testing-library/no-manual-cleanup` in particular assumes
13
15
  * `@testing-library/*`'s automatic `afterEach(cleanup)` is active, which
14
16
  * only happens when `afterEach` is a real global (vitest's `test.globals:
@@ -31,20 +33,22 @@ export const reactConfig: import("eslint").Linter.Config[];
31
33
  export type ReactConfigOptions = {
32
34
  /**
33
35
  * - Enable eslint-plugin-jsx-a11y's
34
- * recommended rules.
36
+ * recommended rules (needs `eslint-plugin-jsx-a11y` installed).
35
37
  */
36
38
  a11y?: boolean | undefined;
37
39
  /**
38
40
  * - Enable eslint-plugin-i18next's
39
- * `no-literal-string` rule, forbidding any hardcoded string in JSX. Only
40
- * makes sense for a project fully committed to i18n (e.g. driven by
41
- * next-intl) — off by default since it flags every literal otherwise.
41
+ * `no-literal-string` rule, forbidding any hardcoded string in JSX (needs
42
+ * `eslint-plugin-i18next` installed). Only makes sense for a project fully
43
+ * committed to i18n (e.g. driven by next-intl) — off by default since it
44
+ * flags every literal otherwise.
42
45
  */
43
46
  i18n?: boolean | undefined;
44
47
  /**
45
48
  * - Enable
46
49
  * eslint-plugin-testing-library's React rules and eslint-plugin-jest-dom's
47
- * recommended rules, scoped to test files only. Off by default:
50
+ * recommended rules, scoped to test files only (needs both installed). Off
51
+ * by default:
48
52
  * `testing-library/no-manual-cleanup` in particular assumes
49
53
  * `@testing-library/*`'s automatic `afterEach(cleanup)` is active, which
50
54
  * only happens when `afterEach` is a real global (vitest's `test.globals:
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,0FAHW,OAAO,WAAW,EAAE,iBAAiB,GAAG,kBAAkB,GACxD,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAwF5C;AAED;;;;8CAI8C;AAC9C,0BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACK"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.js"],"names":[],"mappings":"AA2DA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH,0FAHW,OAAO,WAAW,EAAE,iBAAiB,GAAG,kBAAkB,GACxD,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAyG5C;AAED;;;;8CAI8C;AAC9C,0BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACuB"}
package/dist/react.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import pluginEslintReact from "@eslint-react/eslint-plugin";
2
2
  import { defineConfig } from "eslint/config";
3
- import pluginI18next from "eslint-plugin-i18next";
4
- import pluginJestDom from "eslint-plugin-jest-dom";
5
- import pluginJsxA11y from "eslint-plugin-jsx-a11y";
6
3
  import pluginReact from "eslint-plugin-react";
7
4
  import pluginReactHooks from "eslint-plugin-react-hooks";
8
5
  import pluginReactRefresh from "eslint-plugin-react-refresh";
9
- import pluginTestingLibrary from "eslint-plugin-testing-library";
10
6
  import globals from "globals";
7
+ import { lazyConfig } from "./internal/lazy-config.js";
8
+ import { importOptional, requirePlugin } from "./internal/optional-plugin.js";
11
9
  import { createBaseConfig, createNamingConventionRule } from "./base.js";
10
+ // Optional peer dependencies, each behind an option (see
11
+ // ReactConfigOptions): a project leaving the option off doesn't install them.
12
+ const [pluginJsxA11y, pluginI18next, pluginTestingLibrary, pluginJestDom] = await Promise.all([
13
+ importOptional("eslint-plugin-jsx-a11y", () => import("eslint-plugin-jsx-a11y")),
14
+ importOptional("eslint-plugin-i18next", () => import("eslint-plugin-i18next")),
15
+ importOptional("eslint-plugin-testing-library", () => import("eslint-plugin-testing-library")),
16
+ importOptional("eslint-plugin-jest-dom", () => import("eslint-plugin-jest-dom")),
17
+ ]);
12
18
  /**
13
19
  * Any file matching this is a test file, whatever runner or naming
14
20
  * convention is used to suffix it (`.test.`, `.spec.`, `.dom.test.`...) —
@@ -36,14 +42,16 @@ const RULES_COVERED_BY_ESLINT_REACT = [
36
42
  /**
37
43
  * @typedef {object} ReactConfigOptions
38
44
  * @property {boolean} [a11y=true] - Enable eslint-plugin-jsx-a11y's
39
- * recommended rules.
45
+ * recommended rules (needs `eslint-plugin-jsx-a11y` installed).
40
46
  * @property {boolean} [i18n=false] - Enable eslint-plugin-i18next's
41
- * `no-literal-string` rule, forbidding any hardcoded string in JSX. Only
42
- * makes sense for a project fully committed to i18n (e.g. driven by
43
- * next-intl) — off by default since it flags every literal otherwise.
47
+ * `no-literal-string` rule, forbidding any hardcoded string in JSX (needs
48
+ * `eslint-plugin-i18next` installed). Only makes sense for a project fully
49
+ * committed to i18n (e.g. driven by next-intl) — off by default since it
50
+ * flags every literal otherwise.
44
51
  * @property {boolean} [testingLibrary=false] - Enable
45
52
  * eslint-plugin-testing-library's React rules and eslint-plugin-jest-dom's
46
- * recommended rules, scoped to test files only. Off by default:
53
+ * recommended rules, scoped to test files only (needs both installed). Off
54
+ * by default:
47
55
  * `testing-library/no-manual-cleanup` in particular assumes
48
56
  * `@testing-library/*`'s automatic `afterEach(cleanup)` is active, which
49
57
  * only happens when `afterEach` is a real global (vitest's `test.globals:
@@ -59,21 +67,26 @@ const RULES_COVERED_BY_ESLINT_REACT = [
59
67
  export function createReactConfig({ a11y = true, i18n = false, testingLibrary = false, snakeCase = false, ...options } = {}) {
60
68
  return defineConfig([
61
69
  ...createBaseConfig({ snakeCase, ...options }),
62
- a11y ? pluginJsxA11y.flatConfigs.recommended : [],
70
+ a11y
71
+ ? requirePlugin(pluginJsxA11y, "eslint-plugin-jsx-a11y", "a11y")
72
+ .flatConfigs.recommended
73
+ : [],
63
74
  i18n
64
75
  ? {
65
- plugins: { i18next: pluginI18next },
76
+ plugins: {
77
+ i18next: requirePlugin(pluginI18next, "eslint-plugin-i18next", "i18n"),
78
+ },
66
79
  rules: { "i18next/no-literal-string": "error" },
67
80
  }
68
81
  : [],
69
82
  testingLibrary
70
83
  ? [
71
84
  {
72
- ...pluginTestingLibrary.configs["flat/react"],
85
+ ...requirePlugin(pluginTestingLibrary, "eslint-plugin-testing-library", "testingLibrary").configs["flat/react"],
73
86
  files: TEST_FILE_GLOBS,
74
87
  },
75
88
  {
76
- ...pluginJestDom.configs["flat/recommended"],
89
+ ...requirePlugin(pluginJestDom, "eslint-plugin-jest-dom", "testingLibrary").configs["flat/recommended"],
77
90
  files: TEST_FILE_GLOBS,
78
91
  },
79
92
  ]
@@ -140,4 +153,4 @@ export function createReactConfig({ a11y = true, i18n = false, testingLibrary =
140
153
  * default options. Use `createReactConfig(options)` instead to customize it.
141
154
  *
142
155
  * @type {import("eslint").Linter.Config[]} */
143
- export const reactConfig = createReactConfig();
156
+ export const reactConfig = lazyConfig(() => createReactConfig());
@@ -1 +1 @@
1
- {"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.js"],"names":[],"mappings":"AAKA;;;;iDAIiD;AACjD,gDAFW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAiB5C;AAED;;;;;8CAK8C;AAC9C,8BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACa"}
1
+ {"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.js"],"names":[],"mappings":"AAMA;;;;iDAIiD;AACjD,gDAFW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAC7E,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAiB5C;AAED;;;;;8CAK8C;AAC9C,8BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAC+B"}
package/dist/storybook.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineConfig } from "eslint/config";
2
2
  import storybook from "eslint-plugin-storybook";
3
+ import { lazyConfig } from "./internal/lazy-config.js";
3
4
  import { createReactConfig } from "./react.js";
4
5
  /**
5
6
  * A custom ESLint configuration for libraries that use React and Storybook.
@@ -29,4 +30,4 @@ export function createStorybookConfig(options) {
29
30
  * customize it.
30
31
  *
31
32
  * @type {import("eslint").Linter.Config[]} */
32
- export const storybookConfig = createStorybookConfig();
33
+ export const storybookConfig = lazyConfig(() => createStorybookConfig());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/eslint-config",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -18,14 +18,21 @@
18
18
  ],
19
19
  "exports": {
20
20
  ".": "./dist/base.js",
21
+ "./internal/*": null,
21
22
  "./*": "./dist/*.js"
22
23
  },
23
24
  "dependencies": {
24
- "@eslint-react/eslint-plugin": "^5.18.3",
25
25
  "@eslint/js": "^9.39.5",
26
+ "eslint-config-prettier": "^10.1.8",
27
+ "eslint-plugin-simple-import-sort": "^13.0.0",
28
+ "globals": "^17.9.0",
29
+ "typescript-eslint": "^8.67.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@eslint-react/eslint-plugin": "^5.18.3",
26
33
  "@next/eslint-plugin-next": "^16.3.0",
27
34
  "angular-eslint": "^21.4.0",
28
- "eslint-config-prettier": "^10.1.8",
35
+ "eslint": "^9.0.0 || ^10.0.0",
29
36
  "eslint-plugin-i18next": "^6.1.5",
30
37
  "eslint-plugin-jest-dom": "^5.10.1",
31
38
  "eslint-plugin-jsx-a11y": "^6.10.2",
@@ -33,29 +40,77 @@
33
40
  "eslint-plugin-react-hooks": "^7.1.1",
34
41
  "eslint-plugin-react-native": "^5.0.0",
35
42
  "eslint-plugin-react-refresh": "^0.5.4",
36
- "eslint-plugin-simple-import-sort": "^13.0.0",
37
- "eslint-plugin-storybook": "^9.1.20",
43
+ "eslint-plugin-storybook": "^10.6.1",
38
44
  "eslint-plugin-testing-library": "^7.16.2",
39
45
  "eslint-plugin-turbo": "^2.10.9",
40
- "globals": "^17.9.0",
41
- "typescript-eslint": "^8.67.0"
42
- },
43
- "peerDependencies": {
44
- "eslint": ">=9.0.0",
45
46
  "typescript": ">=5.0.0"
46
47
  },
48
+ "peerDependenciesMeta": {
49
+ "@eslint-react/eslint-plugin": {
50
+ "optional": true
51
+ },
52
+ "@next/eslint-plugin-next": {
53
+ "optional": true
54
+ },
55
+ "angular-eslint": {
56
+ "optional": true
57
+ },
58
+ "eslint-plugin-i18next": {
59
+ "optional": true
60
+ },
61
+ "eslint-plugin-jest-dom": {
62
+ "optional": true
63
+ },
64
+ "eslint-plugin-jsx-a11y": {
65
+ "optional": true
66
+ },
67
+ "eslint-plugin-react": {
68
+ "optional": true
69
+ },
70
+ "eslint-plugin-react-hooks": {
71
+ "optional": true
72
+ },
73
+ "eslint-plugin-react-native": {
74
+ "optional": true
75
+ },
76
+ "eslint-plugin-react-refresh": {
77
+ "optional": true
78
+ },
79
+ "eslint-plugin-storybook": {
80
+ "optional": true
81
+ },
82
+ "eslint-plugin-testing-library": {
83
+ "optional": true
84
+ },
85
+ "eslint-plugin-turbo": {
86
+ "optional": true
87
+ }
88
+ },
47
89
  "devDependencies": {
48
90
  "@angular/core": "^21.0.0",
91
+ "@eslint-react/eslint-plugin": "^5.18.3",
92
+ "@next/eslint-plugin-next": "^16.3.0",
93
+ "angular-eslint": "^21.4.0",
49
94
  "eslint": "10.7.0",
95
+ "eslint-plugin-i18next": "^6.1.5",
96
+ "eslint-plugin-jest-dom": "^5.10.1",
97
+ "eslint-plugin-jsx-a11y": "^6.10.2",
98
+ "eslint-plugin-react": "^7.37.5",
99
+ "eslint-plugin-react-hooks": "^7.1.1",
100
+ "eslint-plugin-react-native": "^5.0.0",
101
+ "eslint-plugin-react-refresh": "^0.5.4",
102
+ "eslint-plugin-storybook": "^10.6.1",
103
+ "eslint-plugin-testing-library": "^7.16.2",
104
+ "eslint-plugin-turbo": "^2.10.9",
50
105
  "rxjs": "^7.8.0",
51
106
  "typescript": "^5.9.3",
52
- "vitest": "^4.1.10",
107
+ "vitest": "^5.0.2",
53
108
  "@forthtilliath/typescript-config": "0.1.1"
54
109
  },
55
110
  "scripts": {
56
111
  "dev": "tsc --watch -p tsconfig.build.json",
57
112
  "build": "tsc -p tsconfig.build.json",
58
- "clean": "rm -rf dist",
113
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
59
114
  "check-types": "tsc --noEmit",
60
115
  "lint": "eslint . --max-warnings 0",
61
116
  "test": "vitest run",