Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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

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

```js
// React Native
import { reactNative } from '@zoldytech/javascript/eslint';
export default reactNative();
```

```js
// plain JS/TS
import { base } from '@zoldytech/javascript/eslint';
Expand Down Expand Up @@ -109,7 +115,8 @@ Extend the matching base in `tsconfig.json`:
}
```

Available: `tsconfig/base.json`, `/react.json`, `/next.json`, `/nest.json` (framework ones extend base).
Available: `tsconfig/base.json`, `/react.json`, `/react-native.json`, `/next.json`, `/nest.json`
(framework ones extend base).

## Git hooks (recommended, not shipped)

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

## Development

Expand Down
124 changes: 124 additions & 0 deletions docs/react-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# React Native support

## What ships today

The `reactNative` preset (`eslint/react-native.js`, exported as
`@zoldytech/javascript/eslint/react-native`) is the `react` preset's foundation
applied to React Native:

- **antfu `react: true`** — `@eslint-react` + `react-refresh` + `react-hooks`
(`react/exhaustive-deps`, `react/rules-of-hooks`, Fast-Refresh checks). These
are React-semantic, not DOM-specific, so they apply to RN unchanged.
- **The full SonarQube layer** (`sonarLayer` + `sonarReactRules` + `sonarTestOff`).
React Native is just React + TS/JS; SonarQube itself ships **no** RN-specific
rules, so this is full SonarQube parity for RN code.
- **RN runtime globals** (`__DEV__`, `HermesInternal`) so they don't trip
`no-undef`. antfu already provides the browser + node globals RN relies on
(`fetch`, `navigator`, `process`, `require`, timers, …).
- **`tsconfig/react-native.json`** — extends base, `jsx: react-jsx`, includes the
DOM lib (see [react-native-web](#react-native-web) below).

Same option shape as every other preset (`typeChecked`, `tsconfigPath`, `tsdoc`,
`ignores`, `testGlobs`, `overrides`, `antfuOptions`).

## react-native-web

Universal codebases that also target the web via
[`react-native-web`](https://necolas.github.io/react-native-web/) are supported
with no extra ESLint config:

- **Linting is identical.** RNW runs in the browser, so antfu's `browser` globals
already cover the web APIs, `__DEV__` is declared by the preset, and web-only
files (`Foo.web.tsx`, `Foo.web.ts`) already match the preset's globs. The same
React rules apply on both platforms. Nothing platform-specific to wire in.
- **TypeScript.** `tsconfig/react-native.json` deliberately keeps the **DOM lib**
(`["ES2022", "DOM", "DOM.Iterable"]`) so web-targeted and web-only code
type-checks — TypeScript can't vary `lib` per file, so the web target sets the
floor. RNW ships augmented type definitions; install `@types/react-native-web`
and, if you want the web-augmented surface, add
`"types": ["react-native-web"]` in your project `tsconfig.json`.

```json
{
"extends": "@zoldytech/javascript/tsconfig/react-native.json",
"compilerOptions": { "types": ["react-native-web"] },
"include": ["src"]
}
```

**Native-only** projects (no web target) can drop the DOM lib to catch
accidental DOM usage on native, by overriding `lib` in their own tsconfig:
`"compilerOptions": { "lib": ["ES2022"] }`.
- The bundler alias (`react-native$` → `react-native-web`) is a build-tool concern
(webpack/Metro/Vite), out of scope for this package.

## Deferred: RN-specific style rules (blocked on ESLint 10)

The RN-idiom lint rules — `no-inline-styles`, `no-color-literals`,
`no-unused-styles`, `split-platform-components`, `no-single-element-style-arrays`,
`no-raw-text`, `sort-styles` — live only in
[`eslint-plugin-react-native`](https://github.com/intellicode/eslint-plugin-react-native).
They are **not** wired in yet because that plugin is incompatible with ESLint 10,
which this package requires (peer `eslint >=10.4`).

**Verified against ESLint 10.7 (2026-07):**

| Rule | Status on ESLint 10 |
| --- | --- |
| `no-unused-styles`, `no-inline-styles`, `no-color-literals`, `sort-styles` | **crash** — call `context.getSourceCode()` (removed in ESLint 10) |
| `split-platform-components` | **crash** — calls `context.getFilename()` (removed in ESLint 10) |
| `no-raw-text`, `no-single-element-style-arrays` | load but are the least useful rules |

`eslint-plugin-react-native@5.0.0` is the latest release and declares peer
`eslint ^3 … ^9`. `@react-native/eslint-plugin` ships no style rules
(`platform-colors`, `no-deep-imports` only). `@react-native/eslint-config` peers
`eslint ^8 || ^9` and pulls in `eslint-plugin-react`, which this package
deliberately avoids (see `eslint/next.js` header).

Registering the raw rule objects ourselves does **not** work around this: the
rule bodies themselves call the removed APIs.

## Plan for adding the style rules

When `eslint-plugin-react-native` publishes an ESLint-10-compatible release (drops
`context.getSourceCode()`/`getFilename()`, widens its `eslint` peer range):

1. Add it as a dependency (`npm i -D eslint-plugin-react-native`) and confirm
`npm install` resolves cleanly against `eslint >=10.4` (no `--legacy-peer-deps`).
2. In `eslint/react-native.js`, extend `reactNativeGlobals()` into a full
`reactNativeLayer()` block that also registers the plugin and its rules
(mirror `nextCoreWebVitals()` in `eslint/next.js`):

```js
import reactNativePlugin from 'eslint-plugin-react-native';

// Replace reactNativeGlobals() with a full layer block that also registers
// the plugin (mirror nextCoreWebVitals()):
const reactNativeLayer = {
name: 'zoldytech/react-native',
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: { 'react-native': reactNativePlugin },
// the plugin exposes the RN env globals directly:
languageOptions: {
globals: { ...reactNativePlugin.environments['react-native'].globals },
},
rules: {
'react-native/no-unused-styles': 'error',
'react-native/no-single-element-style-arrays': 'error',
'react-native/split-platform-components': 'error',
'react-native/no-inline-styles': 'error',
'react-native/no-color-literals': 'error',
'react-native/no-raw-text': 'error', // noisiest — relax via `overrides` if needed
},
};
```

(Omit `sort-styles` — pure ordering, Prettier's domain.)
3. Add a `dirty.tsx` case to `test/fixtures/react-native/` that trips a couple of
the style rules and extend the `presetSuite('react-native', …)` expected-rules
list in `test/presets.test.js` accordingly.
4. Verify with `npm test` and a manual lint of a snippet with an inline style, a
color literal, and an unused `StyleSheet` entry.

Track upstream: <https://github.com/intellicode/eslint-plugin-react-native/issues>
(ESLint 10 / flat-config support).
1 change: 1 addition & 0 deletions eslint/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
export { base } from './base.js';
export { nest } from './nest.js';
export { next } from './next.js';
export { reactNative } from './react-native.js';
export { react } from './react.js';
81 changes: 81 additions & 0 deletions eslint/react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// react-native — house style for React Native apps. Same foundation as the
// `react` preset (antfu's react support = @eslint-react + react-refresh, which
// already enables exhaustive-deps / rules-of-hooks / Fast-Refresh checks) plus
// the SonarQube-compatibility layer. React Native ships no SonarQube-specific
// rules — it is just React + TS/JS — so the `react` guardrails apply verbatim;
// this preset adds the RN runtime globals (`__DEV__`, `HermesInternal`) so they
// don't trip `no-undef`, and a matching `tsconfig/react-native.json` (which keeps
// the DOM lib so react-native-web's web target type-checks — see docs).
//
// NOTE: the RN-specific style rules (no-inline-styles, no-color-literals,
// no-unused-styles, split-platform-components, …) are intentionally NOT wired in
// yet: their only source, eslint-plugin-react-native, crashes on ESLint 10 (it
// calls the removed context.getSourceCode/getFilename APIs). They will be added
// once that plugin supports ESLint 10 — see docs/react-native.md.

import antfu from '@antfu/eslint-config';
import prettier from 'eslint-config-prettier/flat';
import { antfuTypescript, sonarLayer, sonarReactRules, sonarTestOff } from './_shared.js';

/**
* RN runtime globals that antfu's browser+node globals don't already cover.
* Declaring them stops `no-undef` firing on `__DEV__` etc. in `.js` files (in TS
* files typescript-eslint disables `no-undef` and the type checker handles it).
*/
function reactNativeGlobals() {
return {
name: 'zoldytech/react-native-globals',
languageOptions: {
globals: {
__DEV__: 'readonly',
HermesInternal: 'readonly',
},
},
};
}

/**
* @typedef {object} ReactNativeOptions
* @property {boolean} [typeChecked=false] Enable type-aware linting (needs `tsconfigPath`).
* @property {string} [tsconfigPath='tsconfig.json'] tsconfig for type-aware linting.
* @property {boolean} [tsdoc=false] Enable the `tsdoc/syntax` gate.
* @property {string[]} [ignores=[]] Project-specific ignore globs.
* @property {string[]} [testGlobs] Override the tests/config globs for the sonarjs-off block.
* @property {import('eslint').Linter.Config[]} [overrides=[]] Extra blocks, appended last.
* @property {Record<string, unknown>} [antfuOptions] Extra options merged into antfu().
*/

/**
* React Native house-style preset. Returns antfu's FlatConfigComposer (thenable).
* @param {ReactNativeOptions} [options]
*/
export function reactNative(options = {}) {
const {
typeChecked = false,
tsconfigPath,
tsdoc = false,
ignores = [],
testGlobs,
overrides = [],
antfuOptions = {},
} = options;

return antfu(
{
type: 'app',
react: true,
typescript: antfuTypescript({ typeChecked, tsconfigPath }),
stylistic: false,
ignores,
...antfuOptions,
},
reactNativeGlobals(),
...sonarLayer({ tsdoc }),
sonarReactRules(),
sonarTestOff(testGlobs),
prettier,
...overrides
);
}

export default reactNative;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
"./eslint": "./eslint/index.js",
"./eslint/base": "./eslint/base.js",
"./eslint/react": "./eslint/react.js",
"./eslint/react-native": "./eslint/react-native.js",
"./eslint/next": "./eslint/next.js",
"./eslint/nest": "./eslint/nest.js",
"./prettier": "./prettier/prettierrc.json",
"./tsconfig/base.json": "./tsconfig/base.json",
"./tsconfig/react.json": "./tsconfig/react.json",
"./tsconfig/react-native.json": "./tsconfig/react-native.json",
"./tsconfig/next.json": "./tsconfig/next.json",
"./tsconfig/nest.json": "./tsconfig/nest.json",
"./package.json": "./package.json"
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/react-native/clean.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Text, View } from 'react-native';

export function Greeting({ name }: { name: string }) {
// `__DEV__` is an RN runtime global the preset declares, so it must not trip no-undef.
const label = __DEV__ ? `[dev] ${name}` : name;
return (
<View>
<Text>Hello {label}</Text>
</View>
);
}
4 changes: 4 additions & 0 deletions test/fixtures/react-native/dev-global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Guards the preset's RN globals block: in a .js file `no-undef` is active, so
// this errors unless `__DEV__` is declared as a global. (In .tsx, no-undef is
// off, so clean.tsx can't catch a regression here.)
export const isDev = __DEV__;
11 changes: 11 additions & 0 deletions test/fixtures/react-native/dirty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect, useState } from 'react';
import { Text } from 'react-native';

export function Counter({ start }: { start: number }) {
const [n, setN] = useState(0);
useEffect(() => {
setN(start);
}, []);
console.log(n);
return <Text>{n}</Text>;
}
2 changes: 2 additions & 0 deletions test/fixtures/react-native/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { reactNative } from '../../../eslint/react-native.js';
export default reactNative();
10 changes: 10 additions & 0 deletions test/presets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ presetSuite('plain', 'clean.ts', 'dirty.ts', [

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

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

// The RN globals block is the preset's differentiator over `react`; guard it in a
// .js file where no-undef is active (it's off in .tsx, so clean.tsx can't).
test('react-native: __DEV__ RN global does not trip no-undef', async () => {
const result = await lintFixture('react-native', 'dev-global.js');
const undef = result.messages.filter((m) => m.ruleId === 'no-undef');
assert.equal(undef.length, 0, `expected __DEV__ declared as a global, got:\n${detail(result)}`);
});

presetSuite('next', 'clean.tsx', 'dirty.tsx', [
'no-console',
'react/exhaustive-deps',
Expand Down
11 changes: 11 additions & 0 deletions tsconfig/react-native.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "@zoldytech/javascript react-native",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"noEmit": true,
"allowImportingTsExtensions": true
}
}
Loading