Files
itworx-knowledge-hub-Public/scripts/lib/route-map.mjs
T

235 lines
8.7 KiB
JavaScript

function skipQuoted(source, start) {
const quote = source[start];
let escaped = false;
for (let index = start + 1; index < source.length; index += 1) {
const character = source[index];
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
return index + 1;
}
}
return source.length;
}
function readRouteTag(source, start) {
const closing = source[start + 1] === "/";
const nameStart = start + (closing ? 2 : 1);
const nameMatch = /^[A-Za-z_$][\w$.-]*/u.exec(source.slice(nameStart));
if (!nameMatch || nameMatch[0] !== "Route") return null;
let curlyDepth = 0;
let quote = null;
let escaped = false;
for (let index = nameStart + nameMatch[0].length; index < source.length; index += 1) {
const character = source[index];
if (quote !== null) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === quote) quote = null;
continue;
}
if (character === "\"" || character === "'" || character === "`") {
quote = character;
} else if (character === "{") {
curlyDepth += 1;
} else if (character === "}" && curlyDepth > 0) {
curlyDepth -= 1;
} else if (character === ">" && curlyDepth === 0) {
const text = source.slice(nameStart + nameMatch[0].length, index);
return {
closing,
selfClosing: !closing && /\/\s*$/u.test(text),
attributes: closing ? "" : text.replace(/\/\s*$/u, ""),
end: index + 1
};
}
}
return { error: "AuthenticatedRoutes.tsx contains an unterminated <Route> tag." };
}
function findStaticPath(attributes) {
const pathAttribute = /(?:^|\s)path\s*=\s*(?:"([^"]*)"|'([^']*)')/u.exec(attributes);
if (pathAttribute) return { value: pathAttribute[1] ?? pathAttribute[2] };
if (/(?:^|\s)path\s*=\s*\{/u.test(attributes)) return { error: "dynamic" };
return null;
}
function routePath(parentPath, childPath) {
if (childPath === "*") return parentPath ? `${parentPath.replace(/\/$/u, "")}/*` : "*";
if (childPath.startsWith("/")) return childPath.replace(/\/{2,}/gu, "/");
const joined = `${parentPath || ""}/${childPath}`.replace(/\/{2,}/gu, "/");
return joined || "/";
}
function routeFeatures(attributes) {
const features = new Set();
for (const match of attributes.matchAll(/\bfeatures\s*\?\.\s*([A-Za-z_$][\w$]*)/gu)) {
features.add(match[1]);
}
return [...features].sort();
}
function extractRouteStructure(source) {
const errors = [];
const routeStack = [];
const records = [];
for (let index = 0; index < source.length;) {
if (source.startsWith("//", index)) {
const newline = source.indexOf("\n", index + 2);
index = newline < 0 ? source.length : newline + 1;
continue;
}
if (source.startsWith("/*", index)) {
const commentEnd = source.indexOf("*/", index + 2);
index = commentEnd < 0 ? source.length : commentEnd + 2;
continue;
}
if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
index = skipQuoted(source, index);
continue;
}
if (source[index] === "<" && (source[index + 1] === "/" || /[A-Za-z_$]/u.test(source[index + 1] ?? ""))) {
const tag = readRouteTag(source, index);
if (tag?.error) {
errors.push(tag.error);
break;
}
if (tag) {
if (tag.closing) {
if (routeStack.length === 0) errors.push("AuthenticatedRoutes.tsx closes a <Route> without a matching opening tag.");
else routeStack.pop();
} else {
const parent = routeStack.at(-1);
const parentPath = parent?.path ?? "";
const pathAttribute = findStaticPath(tag.attributes);
const hasIndex = /(?:^|\s)index(?:\s|=|$)/u.test(tag.attributes);
const localFeatures = routeFeatures(tag.attributes);
const features = [...new Set([...(parent?.features ?? []), ...localFeatures])].sort();
const administrator = Boolean(parent?.administrator)
|| /\bsession\.role\s*===?\s*["']Administrator["']/u.test(tag.attributes);
if (pathAttribute?.error === "dynamic") {
errors.push("AuthenticatedRoutes.tsx uses a dynamic Route path; route-map validation requires a static path.");
}
if (hasIndex && pathAttribute) errors.push("AuthenticatedRoutes.tsx declares a Route with both index and path.");
let fullPath = null;
if (hasIndex) fullPath = parentPath || "/";
else if (pathAttribute && !pathAttribute.error) fullPath = routePath(parentPath, pathAttribute.value);
if (fullPath !== null) {
records.push({
path: fullPath,
featureGates: features,
auth: administrator ? "administrator" : "authenticated",
depth: routeStack.length,
parentPath
});
}
if (!tag.selfClosing) {
routeStack.push({
path: fullPath ?? parentPath,
features,
administrator
});
}
}
index = tag.end;
continue;
}
}
index += 1;
}
if (routeStack.length > 0) errors.push("AuthenticatedRoutes.tsx contains an unclosed <Route> tag.");
const uniqueRecords = [];
for (const record of records) {
const priorIndex = uniqueRecords.findIndex((candidate) => candidate.path === record.path);
if (priorIndex < 0) {
uniqueRecords.push(record);
continue;
}
const prior = uniqueRecords[priorIndex];
const isNestedSamePath = record.parentPath === record.path && record.depth > prior.depth;
if (isNestedSamePath) {
uniqueRecords[priorIndex] = record;
} else {
errors.push(`AuthenticatedRoutes.tsx declares duplicate route path ${record.path}.`);
}
}
return { records: uniqueRecords, errors };
}
export function extractAuthenticatedRoutes(source) {
return extractRouteStructure(source).records;
}
export function extractAuthenticatedRoutePaths(source) {
return extractAuthenticatedRoutes(source).map((route) => route.path);
}
function mappedFeatureGates(route) {
if (Array.isArray(route?.featureGates)) return [...route.featureGates].sort();
return typeof route?.featureGate === "string" ? [route.featureGate] : [];
}
export function validateRouteMap(routeMap, authenticatedSource, sessionSource) {
const errors = [];
const { records, errors: routeErrors } = extractRouteStructure(authenticatedSource);
errors.push(...routeErrors);
const actualPaths = records.map((route) => route.path);
const mappedRoutes = Array.isArray(routeMap?.routes) ? routeMap.routes : [];
const mappedPaths = mappedRoutes.map((route) => route?.path);
if (new Set(mappedPaths).size !== mappedPaths.length) {
errors.push("product/route-map.json declares duplicate paths.");
}
const missing = actualPaths.filter((routePath) => !mappedPaths.includes(routePath));
const stale = mappedPaths.filter((routePath) => !actualPaths.includes(routePath));
if (missing.length > 0) errors.push(`Route map is missing: ${missing.join(", ")}.`);
if (stale.length > 0) errors.push(`Route map has stale entries: ${stale.join(", ")}.`);
if (routeMap?.sourceOfTruth !== "src/frontend/src/router/AuthenticatedRoutes.tsx") {
errors.push("Route map sourceOfTruth must identify AuthenticatedRoutes.tsx.");
}
const states = routeMap?.sessionStates;
const expectedStates = ["loading", "error", "setupRequired", "unauthenticated"];
if (!states || typeof states !== "object" || Array.isArray(states)) {
errors.push("Route map must document the pre-route session states.");
} else {
const actualStates = Object.keys(states).sort();
if (JSON.stringify(actualStates) !== JSON.stringify([...expectedStates].sort())) {
errors.push(`Route map session states must be ${expectedStates.join(", ")}.`);
}
for (const [condition, screen] of Object.entries(states)) {
if (typeof screen !== "string" || !sessionSource.includes(`<${screen}`)) {
errors.push(`Session screen is not present in App.tsx: ${condition}.`);
}
}
}
for (const actualRoute of records) {
const mappedRoute = mappedRoutes.find((route) => route?.path === actualRoute.path);
if (!mappedRoute) continue;
if (mappedRoute.auth !== actualRoute.auth) {
errors.push(`${actualRoute.path} must be marked ${actualRoute.auth}-only.`);
}
const expectedFeatures = actualRoute.featureGates;
const mappedFeatures = mappedFeatureGates(mappedRoute);
if (JSON.stringify(expectedFeatures) !== JSON.stringify(mappedFeatures)) {
const expected = expectedFeatures.length > 0 ? expectedFeatures.join(", ") : "none";
errors.push(`${actualRoute.path} feature-gate registry must be ${expected}.`);
}
}
return errors;
}