mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
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]>
123 lines
5.4 KiB
TypeScript
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")}`
|
|
)
|
|
})
|
|
})
|