- 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>
173 lines
4.7 KiB
JavaScript
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(" → "),
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|