Files
BanGUI/frontend/eslint-rules/provider-order.js
Lukas c988b4b8b6 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>
2026-05-01 17:33:56 +02:00

173 lines
4.7 KiB
JavaScript

/**
* 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(" → "),
},
});
}
},
};
},
};