Skip to content

fix(turbopack): handle Turbopack hashed external module IDs in workerd#1196

Closed
Thereallo1026 wants to merge 2 commits intoopennextjs:mainfrom
Thereallo1026:fix/turbopack-hashed-external-module-ids
Closed

fix(turbopack): handle Turbopack hashed external module IDs in workerd#1196
Thereallo1026 wants to merge 2 commits intoopennextjs:mainfrom
Thereallo1026:fix/turbopack-hashed-external-module-ids

Conversation

@Thereallo1026
Copy link
Copy Markdown

Problem

Turbopack externalizes some packages using content-hashed module IDs like shiki-43d062b67f27bbdc/core. These are generated by Turbopack's module ID hashing and appear as a.y("shiki-43d062b67f27bbdc/core") calls inside the [externals]_* chunks.

In workerd, await import(id) only works for registered named modules. A hashed ID like shiki-43d062b67f27bbdc/core is not a valid module name, so the call fails with:

Failed to load external module shiki-43d062b67f27bbdc/core:
Error: No such module "shiki-43d062b67f27bbdc/core"

The actual package (shiki) is bundled in the worker's node_modules and is accessible via require("shiki/core") — the hash just needs to be stripped.

Fix

The inlineExternalImportRule AST patch already wraps externalImport in a switch. I've extended the default case to detect Turbopack's hash pattern at runtime:

<packagename>-<16+ hex chars>[/subpath]

When detected, it strips the hash and falls back to require():

default: {
  const __dehashedId = $ID.replace(/^((?:@[^/]+\/)?[^/]+?)-[0-9a-f]{16,}(\/[^]*)?$/, "$1$2");
  if (__dehashedId !== $ID) {
    $RAW = require(__dehashedId || $ID);
  } else {
    $RAW = await import($ID);
  }
}

This handles:

  • shiki-43d062b67f27bbdcshiki
  • shiki-43d062b67f27bbdc/coreshiki/core
  • shiki-43d062b67f27bbdc/wasmshiki/wasm
  • @shikijs/core-43d062b67f27bbdc/dist/index.js@shikijs/core/dist/index.js
  • Normal IDs (react, next/dist/...) → unchanged, use await import() as before

Tests

Added turbopack.spec.ts covering the regex behaviour for all these cases. All 302 existing tests continue to pass.

Reproduction

Any Next.js 16 (Turbopack) app that uses shiki (e.g. via fumadocs-core) will hit this when deployed to Cloudflare Workers.

Turbopack externalizes packages using content-hashed IDs like
`shiki-43d062b67f27bbdc/core`. In workerd, `await import(id)` fails
for these because no module is registered under the hashed name.

The actual package IS bundled in node_modules and accessible via
`require()`. This patch detects the Turbopack hash pattern at runtime
(`<pkg>-<16+hex chars>[/subpath]`) in the `externalImport` default
case, strips the hash, and falls back to `require()` instead.

This fixes runtime errors of the form:
  Failed to load external module shiki-43d062b67f27bbdc/core:
  Error: No such module "shiki-43d062b67f27bbdc/core"

The regex also handles scoped packages (`@scope/pkg-<hash>/subpath`).
Non-hashed module IDs continue to use `await import()` unchanged.

Adds a spec file with tests covering the regex's behaviour across
normal IDs, hashed IDs, and scoped package IDs.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 13, 2026

⚠️ No Changeset found

Latest commit: 7be4963

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vicb
Copy link
Copy Markdown
Contributor

vicb commented Apr 13, 2026

I realized that #1139 was not merged yet.
Does this PR fix the same issue?
@james-elicx ?

Revise the approach from the previous commit. Instead of patching the
`externalImport` function in [turbopack]_runtime.js (which fails because
workerd can't use CJS require() on ESM-only packages), patch the chunk
files themselves before wrangler bundles them.

Turbopack emits hashed module IDs like `shiki-43d062b67f27bbdc/core` in
`a.y("...")` calls across server chunks ([externals]_*.js and
[root-of-the-server]_*.js). Replace these with:

  Promise.resolve().then(() => require("shiki/core"))

The static require() is resolved by wrangler at bundle time, inlining the
real package — the same mechanism as other bundled requires. The
Promise wrapper preserves the async contract of the original a.y() call.

The pathFilter targets all `.next/server/chunks/**/*.js` files and the
contentFilter `a.y("` limits application to files that actually contain
externalImport calls.

Adds patchHashedExternalImports as an exported function for unit testing.
@Thereallo1026
Copy link
Copy Markdown
Author

Hi human here (Opus degrade is real), the previous commit was wrong

Patching externalImport in [turbopack]_runtime.js to use require() fails because by the time that function runs, wrangler has already finished bundling. Calling require() on an ESM-only package like shiki at that point throws:

Dynamic require of "shiki/core" is not supported

But the a.y("shiki-<hash>/sub") calls live inside the chunk files themselves, both [externals]_*.js and [root-of-the-server]_*.js

If we replace them with Promise.resolve().then(() => require("shiki/sub")) before wrangler bundles, wrangler can see the static require() at bundle time and inlines the real package correctly

I added a patch in the second commit targeting all .next/server/chunks/**/*.js files (filtered by contentFilter: /a\.y\("/) that does this, the Promise.resolve().then() wrapper preserves the async contract of the original a.y() call

The regex strips Turbopack's content hash (<pkg>-<16+ hex chars>[/subpath]) and handles scoped packages too

Tested working with a real fumadocs app on opennextjs-cloudflare preview

@Thereallo1026
Copy link
Copy Markdown
Author

@vicb yes, this fixes the same issue as #1139

I wasn't aware of it when I opened this PR, #1139 looks like a more robust solution

I'd suggest merging that one instead and closing this, apologies for the noise.

@vicb
Copy link
Copy Markdown
Contributor

vicb commented Apr 13, 2026

@vicb yes, this fixes the same issue as #1139

I wasn't aware of it when I opened this PR, #1139 looks like a more robust solution

I'd suggest merging that one instead and closing this, apologies for the noise.

No worries @Thereallo1026 and thanks for the PR.
We'll get 1139 merged

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.

2 participants