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
3 changes: 2 additions & 1 deletion apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"check:export": "node scripts/check-export.mjs",
"deploy": "wrangler deploy",
"deploy:dry": "wrangler deploy --dry-run",
"gen:brand-mail": "node scripts/gen-brand-mail-assets.mjs"
"gen:brand-mail": "node scripts/gen-brand-mail-assets.mjs",
"dev:worker": "next build && wrangler dev --port 3002 --ip 127.0.0.1 --inspector-port 4002"
},
"dependencies": {
"@webhook-co/shared": "workspace:*",
Expand Down
19 changes: 18 additions & 1 deletion docs/local-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,31 @@ fast loop and buy nothing.
## Marketing site: two routes its worker adds

`apps/www` runs under `next dev` for a fast content loop, but its wrangler `main` is a custom worker
(`worker/index.ts`). `next dev` does not run it, so locally you do not get:
(`worker/index.ts`). `next dev` does not run it, so **by default** you do not get:

- the cookieless aggregate page-view write to Analytics Engine
- the MTA-STS policy response

Neither blocks content work, which is why www keeps the fast loop. `apps/auth` made the opposite trade —
see below.

**They are now one command away rather than unreachable:**

```
pnpm --filter @webhook-co/www dev:worker
```

which builds the static export and serves it through the real Worker on the same port. Measured, same
request both ways:

```
next dev → GET /.well-known/mta-sts.txt (Host: mta-sts.webhook.co) 404
dev:worker → the same request 200, mode: enforce
```

"Not the default" and "impossible" are different things, and only the first is a trade-off somebody chose.
The build is why this cannot be the default: the Worker serves `./out`, so every change needs a rebuild.

---

## What matches prod by default
Expand Down
49 changes: 47 additions & 2 deletions scripts/dev-ports-guard.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import { test } from "node:test";
import { SERVICE_BINDINGS } from "./wrangler-services.mjs";

import {
CRON_APPS,
DEV_APPS,
LOCAL_INGEST_BASE_URL,
devCommand,
devFastCommand,
devWorkerCommand,
duplicateAssignments,
duplicatePorts,
inspectorPortFor,
portAssignments,
CRON_APPS,
devFastCommand,
} from "./dev-ports.mjs";
import {
appsMissingDevScript,
Expand Down Expand Up @@ -307,3 +308,47 @@ test("web's committed dev scripts match the derived commands", () => {
// The opt-in name is gone: leaving `dev:bindings` behind would imply bindings are still the exception.
assert.equal(pkg.scripts["dev:bindings"], undefined);
});

// --- Exercising a Worker that the fast loop skips ------------------------------------------------
// `apps/www` runs under `next dev`, which never loads its wrangler `main`, so the two routes that Worker
// adds — the cookieless page-view write and the MTA-STS policy response — do not exist locally AT ALL.
// The fast loop is the right default for a content site, but "not the default" and "impossible" are
// different things, and only the first is a trade-off. `dev:worker` builds the static export and serves it
// through the real Worker, so the gap is one command away instead of unreachable.
test("an app whose committed Worker its default skips can still run it on demand", () => {
const cmd = devWorkerCommand("www");
assert.ok(cmd, "www's worker routes are unreachable locally with no way to exercise them");
assert.match(
cmd,
/next build/,
"the Worker serves ./out — without a build it has nothing to serve",
);
assert.match(
cmd,
/wrangler dev/,
"it must be wrangler, not next dev — next dev cannot load a main",
);
assert.match(cmd, new RegExp(`--port ${DEV_APPS.www.port}\\b`));
assert.ok(
cmd.indexOf("next build") < cmd.indexOf("wrangler dev"),
"building after serving would serve a stale ./out",
);
});

test("it is offered ONLY where the default actually skips the Worker", () => {
// web's default is the OpenNext preview, which runs its worker already; a second name for that would
// read as a capability. A wrangler app runs its main by definition.
assert.equal(devWorkerCommand("web"), null);
assert.equal(devWorkerCommand("engine"), null);
assert.equal(devWorkerCommand("auth"), null);
});

test("www's committed dev:worker script matches the derived command", () => {
const pkg = JSON.parse(
readFileSync(new URL("../apps/www/package.json", import.meta.url), "utf8"),
);
assert.equal(pkg.scripts["dev:worker"], devWorkerCommand("www"));
// …and the DEFAULT stays the fast loop. Changing that is a deliberate call, not a silent drift.
assert.equal(pkg.scripts.dev, devCommand("www"));
assert.match(pkg.scripts.dev, /next dev/);
});
25 changes: 25 additions & 0 deletions scripts/dev-ports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ export function duplicatePorts() {
* would read as a capability rather than what it is. Use it for pure UI work, where no service binding is
* in the path; anything touching login, endpoints, or delivery wants the default.
*/
/**
* Run a Next app's committed Worker on demand, for the one app whose default skips it.
*
* `next dev` never loads a wrangler `main`, so for `apps/www` the two routes its Worker adds — the
* cookieless page-view write to Analytics Engine and the MTA-STS policy response — do not exist locally at
* all. Keeping the fast loop as the default is the right call for a content site; leaving those routes
* UNREACHABLE is not. "Not the default" and "impossible" are different things, and only the first is a
* trade-off somebody chose.
*
* The Worker serves the static export through its ASSETS binding, so the build has to happen first — a
* `wrangler dev` against a stale or absent `./out` would serve yesterday's site and look like it worked.
*
* Returns null where the default ALREADY runs the Worker: web's default is the OpenNext preview, and a
* wrangler app runs its main by definition. A second name for the same thing reads as a capability.
*/
export function devWorkerCommand(app) {
const spec = DEV_APPS[app];
if (!spec) throw new Error(`dev-ports: unknown app "${app}"`);
if (spec.kind !== "next" || SERVICE_BINDINGS[app]) return null;
return (
`next build && wrangler dev --port ${spec.port} --ip 127.0.0.1 ` +
`--inspector-port ${spec.port + INSPECTOR_OFFSET}`
);
}

export function devFastCommand(app) {
const spec = DEV_APPS[app];
if (!spec) throw new Error(`dev-ports: unknown app "${app}"`);
Expand Down
Loading