From af74a1f0ea685c02da9a7981ce79e35fff95d4cd Mon Sep 17 00:00:00 2001 From: Daniel C Date: Mon, 2 Mar 2026 13:51:03 -0800 Subject: [PATCH 1/3] feat: add no-react-native-in-web lint rule Detects imports from 'react-native' and 'react-native-web' in web modules, which cause ESM load failures on the server (28K+ production errors/week). Catches both import declarations and require() calls. --- src/rules/index.ts | 2 + src/rules/meta.ts | 1 + src/rules/no-react-native-in-web.ts | 46 +++++++++++++++++++ tests/no-react-native-in-web.test.ts | 67 ++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/rules/no-react-native-in-web.ts create mode 100644 tests/no-react-native-in-web.test.ts diff --git a/src/rules/index.ts b/src/rules/index.ts index dd01b2a..98309fc 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -49,6 +49,7 @@ import { noSyncFs } from './no-sync-fs'; import { preferNamedParams } from './prefer-named-params'; import { requireUseClient } from './require-use-client'; import { noServerImportInClient } from './no-server-import-in-client'; +import { noReactNativeInWeb } from './no-react-native-in-web'; export const rules: Record = { 'no-relative-paths': noRelativePaths, @@ -101,4 +102,5 @@ export const rules: Record = { 'prefer-named-params': preferNamedParams, 'require-use-client': requireUseClient, 'no-server-import-in-client': noServerImportInClient, + 'no-react-native-in-web': noReactNativeInWeb, }; diff --git a/src/rules/meta.ts b/src/rules/meta.ts index ef0a579..0952097 100644 --- a/src/rules/meta.ts +++ b/src/rules/meta.ts @@ -32,6 +32,7 @@ export const rulePlatforms: Partial> = { 'no-tailwind-animation-classes': ['web'], 'require-use-client': ['web'], 'no-server-import-in-client': ['web'], + 'no-react-native-in-web': ['web'], // Expo + Web (shared frontend) 'no-relative-paths': ['expo', 'web'], diff --git a/src/rules/no-react-native-in-web.ts b/src/rules/no-react-native-in-web.ts new file mode 100644 index 0000000..2ebb70d --- /dev/null +++ b/src/rules/no-react-native-in-web.ts @@ -0,0 +1,46 @@ +import traverse from '@babel/traverse'; +import type { File } from '@babel/types'; +import type { LintResult } from '../types'; + +const RULE_NAME = 'no-react-native-in-web'; + +const REACT_NATIVE_MODULES = ['react-native', 'react-native-web']; + +export function noReactNativeInWeb(ast: File, _code: string): LintResult[] { + const results: LintResult[] = []; + + traverse(ast, { + ImportDeclaration(path) { + const source = path.node.source.value; + if (!REACT_NATIVE_MODULES.includes(source)) return; + + const { loc } = path.node; + results.push({ + rule: RULE_NAME, + message: `Do not import from '${source}' in web modules. Use platform-specific files (.native.tsx) or web-compatible alternatives`, + line: loc?.start.line ?? 0, + column: loc?.start.column ?? 0, + severity: 'error', + }); + }, + + CallExpression(path) { + const { callee, loc } = path.node; + if (callee.type !== 'Identifier' || callee.name !== 'require') return; + + const arg = path.node.arguments[0]; + if (!arg || arg.type !== 'StringLiteral') return; + if (!REACT_NATIVE_MODULES.includes(arg.value)) return; + + results.push({ + rule: RULE_NAME, + message: `Do not require '${arg.value}' in web modules. Use platform-specific files (.native.tsx) or web-compatible alternatives`, + line: loc?.start.line ?? 0, + column: loc?.start.column ?? 0, + severity: 'error', + }); + }, + }); + + return results; +} diff --git a/tests/no-react-native-in-web.test.ts b/tests/no-react-native-in-web.test.ts new file mode 100644 index 0000000..ed12dd7 --- /dev/null +++ b/tests/no-react-native-in-web.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { lintJsxCode } from '../src'; + +const config = { rules: ['no-react-native-in-web'] }; + +describe('no-react-native-in-web rule', () => { + it('should detect import from react-native', () => { + const code = ` + import { View, Text } from 'react-native'; + export default function App() { + return Hello; + } + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + expect(results[0].rule).toBe('no-react-native-in-web'); + expect(results[0].message).toContain('react-native'); + expect(results[0].severity).toBe('error'); + }); + + it('should detect import from react-native-web', () => { + const code = ` + import { View } from 'react-native-web'; + export default function App() { + return ; + } + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + expect(results[0].message).toContain('react-native-web'); + }); + + it('should detect require of react-native', () => { + const code = ` + const { View } = require('react-native'); + export default function App() { + return ; + } + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + expect(results[0].message).toContain('react-native'); + }); + + it('should allow other imports', () => { + const code = ` + import React from 'react'; + import { useState } from 'react'; + export default function App() { + return
Hello
; + } + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(0); + }); + + it('should allow react-native subpackage imports', () => { + const code = ` + import something from 'react-native-safe-area-context'; + export default function App() { + return
Hello
; + } + `; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(0); + }); +}); From 11b34227dd4dfa59e6f3772eaf91113377b5f20b Mon Sep 17 00:00:00 2001 From: Daniel C Date: Tue, 3 Mar 2026 10:39:26 -0800 Subject: [PATCH 2/3] fix: update README docs and test count for new rule --- README.md | 3 ++- tests/config-modes.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89390df..2c46c69 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ const webRules = getRulesForPlatform('web'); const backendRules = getRulesForPlatform('backend'); ``` -## Available Rules (50 total) +## Available Rules (51 total) ### Expo Router Rules @@ -155,6 +155,7 @@ const backendRules = getRulesForPlatform('backend'); | ---------------------------- | -------- | -------- | ----------------------------------------------------------------- | | `require-use-client` | error | web | Files using client-only features must have "use client" directive | | `no-server-import-in-client` | error | web | "use client" files must not import server-only modules | +| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | ### React / JSX Rules diff --git a/tests/config-modes.test.ts b/tests/config-modes.test.ts index 112bd5e..81092d3 100644 --- a/tests/config-modes.test.ts +++ b/tests/config-modes.test.ts @@ -123,7 +123,7 @@ describe('config modes', () => { expect(ruleNames).toContain('no-relative-paths'); expect(ruleNames).toContain('expo-image-import'); expect(ruleNames).toContain('no-stylesheet-create'); - expect(ruleNames.length).toBe(50); + expect(ruleNames.length).toBe(51); }); }); }); From 8dbc65c83ef0bddd87060c4cf8347f4445b05ee8 Mon Sep 17 00:00:00 2001 From: Daniel C Date: Tue, 3 Mar 2026 17:02:45 -0800 Subject: [PATCH 3/3] style: format README with prettier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c46c69..8baae42 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ const backendRules = getRulesForPlatform('backend'); | ---------------------------- | -------- | -------- | ----------------------------------------------------------------- | | `require-use-client` | error | web | Files using client-only features must have "use client" directive | | `no-server-import-in-client` | error | web | "use client" files must not import server-only modules | -| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | +| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) | ### React / JSX Rules