Refactor provider composition and ESLint configuration
- Add new provider composition system with validation - Create providerComposition.tsx for centralized provider management - Implement providerOrderValidator.tsx to ensure correct provider order - Add comprehensive tests for provider composition - Create custom ESLint rules in frontend/eslint-rules/ - Update ESLint configuration - Update architecture and tasks documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
14
frontend/eslint-rules/index.js
Normal file
14
frontend/eslint-rules/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* BanGUI Custom ESLint Rules
|
||||
*
|
||||
* This module exports all custom ESLint rules used to enforce
|
||||
* coding standards and best practices in the BanGUI frontend.
|
||||
*/
|
||||
|
||||
import providerOrderRule from "./provider-order.js";
|
||||
|
||||
export default {
|
||||
rules: {
|
||||
"provider-order": providerOrderRule,
|
||||
},
|
||||
};
|
||||
172
frontend/eslint-rules/provider-order.js
Normal file
172
frontend/eslint-rules/provider-order.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* ESLint Rule: no-incorrect-provider-order
|
||||
*
|
||||
* Validates that React context providers are nested in the correct order
|
||||
* according to the BanGUI provider dependency contract.
|
||||
*
|
||||
* Provider Order (enforced by this rule):
|
||||
* 1. ThemeProvider (OUTERMOST — must be first)
|
||||
* 2. FluentProvider (wraps all Fluent UI consumers)
|
||||
* 3. NotificationProvider (must wrap error boundaries)
|
||||
* 4. ErrorBoundary (top-level error boundary)
|
||||
* 5. BrowserRouter (enables routing)
|
||||
* 6. NavigationCancellationProvider (manages route cancellation)
|
||||
* 7. AuthProvider (session validation)
|
||||
* 8. TimezoneProvider (INNERMOST — wraps protected routes only)
|
||||
*/
|
||||
|
||||
const PROVIDER_ORDER = [
|
||||
"ThemeProvider",
|
||||
"FluentProvider",
|
||||
"NotificationProvider",
|
||||
"ErrorBoundary",
|
||||
"BrowserRouter",
|
||||
"NavigationCancellationProvider",
|
||||
"AuthProvider",
|
||||
"TimezoneProvider",
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the provider name from a JSX element
|
||||
*/
|
||||
function getProviderName(node) {
|
||||
if (
|
||||
node.name &&
|
||||
node.name.type === "JSXIdentifier" &&
|
||||
PROVIDER_ORDER.includes(node.name.name)
|
||||
) {
|
||||
return node.name.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of a provider in the ordering sequence
|
||||
*/
|
||||
function getProviderPosition(name) {
|
||||
return PROVIDER_ORDER.indexOf(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a JSX element is a provider
|
||||
*/
|
||||
function isProvider(node) {
|
||||
return getProviderName(node) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all JSX provider elements from a component tree
|
||||
*/
|
||||
function extractProviderStack(node) {
|
||||
const stack = [];
|
||||
let current = node;
|
||||
|
||||
while (current) {
|
||||
if (!current.openingElement) break;
|
||||
|
||||
const providerName = getProviderName(current.openingElement);
|
||||
|
||||
if (providerName) {
|
||||
stack.push({
|
||||
name: providerName,
|
||||
node: current.openingElement,
|
||||
});
|
||||
}
|
||||
|
||||
// Find the next JSX element by looking at children
|
||||
if (current.children && current.children.length > 0) {
|
||||
const firstChild = current.children[0];
|
||||
|
||||
if (firstChild && firstChild.type === "JSXElement") {
|
||||
current = firstChild;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Enforce correct nesting order of React context providers in BanGUI",
|
||||
category: "Best Practices",
|
||||
recommended: "error",
|
||||
url: "https://github.com/bangui/frontend/blob/main/src/providers/PROVIDER_ORDER.md",
|
||||
},
|
||||
fixable: undefined,
|
||||
schema: [],
|
||||
messages: {
|
||||
incorrectOrder:
|
||||
"Provider '{{ outer }}' (position {{ outerPos }}) must wrap '{{ inner }}' (position {{ innerPos }}). Current nesting: {{ outer }} → {{ inner }}",
|
||||
missingParent:
|
||||
"Provider '{{ current }}' (position {{ currentPos }}) requires parent provider '{{ required }}' (position {{ requiredPos }}). Expected: {{ expected }}",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
JSXElement(node) {
|
||||
// Skip non-provider elements
|
||||
if (!node.openingElement || !isProvider(node.openingElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the provider stack from this element
|
||||
const stack = extractProviderStack(node);
|
||||
|
||||
if (stack.length < 2) {
|
||||
// No nesting to validate
|
||||
return;
|
||||
}
|
||||
|
||||
// Check ordering from outermost to innermost
|
||||
for (let i = 0; i < stack.length - 1; i++) {
|
||||
const outer = stack[i];
|
||||
const inner = stack[i + 1];
|
||||
|
||||
const outerPos = getProviderPosition(outer.name);
|
||||
const innerPos = getProviderPosition(inner.name);
|
||||
|
||||
// Inner provider must have a higher position (come later in the sequence)
|
||||
if (innerPos <= outerPos) {
|
||||
context.report({
|
||||
node: inner.node,
|
||||
messageId: "incorrectOrder",
|
||||
data: {
|
||||
outer: outer.name,
|
||||
outerPos: String(outerPos),
|
||||
inner: inner.name,
|
||||
innerPos: String(innerPos),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all providers before the first one in the stack are present
|
||||
const firstProvider = stack[0];
|
||||
const firstPos = getProviderPosition(firstProvider.name);
|
||||
|
||||
if (firstPos > 0) {
|
||||
const required = PROVIDER_ORDER[firstPos - 1];
|
||||
context.report({
|
||||
node: firstProvider.node,
|
||||
messageId: "missingParent",
|
||||
data: {
|
||||
current: firstProvider.name,
|
||||
currentPos: String(firstPos),
|
||||
required,
|
||||
requiredPos: String(firstPos - 1),
|
||||
expected: PROVIDER_ORDER.slice(0, firstPos).join(" → "),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user