Skip to content

fix(docs): get docs.stackpanel.com/docs/* working again#20

Merged
cooper (czxtm) merged 4 commits intomainfrom
fix/docs-bundle-false
Apr 30, 2026
Merged

fix(docs): get docs.stackpanel.com/docs/* working again#20
cooper (czxtm) merged 4 commits intomainfrom
fix/docs-bundle-false

Conversation

@czxtm
Copy link
Copy Markdown
Contributor

@czxtm cooper (czxtm) commented Apr 30, 2026

Summary

Fixes docs.stackpanel.com/docs/* (and every other dynamic Next route on the docs Worker) returning 500 Internal Server Error while / and /api/search work.

This turned out to be three separate bugs stacked on top of each other, where each one masked the next. Each commit peels off one layer.

1. Deploy-time: alchemy re-bundles the OpenNext entrypoint

alchemy@2.0.0-beta.20's Cloudflare.Worker.prepareBundle runs .open-next/worker.js through cloudflareRolldown even when isExternal: true. The re-bundle rewrites OpenNext's runtime import("./server-functions/default/handler.mjs") so its wrapper resolver returns undefined and the deployed Worker dies on first request with

TypeError: Cannot destructure property 'name' of '(intermediate value)'
    at worker.js:1:23445 in createGenericHandler

This is what was failing every Deploy Docs CI job since the alchemy@2 migration — the user-facing 500s were the previous still-running deploy.

Fix: patch alchemy (via bun's patchedDependencies) to add WorkerProps.bundle?: boolean (default true). When false, prepareBundle reads props.main and uploads it byte-for-byte (hash = sha256(content)) — no rolldown step. Same patch is up on my fork as the proposed upstream PR: czxtm/alchemy-effect@df6ed57 / standalone repro at https://github.com/czxtm/repro-alchemy-bundle-false.

2. Build-time: OpenNext's worker.js isn't self-contained

OpenNext's .open-next/worker.js is a ~2KB entrypoint with explicitly comments saying the static ./cloudflare/*.js imports "Will be resolved by wrangler build". Uploading byte-for-byte gives Uncaught Error: No such module "cloudflare/images.js".

Fix: run wrangler deploy --dry-run --outdir=.open-next/dist --minify after opennextjs-cloudflare build. Wrangler bundles the static imports while preserving the dynamic import() paths the way OpenNext expects. Point main: at the wrangler output and keep bundle: false so alchemy uploads the wrangler artifact byte-for-byte. (Without --minify, the bundle is 70 MiB uncompressed — over CF's 64 MiB Workers script limit. With --minify, 32 MiB uncompressed / 4 MiB gzipped after fix #3.)

3. Runtime: fumadocs-typescript calls fs.mkdir on every dynamic page render

Once the Worker actually deployed, runtime traffic surfaced the bug the user was hitting all along. wrangler tail on docs.pr-20.stackpanel.com:

GET /docs - Ok @ 12:55:10
  (error) Error: [unenv] fs.mkdir is not implemented yet!
      at l6 (worker.js:112349:269)
      at worker.js:112349:8151

That stack lands inside ts-morph's FileSystemDocumentCache.mkdir, pulled in transitively from apps/docs/src/mdx-components.tsx:

mdx-components.tsx
  → fumadocs-typescript            (createGenerator + AutoTypeTable UI)
  → fumadocs-typescript/ui         (AutoTypeTable React component)
  → @ts-morph/common + ts-morph    (TS compiler API + virtual FS)

createFileSystemGeneratorCache(".next/fumadocs-typescript") calls fs.mkdir on first render, which the Workers nodejs_compat polyfill (unenv) doesn't implement.

The runtime registration was dead code: <AutoTypeTable /> in MDX is expanded at build time by remarkAutoTypeTable in source.config.ts, so the rendered output never contains a live <AutoTypeTable> element — verified rg AutoTypeTable apps/docs/content returns no matches.

Fix: drop AutoTypeTable and the runtime createGenerator from mdx-components.tsx. Drops ts-morph entirely from the runtime worker bundle (~25 MiB).

Verification

PR preview at docs.pr-20.stackpanel.com:

Route Before After
/ 200 200
/docs 500 200 (<title>Stackpanel</title>)
/docs/agent 500 200 (<title>CLI / Agent</title>)
/docs/cli 500 200
/docs/reference 500 200
/api/search 200 200

Bundle size: 70 MiB → 32 MiB uncompressed, ~10 MiB → 4 MiB gzipped.

Cleanup followups

Filed as separate issues for after merge:

  • stackpanel-49t: closeable — alchemy bundle: false patch + the docs fixes are this PR. Drop the patch + the bundle: false prop once cloudflareRolldown's dynamic-import handling is fixed upstream (PR proposed at https://github.com/czxtm/alchemy-effect ready to be opened against alchemy-run/alchemy-effect).
  • New: revisit re-enabling <AutoTypeTable /> MDX with a non-FS cache implementation (or omitting the runtime cache entirely) if we ever want to use it in MDX content.

…r.js

Closes the regression where every `/docs/*` route on docs.stackpanel.com
returns `500 Internal Server Error` while `/` and `/api/search` work.

Root cause: alchemy@2.0.0-beta.20's `Cloudflare.Worker.prepareBundle`
runs `.open-next/worker.js` through `cloudflareRolldown` even when
`isExternal: true`. The re-bundle rewrites OpenNext's dynamic
`import("./server-functions/default/handler.mjs")` so its wrapper
resolver returns `undefined`, which then throws
`TypeError: Cannot destructure property 'name' of '(intermediate value)'`
inside `createGenericHandler`. Static routes survive because they're
served by the ASSETS binding without ever entering the broken handler.

Fix: a 2-edit patch to alchemy that adds `WorkerProps.bundle?: boolean`
(default `true`). When `bundle: false`, `prepareBundle` short-circuits —
reads `props.main` directly and returns a single-file `Bundle.BundleOutput`
whose hash is `sha256(content)`. No rolldown step, byte-identical upload.

This change applies the patch to our local alchemy install via bun's
`patchedDependencies` and turns it on in apps/docs.

Patch is the same one I just pushed to czxtm/alchemy-effect:main as the
proposed upstream PR; once it ships in a beta release we can drop both
the patch file and the `bundle: false` prop.

bd: closes stackpanel-49t

Other changes:
- bun.lock cleanup: removes `bun2nix` (already removed from the workspace
  package.json in d82d13a but not from the lockfile).
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 30, 2026

PR Summary

Medium Risk
Touches deployment-critical behavior by patching a third-party infra tool (alchemy) and changing how the docs Worker bundle is uploaded, though the new behavior is opt-in (bundle: false) with a default preserving existing behavior elsewhere.

Overview
Fixes Docs Cloudflare Worker deploy/runtime failures by opting out of alchemy’s rolldown re-bundling for the prebuilt OpenNext .open-next/worker.js.

This introduces a Bun patchedDependencies patch to alchemy@2.0.0-beta.20 adding Cloudflare.Worker bundle?: boolean (default true); when set to false, prepareBundle uploads main byte-for-byte and hashes it via sha256 instead of running rolldown. The docs deploy (apps/docs/alchemy.run.ts) now sets bundle: false alongside isExternal: true to keep OpenNext’s dynamic imports intact.

Lockfile/package metadata is updated to apply the patch (and cleans up stale bun2nix entries), and issues.jsonl is updated with new/expanded tracking items around the deploy regressions and state-store bootstrap.

Reviewed by Cursor Bugbot for commit 1d47562. Configure here.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Preview pr-20 has been destroyed.

Follow-up to the previous commit. The `bundle: false` opt-out was the
right opt-out, but I had a wrong mental model of `.open-next/worker.js`:
it's a ~2KB OpenNext entrypoint that *expects* to be passed through a
wrangler-style bundler that resolves the relative `./cloudflare/*.js`
imports and inlines them. Uploading it byte-for-byte fails with
`Uncaught Error: No such module "cloudflare/images.js" — imported from
"worker.js"`.

The PR #20 first attempt confirmed the patch *itself* works (CI got past
the rolldown step and uploaded 2.22 KB to Cloudflare); just the file
content was incomplete. So:

- `bun run build:worker` now also runs `wrangler deploy --dry-run
  --outdir=.open-next/dist`, which produces a self-contained
  `.open-next/dist/worker.js` (~10 MiB gzipped — under CF's limit).
  Wrangler bundles statics and *preserves* the runtime `import()` calls
  the way OpenNext expects.
- `apps/docs/alchemy.run.ts` points `main:` at `.open-next/dist/worker.js`
  and keeps `bundle: false` so alchemy uploads the wrangler artifact
  byte-for-byte instead of running rolldown again.

Net effect: the same `bunx alchemy deploy` that was 500-ing on every
`/docs/*` route should now serve the real fumadocs UI.
Without --minify, the bundled worker.js was 70.1 MiB uncompressed —
just over Cloudflare's 64 MiB uncompressed Workers script limit (the
gzipped 10 MiB limit was fine). With --minify, 57.7 MiB uncompressed /
9.1 MiB gzipped.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Docs preview pr-20 has been destroyed.

…s/* fix)

This is the actual cause of every `/docs/*` route returning 500 — not
the alchemy bundling bug from the previous commits, which was masking
the symptom by failing the deploy outright.

Once the worker actually deployed, runtime traffic surfaced:

  GET https://docs.pr-20.stackpanel.com/docs - Ok @ 12:55:10
    (error) Error: [unenv] fs.mkdir is not implemented yet!
        at l6 (worker.js:112349:269)
        at worker.js:112349:8151

That stack lands on `ts-morph`'s `FileSystemDocumentCache.mkdir`, pulled
in transitively via:

  src/mdx-components.tsx
    → fumadocs-typescript            (createGenerator + AutoTypeTable UI)
    → fumadocs-typescript/ui         (AutoTypeTable React component)
    → @ts-morph/common + ts-morph    (TypeScript compiler API + virtual FS)

The `<AutoTypeTable />` MDX tag doesn't actually need the runtime
component — `remarkAutoTypeTable` in source.config.ts already expands
those tags into plain markdown tables at build time, so the rendered
output never contains a live `<AutoTypeTable>` element. (Verified:
`rg AutoTypeTable apps/docs/content` returns no matches.) The runtime
registration was dead code.

Removing it:
- drops ts-morph (~25 MiB) from the runtime worker bundle
- drops the request-time `fs.mkdir` call that crashed every dynamic
  fumadocs page render
- shrinks the bundled `worker.js` from 57.7 MiB → 31.0 MiB uncompressed
  and 9.0 MiB → 4.0 MiB gzipped (well under both Workers limits)

If we ever need a *runtime* AutoTypeTable in the future, the cache must
be a non-FS implementation (or omitted entirely) so it works under
Workers' `nodejs_compat` polyfill.
@czxtm cooper (czxtm) changed the title fix(docs): patch alchemy + use bundle: false to stop re-bundling .open-next/worker.js fix(docs): get docs.stackpanel.com/docs/* working again Apr 30, 2026
@czxtm cooper (czxtm) merged commit ba6e3d2 into main Apr 30, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant