202 lines
9.3 KiB
JavaScript
202 lines
9.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const portalRouteIds = new Set([
|
|
"root",
|
|
"routes/healthz",
|
|
"routes/readyz",
|
|
"routes/healthcheck",
|
|
"routes/api.metrics.portal",
|
|
"routes/customer-portal/layout",
|
|
"routes/customer-portal/dashboard",
|
|
"routes/customer-portal/projects",
|
|
"routes/customer-portal/project-detail",
|
|
"routes/customer-portal/document-download",
|
|
"routes/customer-portal/upload",
|
|
"routes/customer-portal/settings",
|
|
"routes/customer-portal/branding-logo",
|
|
"routes/customer-portal/export-download",
|
|
"routes/customer-portal/project-upload",
|
|
"routes/customer-portal/project-complaint",
|
|
"routes/customer-portal/logout",
|
|
"routes/customer-portal/auth",
|
|
"routes/customer-portal/layout.__custom-host",
|
|
"routes/customer-portal/dashboard.__custom-host",
|
|
"routes/customer-portal/projects.__custom-host",
|
|
"routes/customer-portal/upload.__custom-host",
|
|
"routes/customer-portal/settings.__custom-host",
|
|
"routes/customer-portal/branding-logo.__custom-host",
|
|
"routes/customer-portal/export-download.__custom-host",
|
|
"routes/customer-portal/project-upload.__custom-host",
|
|
"routes/customer-portal/project-complaint.__custom-host",
|
|
"routes/customer-portal/project-detail.__custom-host",
|
|
"routes/customer-portal/document-download.__custom-host",
|
|
"routes/customer-portal/logout.__custom-host",
|
|
"routes/customer-portal/auth.login.__custom-host",
|
|
"routes/customer-portal/auth.forgot-password.__custom-host",
|
|
"routes/customer-portal/auth.setup.__custom-host",
|
|
"routes/customer-portal/auth.verify-email.__custom-host",
|
|
"routes/customer-portal/auth.reset-password.__custom-host",
|
|
]);
|
|
|
|
const forbiddenServerMarkers = {
|
|
core: ["portal-root.server", "loadPortalRootData", "handlePortalRootAction"],
|
|
portal: ["root-core.server", "runCoreRootMiddleware", "loadCoreRootData", "handleCoreRootAction", "cache-bootstrap", "startCacheSubscribers"],
|
|
};
|
|
|
|
const forbiddenClientMarkers = {
|
|
core: ["Portal build received Core root data"],
|
|
portal: [
|
|
"Core build received Portal root data",
|
|
"__Host-hivegrid_app_session",
|
|
"__Host-hivegrid_admin_session",
|
|
"crisp-client/",
|
|
"cookies.items.crisp",
|
|
"setCookieConsent",
|
|
"app_analytics",
|
|
"front.footer.termsAndConditions",
|
|
"PublicFooter",
|
|
],
|
|
};
|
|
|
|
const portalLocales = ["de", "en", "pl", "ru", "uk", "hu"];
|
|
const portalPublicFiles = new Set([
|
|
"favicon-32x32.png",
|
|
"fonts/instrument-sans-latin.woff2",
|
|
"fonts/instrument-sans-latin-ext.woff2",
|
|
...portalLocales.map((locale) => `locales/${locale}/translations.json`),
|
|
]);
|
|
|
|
export function verifyWebBuildSurfaces(rootDirectory) {
|
|
const core = readReactRouterManifest(rootDirectory, "build");
|
|
const portal = readReactRouterManifest(rootDirectory, "build-portal");
|
|
const coreIds = new Set(Object.keys(core.routes));
|
|
const portalIds = new Set(Object.keys(portal.routes));
|
|
|
|
const corePortalIds = [...coreIds].filter((id) => id.startsWith("routes/customer-portal/"));
|
|
if (corePortalIds.length > 0) {
|
|
throw new Error(`Core manifest contains Customer Portal routes: ${corePortalIds.join(", ")}`);
|
|
}
|
|
const unexpectedPortalIds = [...portalIds].filter((id) => !portalRouteIds.has(id));
|
|
const missingPortalIds = [...portalRouteIds].filter((id) => !portalIds.has(id));
|
|
if (unexpectedPortalIds.length > 0 || missingPortalIds.length > 0) {
|
|
throw new Error(
|
|
`Portal manifest route isolation failed (unexpected=${unexpectedPortalIds.join(",") || "none"}; missing=${missingPortalIds.join(",") || "none"})`
|
|
);
|
|
}
|
|
const portalLayout = portal.routes["routes/customer-portal/layout"];
|
|
if (portalLayout?.path !== ":tenant") {
|
|
throw new Error(`Portal manifest must expose the public :tenant path, received ${portalLayout?.path ?? "missing"}`);
|
|
}
|
|
const customHostLayout = portal.routes["routes/customer-portal/layout.__custom-host"];
|
|
if (customHostLayout?.path !== undefined || customHostLayout?.parentId !== "root") {
|
|
throw new Error("Portal manifest must expose the pathless custom-host layout directly below root");
|
|
}
|
|
const customHostChildren = [...portalIds].filter((id) => id.endsWith(".__custom-host") && id !== "routes/customer-portal/layout.__custom-host");
|
|
if (customHostChildren.some((id) => portal.routes[id]?.parentId !== "routes/customer-portal/layout.__custom-host")) {
|
|
throw new Error("Portal custom-host aliases must remain isolated below the pathless custom-host layout");
|
|
}
|
|
if (JSON.stringify(portal.routes).includes("__portal")) {
|
|
throw new Error("Portal manifest must not contain the trusted internal namespace");
|
|
}
|
|
assertNoOppositeSurfaceServerCode(rootDirectory, "build", "core");
|
|
assertNoOppositeSurfaceServerCode(rootDirectory, "build-portal", "portal");
|
|
assertNoOppositeSurfaceClientCode(rootDirectory, "build", "core");
|
|
assertNoOppositeSurfaceClientCode(rootDirectory, "build-portal", "portal");
|
|
assertPortalPublicArtifact(rootDirectory);
|
|
return { coreRouteCount: coreIds.size, portalRouteCount: portalIds.size };
|
|
}
|
|
|
|
function assertNoOppositeSurfaceServerCode(rootDirectory, buildDirectory, surface) {
|
|
const serverDirectory = path.join(rootDirectory, "apps", "web", buildDirectory, "server");
|
|
for (const filePath of listFiles(serverDirectory).filter((candidate) => candidate.endsWith(".js"))) {
|
|
const searchable = `${path.basename(filePath)}\n${fs.readFileSync(filePath, "utf8")}`;
|
|
const marker = forbiddenServerMarkers[surface].find((candidate) => searchable.includes(candidate));
|
|
if (marker) {
|
|
throw new Error(
|
|
`${surface === "portal" ? "Portal" : "Core"} server build contains opposite-surface code (${marker}) in ${path.relative(serverDirectory, filePath)}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertPortalPublicArtifact(rootDirectory) {
|
|
const clientDirectory = path.join(rootDirectory, "apps", "web", "build-portal", "client");
|
|
const unexpected = listFiles(clientDirectory)
|
|
.map((filePath) => path.relative(clientDirectory, filePath).split(path.sep).join("/"))
|
|
.filter((relativePath) => {
|
|
const identityPath = relativePath.replace(/\.(?:br|gz)$/u, "");
|
|
return !identityPath.startsWith("assets/") && !portalPublicFiles.has(identityPath);
|
|
});
|
|
if (unexpected.length > 0) {
|
|
throw new Error(`Portal public artifact contains Core/unapproved files: ${unexpected.join(", ")}`);
|
|
}
|
|
|
|
for (const locale of portalLocales) {
|
|
const relativePath = `locales/${locale}/translations.json`;
|
|
const filePath = path.join(clientDirectory, ...relativePath.split("/"));
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`Portal public artifact is missing ${relativePath}`);
|
|
}
|
|
const catalog = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
const keys = Object.keys(catalog);
|
|
if (keys.length !== 1 || keys[0] !== "customerPortal" || !catalog.customerPortal || typeof catalog.customerPortal !== "object") {
|
|
throw new Error(`Portal locale ${relativePath} must contain only the customerPortal catalog`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoOppositeSurfaceClientCode(rootDirectory, buildDirectory, surface) {
|
|
const clientDirectory = path.join(rootDirectory, "apps", "web", buildDirectory, "client");
|
|
for (const filePath of listFiles(clientDirectory).filter((candidate) => candidate.endsWith(".js"))) {
|
|
const searchable = `${path.basename(filePath)}\n${fs.readFileSync(filePath, "utf8")}`;
|
|
const marker = forbiddenClientMarkers[surface].find((candidate) => searchable.includes(candidate));
|
|
if (marker) {
|
|
throw new Error(
|
|
`${surface === "portal" ? "Portal" : "Core"} client build contains opposite-surface code (${marker}) in ${path.relative(clientDirectory, filePath)}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function listFiles(directory) {
|
|
const files = [];
|
|
const pending = [directory];
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
const entryPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) pending.push(entryPath);
|
|
else if (entry.isFile()) files.push(entryPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function readReactRouterManifest(rootDirectory, buildDirectory) {
|
|
const serverEntry = path.join(rootDirectory, "apps", "web", buildDirectory, "server", "index.js");
|
|
const assetsDirectory = path.join(rootDirectory, "apps", "web", buildDirectory, "client", "assets");
|
|
if (!fs.existsSync(serverEntry)) {
|
|
throw new Error(`Missing ${buildDirectory} server build`);
|
|
}
|
|
const manifestFiles = fs.readdirSync(assetsDirectory).filter((name) => /^manifest-.+\.js$/u.test(name));
|
|
if (manifestFiles.length !== 1) {
|
|
throw new Error(`Expected one ${buildDirectory} client route manifest, found ${manifestFiles.length}`);
|
|
}
|
|
const source = fs.readFileSync(path.join(assetsDirectory, manifestFiles[0]), "utf8");
|
|
const prefix = "window.__reactRouterManifest=";
|
|
if (!source.startsWith(prefix) || !source.endsWith(";")) {
|
|
throw new Error(`Invalid ${buildDirectory} client route manifest`);
|
|
}
|
|
return JSON.parse(source.slice(prefix.length, -1));
|
|
}
|
|
|
|
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (invokedDirectly) {
|
|
const rootDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const result = verifyWebBuildSurfaces(rootDirectory);
|
|
console.log(`PASS web build surfaces core_routes=${result.coreRouteCount} portal_routes=${result.portalRouteCount}`);
|
|
}
|