[EmDash] A Vitest Regression Guard for publicHoistPattern and ssr.optimizeDeps.include on Cloudflare Workers
In a previous post I described two interlinked issues that cause `exports is not defined` crashes in Cloudflare workerd when running an Astro + EmDash + Vite dev server: a CJS package (`@atproto/api`) reaching workerd without going through the SSR optimizer's CJS→ESM conversion, and the underlying reason it escaped — pnpm hadn't hoisted it where Vite could see it.
That fix works. But it's silent. Nothing stops a future change from quietly reversing it — removing the hoisting pattern, dropping the include entry, or adding a new CJS transitive dep without doing either.
The Stack
- Astro 6.3.3
- Vite 7.3.3 (pinned via `pnpm.overrides`)
- EmDash CMS 0.10.0 + `@emdash-cms/plugin-embeds` 0.1.10
- `@astrojs/cloudflare` adapter with `remoteBindings: true`
- `@cloudflare/vite-plugin` 1.37.0
- `pnpm` 10.x
- Vitest 4.1.6
What the Test Checks
The SSR optimizer can only pre-bundle a package correctly if two prerequisites are satisfied: the package is hoisted to the project root and the package is resolvable through Node's resolution algorithm.
- The package is hoisted. Without a root-level symlink under `node_modules/<name>`, Vite cannot resolve the dependency during SSR optimization.
- The package is resolvable. Even with a symlink, its exports map or `main` field must still point to a real file Node can load.
// src/lib/ssr-deps.test.ts
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const SSR_INCLUDE = [
"emdash/middleware",
"emdash/middleware/auth",
"emdash/middleware/redirect",
"emdash/middleware/request-context",
"emdash/middleware/setup",
"emdash/runtime",
"emdash/ui",
"emdash/media/local-runtime",
"@emdash-cms/cloudflare/db/d1",
"@emdash-cms/cloudflare/storage/r2",
"@emdash-cms/plugin-embeds",
"@emdash-cms/plugin-embeds/astro",
"@atproto/api",
"astro/zod",
];
const req = createRequire(import.meta.url);
const root = process.cwd();
function packageName(specifier: string): string {
const parts = specifier.split("/");
return specifier.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
}
describe("ssr.optimizeDeps.include prerequisites", () => {
for (const pkg of SSR_INCLUDE) {
it(`${pkg} is hoisted and resolvable`, () => {
const name = packageName(pkg);
expect(
existsSync(join(root, "node_modules", name)),
).toBe(true);
expect(
() => req.resolve(pkg),
).not.toThrow();
});
}
}); What a failure looks like
❌ @atproto/api is hoisted and resolvable
AssertionError: @atproto/api is not hoisted to node_modules/ — add "@atproto/*"
to publicHoistPattern in pnpm-workspace.yaml and re-run pnpm install Compared to the opaque workerd runtime crash, the Vitest failure clearly identifies the package, explains the root cause, and points directly to the configuration that needs to change.
What It Doesn't Catch
1. New CJS packages entering the import chain
The test only validates packages already listed in `SSR_INCLUDE`. If a new transitive CJS package appears behind a virtual module or plugin-generated import chain, it can still bypass the optimizer and crash workerd at runtime.
2. The `SSR_INCLUDE` list can drift
The list in the test file is a manual copy of `vite.ssr.optimizeDeps.include`. If one changes without the other, the regression guard becomes incomplete. A shared constant would eliminate the duplication, but Astro's config loader complicates direct TypeScript imports from `astro.config.mjs`.
The Maintenance Checklist
When a new CJS transitive dependency needs SSR pre-bundling, three files must change together:
- `pnpm-workspace.yaml` → add the package scope to `publicHoistPattern`
- `astro.config.mjs` → add the package to `vite.ssr.optimizeDeps.include`
- `src/lib/ssr-deps.test.ts` → add the same specifier to `SSR_INCLUDE`
Why Not Just Run `pnpm dev:ui`?
You could rely on the dev server to expose regressions, but the failure mode is poor: opaque workerd stack traces, partial route breakage, and delayed discovery. Running the regression guard in `pnpm test` surfaces known failure modes earlier and more clearly, especially in CI.