Skip to content

Brian B. Tung

[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.

  1. `emdash/ui` → pre-bundled into `node_modules/.vite/deps_ssr/emdash_ui.js`
  2. `emdash_ui.js` → imports Astro's `PortableText.astro`
  3. `PortableText.astro` → imports `virtual:emdash/block-components`
  4. 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.