Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const webRules = getRulesForPlatform('web');
const backendRules = getRulesForPlatform('backend');
```

## Available Rules (50 total)
## Available Rules (51 total)

### Expo Router Rules

Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RuleFunction> = {
'no-relative-paths': noRelativePaths,
Expand Down Expand Up @@ -101,4 +102,5 @@ export const rules: Record<string, RuleFunction> = {
'prefer-named-params': preferNamedParams,
'require-use-client': requireUseClient,
'no-server-import-in-client': noServerImportInClient,
'no-react-native-in-web': noReactNativeInWeb,
};
1 change: 1 addition & 0 deletions src/rules/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const rulePlatforms: Partial<Record<string, Platform[]>> = {
'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'],
Expand Down
46 changes: 46 additions & 0 deletions src/rules/no-react-native-in-web.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion tests/config-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
67 changes: 67 additions & 0 deletions tests/no-react-native-in-web.test.ts
Original file line number Diff line number Diff line change
@@ -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 <View><Text>Hello</Text></View>;
}
`;
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 <View />;
}
`;
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 <View />;
}
`;
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 <div>Hello</div>;
}
`;
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 <div>Hello</div>;
}
`;
const results = lintJsxCode(code, config);
expect(results).toHaveLength(0);
});
});
Loading