@hypersolid-dev/eslint-config 0.0.0-stage → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [0.1.1](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv0.1.0&targetVersion=GTv0.1.1) (2026-11-06)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **react-native:** disable unicorn rules that suggest APIs Hermes lacks ([b93eaa2](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/b93eaa2812181a277b5a3c551338de320a808586)), closes [AB#5173](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5173)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hypersolid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,154 @@
1
- # Temporary Holding Version
1
+ # @hypersolid-dev/eslint-config
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared ESLint flat config for Hypersolid TypeScript projects. Type-aware by default, every enabled rule is an `error`, and formatting is left to your formatter.
4
+
5
+ ## Requirements
6
+
7
+ - Node 24+
8
+ - ESLint `^10.4.0` (flat config only, no `.eslintrc`)
9
+ - TypeScript `>=5.8 <6.1` (typescript-eslint does not support TypeScript 7 yet)
10
+ - A `tsconfig.json` that includes the files you lint
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add -D @hypersolid-dev/eslint-config eslint typescript
16
+ ```
17
+
18
+ The plugins are regular dependencies of this package, so you don't install them yourself.
19
+
20
+ ## Presets
21
+
22
+ | Preset | Import | For |
23
+ | --- | --- | --- |
24
+ | `base` | `@hypersolid-dev/eslint-config/base` | Every TypeScript project. Required, and always first. |
25
+ | `react` | `@hypersolid-dev/eslint-config/react` | React for the web. |
26
+ | `react-native` | `@hypersolid-dev/eslint-config/react-native` | React Native and Expo. Use instead of `react`. |
27
+ | `node` | `@hypersolid-dev/eslint-config/node` | Node.js services, scripts and CLIs. |
28
+ | `next` | `@hypersolid-dev/eslint-config/next` | Next.js. Stack on top of `react`. |
29
+
30
+ Each preset is a function that returns an array of config objects. Presets don't include `base`: spread `base` first, then the presets you need, then your own overrides.
31
+
32
+ `base` takes `tsconfigRootDir`, the directory your `tsconfig.json` lives in, and throws without it. In an `eslint.config.js` next to your `tsconfig.json`, that's `import.meta.dirname`.
33
+
34
+ ### TypeScript library or package
35
+
36
+ ```js
37
+ // eslint.config.js
38
+ import base from '@hypersolid-dev/eslint-config/base';
39
+
40
+ export default [...base({ tsconfigRootDir: import.meta.dirname })];
41
+ ```
42
+
43
+ ### Node.js
44
+
45
+ ```js
46
+ import base from '@hypersolid-dev/eslint-config/base';
47
+ import node from '@hypersolid-dev/eslint-config/node';
48
+
49
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...node()];
50
+ ```
51
+
52
+ ### React
53
+
54
+ ```js
55
+ import base from '@hypersolid-dev/eslint-config/base';
56
+ import react from '@hypersolid-dev/eslint-config/react';
57
+
58
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react()];
59
+ ```
60
+
61
+ ### React Native / Expo
62
+
63
+ ```js
64
+ import base from '@hypersolid-dev/eslint-config/base';
65
+ import reactNative from '@hypersolid-dev/eslint-config/react-native';
66
+
67
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...reactNative()];
68
+ ```
69
+
70
+ `react-native` bans browser globals that don't exist on React Native (`document`, `localStorage`, …), and ignores `.expo`, `android`, `ios` and `expo-env.d.ts`.
71
+
72
+ ### Next.js
73
+
74
+ ```js
75
+ import base from '@hypersolid-dev/eslint-config/base';
76
+ import next from '@hypersolid-dev/eslint-config/next';
77
+ import react from '@hypersolid-dev/eslint-config/react';
78
+
79
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react(), ...next()];
80
+ ```
81
+
82
+ ## Overriding rules
83
+
84
+ Add your own config objects after the presets. A later object wins for the files it matches.
85
+
86
+ ```js
87
+ import base from '@hypersolid-dev/eslint-config/base';
88
+
89
+ export default [
90
+ ...base({ tsconfigRootDir: import.meta.dirname }),
91
+ {
92
+ rules: {
93
+ // Turn a rule off everywhere.
94
+ 'unicorn/consistent-boolean-name': 'off',
95
+ // Change a rule's options; restate the severity.
96
+ curly: ['error', 'all'],
97
+ },
98
+ },
99
+ {
100
+ // Relax rules for one part of the project.
101
+ files: ['scripts/**'],
102
+ rules: {
103
+ 'no-console': 'off',
104
+ },
105
+ },
106
+ ];
107
+ ```
108
+
109
+ A rule's options are replaced, not merged. For example, overriding `no-restricted-globals` drops the presets' entries (`isNaN`, `isFinite`, and the browser globals in `react-native`), so repeat any you want to keep.
110
+
111
+ To ignore files, add an object with only `ignores`:
112
+
113
+ ```js
114
+ export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['generated/**'] }];
115
+ ```
116
+
117
+ `base` already ignores `node_modules`, `dist`, `build`, `coverage` and lockfiles.
118
+
119
+ ## Formatting and import order
120
+
121
+ This config has no formatting or stylistic rules, and it doesn't sort imports (`import-x/order` is off). Use a formatter such as Prettier or Biome for both.
122
+
123
+ ## Root config files
124
+
125
+ Root files such as `eslint.config.js` or `vite.config.ts` are often outside the tsconfig's `include`. `base` lints `*.config.{js,mjs,cjs,ts,mts,cts}` in the project root with a default project, so they don't need to be in your tsconfig.
126
+
127
+ ## Known issues
128
+
129
+ - `eslint-plugin-jsx-a11y` (used by `react`) declares support for ESLint 9 only. It works on ESLint 10, but installing prints a peer-dependency warning, and installs with `strict-peer-dependencies` fail.
130
+ - In `next`, `@next/next/no-html-link-for-pages` prints "Pages directory cannot be found" on App Router projects. Turn it off if you don't use the `pages` directory:
131
+
132
+ ```js
133
+ { rules: { '@next/next/no-html-link-for-pages': 'off' } }
134
+ ```
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ pnpm install --ignore-scripts
140
+ ```
141
+
142
+ ```bash
143
+ pnpm lint
144
+ ```
145
+
146
+ ```bash
147
+ pnpm test
148
+ ```
149
+
150
+ `pnpm lint` builds the package and lints this repo with its own `base` preset. `pnpm test` runs fixture tests: for each preset, every invalid fixture in `test/fixtures/<preset>/invalid` must trigger the rule it's named after, and the valid fixtures must produce no messages. See [AGENTS.md](AGENTS.md) for conventions.
151
+
152
+ ## Versioning
153
+
154
+ Releases follow [semver](https://semver.org/) and are published from CI on every merge to `main`, with the version derived from [Conventional Commits](https://www.conventionalcommits.org/). A new or stricter rule is a major release, because it can fail your lint run. See [CHANGELOG.md](CHANGELOG.md) for what changed in each version.
package/dist/base.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Linter } from 'eslint';
2
+ export type BaseOptions = {
3
+ /** Directory of the consumer's tsconfig, usually `import.meta.dirname`. */
4
+ tsconfigRootDir: string;
5
+ };
6
+ export default function base(options: BaseOptions): Linter.Config[];
package/dist/base.js ADDED
@@ -0,0 +1,121 @@
1
+ import js from '@eslint/js';
2
+ import json from '@eslint/json';
3
+ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
4
+ import { createNodeResolver, flatConfigs as importX } from 'eslint-plugin-import-x';
5
+ import noSecrets from 'eslint-plugin-no-secrets';
6
+ import unicorn from 'eslint-plugin-unicorn';
7
+ import { configs as tsConfigs } from 'typescript-eslint';
8
+ import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode } from './shared.js';
9
+ export default function base(options) {
10
+ if (!options.tsconfigRootDir) {
11
+ throw new Error('@hypersolid-dev/eslint-config: base() requires tsconfigRootDir');
12
+ }
13
+ return raiseWarnings([
14
+ {
15
+ name: '@hypersolid-dev/base/ignores',
16
+ ignores: ['**/node_modules', '**/dist', '**/build', '**/coverage', '**/pnpm-lock.yaml', '**/package-lock.json', '**/yarn.lock'],
17
+ },
18
+ ...scopeToCode([
19
+ // ESLint's own bug-catching rules. Bad: `if (x = 1)`, `const a = 1; const a = 2;`.
20
+ js.configs.recommended,
21
+ // Type-aware TypeScript rules, the strict set. Bad: `const n: any = data; n.foo()`, `if (alwaysTrue)`.
22
+ ...tsConfigs.strictTypeChecked,
23
+ // Unicorn's recommended set minus its taste rules (file naming, `no-null`, `no-array-reduce`,
24
+ // abbreviations, formatting); what remains catches bugs and outdated APIs.
25
+ // Bad: `array.indexOf(x) !== -1` (use `includes`), `new Error()` without a message.
26
+ unicorn.configs.unopinionated,
27
+ // Import mistakes. Bad: `import { missing } from './module.js'`, importing the same module twice.
28
+ importX.recommended,
29
+ // Makes import-x understand TypeScript files and `import type`.
30
+ importX.typescript,
31
+ ]),
32
+ {
33
+ name: '@hypersolid-dev/base',
34
+ files: CODE_FILES,
35
+ languageOptions: {
36
+ parserOptions: {
37
+ // Root config files usually sit outside the tsconfig's `include`.
38
+ projectService: { allowDefaultProject: ['*.config.{js,mjs,cjs,ts,mts,cts}'] },
39
+ tsconfigRootDir: options.tsconfigRootDir,
40
+ },
41
+ },
42
+ plugins: { 'no-secrets': noSecrets },
43
+ settings: {
44
+ 'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
45
+ },
46
+ rules: {
47
+ // Braces on any multi-line body. Bad: `if (a)\n return 1;`.
48
+ curly: ['error', 'multi-line'],
49
+ // Strict equality only, `null` included. Bad: `a == b`.
50
+ eqeqeq: ['error', 'always'],
51
+ // No leftover debug output. Bad: `console.log(user)`.
52
+ 'no-console': 'error',
53
+ // One ternary per expression. Bad: `a ? 'x' : b ? 'y' : 'z'`.
54
+ 'no-nested-ternary': 'error',
55
+ // Arrow functions as callbacks. Bad: `list.map(function (x) { return x; })`.
56
+ 'prefer-arrow-callback': 'error',
57
+ // A rethrow keeps the original error as `cause`. Bad: `catch (error) { throw new Error('failed'); }`.
58
+ 'preserve-caught-error': 'error',
59
+ // The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
60
+ 'no-restricted-globals': ['error', ...NUMBER_GLOBAL_RESTRICTIONS],
61
+ // `type` over `interface`. Bad: `interface User { name: string }`.
62
+ '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
63
+ // Every promise is awaited or handled; `void` does not count. Bad: `save();`, `void save();`.
64
+ '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }],
65
+ // Type-only imports use `import type`, so nothing is loaded at runtime. Bad: `import { type User } from './user.js'`.
66
+ '@typescript-eslint/no-import-type-side-effects': 'error',
67
+ // `??` over `||` for defaults, so `0` and `''` survive. Bad: `count || 10`.
68
+ '@typescript-eslint/prefer-nullish-coalescing': 'error',
69
+ // `sort()` needs a comparator; the default sorts numbers as strings. Bad: `[10, 9].sort()`.
70
+ '@typescript-eslint/require-array-sort-compare': 'error',
71
+ // No value returned where `void` is expected. Bad: `run(() => 1)` with `run(callback: () => void)`.
72
+ '@typescript-eslint/strict-void-return': ['error', { allowReturnAny: true }],
73
+ // A `switch` on a union handles every member. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
74
+ '@typescript-eslint/switch-exhaustiveness-check': 'error',
75
+ // The caught value is named `error`. Bad: `catch (e)`.
76
+ 'unicorn/catch-error-name': 'error',
77
+ // A boolean reads as a claim. Bad: `const visible = true` (use `isVisible`).
78
+ 'unicorn/consistent-boolean-name': ['error', { checkFunctions: 'never' }],
79
+ // A compound word is one word. Bad: `callBack`, `dataBase`.
80
+ 'unicorn/consistent-compound-words': 'error',
81
+ // No non-standard abbreviations. Bad: `btn`, `cfg`. Allowed: `props`, `args`, `fn`, `ref`, `env`, `params`.
82
+ 'unicorn/name-replacements': [
83
+ 'error',
84
+ { replacements: { props: false, args: false, fn: false, ref: false, env: false, params: false } },
85
+ ],
86
+ // A verb prefix promises a function. Bad: `const getUser = { name: 'Ada' }`.
87
+ 'unicorn/no-non-function-verb-prefix': 'error',
88
+ // `finally` does not wait for an async callback. Bad: `promise.finally(async () => { await close(); })`.
89
+ 'unicorn/no-async-promise-finally': 'error',
90
+ // `await` inside `Promise.all` runs the calls one by one. Bad: `Promise.all([await a, await b])`.
91
+ 'unicorn/no-await-in-promise-methods': 'error',
92
+ // Only the first resolve/reject counts. Bad: `new Promise((resolve) => { resolve(1); resolve(2); })`.
93
+ 'unicorn/no-multiple-promise-resolver-calls': 'error',
94
+ // `Promise.all` around one promise. Bad: `Promise.all([fetchUser()])`.
95
+ 'unicorn/no-single-promise-in-promise-methods': 'error',
96
+ // A `then` property makes `await` treat the object as a promise. Bad: `const task = { then() {} }`.
97
+ 'unicorn/no-thenable': 'error',
98
+ // `.value` exists only on fulfilled results. Bad: `(await Promise.allSettled(list)).map((result) => result.value)`.
99
+ 'unicorn/no-unsafe-promise-all-settled-values': 'error',
100
+ // An async function already wraps its return value. Bad: `async () => Promise.resolve(1)`.
101
+ 'unicorn/no-useless-promise-resolve-reject': 'error',
102
+ // Import order is formatting, out of scope (AGENTS.md).
103
+ 'import-x/order': 'off',
104
+ // High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
105
+ 'no-secrets/no-secrets': 'error',
106
+ },
107
+ },
108
+ {
109
+ name: '@hypersolid-dev/base/json',
110
+ files: ['**/*.json'],
111
+ language: 'json/json',
112
+ plugins: { json, 'no-secrets': noSecrets },
113
+ rules: {
114
+ // JSON correctness. Bad: `{ "name": "a", "name": "b" }`.
115
+ ...json.configs.recommended.rules,
116
+ // Letters-only strings are keys and words (`verbatimModuleSyntax`), not credentials.
117
+ 'no-secrets/no-secrets': ['error', { ignoreContent: ['^[A-Za-z]+$'] }],
118
+ },
119
+ },
120
+ ]);
121
+ }
package/dist/next.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export default function next(): Linter.Config[];
package/dist/next.js ADDED
@@ -0,0 +1,16 @@
1
+ import nextPlugin from '@next/eslint-plugin-next';
2
+ import { raiseWarnings, scopeToCode } from './shared.js';
3
+ // eslint-disable-next-line import-x/no-named-as-default-member -- CommonJS: its types list a named `configs` export that Node cannot import from ESM.
4
+ const { configs: nextConfigs } = nextPlugin;
5
+ export default function next() {
6
+ return raiseWarnings([
7
+ {
8
+ name: '@hypersolid-dev/next/ignores',
9
+ ignores: ['**/.next', '**/out'],
10
+ },
11
+ ...scopeToCode([
12
+ // Next.js rules, `recommended` plus the Core Web Vitals ones. Bad: `<img src={src} />` (use `next/image`), `<script src={url} />` without `async`.
13
+ nextConfigs['core-web-vitals'],
14
+ ]),
15
+ ]);
16
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export default function node(): Linter.Config[];
package/dist/node.js ADDED
@@ -0,0 +1,12 @@
1
+ import globals from 'globals';
2
+ import { CODE_FILES } from './shared.js';
3
+ export default function node() {
4
+ return [
5
+ {
6
+ name: '@hypersolid-dev/node/globals',
7
+ files: CODE_FILES,
8
+ // Node's globals (`process`, `Buffer`, `__dirname`) for `no-undef` in JavaScript files; TypeScript files get them from `@types/node`.
9
+ languageOptions: { globals: globals.node },
10
+ },
11
+ ];
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export default function reactNative(): Linter.Config[];
@@ -0,0 +1,54 @@
1
+ import globals from 'globals';
2
+ import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, reactConfigs } from './shared.js';
3
+ // Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
4
+ // `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
5
+ // so new browser APIs are picked up without editing this file.
6
+ // Globals Hermes provides that `globals['react-native']` does not list yet; remove a name once `globals` adds it.
7
+ const HERMES_GLOBALS = new Set(['atob', 'btoa', 'TextEncoder']);
8
+ const BROWSER_GLOBALS = Object.keys(globals.browser).filter((name) => !(name in globals['react-native']) && !HERMES_GLOBALS.has(name));
9
+ export default function reactNative() {
10
+ return raiseWarnings([
11
+ {
12
+ name: '@hypersolid-dev/react-native/ignores',
13
+ ignores: ['**/.expo', '**/android', '**/ios', '**/expo-env.d.ts'],
14
+ },
15
+ ...reactConfigs(),
16
+ {
17
+ name: '@hypersolid-dev/react-native',
18
+ files: CODE_FILES,
19
+ rules: {
20
+ // Browser globals React Native lacks, and the coercing number globals. Bad: `document.title`, `isNaN(value)`.
21
+ 'no-restricted-globals': [
22
+ 'error',
23
+ ...NUMBER_GLOBAL_RESTRICTIONS,
24
+ ...BROWSER_GLOBALS.map((name) => ({
25
+ name,
26
+ message: `\`${name}\` is a browser global, undefined on React Native: a local of that name was meant, or the value comes from a React Native API.`,
27
+ })),
28
+ ],
29
+ // Unicorn rules that push toward APIs the React Native runtime lacks (Hermes in RN 0.85 and RN's fetch/Blob/URL
30
+ // polyfills); the suggested fix crashes at runtime. Re-check when RN bumps Hermes.
31
+ // `toSorted` is missing from Hermes. Bad: `values.toSorted(compare)` suggested for `values.sort(compare)`.
32
+ 'unicorn/no-array-sort': 'off',
33
+ // Iterator helpers are missing from Hermes. Bad: `map.values().map(double).toArray()`.
34
+ 'unicorn/prefer-iterator-helpers': 'off',
35
+ // Iterator helpers are missing from Hermes. Bad: `iterator.map(double).toArray()` reordering.
36
+ 'unicorn/prefer-iterator-to-array-at-end': 'off',
37
+ // `Promise.withResolvers` is missing from Hermes. Bad: `const { promise, resolve } = Promise.withResolvers()`.
38
+ 'unicorn/prefer-promise-with-resolvers': 'off',
39
+ // `structuredClone` is not a React Native global. Bad: `structuredClone(value)`.
40
+ 'unicorn/prefer-structured-clone': 'off',
41
+ // React Native's `URL` has no `canParse`. Bad: `URL.canParse(value)`.
42
+ 'unicorn/prefer-url-can-parse': 'off',
43
+ // React Native's fetch polyfill has no static `Response.json`. Bad: `Response.json(data)`.
44
+ 'unicorn/prefer-response-static-json': 'off',
45
+ // React Native's `Blob` has no `text()`/`arrayBuffer()`; `FileReader` is the way. Bad: `await blob.text()`.
46
+ 'unicorn/prefer-blob-reading-methods': 'off',
47
+ // `EventTarget` is not a React Native global. Bad: `class Store extends EventTarget {}`.
48
+ 'unicorn/prefer-event-target': 'off',
49
+ // Metro emits CommonJS, so top-level await does not bundle. Bad: `await init();` at module scope.
50
+ 'unicorn/prefer-top-level-await': 'off',
51
+ },
52
+ },
53
+ ]);
54
+ }
@@ -0,0 +1,2 @@
1
+ import type { Linter } from 'eslint';
2
+ export default function react(): Linter.Config[];
package/dist/react.js ADDED
@@ -0,0 +1,17 @@
1
+ import jsxA11y from 'eslint-plugin-jsx-a11y';
2
+ import globals from 'globals';
3
+ import { CODE_FILES, raiseWarnings, reactConfigs, scopeToCode } from './shared.js';
4
+ export default function react() {
5
+ return raiseWarnings([
6
+ ...reactConfigs(),
7
+ ...scopeToCode([
8
+ // Accessible markup. Bad: `<img src={src} />` without `alt`, `<div onClick={open}>` without a role.
9
+ jsxA11y.flatConfigs.recommended,
10
+ ]),
11
+ {
12
+ name: '@hypersolid-dev/react/globals',
13
+ files: CODE_FILES,
14
+ languageOptions: { globals: globals.browser },
15
+ },
16
+ ]);
17
+ }
@@ -0,0 +1,9 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const CODE_FILES: string[];
3
+ export declare const NUMBER_GLOBAL_RESTRICTIONS: {
4
+ name: string;
5
+ message: string;
6
+ }[];
7
+ export declare function raiseWarnings(configs: Linter.Config[]): Linter.Config[];
8
+ export declare function scopeToCode(configs: readonly Linter.Config[]): Linter.Config[];
9
+ export declare function reactConfigs(): Linter.Config[];
package/dist/shared.js ADDED
@@ -0,0 +1,58 @@
1
+ import eslintReact from '@eslint-react/eslint-plugin';
2
+ import reactHooks from 'eslint-plugin-react-hooks';
3
+ export const CODE_FILES = ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
4
+ // Shared by every preset that sets `no-restricted-globals`: a later config replaces the rule's options, not merges them.
5
+ export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
6
+ name,
7
+ message: `Use Number.${name}; the global coerces its argument.`,
8
+ }));
9
+ // Every enabled rule is an error: a warning is either worth fixing or not worth reporting.
10
+ export function raiseWarnings(configs) {
11
+ return configs.map((config) => {
12
+ if (!config.rules) {
13
+ return config;
14
+ }
15
+ const rules = {};
16
+ for (const [name, entry] of Object.entries(config.rules)) {
17
+ if (entry === undefined) {
18
+ continue;
19
+ }
20
+ const [severity, ...ruleOptions] = Array.isArray(entry) ? entry : [entry];
21
+ rules[name] = severity === 'warn' || severity === 1 ? ['error', ...ruleOptions] : entry;
22
+ }
23
+ return { ...config, rules };
24
+ });
25
+ }
26
+ export function scopeToCode(configs) {
27
+ return configs.map((config) => ({ files: CODE_FILES, ...config }));
28
+ }
29
+ // The React block shared by `react` and `react-native`.
30
+ export function reactConfigs() {
31
+ return [
32
+ ...scopeToCode([
33
+ // React correctness for TypeScript projects. Bad: `this.state.count = 1`, `<li>` in a `.map()` without `key`.
34
+ eslintReact.configs['recommended-typescript'],
35
+ // Turns off @eslint-react's own hooks rules so react-hooks (below) is the one source for them.
36
+ eslintReact.configs['disable-conflict-eslint-plugin-react-hooks'],
37
+ // The official hooks rules plus the React Compiler checks. Bad: `if (isOpen) useEffect(…)`, a missing effect dependency.
38
+ reactHooks.configs.flat['recommended-latest'],
39
+ ]),
40
+ {
41
+ name: '@hypersolid-dev/react',
42
+ files: CODE_FILES,
43
+ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
44
+ rules: {
45
+ // A component defined inside another remounts on every render. Bad: `function List() { function Row() {…} return <Row />; }`.
46
+ '@eslint-react/no-nested-component-definitions': 'error',
47
+ // List items need a `key`. Bad: `items.map((item) => <Row item={item} />)`.
48
+ '@eslint-react/no-missing-key': 'error',
49
+ // Sibling keys are unique. Bad: `<Row key="a" /><Row key="a" />`.
50
+ '@eslint-react/no-duplicate-key': 'error',
51
+ // An index key breaks state when the list reorders. Bad: `items.map((item, index) => <Row key={index} />)`.
52
+ '@eslint-react/no-array-index-key': 'error',
53
+ // A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
54
+ '@eslint-react/no-unstable-context-value': 'error',
55
+ },
56
+ },
57
+ ];
58
+ }
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "@hypersolid-dev/eslint-config",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Shared ESLint flat config for Hypersolid TypeScript projects",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "eslint",
8
+ "eslint-config",
9
+ "eslintconfig",
10
+ "flat-config",
11
+ "typescript",
12
+ "react",
13
+ "react-native",
14
+ "next",
15
+ "node"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ "./base": {
20
+ "types": "./dist/base.d.ts",
21
+ "default": "./dist/base.js"
22
+ },
23
+ "./react": {
24
+ "types": "./dist/react.d.ts",
25
+ "default": "./dist/react.js"
26
+ },
27
+ "./react-native": {
28
+ "types": "./dist/react-native.d.ts",
29
+ "default": "./dist/react-native.js"
30
+ },
31
+ "./node": {
32
+ "types": "./dist/node.d.ts",
33
+ "default": "./dist/node.js"
34
+ },
35
+ "./next": {
36
+ "types": "./dist/next.d.ts",
37
+ "default": "./dist/next.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "CHANGELOG.md"
43
+ ],
44
+ "engines": {
45
+ "node": ">=24"
46
+ },
47
+ "packageManager": "pnpm@10.32.1",
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "lint": "pnpm build && eslint .",
51
+ "test": "pnpm build && node --test 'test/*.test.ts'",
52
+ "prepublishOnly": "pnpm test"
53
+ },
54
+ "peerDependencies": {
55
+ "eslint": "^10.4.0",
56
+ "typescript": ">=5.8 <6.1"
57
+ },
58
+ "devDependencies": {
59
+ "@semantic-release/changelog": "^7.0.0",
60
+ "@semantic-release/git": "^11.0.1",
61
+ "@types/node": "^24.19.2",
62
+ "@types/react": "^19.3.0",
63
+ "conventional-changelog-conventionalcommits": "^9.3.1",
64
+ "eslint": "^10.14.0",
65
+ "semantic-release": "^25.0.9",
66
+ "typescript": "^6.0.3"
67
+ },
68
+ "dependencies": {
69
+ "@eslint-react/eslint-plugin": "^5.28.8",
70
+ "@eslint/js": "^10.0.1",
71
+ "@eslint/json": "^2.1.1",
72
+ "@next/eslint-plugin-next": "^16.4.3",
73
+ "eslint-import-resolver-typescript": "^4.4.5",
74
+ "eslint-plugin-import-x": "^4.17.1",
75
+ "eslint-plugin-jsx-a11y": "^6.10.2",
76
+ "eslint-plugin-no-secrets": "^2.3.3",
77
+ "eslint-plugin-react-hooks": "^7.1.1",
78
+ "eslint-plugin-unicorn": "^79.0.0",
79
+ "globals": "^17.13.0",
80
+ "typescript-eslint": "^8.72.1"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ }
85
+ }