eslint-plugin-react-server-components
Version:
Rules for React server components
357 lines (329 loc) • 8.12 kB
text/typescript
"use strict";
import { RuleTester } from "eslint";
import { ClientComponents as rule } from "./use-client";
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: { jsx: true },
},
settings: {
react: {
version: "18.2.0",
},
},
});
describe("use client", () => {
describe("browser globals", () => {
ruleTester.run("BROWSER GLOBALS", rule, {
valid: [
{
code: 'const foo = "bar"',
},
{
code: `import {createContext, useContext, useEffect} from 'react';
const context = createContext()
export function useTheme() {
const context = useContext(context);
useEffect(() => {
window.setTimeout(() => {});
});
return context;
}`,
},
{
code: `import * as React from 'react';
const context = React.createContext()
export function Foo() {
return <div />;
}
export function useTheme() {
const context = React.useContext(context);
React.useEffect(() => {
window.setTimeout(() => {});
});
return context;
}`,
},
],
invalid: [
// DOCUMENT
{
code: `const foo = "bar";
document.addEventListener('scroll', () => {})`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const foo = "bar";
document.addEventListener('scroll', () => {})`,
},
{
code: `const foo = "bar";
function Bar() {
document.addEventListener('scroll', () => {})
return <div />;
}`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const foo = "bar";
function Bar() {
document.addEventListener('scroll', () => {})
return <div />;
}`,
},
// WINDOW
{
code: `const foo = "bar";
window.addEventListener('scroll', () => {})`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const foo = "bar";
window.addEventListener('scroll', () => {})`,
},
{
code: `const foo = "bar";
function Bar() {
window.addEventListener('scroll', () => {})
return <div />;
}`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const foo = "bar";
function Bar() {
window.addEventListener('scroll', () => {})
return <div />;
}`,
},
// OBSERVERS
{
code: `const observer = new IntersectionObserver()`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const observer = new IntersectionObserver()`,
},
{
code: `const observer = new MutationObserver()`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const observer = new MutationObserver()`,
},
{
code: `const observer = new ResizeObserver()`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const observer = new ResizeObserver()`,
},
// GLOBAL PROPERTY REFERENCE
{
code: `const foo = window.foo;`,
errors: [{ messageId: "addUseClientBrowserAPI" }],
output: `'use client';
const foo = window.foo;`,
},
],
});
});
describe("hooks", () => {
ruleTester.run("HOOKS", rule, {
valid: [
{
code: 'const foo = "bar";',
},
{
code: `'use client';
import {useState} from 'react';
const Button = () => {
const [value, setValue] = useState('');
return <div />;
}`,
},
{
code: `'use client';
import {useEffect} from 'react';
const Button = () => {
useEffect(() => {}, [])
return <div />;
}`,
},
{
code: `import {useId} from 'react';
const Button = () => {
const id = useId();
return <div id={id} />;
}`,
},
{
code: `import {useTranslations} from 'next-intl';
export function Foo() {
const t = useTranslations();
return <div />;
}`,
options: [{ allowedServerHooks: ["useTranslations"] }],
},
],
invalid: [
{
code: `import {useState} from 'react';
const Button = () => {
const [value, setValue] = useState('');
return <div />;
}`,
errors: [
{ messageId: "addUseClientHooks", data: { hook: "useState" } },
],
output: `'use client';
import {useState} from 'react';
const Button = () => {
const [value, setValue] = useState('');
return <div />;
}`,
},
{
code: `import * as React from 'react';
const Button = () => {
const [value, setValue] = React.useState('');
return <div />;
}`,
errors: [
{ messageId: "addUseClientHooks", data: { hook: "useState" } },
],
output: `'use client';
import * as React from 'react';
const Button = () => {
const [value, setValue] = React.useState('');
return <div />;
}`,
},
{
code: `import {useEffect} from 'react';
const Button = () => {
useEffect(() => {}, [])
return <div />;
}`,
errors: [
{ messageId: "addUseClientHooks", data: { hook: "useEffect" } },
],
output: `'use client';
import {useEffect} from 'react';
const Button = () => {
useEffect(() => {}, [])
return <div />;
}`,
},
{
code: `const Button = () => {
const id = useIdentifier();
return <div id={id} />;
}`,
errors: [
{ messageId: "addUseClientHooks", data: { hook: "useIdentifier" } },
],
output: `'use client';
const Button = () => {
const id = useIdentifier();
return <div id={id} />;
}`,
},
],
});
});
describe("event callbacks", () => {
ruleTester.run("EVENT CALLBACKS", rule, {
valid: [{ code: 'const foo = "bar";' }],
invalid: [
{
code: `import React from 'react';
function App() {
return (
<button onClick={() => console.log('hello')}>Hello</button>
);
}`,
errors: [{ messageId: "addUseClientCallbacks" }],
filename: "foo.jsx",
output: `'use client';
import React from 'react';
function App() {
return (
<button onClick={() => console.log('hello')}>Hello</button>
);
}`,
},
],
});
});
describe("CLASS COMPONENTS", () => {
ruleTester.run("class components", rule, {
valid: [
{
code: `'use client';
import React, {Component} from 'react';
class Foo extends Component {
render() {
return <div />
}
}`,
},
],
invalid: [
{
code: `import React, {Component} from 'react';
class Foo extends Component {
render() {
return <div />
}
}`,
errors: [{ messageId: "addUseClientClassComponent" }],
output: `'use client';
import React, {Component} from 'react';
class Foo extends Component {
render() {
return <div />
}
}`,
},
],
});
});
describe("behaviors", () => {
describe("comments at the top of the file", () => {
ruleTester.run("comments", rule, {
valid: [
{
code: `//top level comment
'use client';
import React from 'react';
function App() {
const foo = React.useState();
return (
<button onClick={() => console.log('hello')}>Hello</button>
);
}`,
},
],
invalid: [
{
code: `//top level comment
import React from 'react';
function App() {
const foo = React.useState();
return (
<button onClick={() => console.log('hello')}>Hello</button>
);
}`,
errors: [
{ messageId: "addUseClientHooks", data: { hook: "useState" } },
],
output: `//top level comment
'use client';
import React from 'react';
function App() {
const foo = React.useState();
return (
<button onClick={() => console.log('hello')}>Hello</button>
);
}`,
},
],
});
});
});
});