website refactor
This commit is contained in:
172
apps/website/eslint-rules/no-raw-html.js
Normal file
172
apps/website/eslint-rules/no-raw-html.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* ESLint rule to forbid raw HTML across all UI layers
|
||||
*
|
||||
* All HTML must be encapsulated in UI elements from ui/ directory
|
||||
*
|
||||
* Rationale:
|
||||
* - app/ should only contain page/layout components
|
||||
* - components/ should use ui/ elements for all rendering
|
||||
* - templates/ should use ui/ elements for all rendering
|
||||
* - Raw HTML with styling violates separation of concerns
|
||||
* - UI elements ensure consistency and reusability
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid raw HTML with styling in app/, components/, and templates/ directories',
|
||||
category: 'Architecture',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
noRawHtml: 'Raw HTML with styling is forbidden. Use a UI element from ui/ directory.',
|
||||
noRawHtmlInApp: 'Raw HTML in app/ is forbidden. Use components/ or ui/ elements.',
|
||||
noRawHtmlInComponents: 'Raw HTML in components/ should use ui/ elements instead.',
|
||||
noRawHtmlInTemplates: 'Raw HTML in templates/ should use ui/ elements instead.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Determine which layer we're in
|
||||
const isInApp = filename.includes('/app/');
|
||||
const isInComponents = filename.includes('/components/');
|
||||
const isInTemplates = filename.includes('/templates/');
|
||||
const isInUi = filename.includes('/ui/');
|
||||
|
||||
// Only apply to UI layers (not ui/ itself)
|
||||
if (!isInApp && !isInComponents && !isInTemplates) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// HTML tags that should be wrapped in UI elements
|
||||
const htmlTags = [
|
||||
'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'p', 'button', 'input', 'form', 'label', 'select', 'textarea',
|
||||
'ul', 'ol', 'li', 'table', 'tr', 'td', 'th', 'thead', 'tbody',
|
||||
'section', 'article', 'header', 'footer', 'nav', 'aside',
|
||||
'main', 'aside', 'figure', 'figcaption', 'blockquote', 'code',
|
||||
'pre', 'a', 'img', 'svg', 'path', 'g', 'rect', 'circle',
|
||||
'hr', 'br', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark'
|
||||
];
|
||||
|
||||
// UI elements that are allowed (from ui/ directory)
|
||||
const allowedUiElements = [
|
||||
'Button', 'Input', 'Form', 'Label', 'Select', 'Textarea',
|
||||
'Card', 'Container', 'Grid', 'Stack', 'Box', 'Text', 'Heading',
|
||||
'List', 'Table', 'Section', 'Article', 'Header', 'Footer', 'Nav', 'Aside',
|
||||
'Link', 'Image', 'Icon', 'Avatar', 'Badge', 'Chip', 'Pill',
|
||||
'Modal', 'Dialog', 'Toast', 'Notification', 'Alert',
|
||||
'StepIndicator', 'Loading', 'Spinner', 'Progress'
|
||||
];
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
const openingElement = node.openingElement;
|
||||
|
||||
if (openingElement.name.type !== 'JSXIdentifier') return;
|
||||
|
||||
const tagName = openingElement.name.name;
|
||||
|
||||
// Skip allowed UI elements
|
||||
if (allowedUiElements.includes(tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a raw HTML element (lowercase)
|
||||
if (htmlTags.includes(tagName) && tagName[0] === tagName[0].toLowerCase()) {
|
||||
|
||||
// Check for styling attributes
|
||||
const hasClassName = openingElement.attributes.some(
|
||||
attr => attr.type === 'JSXAttribute' && attr.name.name === 'className'
|
||||
);
|
||||
const hasStyle = openingElement.attributes.some(
|
||||
attr => attr.type === 'JSXAttribute' && attr.name.name === 'style'
|
||||
);
|
||||
|
||||
// Check for inline event handlers
|
||||
const hasInlineHandlers = openingElement.attributes.some(
|
||||
attr => attr.type === 'JSXAttribute' &&
|
||||
attr.name.name &&
|
||||
attr.name.name.startsWith('on')
|
||||
);
|
||||
|
||||
// Check for other common attributes that suggest styling/behavior
|
||||
const hasCommonAttrs = openingElement.attributes.some(
|
||||
attr => attr.type === 'JSXAttribute' &&
|
||||
['id', 'role', 'aria-label', 'aria-hidden'].includes(attr.name.name)
|
||||
);
|
||||
|
||||
if (hasClassName || hasStyle || hasInlineHandlers || hasCommonAttrs) {
|
||||
let messageId = 'noRawHtml';
|
||||
|
||||
if (isInApp) {
|
||||
messageId = 'noRawHtmlInApp';
|
||||
} else if (isInComponents) {
|
||||
messageId = 'noRawHtmlInComponents';
|
||||
} else if (isInTemplates) {
|
||||
messageId = 'noRawHtmlInTemplates';
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Also check for dangerouslySetInnerHTML
|
||||
JSXAttribute(node) {
|
||||
if (node.name.name === 'dangerouslySetInnerHTML') {
|
||||
let messageId = 'noRawHtml';
|
||||
|
||||
if (isInApp) {
|
||||
messageId = 'noRawHtmlInApp';
|
||||
} else if (isInComponents) {
|
||||
messageId = 'noRawHtmlInComponents';
|
||||
} else if (isInTemplates) {
|
||||
messageId = 'noRawHtmlInTemplates';
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Check for HTML strings in JSX expressions
|
||||
JSXExpressionContainer(node) {
|
||||
if (node.expression.type === 'Literal' && typeof node.expression.value === 'string') {
|
||||
const value = node.expression.value.trim();
|
||||
|
||||
// Check if it contains HTML-like content
|
||||
if (value.includes('<') && value.includes('>') &&
|
||||
(value.includes('class=') || value.includes('style=') ||
|
||||
value.match(/<\w+[^>]*>/))) {
|
||||
|
||||
let messageId = 'noRawHtml';
|
||||
|
||||
if (isInApp) {
|
||||
messageId = 'noRawHtmlInApp';
|
||||
} else if (isInComponents) {
|
||||
messageId = 'noRawHtmlInComponents';
|
||||
} else if (isInTemplates) {
|
||||
messageId = 'noRawHtmlInTemplates';
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user