Files
tech-log-frontend/scripts/lib/tech-log-serving-contract.ts
DongHyeonka ab8c6c14db fix: derive the Studio serving patterns from the route contract
`/studio/releases` answered a plain-text 404 from nginx. The route existed, the
chunk was built, and the SPA could reach the screen by client-side navigation —
but a hard load or a reload never got that far, because the web server had
never been told the path exists.

The public half of the serving contract derives its patterns from the route
registry. The Studio half was a hand-maintained array, and it failed the way
hand-maintained arrays fail: the comment above `^/studio/assets$` records that
exact bug being fixed once already, and adding a route repeated it immediately.
Both halves now come from the same source, so a Studio route that exists is
served without anyone having to remember.

Deriving them yields one pattern per route rather than the old alternation that
folded the four document sub-screens together. Same matched set, and it no
longer needs a human to keep the grouping honest.
2026-08-21 03:29:47 +09:00

96 lines
3.3 KiB
TypeScript

export type TechLogServingContract = Readonly<{
schemaVersion: 2;
/**
* Patterns, not an enumeration.
*
* This used to list every public path the bundled fixture happened to
* contain, and the generated nginx served exactly those. A record published
* after the build — which is the entire point of a backend — answered 404 at
* the edge before the SPA was ever asked, and no amount of correct routing
* inside the bundle could recover it.
*
* The route contract already declares which paths exist; the catalog only
* decides which of them currently resolve, and that is the SPA's call, not
* the web server's. Studio has been pattern-based all along — this brings the
* public half to the same footing.
*/
publicSpaPathPatterns: readonly string[];
studioPathPrefix: "/studio";
studioSpaPathPatterns: readonly string[];
notFound: Readonly<{
status: 404;
contentType: "text/plain;charset=UTF-8";
body: "Not Found";
}>;
}>;
type ServingContractInput = Readonly<{
/**
* The public route templates the router registers, in route-contract form
* (`/cases/:slug`). Passed in rather than imported so this module stays a
* pure transform the tests can drive directly.
*/
publicRoutePaths: readonly string[];
/** The Studio route templates, same form and same reason. */
studioRoutePaths: readonly string[];
}>;
/**
* `/cases/:slug` -> `^/cases/[^/]+$`. A parameter matches one segment and never
* a slash, which is what keeps `/cases/a/b` a 404 instead of a case page.
*/
function patternOf(routePath: string): string {
const escaped = routePath
.split("/")
.map((segment) =>
segment.startsWith(":")
? "[^/]+"
: segment.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"),
)
.join("/");
return `^${escaped === "" ? "/" : escaped}$`;
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
/**
* The catch-all is the SPA's own not-found screen; serving index.html for every
* unmatched URL would turn the edge 404 into a soft 200 and hide broken links
* from crawlers and from us.
*/
function patternsFor(routePaths: readonly string[]): readonly string[] {
const patterns = new Set<string>();
for (const routePath of routePaths) {
if (routePath === "*" || routePath.includes("*")) continue;
patterns.add(patternOf(routePath));
}
return Object.freeze([...patterns].sort(asciiCompare));
}
export function createTechLogServingContract({
publicRoutePaths,
studioRoutePaths,
}: ServingContractInput): TechLogServingContract {
return Object.freeze({
schemaVersion: 2,
publicSpaPathPatterns: patternsFor(publicRoutePaths),
studioPathPrefix: "/studio",
// Derived, not listed. This was a hand-maintained array, and it went stale
// exactly the way a hand-maintained array does: /studio/assets was missing
// for its whole life, and /studio/releases repeated the mistake the moment
// it was added — the route worked by client-side navigation and 404'd on
// reload, because nginx had never heard of it. The route contract already
// knows which Studio paths exist, so ask it.
studioSpaPathPatterns: patternsFor(studioRoutePaths),
notFound: Object.freeze({
status: 404,
contentType: "text/plain;charset=UTF-8",
body: "Not Found",
}),
});
}