@forthtilliath/eslint-config 0.9.0 → 0.10.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
@@ -32,24 +32,25 @@ the variant and options it actually uses. A React Native app doesn't get
32
32
  `angular-eslint` (and the Angular CLI it pulls in), nor a TypeScript library
33
33
  every React plugin.
34
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` |
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`\*\* | `eslint-plugin-turbo` — \*\*default inside a Turborepo (a `turbo.json` in the working directory or above) |
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
+ | `expo: true` | `eslint-plugin-expo` — `/react-native` only |
47
48
 
48
49
  ```bash
49
- # e.g. a standalone React Native / Expo app, with createReactNativeConfig({ turbo: false })
50
+ # e.g. a standalone React Native / Expo app, with createReactNativeConfig({ expo: true })
50
51
  npm install --save-dev @forthtilliath/eslint-config eslint typescript \
51
52
  @eslint-react/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks \
52
- eslint-plugin-react-refresh eslint-plugin-react-native
53
+ eslint-plugin-react-refresh eslint-plugin-react-native eslint-plugin-expo
53
54
  ```
54
55
 
55
56
  A missing plugin fails with an explicit error naming the package to install
@@ -123,9 +124,21 @@ export default reactNativeConfig;
123
124
 
124
125
  All six are **named** exports — a default import (`import config from "..."`)
125
126
  resolves to the whole module namespace object instead of the config array and
126
- crashes ESLint's flat-config loader outright. Two of the five variants had
127
+ crashes ESLint's flat-config loader outright. Two of the variants had
127
128
  exactly this bug at one point; use the named-import form above.
128
129
 
130
+ The config loads from a CommonJS `eslint.config.js` too (the one Expo's
131
+ templates generate), through `require()` — no top-level `await` anywhere in
132
+ the package. That needs Node 20.19+ or 22.12+ (`require()` of an ES module):
133
+
134
+ ```js
135
+ // eslint.config.js (CommonJS)
136
+ const {
137
+ createReactNativeConfig,
138
+ } = require("@forthtilliath/eslint-config/react-native");
139
+ module.exports = createReactNativeConfig({ expo: true });
140
+ ```
141
+
129
142
  Each variant is additive: `reactConfig`/`angularConfig` extend `baseConfig`,
130
143
  `nextJsConfig`/`storybookConfig`/`reactNativeConfig` extend `reactConfig`.
131
144
 
@@ -146,20 +159,21 @@ export default createReactConfig({ prettier: false });
146
159
  ```ts
147
160
  // Fall back to typescript-eslint's `recommended` preset instead of
148
161
  // `strictTypeChecked`/`stylisticTypeChecked` (e.g. while migrating an
149
- // existing codebase), and skip the Turborepo-only rule outside a Turborepo.
162
+ // existing codebase).
150
163
  import { createBaseConfig } from "@forthtilliath/eslint-config";
151
- export default createBaseConfig({ strict: false, turbo: false });
164
+ export default createBaseConfig({ strict: false });
152
165
  ```
153
166
 
154
167
  | Option | Default | Effect |
155
168
  | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
169
  | `prettier` | `true` | Append `eslint-config-prettier` at the end. |
157
170
  | `strict` | `true` | Use `strictTypeChecked`/`stylisticTypeChecked` instead of plain `recommended`. |
158
- | `turbo` | `true` | Enable `eslint-plugin-turbo`'s `no-undeclared-env-vars` rule. |
171
+ | `turbo` | auto | Enable `eslint-plugin-turbo`'s `no-undeclared-env-vars` rule. Defaults to `true` when a `turbo.json` sits in ESLint's working directory or one of its parents, `false` otherwise. |
159
172
  | `snakeCase` | `false` | Also accept `snake_case` variable names in `@typescript-eslint/naming-convention` (e.g. rows destructured straight from a Supabase/SQL database). |
160
173
  | `a11y` | `true`\* | _(react/nextjs/storybook/react-native)_ Enable `eslint-plugin-jsx-a11y`'s recommended rules. \*Defaults to `false` in `createReactNativeConfig` (jsx-a11y targets DOM semantics, which RN doesn't have). |
161
174
  | `i18n` | `false` | _(react/nextjs/storybook/react-native)_ Enable `eslint-plugin-i18next`'s `no-literal-string` rule. |
162
175
  | `testingLibrary` | `false` | _(react/nextjs/storybook/react-native)_ Enable `eslint-plugin-testing-library` + `eslint-plugin-jest-dom` on test files. |
176
+ | `expo` | `false` | _(react-native)_ Enable the `eslint-plugin-expo` rules `eslint-config-expo` turns on: `use-dom-exports`, `no-env-var-destructuring`, `no-dynamic-env-var`. |
163
177
 
164
178
  Options are forwarded down the chain, so `createNextJsConfig({ prettier: false })`
165
179
  also disables Prettier in the `reactConfig`/`baseConfig` layers it builds on.
package/dist/base.d.ts CHANGED
@@ -8,10 +8,11 @@
8
8
  * `strictTypeChecked` + `stylisticTypeChecked` presets. Set to `false` to
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
- * @property {boolean} [turbo=true] - Enable eslint-plugin-turbo's
11
+ * @property {boolean} [turbo] - Enable eslint-plugin-turbo's
12
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.
13
+ * Only relevant inside a Turborepo: defaults to `true` when a `turbo.json`
14
+ * sits in ESLint's working directory or one of its parents, `false`
15
+ * otherwise.
15
16
  * @property {boolean} [snakeCase=false] - Also accept `snake_case` variable
16
17
  * names in `@typescript-eslint/naming-convention` — for code that
17
18
  * destructures rows straight from a SQL database (e.g. Supabase), whose
@@ -72,8 +73,9 @@ export type BaseConfigOptions = {
72
73
  /**
73
74
  * - Enable eslint-plugin-turbo's
74
75
  * `no-undeclared-env-vars` rule (needs `eslint-plugin-turbo` installed).
75
- * Only relevant inside a Turborepo; set to `false` for a standalone
76
- * project.
76
+ * Only relevant inside a Turborepo: defaults to `true` when a `turbo.json`
77
+ * sits in ESLint's working directory or one of its parents, `false`
78
+ * otherwise.
77
79
  */
78
80
  turbo?: boolean | undefined;
79
81
  /**
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.js"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;GAmBG;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,CAmH5C;AArMD;;;;;;GAMG;AACH,wCAFU,MAAM,EAAE,CAQhB;AA0LF;;;;;KAKK;AACL,yBAFU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAEqB"}
package/dist/base.js CHANGED
@@ -5,10 +5,8 @@ import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
5
5
  import globals from "globals";
6
6
  import tseslint from "typescript-eslint";
7
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
+ import { requireOptional } from "./internal/optional-plugin.js";
9
+ import { isInsideTurborepo } from "./internal/turborepo.js";
12
10
  /**
13
11
  * Node.js builtins without the `node:` prefix, as the first
14
12
  * `simple-import-sort/imports` group. Exported so other variants (e.g.
@@ -33,10 +31,11 @@ export const NODE_BUILTIN_IMPORT_GROUP = [
33
31
  * `strictTypeChecked` + `stylisticTypeChecked` presets. Set to `false` to
34
32
  * fall back to the plain `recommended` preset instead (useful while
35
33
  * migrating an existing codebase that isn't ready for the strict rules).
36
- * @property {boolean} [turbo=true] - Enable eslint-plugin-turbo's
34
+ * @property {boolean} [turbo] - Enable eslint-plugin-turbo's
37
35
  * `no-undeclared-env-vars` rule (needs `eslint-plugin-turbo` installed).
38
- * Only relevant inside a Turborepo; set to `false` for a standalone
39
- * project.
36
+ * Only relevant inside a Turborepo: defaults to `true` when a `turbo.json`
37
+ * sits in ESLint's working directory or one of its parents, `false`
38
+ * otherwise.
40
39
  * @property {boolean} [snakeCase=false] - Also accept `snake_case` variable
41
40
  * names in `@typescript-eslint/naming-convention` — for code that
42
41
  * destructures rows straight from a SQL database (e.g. Supabase), whose
@@ -86,7 +85,7 @@ export function createNamingConventionRule({ snakeCase = false, react = false, }
86
85
  * @param {BaseConfigOptions} [options]
87
86
  * @returns {import("eslint").Linter.Config[]}
88
87
  */
89
- export function createBaseConfig({ prettier = true, strict = true, turbo = true, snakeCase = false, } = {}) {
88
+ export function createBaseConfig({ prettier = true, strict = true, turbo = isInsideTurborepo(), snakeCase = false, } = {}) {
90
89
  return defineConfig([
91
90
  eslint.configs.recommended,
92
91
  strict ? tseslint.configs.strictTypeChecked : tseslint.configs.recommended,
@@ -157,7 +156,7 @@ export function createBaseConfig({ prettier = true, strict = true, turbo = true,
157
156
  turbo
158
157
  ? {
159
158
  plugins: {
160
- turbo: requirePlugin(turboPlugin, "eslint-plugin-turbo", "turbo"),
159
+ turbo: /** @type {typeof import("eslint-plugin-turbo").default} */ (requireOptional("eslint-plugin-turbo", "turbo")),
161
160
  },
162
161
  rules: {
163
162
  "turbo/no-undeclared-env-vars": "error",
@@ -1,36 +1,16 @@
1
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.
2
+ * Loads an optional plugin, or throws an error explaining which package to
3
+ * install (or which option to turn off) when it isn't installed. Any other
4
+ * failure (a broken install, a missing transitive dependency of the plugin
5
+ * itself) is rethrown rather than hidden.
15
6
  *
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.
7
+ * Callers cast the result to the package's own type (its `import()`
8
+ * default export) to keep the plugin's type, which `require()` loses.
28
9
  *
29
- * @template T
30
- * @param {T | undefined} plugin
31
10
  * @param {string} name - The package name.
32
11
  * @param {string} option - The option that needs it, e.g. `a11y`.
33
- * @returns {T}
12
+ * @param {(name: string) => unknown} [load] - Stands in for `require` in tests.
13
+ * @returns {unknown}
34
14
  */
35
- export function requirePlugin<T>(plugin: T | undefined, name: string, option: string): T;
15
+ export function requireOptional(name: string, option: string, load?: (name: string) => unknown): unknown;
36
16
  //# sourceMappingURL=optional-plugin.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"optional-plugin.d.ts","sourceRoot":"","sources":["../../src/internal/optional-plugin.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;GAaG;AACH,sCALW,MAAM,UACN,MAAM,SACN,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACvB,OAAO,CAmBnB"}
@@ -1,33 +1,62 @@
1
+ import { createRequire } from "node:module";
1
2
  /**
2
3
  * Plugins that only some variants or options use are optional peer
3
4
  * dependencies (see package.json's `peerDependenciesMeta`): a React Native app
4
5
  * must not end up installing angular-eslint (and the whole Angular CLI it
5
6
  * pulls in), nor a plain TypeScript library every React plugin. Plugins a
6
7
  * 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.
8
+ * plugins behind an option (`a11y`, `expo`, `i18n`, `testingLibrary`,
9
+ * `turbo`) go through {@link requireOptional} instead, only when the option is
10
+ * on, so a project that leaves it off doesn't have to install them.
11
+ *
12
+ * Loaded with `require()` rather than `import()`: an `await import()` at the
13
+ * top of a variant module would make the whole package an async module, which
14
+ * Node refuses to `require()` (`ERR_REQUIRE_ASYNC_MODULE`) — and the CommonJS
15
+ * `eslint.config.js` generated by Expo's templates does exactly that.
10
16
  */
17
+ const require = createRequire(import.meta.url);
11
18
  /**
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.
19
+ * Loads an optional plugin, or throws an error explaining which package to
20
+ * install (or which option to turn off) when it isn't installed. Any other
21
+ * failure (a broken install, a missing transitive dependency of the plugin
22
+ * itself) is rethrown rather than hidden.
23
+ *
24
+ * Callers cast the result to the package's own type (its `import()`
25
+ * default export) to keep the plugin's type, which `require()` loses.
15
26
  *
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>}
27
+ * @param {string} name - The package name.
28
+ * @param {string} option - The option that needs it, e.g. `a11y`.
29
+ * @param {(name: string) => unknown} [load] - Stands in for `require` in tests.
30
+ * @returns {unknown}
21
31
  */
22
- export async function importOptional(name, load) {
32
+ export function requireOptional(name, option, load = require) {
33
+ /** @type {unknown} */
34
+ let loaded;
23
35
  try {
24
- return (await load()).default;
36
+ loaded = load(name);
25
37
  }
26
38
  catch (error) {
27
- if (isPackageNotFound(error, name))
28
- return undefined;
39
+ if (isPackageNotFound(error, name)) {
40
+ throw new Error(`@forthtilliath/eslint-config: the \`${option}\` option needs ${name}, ` +
41
+ `which isn't installed. Install it as a devDependency ` +
42
+ `(npm install --save-dev ${name}), or pass \`${option}: false\`.`, { cause: error });
43
+ }
29
44
  throw error;
30
45
  }
46
+ return isModuleNamespace(loaded) ? loaded.default : loaded;
47
+ }
48
+ /**
49
+ * `require()` of an ES module returns its namespace object, where `import()`'s
50
+ * `.default` is the plugin; a CommonJS module is returned as `module.exports`,
51
+ * which is already what `import()` puts in `.default`.
52
+ *
53
+ * @param {unknown} value
54
+ * @returns {value is { default: unknown }}
55
+ */
56
+ function isModuleNamespace(value) {
57
+ return (typeof value === "object" &&
58
+ value !== null &&
59
+ Object.prototype.toString.call(value) === "[object Module]");
31
60
  }
32
61
  /**
33
62
  * @param {unknown} error
@@ -37,27 +66,11 @@ export async function importOptional(name, load) {
37
66
  function isPackageNotFound(error, name) {
38
67
  return (error instanceof Error &&
39
68
  "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;
69
+ (error.code === "MODULE_NOT_FOUND" ||
70
+ error.code === "ERR_MODULE_NOT_FOUND") &&
71
+ // Node names the package it couldn't find on the first line (the
72
+ // require stack follows): checking it tells "the plugin isn't installed"
73
+ // apart from "the plugin is installed but one of its own dependencies is
74
+ // missing", which must not be swallowed.
75
+ (error.message.split("\n")[0] ?? "").includes(`'${name}'`));
63
76
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether `dir` or one of its parents holds a `turbo.json` — i.e. ESLint runs
3
+ * inside a Turborepo, where eslint-plugin-turbo's rule makes sense. Used as
4
+ * the default of the `turbo` option, so a standalone app doesn't have to pass
5
+ * `turbo: false` (nor install the plugin) to load the config.
6
+ *
7
+ * @param {string} [dir] - Where to start looking; ESLint's working directory
8
+ * by default.
9
+ * @returns {boolean}
10
+ */
11
+ export function isInsideTurborepo(dir?: string): boolean;
12
+ //# sourceMappingURL=turborepo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turborepo.d.ts","sourceRoot":"","sources":["../../src/internal/turborepo.js"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wCAJW,MAAM,GAEJ,OAAO,CAOnB"}
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ /**
4
+ * Whether `dir` or one of its parents holds a `turbo.json` — i.e. ESLint runs
5
+ * inside a Turborepo, where eslint-plugin-turbo's rule makes sense. Used as
6
+ * the default of the `turbo` option, so a standalone app doesn't have to pass
7
+ * `turbo: false` (nor install the plugin) to load the config.
8
+ *
9
+ * @param {string} [dir] - Where to start looking; ESLint's working directory
10
+ * by default.
11
+ * @returns {boolean}
12
+ */
13
+ export function isInsideTurborepo(dir = process.cwd()) {
14
+ for (let current = dir;; current = dirname(current)) {
15
+ if (existsSync(join(current, "turbo.json")))
16
+ return true;
17
+ if (dirname(current) === current)
18
+ return false;
19
+ }
20
+ }
@@ -4,12 +4,20 @@
4
4
  * hand here instead of spreading a ready-made flat config like storybook.js
5
5
  * does.
6
6
  */
7
+ /**
8
+ * @typedef {object} ReactNativeConfigOptions
9
+ * @property {boolean} [expo=false] - Enable the eslint-plugin-expo rules that
10
+ * eslint-config-expo turns on (`use-dom-exports`,
11
+ * `no-env-var-destructuring`, `no-dynamic-env-var`), so an Expo app
12
+ * migrating from eslint-config-expo keeps them (needs `eslint-plugin-expo`
13
+ * installed).
14
+ */
7
15
  /**
8
16
  * A custom ESLint configuration for React Native / Expo apps.
9
17
  *
10
- * @param {import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions} [options]
18
+ * @param {import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions & ReactNativeConfigOptions} [options]
11
19
  * @returns {import("eslint").Linter.Config[]} */
12
- export function createReactNativeConfig({ a11y, ...options }?: import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions): import("eslint").Linter.Config[];
20
+ export function createReactNativeConfig({ a11y, expo, ...options }?: import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions & ReactNativeConfigOptions): import("eslint").Linter.Config[];
13
21
  /**
14
22
  * A custom ESLint configuration for React Native / Expo apps, with the
15
23
  * default options. Use `createReactNativeConfig(options)` instead to
@@ -17,4 +25,14 @@ export function createReactNativeConfig({ a11y, ...options }?: import("./base.js
17
25
  *
18
26
  * @type {import("eslint").Linter.Config[]} */
19
27
  export const reactNativeConfig: import("eslint").Linter.Config[];
28
+ export type ReactNativeConfigOptions = {
29
+ /**
30
+ * - Enable the eslint-plugin-expo rules that
31
+ * eslint-config-expo turns on (`use-dom-exports`,
32
+ * `no-env-var-destructuring`, `no-dynamic-env-var`), so an Expo app
33
+ * migrating from eslint-config-expo keeps them (needs `eslint-plugin-expo`
34
+ * installed).
35
+ */
36
+ expo?: boolean | undefined;
37
+ };
20
38
  //# sourceMappingURL=react-native.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"react-native.d.ts","sourceRoot":"","sources":["../src/react-native.js"],"names":[],"mappings":"AAOA;;;;;GAKG;AAEH;;;;;;;GAOG;AAEH;;;;iDAIiD;AACjD,qEAFW,OAAO,WAAW,EAAE,iBAAiB,GAAG,OAAO,YAAY,EAAE,kBAAkB,GAAG,wBAAwB,GACxG,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CA6D5C;AAED;;;;;8CAK8C;AAC9C,gCADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACmC"}
@@ -1,6 +1,7 @@
1
1
  import { defineConfig } from "eslint/config";
2
2
  import pluginReactNative from "eslint-plugin-react-native";
3
3
  import { lazyConfig } from "./internal/lazy-config.js";
4
+ import { requireOptional } from "./internal/optional-plugin.js";
4
5
  import { createReactConfig } from "./react.js";
5
6
  /**
6
7
  * eslint-plugin-react-native only ships a legacy (eslintrc) `configs.all`
@@ -8,14 +9,37 @@ import { createReactConfig } from "./react.js";
8
9
  * hand here instead of spreading a ready-made flat config like storybook.js
9
10
  * does.
10
11
  */
12
+ /**
13
+ * @typedef {object} ReactNativeConfigOptions
14
+ * @property {boolean} [expo=false] - Enable the eslint-plugin-expo rules that
15
+ * eslint-config-expo turns on (`use-dom-exports`,
16
+ * `no-env-var-destructuring`, `no-dynamic-env-var`), so an Expo app
17
+ * migrating from eslint-config-expo keeps them (needs `eslint-plugin-expo`
18
+ * installed).
19
+ */
11
20
  /**
12
21
  * A custom ESLint configuration for React Native / Expo apps.
13
22
  *
14
- * @param {import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions} [options]
23
+ * @param {import("./base.js").BaseConfigOptions & import("./react.js").ReactConfigOptions & ReactNativeConfigOptions} [options]
15
24
  * @returns {import("eslint").Linter.Config[]} */
16
- export function createReactNativeConfig({ a11y = false, ...options } = {}) {
25
+ export function createReactNativeConfig({ a11y = false, expo = false, ...options } = {}) {
17
26
  return defineConfig([
18
27
  ...createReactConfig({ a11y, ...options }),
28
+ expo
29
+ ? {
30
+ plugins: {
31
+ expo: /** @type {typeof import("eslint-plugin-expo")} */ (requireOptional("eslint-plugin-expo", "expo")),
32
+ },
33
+ rules: {
34
+ // Every `EXPO_PUBLIC_*` variable is inlined at build time by
35
+ // reading `process.env.EXPO_PUBLIC_X` literally: a destructured or
36
+ // computed access stays `undefined` in the bundle.
37
+ "expo/no-env-var-destructuring": "error",
38
+ "expo/no-dynamic-env-var": "error",
39
+ "expo/use-dom-exports": "error",
40
+ },
41
+ }
42
+ : [],
19
43
  {
20
44
  languageOptions: {
21
45
  globals: {
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.js"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH,0FAHW,OAAO,WAAW,EAAE,iBAAiB,GAAG,kBAAkB,GACxD,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAqG5C;AAED;;;;8CAI8C;AAC9C,0BADU,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CACuB"}
package/dist/react.js CHANGED
@@ -5,16 +5,8 @@ import pluginReactHooks from "eslint-plugin-react-hooks";
5
5
  import pluginReactRefresh from "eslint-plugin-react-refresh";
6
6
  import globals from "globals";
7
7
  import { lazyConfig } from "./internal/lazy-config.js";
8
- import { importOptional, requirePlugin } from "./internal/optional-plugin.js";
8
+ import { requireOptional } from "./internal/optional-plugin.js";
9
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
- ]);
18
10
  /**
19
11
  * Any file matching this is a test file, whatever runner or naming
20
12
  * convention is used to suffix it (`.test.`, `.spec.`, `.dom.test.`...) —
@@ -68,13 +60,13 @@ export function createReactConfig({ a11y = true, i18n = false, testingLibrary =
68
60
  return defineConfig([
69
61
  ...createBaseConfig({ snakeCase, ...options }),
70
62
  a11y
71
- ? requirePlugin(pluginJsxA11y, "eslint-plugin-jsx-a11y", "a11y")
72
- .flatConfigs.recommended
63
+ ? /** @type {typeof import("eslint-plugin-jsx-a11y").default} */ (requireOptional("eslint-plugin-jsx-a11y", "a11y")).flatConfigs.recommended
73
64
  : [],
74
65
  i18n
75
66
  ? {
76
67
  plugins: {
77
- i18next: requirePlugin(pluginI18next, "eslint-plugin-i18next", "i18n"),
68
+ i18next:
69
+ /** @type {typeof import("eslint-plugin-i18next").default} */ (requireOptional("eslint-plugin-i18next", "i18n")),
78
70
  },
79
71
  rules: { "i18next/no-literal-string": "error" },
80
72
  }
@@ -82,11 +74,11 @@ export function createReactConfig({ a11y = true, i18n = false, testingLibrary =
82
74
  testingLibrary
83
75
  ? [
84
76
  {
85
- ...requirePlugin(pluginTestingLibrary, "eslint-plugin-testing-library", "testingLibrary").configs["flat/react"],
77
+ ... /** @type {typeof import("eslint-plugin-testing-library").default} */(requireOptional("eslint-plugin-testing-library", "testingLibrary")).configs["flat/react"],
86
78
  files: TEST_FILE_GLOBS,
87
79
  },
88
80
  {
89
- ...requirePlugin(pluginJestDom, "eslint-plugin-jest-dom", "testingLibrary").configs["flat/recommended"],
81
+ ... /** @type {typeof import("eslint-plugin-jest-dom").default} */(requireOptional("eslint-plugin-jest-dom", "testingLibrary")).configs["flat/recommended"],
90
82
  files: TEST_FILE_GLOBS,
91
83
  },
92
84
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/eslint-config",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -33,6 +33,7 @@
33
33
  "@next/eslint-plugin-next": "^16.3.0",
34
34
  "angular-eslint": "^21.4.0",
35
35
  "eslint": "^9.0.0 || ^10.0.0",
36
+ "eslint-plugin-expo": "^1.2.1",
36
37
  "eslint-plugin-i18next": "^6.1.5",
37
38
  "eslint-plugin-jest-dom": "^5.10.1",
38
39
  "eslint-plugin-jsx-a11y": "^6.10.2",
@@ -55,6 +56,9 @@
55
56
  "angular-eslint": {
56
57
  "optional": true
57
58
  },
59
+ "eslint-plugin-expo": {
60
+ "optional": true
61
+ },
58
62
  "eslint-plugin-i18next": {
59
63
  "optional": true
60
64
  },
@@ -87,11 +91,12 @@
87
91
  }
88
92
  },
89
93
  "devDependencies": {
90
- "@angular/core": "^21.0.0",
94
+ "@angular/core": "^21.2.25",
91
95
  "@eslint-react/eslint-plugin": "^5.18.3",
92
96
  "@next/eslint-plugin-next": "^16.3.0",
93
97
  "angular-eslint": "^21.4.0",
94
98
  "eslint": "10.7.0",
99
+ "eslint-plugin-expo": "^1.2.1",
95
100
  "eslint-plugin-i18next": "^6.1.5",
96
101
  "eslint-plugin-jest-dom": "^5.10.1",
97
102
  "eslint-plugin-jsx-a11y": "^6.10.2",
@@ -104,7 +109,7 @@
104
109
  "eslint-plugin-turbo": "^2.10.9",
105
110
  "rxjs": "^7.8.0",
106
111
  "typescript": "^5.9.3",
107
- "vitest": "^5.0.2",
112
+ "vitest": "^5.0.5",
108
113
  "@forthtilliath/typescript-config": "0.1.1"
109
114
  },
110
115
  "scripts": {