diff --git a/Dockerfile.worker b/Dockerfile.worker index 26c1499d9..f35253400 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -35,4 +35,10 @@ COPY . . USER node # Long-poll worker; WORKER_* env vars control claim batch size, concurrency, # and the stale-claim window (see src/lib/env.ts). -CMD ["node", "node_modules/tsx/dist/cli.mjs", "worker/index.ts"] +# +# Route through scripts/run-tsx.mjs, NOT bare tsx: worker/index.ts imports +# src/lib/env, which begins with `import "server-only"`. Under bare tsx that +# import throws and the worker crash-loops on boot. run-tsx.mjs registers the +# server-only stub hook so the import resolves outside the Next bundler. +# Guarded by tests/tsx-server-only-runner.test.ts. +CMD ["node", "scripts/run-tsx.mjs", "worker/index.ts"] diff --git a/tests/tsx-server-only-runner.test.ts b/tests/tsx-server-only-runner.test.ts index 78b164f07..3f6d058e1 100644 --- a/tests/tsx-server-only-runner.test.ts +++ b/tests/tsx-server-only-runner.test.ts @@ -16,6 +16,21 @@ describe("standalone TSX server-only compatibility", () => { expect(packageJson.scripts["check:supabase-project"]).toContain("scripts/run-tsx.mjs"); }); + it("boots the worker image through the server-only-aware runner, not bare tsx", () => { + const dockerfile = readFileSync(new URL("../Dockerfile.worker", import.meta.url), "utf8"); + const cmdLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("CMD")); + expect(cmdLine, "Dockerfile.worker must define a CMD").toBeDefined(); + // worker/index.ts imports src/lib/env, which is `import "server-only"`. + // Bare tsx throws on that import at boot; the entrypoint must route through + // scripts/run-tsx.mjs, which registers the server-only stub hook. Assert the + // exact exec-form vector so no bare-tsx variant (["tsx", …], ["npx","tsx", …], + // tsx/dist/cli.mjs, or a reordered command) can slip through. + const bracket = cmdLine!.indexOf("["); + expect(bracket, "worker CMD must use JSON exec form").toBeGreaterThan(-1); + const execVector = JSON.parse(cmdLine!.slice(bracket)) as string[]; + expect(execVector).toEqual(["node", "scripts/run-tsx.mjs", "worker/index.ts"]); + }); + it("keeps the Next server-only marker while stubbing it only for standalone runners", () => { expect(readFileSync(new URL("../src/lib/env.ts", import.meta.url), "utf8")).toMatch(/^import ["']server-only["'];/); expect(readFileSync(new URL("../scripts/register-server-only.mjs", import.meta.url), "utf8")).toContain(