From 799a20b18eabde688bce6e187847a391b56218ae Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 20 Aug 2026 09:43:28 -0700 Subject: [PATCH 1/2] Serve Go vanity import paths from moq.dev `go get` resolves an import path by fetching it over HTTPS with ?go-get=1 and reading the repo out of a tag, so serving that tag here makes moq.dev own the published import path instead of whichever mirror repo the code lands in. The Go wrapper can then be imported as `moq.dev/moq` rather than `github.com/moq-dev/moq-go/moq`, which also means the identifier is `moq` with no alias, and a future mirror rename costs a line in the table rather than a breaking change for every consumer. Two paths are served, both inert until the matching go.mod switches its module path: moq.dev/moq -> github.com/moq-dev/moq-go moq.dev/moq-ffi -> github.com/moq-dev/moq-go-ffi A module owns every path beneath it, because the go command asks about the full import path (package directory included) and the tag has to answer with the module path regardless. Requests without ?go-get=1 are people, so they redirect to pkg.go.dev. `run_worker_first` needs the /moq* wildcard for the same reason #125 needed /api/*: an asset miss is answered with the 404 page rather than falling through, so the Worker would never run. Verified against `wrangler dev`, which reproduces that routing. Co-Authored-By: Claude Opus 5 --- worker/index.ts | 10 +++++- worker/vanity.test.ts | 40 ++++++++++++++++++++++ worker/vanity.ts | 77 +++++++++++++++++++++++++++++++++++++++++++ wrangler.jsonc | 10 +++--- 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 worker/vanity.test.ts create mode 100644 worker/vanity.ts diff --git a/worker/index.ts b/worker/index.ts index 4f302e6..293e500 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -1,5 +1,8 @@ // Cloudflare Worker entry. Static asset requests fall through to the ASSETS -// binding (Workers-with-Static-Assets). Only /api/* is handled here. +// binding (Workers-with-Static-Assets). Only /api/*, the de.moq.dev rewrite, +// and the Go vanity import paths are handled here. + +import { vanity } from "./vanity"; interface Env { ASSETS: { fetch: (request: Request) => Promise }; @@ -20,6 +23,11 @@ export default { return handleSubscribe(request, env); } + // `go get moq.dev/moq` and friends, which are served from a mirror repo + // rather than by this site. + const module = vanity(url); + if (module) return module; + // de.moq.dev is the DEMOQED page, which lives at /de in the static build. const alreadyRewritten = url.pathname === "/de" || url.pathname.startsWith("/de/"); if (url.hostname.split(".")[0] === "de" && !alreadyRewritten) { diff --git a/worker/vanity.test.ts b/worker/vanity.test.ts new file mode 100644 index 0000000..d7bf07f --- /dev/null +++ b/worker/vanity.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test"; +import { vanity } from "./vanity"; + +const goGet = (path: string) => vanity(new URL(`https://moq.dev${path}?go-get=1`)); + +describe("vanity", () => { + test("points the go command at the mirror", async () => { + const res = goGet("/moq"); + expect(res?.status).toBe(200); + expect(await res?.text()).toContain( + ``, + ); + }); + + test("claims a package directory for its module", async () => { + // The go command asks about the full import path, so the prefix in the tag + // has to stay the module path rather than what was requested. + const res = goGet("/moq-ffi/moq"); + expect(await res?.text()).toContain( + ``, + ); + }); + + test("keeps modules with a shared prefix apart", async () => { + expect(await goGet("/moq-ffi")?.text()).toContain("moq-go-ffi"); + expect(await goGet("/moq")?.text()).not.toContain("moq-go-ffi"); + }); + + test("sends people to the docs", () => { + const res = vanity(new URL("https://moq.dev/moq/subpkg")); + expect(res?.status).toBe(302); + expect(res?.headers.get("location")).toBe("https://pkg.go.dev/moq.dev/moq/subpkg"); + }); + + test("ignores everything else", () => { + for (const path of ["/", "/blog", "/moqadillo", "/de/moq"]) { + expect(vanity(new URL(`https://moq.dev${path}?go-get=1`))).toBeUndefined(); + } + }); +}); diff --git a/worker/vanity.ts b/worker/vanity.ts new file mode 100644 index 0000000..28ecfb4 --- /dev/null +++ b/worker/vanity.ts @@ -0,0 +1,77 @@ +/** + * Vanity import paths for the Go modules, so a program imports `moq.dev/moq` + * rather than whichever mirror repo the code happens to live in. + * + * The go command fetches `?go-get=1` and reads the repo to clone + * out of a `` tag, so this hostname owns the published + * import path. Moving or renaming a mirror then costs a line in the table below + * instead of a breaking change for everyone who imported it. + */ + +/** A published module: the path it answers to, and the repo behind it. */ +interface Module { + /** Path on this host, which is also the module path in its `go.mod`. */ + dir: string; + /** Repo the go command clones. */ + repo: string; +} + +/** + * The host the modules are published under. + * + * Hard-coded rather than read off the request: the tag has to name the module + * path in `go.mod`, so staging serves the same `moq.dev/...` prefix it will + * serve in production (and `go get new.moq.dev/moq` correctly refuses). + */ +const HOST = "moq.dev"; + +/** + * Every module published under HOST. + * + * Paths have to start with `/moq` to match the `run_worker_first` wildcard in + * wrangler.jsonc. Without that, an asset miss answers with the 404 page and the + * go command never reaches this Worker at all. + */ +const MODULES: Module[] = [ + { dir: "/moq", repo: "https://github.com/moq-dev/moq-go" }, + { dir: "/moq-ffi", repo: "https://github.com/moq-dev/moq-go-ffi" }, +]; + +/** The go-get response for a vanity path, or undefined to serve the request normally. */ +export function vanity(url: URL): Response | undefined { + // The go command asks about the full import path, package directory and all, + // so a module owns everything beneath it. + const module = MODULES.find((m) => url.pathname === m.dir || url.pathname.startsWith(`${m.dir}/`)); + if (!module) return undefined; + + // Only the go command wants the meta tag; send people to the docs. + if (url.searchParams.get("go-get") !== "1") { + return Response.redirect(`https://pkg.go.dev/${HOST}${url.pathname}`, 302); + } + + return new Response(page(module), { + headers: { + "content-type": "text/html; charset=utf-8", + "cache-control": "public, max-age=3600", + }, + }); +} + +function page(module: Module): string { + const path = HOST + module.dir; + + // go-import is the one the go command needs. go-source is what pkg.go.dev + // follows to link a symbol back to the line that declares it. + return ` + + + + + + + + ${path} is served from ${module.repo}. + + +`; +} diff --git a/wrangler.jsonc b/wrangler.jsonc index bd5e2e9..92b58c0 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -15,10 +15,12 @@ "binding": "ASSETS", // Static assets are normally served before the Worker ever runs, which - // would skip the de.moq.dev -> /de rewrite for "/" and swallow POSTs to - // /api/* with a bare 405. Route those through the Worker; everything else - // still short-circuits to assets. - "run_worker_first": ["/", "/api/*"] + // would skip the de.moq.dev -> /de rewrite for "/", swallow POSTs to + // /api/* with a bare 405, and answer the Go vanity paths (worker/vanity.ts) + // with the 404 page, since a miss lands there rather than falling through. + // The /moq* wildcard covers every module, so adding one to that table + // doesn't mean editing this list. Everything else short-circuits to assets. + "run_worker_first": ["/", "/api/*", "/moq*"] }, // Environment-specific configurations From e759b43a4ab59b9ff929389d68a6953a51319ecd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 20 Aug 2026 11:02:41 -0700 Subject: [PATCH 2/2] Keep de.moq.dev out of the vanity paths, and run the tests in CI Review findings from Codex. The vanity lookup ran ahead of the de.moq.dev -> /de rewrite and never looked at the hostname, so DEMOQED lost every URL beginning with /moq: de.moq.dev/moq redirected to pkg.go.dev instead of resolving against /de. Doing the rewrite first fixes it, since that branch returns, and it means the import paths only ever answer for the host they're published under. The tests only reached vanity() directly, which is the half that can't break the site. worker/index.test.ts drives the Worker's fetch handler against a stub ASSETS binding instead, so the ordering above is pinned by a test that fails without it. That still doesn't cover `run_worker_first` -- only Cloudflare routes that -- so index.ts says out loud that the Worker doesn't see a request unless wrangler.jsonc lists it. None of this ran in CI: the workflow calls `bun run check` (biome + tsc), which never runs `bun test`, so these assertions and the existing broadcast tests were all decorative. Co-Authored-By: Claude Opus 5 --- .github/workflows/pr.yml | 1 + worker/index.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++ worker/index.ts | 15 ++++++++---- 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 worker/index.test.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 648fcbc..47a0a67 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,3 +16,4 @@ jobs: - run: bun install --frozen-lockfile - run: bun run check + - run: bun test diff --git a/worker/index.test.ts b/worker/index.test.ts new file mode 100644 index 0000000..549e71d --- /dev/null +++ b/worker/index.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test"; +import worker from "./index"; + +/** A stand-in for the static assets, recording what the Worker asked it for. */ +const assets = () => { + const seen: string[] = []; + return { + seen, + ASSETS: { + fetch: async (request: Request) => { + const { pathname } = new URL(request.url); + seen.push(pathname); + return new Response(pathname); + }, + }, + }; +}; + +type Env = Parameters[1]; + +const send = (url: string, env: ReturnType, init?: RequestInit) => + worker.fetch(new Request(url, init), env as unknown as Env); + +describe("fetch", () => { + test("serves a vanity import path", async () => { + const env = assets(); + const res = await send("https://moq.dev/moq?go-get=1", env); + expect(await res.text()).toContain('content="moq.dev/moq git https://github.com/moq-dev/moq-go"'); + expect(env.seen).toEqual([]); + }); + + test("leaves de.moq.dev its own namespace", async () => { + // The rewrite has to win over the vanity paths, or DEMOQED loses every URL + // starting with /moq to a pkg.go.dev redirect. + const env = assets(); + const res = await send("https://de.moq.dev/moq", env); + expect(res.status).toBe(200); + expect(env.seen).toEqual(["/de/moq"]); + }); + + test("passes anything else to the assets", async () => { + const env = assets(); + await send("https://moq.dev/blog/", env); + expect(env.seen).toEqual(["/blog/"]); + }); + + test("rejects a non-POST subscribe", async () => { + const env = assets(); + expect((await send("https://moq.dev/api/subscribe", env)).status).toBe(405); + expect(env.seen).toEqual([]); + }); +}); diff --git a/worker/index.ts b/worker/index.ts index 293e500..b636e2e 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -1,6 +1,10 @@ // Cloudflare Worker entry. Static asset requests fall through to the ASSETS // binding (Workers-with-Static-Assets). Only /api/*, the de.moq.dev rewrite, // and the Go vanity import paths are handled here. +// +// Note that the Worker only sees a request if wrangler.jsonc says so: an asset +// miss is answered with the 404 page rather than falling through, so every path +// below has to be listed in `run_worker_first`. import { vanity } from "./vanity"; @@ -23,11 +27,6 @@ export default { return handleSubscribe(request, env); } - // `go get moq.dev/moq` and friends, which are served from a mirror repo - // rather than by this site. - const module = vanity(url); - if (module) return module; - // de.moq.dev is the DEMOQED page, which lives at /de in the static build. const alreadyRewritten = url.pathname === "/de" || url.pathname.startsWith("/de/"); if (url.hostname.split(".")[0] === "de" && !alreadyRewritten) { @@ -36,6 +35,12 @@ export default { return env.ASSETS.fetch(new Request(rewritten, request)); } + // `go get moq.dev/moq` and friends, served from a mirror repo rather than + // by this site. After the rewrite above, so it only ever answers for the + // host the import paths are published under. + const module = vanity(url); + if (module) return module; + return env.ASSETS.fetch(request); }, };