Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ jobs:

- run: bun install --frozen-lockfile
- run: bun run check
- run: bun test
52 changes: 52 additions & 0 deletions worker/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof worker.fetch>[1];

const send = (url: string, env: ReturnType<typeof assets>, 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([]);
});
});
15 changes: 14 additions & 1 deletion worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// 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.
//
// 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";

interface Env {
ASSETS: { fetch: (request: Request) => Promise<Response> };
Expand Down Expand Up @@ -28,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);
},
};
Expand Down
40 changes: 40 additions & 0 deletions worker/vanity.test.ts
Original file line number Diff line number Diff line change
@@ -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(
`<meta name="go-import" content="moq.dev/moq git https://github.com/moq-dev/moq-go">`,
);
});

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(
`<meta name="go-import" content="moq.dev/moq-ffi git https://github.com/moq-dev/moq-go-ffi">`,
);
});

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();
}
});
});
77 changes: 77 additions & 0 deletions worker/vanity.ts
Original file line number Diff line number Diff line change
@@ -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 `<import path>?go-get=1` and reads the repo to clone
* out of a `<meta name="go-import">` 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 `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="go-import" content="${path} git ${module.repo}">
<meta name="go-source" content="${path} ${module.repo} ${module.repo}/tree/main{/dir} ${module.repo}/blob/main{/dir}/{file}#L{line}">
</head>
<body>
<a href="https://pkg.go.dev/${path}">${path}</a> is served from <a href="${module.repo}">${module.repo}</a>.
</body>
</html>
`;
}
10 changes: 6 additions & 4 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading