Files
bootnode/console/lib/routes.test.ts
zeekayandHanzo Dev 7d293212cb refactor: web/ is now console/ — bootnode/bootnode is the backend, bootnode/console the frontend
Names the two halves for what they are. The repo already holds both (12407e9 merged
bootnode/bootnode into this history); this makes the frontend directory say so, so the layout
reads the way the owner describes the system: backend and frontend separate, one repo.

git mv throughout — 187 renames, history preserved. Every path reference follows: hanzo.yml
build context and dockerfile, compose.yml, compose.prod.yml, start.sh, LLM.md,
infra/k8s/web-deployments.yaml, infra/cloud/deploy.sh, and the Dockerfile's own COPY paths.
Image and container names (bootnode-web, the -web tag suffix) are deliberately NOT renamed here:
those are a deploy contract the App CR and ingress bind to on port 3001, and breaking them is a
separate, staged change.

Console builds green after the move; the Go tree builds.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 17:47:51 -07:00

123 lines
5.4 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("does not add dead links to page bodies", () => {
// 36 dead links predate this work — the docs index and the product pages
// link to a documentation tree (/docs/node/*, /docs/wallets/*, /docs/sdk/*
// …) that was never built. Fixing them means either writing those pages or
// pointing them elsewhere: a content decision, not a shell one.
//
// So this is a RATCHET, not a clean-zero assertion. The debt is counted and
// may only go down. A test that fails on the day it is written gets
// deleted; one that stops the bleeding gets kept.
const KNOWN_DEAD = 36
const dead = [...new Set(internalLinks().filter((l) => !SHELL.includes(l.file) && isDead(l.href)).map((l) => l.href))]
assert.ok(
dead.length <= KNOWN_DEAD,
`dead internal links rose from ${KNOWN_DEAD} to ${dead.length}. New:\n${dead.join("\n")}`
)
})
})