From 8007aae20e6bb240dd76ca453588e8f6219e6045 Mon Sep 17 00:00:00 2001 From: Ahmed Tokyo Date: Sun, 12 Jul 2026 18:28:22 +0100 Subject: [PATCH 1/2] feat: add reactNative preset + tsconfig (react-native-web ready) Adds a fifth stack preset for React Native, mirroring the `react` preset's foundation (antfu react support + the SonarQube-compatibility layer) since RN is just React + TS/JS and SonarQube ships no RN-specific rules. The preset adds the RN runtime globals (`__DEV__`, Hermes) so they don't trip `no-undef`, and a matching `tsconfig/react-native.json`. Works for react-native-web universal codebases with no extra ESLint config (browser globals + `.web.tsx` globs are already covered); the tsconfig keeps the DOM lib for the web target. The RN-idiom style rules (no-inline-styles, no-color-literals, no-unused-styles, split-platform-components) are intentionally deferred: their only source, eslint-plugin-react-native@5, crashes on ESLint 10 (calls the removed context.getSourceCode/getFilename APIs). The upgrade path is tracked in docs/react-native.md. - eslint/react-native.js, eslint/index.js, package.json exports - tsconfig/react-native.json (extends base, jsx, DOM lib for RNW) - test fixture + suite, incl. a .js guard that __DEV__ is declared - README + docs/react-native.md (RNW notes, deferred-rules plan) Co-Authored-By: Claude Opus 4.8 --- README.md | 20 ++- docs/react-native.md | 124 +++++++++++++++++++ eslint/index.js | 1 + eslint/react-native.js | 80 ++++++++++++ package.json | 2 + test/fixtures/react-native/clean.tsx | 11 ++ test/fixtures/react-native/dev-global.js | 4 + test/fixtures/react-native/dirty.tsx | 11 ++ test/fixtures/react-native/eslint.config.mjs | 2 + test/presets.test.js | 10 ++ tsconfig/react-native.json | 11 ++ 11 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 docs/react-native.md create mode 100644 eslint/react-native.js create mode 100644 test/fixtures/react-native/clean.tsx create mode 100644 test/fixtures/react-native/dev-global.js create mode 100644 test/fixtures/react-native/dirty.tsx create mode 100644 test/fixtures/react-native/eslint.config.mjs create mode 100644 tsconfig/react-native.json diff --git a/README.md b/README.md index b48b92f..6fdcc49 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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'; @@ -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) @@ -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 diff --git a/docs/react-native.md b/docs/react-native.md new file mode 100644 index 0000000..9f07abf --- /dev/null +++ b/docs/react-native.md @@ -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: +(ESLint 10 / flat-config support). diff --git a/eslint/index.js b/eslint/index.js index fb3c42b..a0bafe2 100644 --- a/eslint/index.js +++ b/eslint/index.js @@ -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'; diff --git a/eslint/react-native.js b/eslint/react-native.js new file mode 100644 index 0000000..7cfe916 --- /dev/null +++ b/eslint/react-native.js @@ -0,0 +1,80 @@ +// 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__`, Hermes) so they don't trip +// `no-undef`, and a matching `tsconfig/react-native.json` (no DOM lib). +// +// 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} [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; diff --git a/package.json b/package.json index e273616..7147717 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/fixtures/react-native/clean.tsx b/test/fixtures/react-native/clean.tsx new file mode 100644 index 0000000..30fa678 --- /dev/null +++ b/test/fixtures/react-native/clean.tsx @@ -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 ( + + Hello {label} + + ); +} diff --git a/test/fixtures/react-native/dev-global.js b/test/fixtures/react-native/dev-global.js new file mode 100644 index 0000000..8d8a8d6 --- /dev/null +++ b/test/fixtures/react-native/dev-global.js @@ -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__; diff --git a/test/fixtures/react-native/dirty.tsx b/test/fixtures/react-native/dirty.tsx new file mode 100644 index 0000000..2b6ba90 --- /dev/null +++ b/test/fixtures/react-native/dirty.tsx @@ -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 {n}; +} diff --git a/test/fixtures/react-native/eslint.config.mjs b/test/fixtures/react-native/eslint.config.mjs new file mode 100644 index 0000000..19cddfe --- /dev/null +++ b/test/fixtures/react-native/eslint.config.mjs @@ -0,0 +1,2 @@ +import { reactNative } from '../../../eslint/react-native.js'; +export default reactNative(); diff --git a/test/presets.test.js b/test/presets.test.js index 6885c8c..8377b12 100644 --- a/test/presets.test.js +++ b/test/presets.test.js @@ -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', diff --git a/tsconfig/react-native.json b/tsconfig/react-native.json new file mode 100644 index 0000000..fbc00f5 --- /dev/null +++ b/tsconfig/react-native.json @@ -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 + } +} From dba2caba74fa16d90368fa5a82a0e544490b208c Mon Sep 17 00:00:00 2001 From: Ahmed Tokyo Date: Sun, 12 Jul 2026 18:50:52 +0100 Subject: [PATCH 2/2] docs: fix stale "no DOM lib" comment in reactNative preset header Addresses Copilot review: the header still said the tsconfig had no DOM lib, but it keeps DOM for the react-native-web target. Also spell the Hermes global `HermesInternal` to match the implementation. Co-Authored-By: Claude Opus 4.8 --- eslint/react-native.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eslint/react-native.js b/eslint/react-native.js index 7cfe916..6d060cb 100644 --- a/eslint/react-native.js +++ b/eslint/react-native.js @@ -3,8 +3,9 @@ // 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__`, Hermes) so they don't trip -// `no-undef`, and a matching `tsconfig/react-native.json` (no DOM lib). +// 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