116 lines
3.6 KiB
JavaScript
116 lines
3.6 KiB
JavaScript
/**
|
|
* ESLint rules for Client-Only Guardrails
|
|
*
|
|
* Enforces client-side only boundaries
|
|
*/
|
|
|
|
module.exports = {
|
|
// Rule 1: No server-side code in client-only files
|
|
'no-server-code-in-client-only': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Forbid server-side code in client-only files',
|
|
category: 'Client-Only',
|
|
},
|
|
messages: {
|
|
message: 'Client-only files cannot contain server-side code - see apps/website/lib/contracts/view-models/ViewModel.ts',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
Program(node) {
|
|
const filename = context.getFilename();
|
|
if (filename.includes('/app/') &&
|
|
filename.endsWith('.tsx') &&
|
|
!filename.endsWith('page.tsx') &&
|
|
!filename.endsWith('layout.tsx')) {
|
|
|
|
const sourceCode = context.getSourceCode();
|
|
const text = sourceCode.getText();
|
|
|
|
// Check for server-side patterns
|
|
const serverPatterns = [
|
|
/getServerSideProps/,
|
|
/cookies\(\)/,
|
|
/headers\(\)/,
|
|
/next\/headers/,
|
|
];
|
|
|
|
for (const pattern of serverPatterns) {
|
|
if (pattern.test(text)) {
|
|
context.report({
|
|
loc: { line: 1, column: 0 },
|
|
messageId: 'message',
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
// Rule 2: Client-only files must have 'use client' directive
|
|
'client-only-must-have-directive': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Enforce use client directive',
|
|
category: 'Client-Only',
|
|
},
|
|
messages: {
|
|
message: 'Client-only files must have "use client" directive at the top - see apps/website/lib/contracts/view-models/ViewModel.ts',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
Program(node) {
|
|
const filename = context.getFilename();
|
|
if (filename.includes('/app/') &&
|
|
filename.endsWith('.tsx') &&
|
|
!filename.endsWith('page.tsx') &&
|
|
!filename.endsWith('layout.tsx')) {
|
|
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
// Check for 'use client' as a string literal directive
|
|
// This can be either a comment or a string literal statement
|
|
const comments = sourceCode.getAllComments();
|
|
const firstComment = comments[0];
|
|
|
|
let hasDirective = false;
|
|
|
|
// Check if it's a comment
|
|
if (firstComment &&
|
|
firstComment.type === 'Line' &&
|
|
(firstComment.value.trim() === '"use client"' ||
|
|
firstComment.value.trim() === 'use client')) {
|
|
hasDirective = true;
|
|
}
|
|
|
|
// Check if it's a string literal statement (the actual Next.js way)
|
|
if (!hasDirective && node.body.length > 0) {
|
|
const firstStmt = node.body[0];
|
|
if (firstStmt &&
|
|
firstStmt.type === 'ExpressionStatement' &&
|
|
firstStmt.expression.type === 'Literal' &&
|
|
(firstStmt.expression.value === 'use client' ||
|
|
firstStmt.expression.value === '"use client"')) {
|
|
hasDirective = true;
|
|
}
|
|
}
|
|
|
|
if (!hasDirective) {
|
|
context.report({
|
|
loc: { line: 1, column: 0 },
|
|
messageId: 'message',
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
}; |