[PR #306] [MERGED] fix: stub node:async_hooks in client builds via virtual module #461

Closed
opened 2026-05-06 13:08:13 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/306
Author: @Divkix
Created: 3/6/2026
Status: Merged
Merged: 3/7/2026
Merged by: @james-elicx

Base: mainHead: fix/async-hooks-client-stub


📝 Commits (10+)

  • 4044da5 fix: stub node:async_hooks in client builds via virtual module
  • 2ec2f83 fix: address async-hooks-stub code review feedback
  • 538973a refactor: extract async-hooks-stub plugin to plugins/ and test against real implementation
  • 09c03b0 fix: add filter to load hook, evaluate stub at runtime in test
  • 0fe4052 fix: remove control-char regex from load filter to fix lint
  • 4e86e0b fix: suppress no-control-regex for load filter via eslint-disable comment
  • 3916820 refactor: derive load filter regex from ASYNC_HOOKS_STUB_ID constant
  • f087b20 Update build-optimization.test.ts
  • 6e3c567 Update index.ts
  • c9c8a3d fix: widen run() type signature to accept rest args and typed callback

📊 Changes

3 files changed (+127 additions, -0 deletions)

View changed files

📝 packages/vinext/src/index.ts (+4 -0)
packages/vinext/src/plugins/async-hooks-stub.ts (+49 -0)
📝 tests/build-optimization.test.ts (+74 -0)

📄 Description

Summary

  • Replaces the approach in #293 (external stub file) with a lightweight virtual module
  • Adds vinext:async-hooks-stub plugin that intercepts node:async_hooks only in the client environment and provides a no-op AsyncLocalStorage
  • Server environments (rsc, ssr) are unaffected — they continue using real node:async_hooks

Why a stub is the correct fix

Several shims (headers, cache, navigation-state, head-state, router-state, cache-runtime, fetch-cache) import AsyncLocalStorage from node:async_hooks. These shims are aliased globally via resolve.alias because they must resolve in every environment. In client builds, Vite externalizes node:async_hooks to __vite-browser-external — an empty stub with no named exports — causing Rollup "AsyncLocalStorage" is not exported errors.

The shims already guard against missing ALS state with _als.getStore() ?? fallback patterns, so a no-op stub (getStore() returns undefined, run() passes through) is semantically correct for client builds.

Addresses @james-elicx's feedback: the leakage is inherent — shims like next/headers are server-only but resolve globally via resolve.alias. Splitting aliases per-environment would require knowing which shims are client-safe and would break transitive imports. The virtual module approach is minimal (no extra files) and surgical (only affects node:async_hooks in client).

Closes #293

Test plan

  • tsc --noEmit passes (exit 0)
  • bun run build succeeds
  • biome check — no new errors from this change
  • Server environments unchanged — no resolveId interception when environment.name !== "client"
  • No-op stub's run() passes through callback return value, matching real ALS behavior

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/306 **Author:** [@Divkix](https://github.com/Divkix) **Created:** 3/6/2026 **Status:** ✅ Merged **Merged:** 3/7/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/async-hooks-client-stub` --- ### 📝 Commits (10+) - [`4044da5`](https://github.com/cloudflare/vinext/commit/4044da568df30093d7e3399b6026861b15c7c6eb) fix: stub node:async_hooks in client builds via virtual module - [`2ec2f83`](https://github.com/cloudflare/vinext/commit/2ec2f832d89ed82db0cf329583a71f896bbba696) fix: address async-hooks-stub code review feedback - [`538973a`](https://github.com/cloudflare/vinext/commit/538973a90ae3cd625e31184db2aa40227981bb16) refactor: extract async-hooks-stub plugin to plugins/ and test against real implementation - [`09c03b0`](https://github.com/cloudflare/vinext/commit/09c03b0de7e67916505917699b2c62af31a1eb73) fix: add filter to load hook, evaluate stub at runtime in test - [`0fe4052`](https://github.com/cloudflare/vinext/commit/0fe40528c9b6ba0fee9acc555f610297de865740) fix: remove control-char regex from load filter to fix lint - [`4e86e0b`](https://github.com/cloudflare/vinext/commit/4e86e0b0711d7351069907436d7ee221790f3460) fix: suppress no-control-regex for load filter via eslint-disable comment - [`3916820`](https://github.com/cloudflare/vinext/commit/3916820d7e3c2d65356b1c53eaaf6266bcc23cd9) refactor: derive load filter regex from ASYNC_HOOKS_STUB_ID constant - [`f087b20`](https://github.com/cloudflare/vinext/commit/f087b20a6f2b0223d1c9903cad75dc5e98ea30a2) Update build-optimization.test.ts - [`6e3c567`](https://github.com/cloudflare/vinext/commit/6e3c567e881a7633acdf83865d2468c4a6a3c669) Update index.ts - [`c9c8a3d`](https://github.com/cloudflare/vinext/commit/c9c8a3d48a0e4bb1fa2ea69b2343bfde3eac2bb9) fix: widen run() type signature to accept rest args and typed callback ### 📊 Changes **3 files changed** (+127 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/index.ts` (+4 -0) ➕ `packages/vinext/src/plugins/async-hooks-stub.ts` (+49 -0) 📝 `tests/build-optimization.test.ts` (+74 -0) </details> ### 📄 Description ## Summary - Replaces the approach in #293 (external stub file) with a lightweight virtual module - Adds `vinext:async-hooks-stub` plugin that intercepts `node:async_hooks` **only** in the `client` environment and provides a no-op `AsyncLocalStorage` - Server environments (rsc, ssr) are unaffected — they continue using real `node:async_hooks` ### Why a stub is the correct fix Several shims (`headers`, `cache`, `navigation-state`, `head-state`, `router-state`, `cache-runtime`, `fetch-cache`) import `AsyncLocalStorage` from `node:async_hooks`. These shims are aliased globally via `resolve.alias` because they must resolve in every environment. In client builds, Vite externalizes `node:async_hooks` to `__vite-browser-external` — an empty stub with no named exports — causing Rollup `"AsyncLocalStorage" is not exported` errors. The shims already guard against missing ALS state with `_als.getStore() ?? fallback` patterns, so a no-op stub (`getStore()` returns `undefined`, `run()` passes through) is semantically correct for client builds. Addresses @james-elicx's [feedback](https://github.com/cloudflare/vinext/pull/293#issuecomment-4014500485): the leakage is inherent — shims like `next/headers` are server-only but resolve globally via `resolve.alias`. Splitting aliases per-environment would require knowing which shims are client-safe and would break transitive imports. The virtual module approach is minimal (no extra files) and surgical (only affects `node:async_hooks` in client). Closes #293 ## Test plan - [x] `tsc --noEmit` passes (exit 0) - [x] `bun run build` succeeds - [x] `biome check` — no new errors from this change - [x] Server environments unchanged — no `resolveId` interception when `environment.name !== "client"` - [x] No-op stub's `run()` passes through callback return value, matching real ALS behavior --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:13 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#461
No description provided.