[EmDash] Why @atproto/api Caused exports is not defined in workerd and How pnpm Hoisting + ssr.optimizeDeps.include Fixes It
This post details a runtime crash on this site that took a while to fix. Every page that rendered a CMS rich-text field returned many `exports is not defined` error messages. No hint in the error about which package was at fault. Hopefully this will be helpful for others in the future.
The Stack
- Astro 6.3.3
- Vite 7.3.3 (pinned via `pnpm.overrides` — Astro 6's bundled rolldown-vite breaks `@cloudflare/vite-plugin`)
- EmDash CMS 0.10.0 (`emdash` + `@emdash-cms/cloudflare` + `@emdash-cms/plugin-embeds` 0.1.10)
- `@astrojs/cloudflare` adapter with `remoteBindings: true`
- `@cloudflare/vite-plugin` 1.37.0
- `pnpm` 10.x
- Node.js 22 (host), Cloudflare workerd (runtime inside the Vite/Miniflare dev environment)
The Error
Error: exports is not defined
at runInRunnerObject (workers/runner-worker/index.js:107:3) It appeared on every request to `/` and `/about/` — both pages that called `<PortableText value={...} />` from `emdash/ui`. Pages like `/blog/` and `/cv/` that don't use `PortableText` worked fine.
The error is a classic sign that CJS code reached Cloudflare's workerd runtime. workerd is a pure ESM environment: it has no `module`, `exports`, `require`, or `__dirname` globals.
Tracing the Dep Chain
The stack trace pointed to workerd internals, not application code, so the dependency chain had to be followed manually from `PortableText` through EmDash embeds and Astro integrations until the offending package surfaced.
- `emdash/ui` → pre-bundled into `node_modules/.vite/deps_ssr/emdash_ui.js`
- `emdash_ui.js` → imports Astro's `PortableText.astro`
- `PortableText.astro` → imports `virtual:emdash/block-components`
- The Bluesky embed component → imports `@atproto/api`
"use strict";
Object.defineProperty(exports, "__esModule", { value: true }); `@atproto/api` is CommonJS. No `type: module`, no ESM exports map, and a `main` field pointing directly to a CJS build.
The Fix
Two changes were required: hoisting the transitive dependency so Vite could resolve it, and forcing it through the SSR dep optimizer so workerd received ESM output.
Step 1: Hoist `@atproto/*` with `publicHoistPattern`
publicHoistPattern:
- "@emdash-cms/*"
- "@atproto/*"
autoInstallPeers: true
minimumReleaseAge: 10080
allowBuilds:
better-sqlite3: true
esbuild: true
sharp: true
workerd: true This forces pnpm to create root-level symlinks for transitive `@atproto/*` packages so Vite can resolve them from `node_modules/@atproto/...` instead of only from `.pnpm/`.
Step 2: Add `@atproto/api` to `ssr.optimizeDeps.include`
vite: {
optimizeDeps: {
exclude: ["emdash"],
},
ssr: {
optimizeDeps: {
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",
],
},
},
}, With the hoist in place, Vite can now resolve `@atproto/api`, wrap it in a `__commonJS` helper during SSR optimization, and emit ESM-safe output into `node_modules/.vite/deps_ssr/`.
Why `ssr.optimizeDeps.exclude` Doesn't Work
Excluding a package from the SSR optimizer skips the CJS→ESM conversion step entirely. In Node.js SSR that's usually fine because Node provides CommonJS globals. In workerd, it guarantees a crash.
Summary
- `publicHoistPattern` makes transitive `@atproto/*` packages resolvable from the project root.
- `ssr.optimizeDeps.include` forces `@atproto/api` through Vite's SSR optimizer so workerd receives ESM output instead of raw CJS.
- Both steps are required: the hoist makes the package discoverable, and the include makes it converted.