mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Hanzo CI/CD / cicd (push) Failing after 1h10m29s
CI/CD / cicd (push) Failing after 2h37m43s
A static export has no host when it prerenders, so BrandProvider renders the
tree only once the browser has resolved the host, pages that show the brand
are client components, the static head names no brand, and the title and
favicon come from the host. The chain page keeps its static params and
metadata on the server and renders its body on the client, with RPC endpoints
on the brand's API origin at /v1/rpc/{chain}/{network}.
Hanzo's API origin is web3.hanzo.ai itself. The node fleet view, its landing
strip and its nav link belong to the brand that operates a fleet, which is
Lux alone. llms.txt, one file for every host, names no brand and points at
each host's own /docs/api.
Marketing copy states no certification the company does not hold and no
unmeasured uptime, latency, volume or response-time figure. Every internal
link points at a page that exists, every catalog chain is prerendered, and
the routes test holds page bodies to zero dead links.
Co-authored-by: Hanzo Dev <[email protected]>
113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
// Every internal link in the app must point at a page that exists.
|
|
//
|
|
// Run: `pnpm test`.
|
|
//
|
|
// This is a whole-source check rather than a list someone maintains: the route
|
|
// set is derived by walking app/ for page.tsx / route.ts, and the link set by
|
|
// scanning every component and page for `href="/…"`. So it cannot go stale,
|
|
// and a page deleted or renamed fails the build step that runs it.
|
|
//
|
|
// It exists because two dead links were shipping and nothing noticed:
|
|
// • the header's "Products" pointed at /products, which is a DIRECTORY of
|
|
// product pages with no index — the static server answers it with the SPA
|
|
// fallback, i.e. the home page rendered under a /products URL.
|
|
// • the footer's "Tutorials" pointed at /docs/tutorials, a plain 404.
|
|
|
|
import { strict as assert } from "node:assert"
|
|
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
import { join, relative } from "node:path"
|
|
import { describe, it } from "node:test"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
const WEB = join(fileURLToPath(new URL(".", import.meta.url)), "..")
|
|
const APP = join(WEB, "app")
|
|
|
|
function walk(dir: string, out: string[] = []): string[] {
|
|
for (const entry of readdirSync(dir)) {
|
|
const full = join(dir, entry)
|
|
if (statSync(full).isDirectory()) walk(full, out)
|
|
else out.push(full)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Routes the App Router actually emits, derived from the filesystem. */
|
|
function builtRoutes(): { staticRoutes: Set<string>; dynamicPrefixes: string[] } {
|
|
const staticRoutes = new Set<string>()
|
|
const dynamicPrefixes: string[] = []
|
|
for (const file of walk(APP)) {
|
|
const name = file.split("/").pop() ?? ""
|
|
if (name !== "page.tsx" && name !== "route.ts") continue
|
|
const segments = relative(APP, file).split("/").slice(0, -1)
|
|
// Route groups — (marketing) — are not URL segments.
|
|
const url = "/" + segments.filter((s) => !s.startsWith("(")).join("/")
|
|
if (segments.some((s) => s.startsWith("["))) {
|
|
dynamicPrefixes.push(url.slice(0, url.indexOf("/[")))
|
|
} else {
|
|
staticRoutes.add(url === "/" ? "/" : url.replace(/\/$/, ""))
|
|
}
|
|
}
|
|
return { staticRoutes, dynamicPrefixes }
|
|
}
|
|
|
|
/** Every internal href literal in the app's own source. */
|
|
function internalLinks(): { href: string; file: string }[] {
|
|
const found: { href: string; file: string }[] = []
|
|
for (const dir of [APP, join(WEB, "components"), join(WEB, "lib")]) {
|
|
for (const file of walk(dir)) {
|
|
if (!/\.tsx?$/.test(file) || file.endsWith(".test.ts")) continue
|
|
const src = readFileSync(file, "utf8")
|
|
for (const m of src.matchAll(/href=["'](\/[^"'{}#?]*)["']/g)) {
|
|
found.push({ href: m[1], file: relative(WEB, file) })
|
|
}
|
|
// The nav data carries hrefs as plain object fields, not JSX attributes.
|
|
for (const m of src.matchAll(/href:\s*["'](\/[^"'{}#?]*)["']/g)) {
|
|
found.push({ href: m[1], file: relative(WEB, file) })
|
|
}
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
describe("internal links", () => {
|
|
const { staticRoutes, dynamicPrefixes } = builtRoutes()
|
|
|
|
it("derives a sane route set from the filesystem", () => {
|
|
// Sanity: if the walk broke, every other assertion here would pass vacuously.
|
|
assert.ok(staticRoutes.size > 50, `only found ${staticRoutes.size} routes`)
|
|
for (const expected of ["/", "/dashboard", "/pricing", "/login", "/products/node"]) {
|
|
assert.ok(staticRoutes.has(expected), `expected route ${expected} to exist`)
|
|
}
|
|
// /products is a directory of pages with NO page.tsx of its own.
|
|
assert.ok(!staticRoutes.has("/products"), "/products unexpectedly has a page")
|
|
})
|
|
|
|
const isDead = (href: string) => {
|
|
const path = href.length > 1 ? href.replace(/\/+$/, "") : href
|
|
if (staticRoutes.has(path)) return false
|
|
// A dynamic route: /chains/[slug] serves /chains/anything.
|
|
if (dynamicPrefixes.some((p) => p && path.startsWith(p + "/"))) return false
|
|
// Static files served straight out of public/.
|
|
if (/\.(svg|png|jpg|json|txt|ico|webp|mp4)$/.test(path)) return false
|
|
return true
|
|
}
|
|
|
|
// The chrome every page renders: header, footer, console nav. A dead link
|
|
// here is dead on all 67 pages at once, so this is held to zero.
|
|
const SHELL = ["components/navbar.tsx", "components/footer.tsx", "components/console-sidebar.tsx", "lib/dashboard-navigation.ts"]
|
|
|
|
it("has zero dead links in the shell", () => {
|
|
const dead = internalLinks()
|
|
.filter((l) => SHELL.includes(l.file) && isDead(l.href))
|
|
.map((l) => `${l.href} (${l.file})`)
|
|
assert.deepEqual([...new Set(dead)].sort(), [], `dead links in the shell:\n${dead.join("\n")}`)
|
|
})
|
|
|
|
it("has zero dead links in page bodies", () => {
|
|
const dead = internalLinks()
|
|
.filter((l) => !SHELL.includes(l.file) && isDead(l.href))
|
|
.map((l) => `${l.href} (${l.file})`)
|
|
assert.deepEqual([...new Set(dead)].sort(), [], `dead links in page bodies:\n${dead.join("\n")}`)
|
|
})
|
|
})
|