Skip to content

Commit fa15f26

Browse files
authored
feat: add reactNative preset + tsconfig (react-native-web ready) - Merge pull request #1 from Zoldytech/feat/react-native-preset
feat: add reactNative preset + tsconfig (react-native-web ready)
1 parent 7937268 commit fa15f26

11 files changed

Lines changed: 274 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
A general-purpose ESLint **house style**, built on [`@antfu/eslint-config`](https://github.com/antfu/eslint-config)
44
and made **SonarQube-compatible**: local ESLint mirrors what the SonarQube gate flags, so you fix
5-
Sonar issues before they reach CI. Four stack presets — **plain JS/TS**, **React + Vite**,
6-
**Next.js**, **NestJS**.
5+
Sonar issues before they reach CI. Five stack presets — **plain JS/TS**, **React + Vite**,
6+
**React Native**, **Next.js**, **NestJS**.
77

88
- **antfu foundation** — TypeScript, imports, `unicorn`, node, `jsonc`/`yaml`/`markdown`, sensible
99
modern defaults, one `--fix`.
@@ -60,6 +60,12 @@ import { react } from '@zoldytech/javascript/eslint';
6060
export default react();
6161
```
6262

63+
```js
64+
// React Native
65+
import { reactNative } from '@zoldytech/javascript/eslint';
66+
export default reactNative();
67+
```
68+
6369
```js
6470
// plain JS/TS
6571
import { base } from '@zoldytech/javascript/eslint';
@@ -109,7 +115,8 @@ Extend the matching base in `tsconfig.json`:
109115
}
110116
```
111117

112-
Available: `tsconfig/base.json`, `/react.json`, `/next.json`, `/nest.json` (framework ones extend base).
118+
Available: `tsconfig/base.json`, `/react.json`, `/react-native.json`, `/next.json`, `/nest.json`
119+
(framework ones extend base).
113120

114121
## Git hooks (recommended, not shipped)
115122

@@ -144,6 +151,13 @@ and CI runs `eslint . --pass-on-unpruned-suppressions` so only _new_ violations
144151
- **`no-console`** bans all `console.*` (SonarQube S106), overriding antfu's `warn`/`error` allowance.
145152
- **First `eslint --fix` reorders imports** — antfu's `perfectionist` rules sort imports and named
146153
members. Expect a one-time formatting diff when a project first adopts the style.
154+
- **React Native** — `reactNative` gives full SonarQube/React parity plus the RN runtime globals
155+
(`__DEV__`, Hermes), and works for **react-native-web** universal codebases with no extra config
156+
(its `tsconfig/react-native.json` keeps the DOM lib for the web target). The RN-idiom style rules
157+
(`no-inline-styles`, `no-color-literals`, …) are deferred: their only source,
158+
`eslint-plugin-react-native`, crashes on ESLint 10. See
159+
[docs/react-native.md](docs/react-native.md) for the react-native-web notes and the tracked
160+
upgrade path.
147161

148162
## Development
149163

‎docs/react-native.md‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# React Native support
2+
3+
## What ships today
4+
5+
The `reactNative` preset (`eslint/react-native.js`, exported as
6+
`@zoldytech/javascript/eslint/react-native`) is the `react` preset's foundation
7+
applied to React Native:
8+
9+
- **antfu `react: true`** — `@eslint-react` + `react-refresh` + `react-hooks`
10+
(`react/exhaustive-deps`, `react/rules-of-hooks`, Fast-Refresh checks). These
11+
are React-semantic, not DOM-specific, so they apply to RN unchanged.
12+
- **The full SonarQube layer** (`sonarLayer` + `sonarReactRules` + `sonarTestOff`).
13+
React Native is just React + TS/JS; SonarQube itself ships **no** RN-specific
14+
rules, so this is full SonarQube parity for RN code.
15+
- **RN runtime globals** (`__DEV__`, `HermesInternal`) so they don't trip
16+
`no-undef`. antfu already provides the browser + node globals RN relies on
17+
(`fetch`, `navigator`, `process`, `require`, timers, …).
18+
- **`tsconfig/react-native.json`** — extends base, `jsx: react-jsx`, includes the
19+
DOM lib (see [react-native-web](#react-native-web) below).
20+
21+
Same option shape as every other preset (`typeChecked`, `tsconfigPath`, `tsdoc`,
22+
`ignores`, `testGlobs`, `overrides`, `antfuOptions`).
23+
24+
## react-native-web
25+
26+
Universal codebases that also target the web via
27+
[`react-native-web`](https://necolas.github.io/react-native-web/) are supported
28+
with no extra ESLint config:
29+
30+
- **Linting is identical.** RNW runs in the browser, so antfu's `browser` globals
31+
already cover the web APIs, `__DEV__` is declared by the preset, and web-only
32+
files (`Foo.web.tsx`, `Foo.web.ts`) already match the preset's globs. The same
33+
React rules apply on both platforms. Nothing platform-specific to wire in.
34+
- **TypeScript.** `tsconfig/react-native.json` deliberately keeps the **DOM lib**
35+
(`["ES2022", "DOM", "DOM.Iterable"]`) so web-targeted and web-only code
36+
type-checks — TypeScript can't vary `lib` per file, so the web target sets the
37+
floor. RNW ships augmented type definitions; install `@types/react-native-web`
38+
and, if you want the web-augmented surface, add
39+
`"types": ["react-native-web"]` in your project `tsconfig.json`.
40+
41+
```json
42+
{
43+
"extends": "@zoldytech/javascript/tsconfig/react-native.json",
44+
"compilerOptions": { "types": ["react-native-web"] },
45+
"include": ["src"]
46+
}
47+
```
48+
49+
**Native-only** projects (no web target) can drop the DOM lib to catch
50+
accidental DOM usage on native, by overriding `lib` in their own tsconfig:
51+
`"compilerOptions": { "lib": ["ES2022"] }`.
52+
- The bundler alias (`react-native$` → `react-native-web`) is a build-tool concern
53+
(webpack/Metro/Vite), out of scope for this package.
54+
55+
## Deferred: RN-specific style rules (blocked on ESLint 10)
56+
57+
The RN-idiom lint rules — `no-inline-styles`, `no-color-literals`,
58+
`no-unused-styles`, `split-platform-components`, `no-single-element-style-arrays`,
59+
`no-raw-text`, `sort-styles` — live only in
60+
[`eslint-plugin-react-native`](https://github.com/intellicode/eslint-plugin-react-native).
61+
They are **not** wired in yet because that plugin is incompatible with ESLint 10,
62+
which this package requires (peer `eslint >=10.4`).
63+
64+
**Verified against ESLint 10.7 (2026-07):**
65+
66+
| Rule | Status on ESLint 10 |
67+
| --- | --- |
68+
| `no-unused-styles`, `no-inline-styles`, `no-color-literals`, `sort-styles` | **crash** — call `context.getSourceCode()` (removed in ESLint 10) |
69+
| `split-platform-components` | **crash** — calls `context.getFilename()` (removed in ESLint 10) |
70+
| `no-raw-text`, `no-single-element-style-arrays` | load but are the least useful rules |
71+
72+
`eslint-plugin-react-native@5.0.0` is the latest release and declares peer
73+
`eslint ^3 … ^9`. `@react-native/eslint-plugin` ships no style rules
74+
(`platform-colors`, `no-deep-imports` only). `@react-native/eslint-config` peers
75+
`eslint ^8 || ^9` and pulls in `eslint-plugin-react`, which this package
76+
deliberately avoids (see `eslint/next.js` header).
77+
78+
Registering the raw rule objects ourselves does **not** work around this: the
79+
rule bodies themselves call the removed APIs.
80+
81+
## Plan for adding the style rules
82+
83+
When `eslint-plugin-react-native` publishes an ESLint-10-compatible release (drops
84+
`context.getSourceCode()`/`getFilename()`, widens its `eslint` peer range):
85+
86+
1. Add it as a dependency (`npm i -D eslint-plugin-react-native`) and confirm
87+
`npm install` resolves cleanly against `eslint >=10.4` (no `--legacy-peer-deps`).
88+
2. In `eslint/react-native.js`, extend `reactNativeGlobals()` into a full
89+
`reactNativeLayer()` block that also registers the plugin and its rules
90+
(mirror `nextCoreWebVitals()` in `eslint/next.js`):
91+
92+
```js
93+
import reactNativePlugin from 'eslint-plugin-react-native';
94+
95+
// Replace reactNativeGlobals() with a full layer block that also registers
96+
// the plugin (mirror nextCoreWebVitals()):
97+
const reactNativeLayer = {
98+
name: 'zoldytech/react-native',
99+
files: ['**/*.{js,jsx,ts,tsx}'],
100+
plugins: { 'react-native': reactNativePlugin },
101+
// the plugin exposes the RN env globals directly:
102+
languageOptions: {
103+
globals: { ...reactNativePlugin.environments['react-native'].globals },
104+
},
105+
rules: {
106+
'react-native/no-unused-styles': 'error',
107+
'react-native/no-single-element-style-arrays': 'error',
108+
'react-native/split-platform-components': 'error',
109+
'react-native/no-inline-styles': 'error',
110+
'react-native/no-color-literals': 'error',
111+
'react-native/no-raw-text': 'error', // noisiest — relax via `overrides` if needed
112+
},
113+
};
114+
```
115+
116+
(Omit `sort-styles` — pure ordering, Prettier's domain.)
117+
3. Add a `dirty.tsx` case to `test/fixtures/react-native/` that trips a couple of
118+
the style rules and extend the `presetSuite('react-native', …)` expected-rules
119+
list in `test/presets.test.js` accordingly.
120+
4. Verify with `npm test` and a manual lint of a snippet with an inline style, a
121+
color literal, and an unused `StyleSheet` entry.
122+
123+
Track upstream: <https://github.com/intellicode/eslint-plugin-react-native/issues>
124+
(ESLint 10 / flat-config support).

‎eslint/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
export { base } from './base.js';
99
export { nest } from './nest.js';
1010
export { next } from './next.js';
11+
export { reactNative } from './react-native.js';
1112
export { react } from './react.js';

‎eslint/react-native.js‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// react-native — house style for React Native apps. Same foundation as the
2+
// `react` preset (antfu's react support = @eslint-react + react-refresh, which
3+
// already enables exhaustive-deps / rules-of-hooks / Fast-Refresh checks) plus
4+
// the SonarQube-compatibility layer. React Native ships no SonarQube-specific
5+
// rules — it is just React + TS/JS — so the `react` guardrails apply verbatim;
6+
// this preset adds the RN runtime globals (`__DEV__`, `HermesInternal`) so they
7+
// don't trip `no-undef`, and a matching `tsconfig/react-native.json` (which keeps
8+
// the DOM lib so react-native-web's web target type-checks — see docs).
9+
//
10+
// NOTE: the RN-specific style rules (no-inline-styles, no-color-literals,
11+
// no-unused-styles, split-platform-components, …) are intentionally NOT wired in
12+
// yet: their only source, eslint-plugin-react-native, crashes on ESLint 10 (it
13+
// calls the removed context.getSourceCode/getFilename APIs). They will be added
14+
// once that plugin supports ESLint 10 — see docs/react-native.md.
15+
16+
import antfu from '@antfu/eslint-config';
17+
import prettier from 'eslint-config-prettier/flat';
18+
import { antfuTypescript, sonarLayer, sonarReactRules, sonarTestOff } from './_shared.js';
19+
20+
/**
21+
* RN runtime globals that antfu's browser+node globals don't already cover.
22+
* Declaring them stops `no-undef` firing on `__DEV__` etc. in `.js` files (in TS
23+
* files typescript-eslint disables `no-undef` and the type checker handles it).
24+
*/
25+
function reactNativeGlobals() {
26+
return {
27+
name: 'zoldytech/react-native-globals',
28+
languageOptions: {
29+
globals: {
30+
__DEV__: 'readonly',
31+
HermesInternal: 'readonly',
32+
},
33+
},
34+
};
35+
}
36+
37+
/**
38+
* @typedef {object} ReactNativeOptions
39+
* @property {boolean} [typeChecked=false] Enable type-aware linting (needs `tsconfigPath`).
40+
* @property {string} [tsconfigPath='tsconfig.json'] tsconfig for type-aware linting.
41+
* @property {boolean} [tsdoc=false] Enable the `tsdoc/syntax` gate.
42+
* @property {string[]} [ignores=[]] Project-specific ignore globs.
43+
* @property {string[]} [testGlobs] Override the tests/config globs for the sonarjs-off block.
44+
* @property {import('eslint').Linter.Config[]} [overrides=[]] Extra blocks, appended last.
45+
* @property {Record<string, unknown>} [antfuOptions] Extra options merged into antfu().
46+
*/
47+
48+
/**
49+
* React Native house-style preset. Returns antfu's FlatConfigComposer (thenable).
50+
* @param {ReactNativeOptions} [options]
51+
*/
52+
export function reactNative(options = {}) {
53+
const {
54+
typeChecked = false,
55+
tsconfigPath,
56+
tsdoc = false,
57+
ignores = [],
58+
testGlobs,
59+
overrides = [],
60+
antfuOptions = {},
61+
} = options;
62+
63+
return antfu(
64+
{
65+
type: 'app',
66+
react: true,
67+
typescript: antfuTypescript({ typeChecked, tsconfigPath }),
68+
stylistic: false,
69+
ignores,
70+
...antfuOptions,
71+
},
72+
reactNativeGlobals(),
73+
...sonarLayer({ tsdoc }),
74+
sonarReactRules(),
75+
sonarTestOff(testGlobs),
76+
prettier,
77+
...overrides
78+
);
79+
}
80+
81+
export default reactNative;

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
"./eslint": "./eslint/index.js",
1111
"./eslint/base": "./eslint/base.js",
1212
"./eslint/react": "./eslint/react.js",
13+
"./eslint/react-native": "./eslint/react-native.js",
1314
"./eslint/next": "./eslint/next.js",
1415
"./eslint/nest": "./eslint/nest.js",
1516
"./prettier": "./prettier/prettierrc.json",
1617
"./tsconfig/base.json": "./tsconfig/base.json",
1718
"./tsconfig/react.json": "./tsconfig/react.json",
19+
"./tsconfig/react-native.json": "./tsconfig/react-native.json",
1820
"./tsconfig/next.json": "./tsconfig/next.json",
1921
"./tsconfig/nest.json": "./tsconfig/nest.json",
2022
"./package.json": "./package.json"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Text, View } from 'react-native';
2+
3+
export function Greeting({ name }: { name: string }) {
4+
// `__DEV__` is an RN runtime global the preset declares, so it must not trip no-undef.
5+
const label = __DEV__ ? `[dev] ${name}` : name;
6+
return (
7+
<View>
8+
<Text>Hello {label}</Text>
9+
</View>
10+
);
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Guards the preset's RN globals block: in a .js file `no-undef` is active, so
2+
// this errors unless `__DEV__` is declared as a global. (In .tsx, no-undef is
3+
// off, so clean.tsx can't catch a regression here.)
4+
export const isDev = __DEV__;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useEffect, useState } from 'react';
2+
import { Text } from 'react-native';
3+
4+
export function Counter({ start }: { start: number }) {
5+
const [n, setN] = useState(0);
6+
useEffect(() => {
7+
setN(start);
8+
}, []);
9+
console.log(n);
10+
return <Text>{n}</Text>;
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { reactNative } from '../../../eslint/react-native.js';
2+
export default reactNative();

‎test/presets.test.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ presetSuite('plain', 'clean.ts', 'dirty.ts', [
5454

5555
presetSuite('react-vite', 'clean.tsx', 'dirty.tsx', ['no-console', 'react/exhaustive-deps']);
5656

57+
presetSuite('react-native', 'clean.tsx', 'dirty.tsx', ['no-console', 'react/exhaustive-deps']);
58+
59+
// The RN globals block is the preset's differentiator over `react`; guard it in a
60+
// .js file where no-undef is active (it's off in .tsx, so clean.tsx can't).
61+
test('react-native: __DEV__ RN global does not trip no-undef', async () => {
62+
const result = await lintFixture('react-native', 'dev-global.js');
63+
const undef = result.messages.filter((m) => m.ruleId === 'no-undef');
64+
assert.equal(undef.length, 0, `expected __DEV__ declared as a global, got:\n${detail(result)}`);
65+
});
66+
5767
presetSuite('next', 'clean.tsx', 'dirty.tsx', [
5868
'no-console',
5969
'react/exhaustive-deps',

0 commit comments

Comments
 (0)