Skip to content

Commit a709c96

Browse files
justin808claude
andcommitted
Add ESLint rule to prevent 'use client' in server files
Add custom ESLint rule no-use-client-in-server-files to catch and prevent 'use client' directives in .server.tsx files, which causes React 19 server bundle compilation errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 777bee2 commit a709c96

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @fileoverview Prevent 'use client' directive in .server.tsx files
3+
* @author React on Rails Team
4+
*/
5+
6+
/**
7+
* ESLint rule to prevent 'use client' directives in .server.tsx files.
8+
*
9+
* Files ending with .server.tsx are intended for server-side rendering in
10+
* React Server Components architecture. The 'use client' directive forces
11+
* webpack to bundle these as client components, which causes errors when
12+
* using React's react-server conditional exports with Shakapacker 9.3.0+.
13+
*
14+
* @type {import('eslint').Rule.RuleModule}
15+
*/
16+
module.exports = {
17+
meta: {
18+
type: 'problem',
19+
docs: {
20+
description: "Prevent 'use client' directive in .server.tsx files",
21+
category: 'Best Practices',
22+
recommended: true,
23+
url: 'https://github.com/shakacode/react_on_rails/pull/1896',
24+
},
25+
messages: {
26+
useClientInServerFile:
27+
"Files with '.server.tsx' extension should not have 'use client' directive. " +
28+
'Server files are for React Server Components and should not use client-only APIs. ' +
29+
'If this component needs client-side features, rename it to .client.tsx or .tsx instead.',
30+
},
31+
schema: [],
32+
fixable: 'code',
33+
},
34+
35+
create(context) {
36+
const filename = context.filename || context.getFilename();
37+
38+
// Only check .server.tsx files
39+
if (!filename.endsWith('.server.tsx') && !filename.endsWith('.server.ts')) {
40+
return {};
41+
}
42+
43+
return {
44+
Program(node) {
45+
const sourceCode = context.sourceCode || context.getSourceCode();
46+
const text = sourceCode.getText();
47+
48+
// Check for 'use client' directive at the start of the file
49+
// Handle both single and double quotes, with or without semicolon
50+
const useClientPattern = /^\s*['"]use client['"];?\s*$/m;
51+
const match = text.match(useClientPattern);
52+
53+
if (match) {
54+
// Find the exact position of the directive
55+
const directiveIndex = text.indexOf(match[0]);
56+
57+
context.report({
58+
node,
59+
messageId: 'useClientInServerFile',
60+
fix(fixer) {
61+
// Remove the 'use client' directive and any trailing newlines
62+
const start = directiveIndex;
63+
let end = directiveIndex + match[0].length;
64+
65+
// Also remove the newline after the directive if present
66+
if (text[end] === '\n') {
67+
end += 1;
68+
}
69+
70+
return fixer.removeRange([start, end]);
71+
},
72+
});
73+
}
74+
},
75+
};
76+
},
77+
};
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* @fileoverview Tests for no-use-client-in-server-files rule
3+
*/
4+
5+
const { RuleTester } = require('eslint');
6+
const rule = require('./no-use-client-in-server-files.cjs');
7+
8+
const ruleTester = new RuleTester({
9+
languageOptions: {
10+
ecmaVersion: 2022,
11+
sourceType: 'module',
12+
parserOptions: {
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
},
18+
});
19+
20+
ruleTester.run('no-use-client-in-server-files', rule, {
21+
valid: [
22+
{
23+
code: `
24+
import React from 'react';
25+
26+
export function ServerComponent() {
27+
return <div>Server Component</div>;
28+
}
29+
`,
30+
filename: 'Component.server.tsx',
31+
},
32+
{
33+
code: `
34+
import { renderToString } from 'react-dom/server';
35+
36+
export function render() {
37+
return renderToString(<div>Hello</div>);
38+
}
39+
`,
40+
filename: 'ComponentRenderer.server.tsx',
41+
},
42+
{
43+
code: `
44+
'use client';
45+
46+
import React from 'react';
47+
48+
export function ClientComponent() {
49+
return <div>Client Component</div>;
50+
}
51+
`,
52+
filename: 'Component.client.tsx',
53+
},
54+
{
55+
code: `
56+
'use client';
57+
58+
import React from 'react';
59+
60+
export function ClientComponent() {
61+
return <div>Client Component</div>;
62+
}
63+
`,
64+
filename: 'Component.tsx',
65+
},
66+
{
67+
code: `
68+
import React from 'react';
69+
70+
// This is fine - no 'use client' directive
71+
export function ServerComponent() {
72+
return <div>Server</div>;
73+
}
74+
`,
75+
filename: 'App.server.ts',
76+
},
77+
],
78+
79+
invalid: [
80+
{
81+
code: `'use client';
82+
83+
import React from 'react';
84+
85+
export function Component() {
86+
return <div>Component</div>;
87+
}
88+
`,
89+
filename: 'Component.server.tsx',
90+
errors: [
91+
{
92+
messageId: 'useClientInServerFile',
93+
},
94+
],
95+
output: `import React from 'react';
96+
97+
export function Component() {
98+
return <div>Component</div>;
99+
}
100+
`,
101+
},
102+
{
103+
code: `"use client";
104+
105+
import React from 'react';
106+
`,
107+
filename: 'Component.server.tsx',
108+
errors: [
109+
{
110+
messageId: 'useClientInServerFile',
111+
},
112+
],
113+
output: `import React from 'react';
114+
`,
115+
},
116+
{
117+
code: `'use client'
118+
119+
import React from 'react';
120+
`,
121+
filename: 'Component.server.tsx',
122+
errors: [
123+
{
124+
messageId: 'useClientInServerFile',
125+
},
126+
],
127+
output: `import React from 'react';
128+
`,
129+
},
130+
{
131+
code: ` 'use client';
132+
133+
import wrapServerComponentRenderer from 'react-on-rails-pro/wrapServerComponentRenderer/server';
134+
`,
135+
filename: 'AsyncOnServerSyncOnClient.server.tsx',
136+
errors: [
137+
{
138+
messageId: 'useClientInServerFile',
139+
},
140+
],
141+
output: `import wrapServerComponentRenderer from 'react-on-rails-pro/wrapServerComponentRenderer/server';
142+
`,
143+
},
144+
{
145+
code: `'use client';
146+
147+
import React from 'react';
148+
`,
149+
filename: 'Component.server.ts',
150+
errors: [
151+
{
152+
messageId: 'useClientInServerFile',
153+
},
154+
],
155+
output: `import React from 'react';
156+
`,
157+
},
158+
],
159+
});
160+
161+
console.log('All tests passed!');

0 commit comments

Comments
 (0)