website refactor
This commit is contained in:
370
apps/website/eslint-rules/rsc-boundary-rules.js
Normal file
370
apps/website/eslint-rules/rsc-boundary-rules.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* ESLint rules for RSC Boundary Guardrails
|
||||
*
|
||||
* Enforces server-side code boundaries in Next.js app directory
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Rule 1: No ContainerManager in server code
|
||||
'no-container-manager-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid ContainerManager usage in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'ContainerManager usage forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Identifier(node) {
|
||||
if (node.name === 'ContainerManager' && !isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 2: No PageDataFetcher.fetch() in server code
|
||||
'no-page-data-fetcher-fetch-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid PageDataFetcher.fetch() in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'PageDataFetcher.fetch() forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.name === 'PageDataFetcher' &&
|
||||
node.callee.property.name === 'fetch' &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 3: No ViewModels/ViewModels imports in server code
|
||||
'no-view-models-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid ViewModels/Presenters imports in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'ViewModels or Presenters import forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if ((importPath.includes('@/lib/view-models/') ||
|
||||
importPath.includes('@/lib/presenters/')) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 4: No Presenter imports in server code
|
||||
'no-presenters-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid Presenter imports in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Presenter import forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if ((importPath.includes('@/lib/presenters/') ||
|
||||
importPath.includes('@/lib/presenters/')) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 5: No Intl usage in presentation paths
|
||||
'no-intl-in-presentation': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid Intl.* or toLocale* usage in presentation paths',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Intl.* or toLocale* usage forbidden in presentation paths',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if ((node.object.name === 'Intl' ||
|
||||
(node.property && node.property.name && node.property.name.startsWith('toLocale'))) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 6: No sorting/filtering/reduce in server code
|
||||
'no-sorting-filtering-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid sorting/filtering/reduce operations in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Sorting/filtering/reduce operations forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
['sort', 'filter', 'reduce'].includes(node.callee.property.name) &&
|
||||
!isInComment(node) &&
|
||||
!node.loc.start.line.toString().includes('null check')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 7: No DisplayObjects imports in server code
|
||||
'no-display-objects-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid DisplayObjects imports in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'DisplayObjects import forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (importPath.includes('@/lib/display-objects/') &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 8: No unsafe services imports in server code
|
||||
'no-unsafe-services-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid unsafe services imports in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Services import must be explicitly marked as server-safe',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (importPath.includes('@/lib/services/') &&
|
||||
!isInComment(node) &&
|
||||
!isMarkedServerSafe(context.getSourceCode().getText(node))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 9: No DI imports in server code
|
||||
'no-di-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid DI imports in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'DI import forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (importPath.includes('@/lib/di/') &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 10: No local helpers in server code
|
||||
'no-local-helpers-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid local helper functions in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Local helper functions forbidden (only assert*/invariant* allowed)',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
FunctionDeclaration(node) {
|
||||
if (!node.id.name.startsWith('assert') &&
|
||||
!node.id.name.startsWith('invariant') &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (node.init &&
|
||||
(node.init.type === 'FunctionExpression' || node.init.type === 'ArrowFunctionExpression') &&
|
||||
!node.id.name.startsWith('assert') &&
|
||||
!node.id.name.startsWith('invariant') &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 11: No object construction in server code
|
||||
'no-object-construction-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid object construction with new in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'Object construction with new forbidden (use PageQueries)',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
NewExpression(node) {
|
||||
if (node.callee.type === 'Identifier' &&
|
||||
/^[A-Z]/.test(node.callee.name) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 12: No ContainerManager calls in server code
|
||||
'no-container-manager-calls-in-server': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid ContainerManager calls in server code',
|
||||
category: 'RSC Boundary',
|
||||
},
|
||||
messages: {
|
||||
message: 'ContainerManager calls forbidden in server code',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.name === 'ContainerManager' &&
|
||||
(node.callee.property.name === 'getInstance' ||
|
||||
node.callee.property.name === 'getContainer') &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function isInComment(node) {
|
||||
// This is a simplified check - in practice you'd need to check the actual comment
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMarkedServerSafe(text) {
|
||||
return text.includes('// @server-safe') || text.includes('/* @server-safe */');
|
||||
}
|
||||
Reference in New Issue
Block a user