Skip to content

fix(core): resolve new URL() and wasm from on-disk source (#1455)#1464

Merged
fi3ework merged 1 commit into
mainfrom
fix-core-wasm-url-loading
Jul 1, 2026
Merged

fix(core): resolve new URL() and wasm from on-disk source (#1455)#1464
fi3ework merged 1 commit into
mainfrom
fix-core-wasm-url-loading

Conversation

@fi3ework

@fi3ework fi3ework commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

new URL('./file', import.meta.url) was rewritten by rspack into a hashed bundled asset path, which the vm worker cannot find at runtime → ENOENT (#1455). Setting module.parser.javascript.url = false keeps these expressions source-relative, matching Node.js and Vitest.

Building on that, this reworks how .wasm is loaded. The old path leaned on rspack's webassembly/async runtime and string-replaced the generated runtime code to redirect its byte read into rstest's in-memory bundle. That is now gone: experiments.asyncWebAssembly is disabled and a build-time loader turns each .wasm into a self-contained JS module that reads its on-disk source and instantiates it. This also transparently supports import-bearing wasm (wasm-bindgen --target bundler, whose foo_bg.wasmfoo_bg.js glue is circular) by wiring an importObject from statically imported glue modules.

Removed: hardcoded patch of rspack's generated runtime

The previous implementation reached into rspack's async_wasm_loading runtime module and string-replaced its source — readFile(readWasmFile( (import.meta.readWasmFile( for ESM) — so it could feed bytes from the in-memory assetFiles instead of disk:

// before — packages/core/src/core/plugins/mockRuntime.ts (deleted)
compilation.hooks.runtimeModule.tap('RstestWasmRuntimePlugin', (module) => {
  if (module.name === 'async_wasm_loading') {
    const finalSource = module.source.source.toString('utf-8').replace(
      'readFile(',                                  // hard-coded against rspack's
      this.outputModule ? 'import.meta.readWasmFile(' : 'readWasmFile(', // EJS template
    );
    module.source.source = Buffer.from(finalSource);
  }
});

This was coupled to the exact text of rspack's Rust-generated runtime template and broke silently if that template changed. It is deleted, along with the paired readWasmFile runtime callbacks, the in-memory assetFiles wasm path, and the now-dead base64 asset encoding / wasm-asset filtering in rsbuild.ts.

wasm load branches — before / after

Before — one path, dependent on patching generated runtime code:

import './x.wasm'  /  import(url)
        │
        ▼
rspack webassembly/async   ──►  generates `async_wasm_loading` runtime
(asyncWebAssembly = true)            └─ readFile(<virtual dist path>)
        │
        ▼
MockRuntimePlugin string-replaces  readFile( → readWasmFile(   ◄─ HARDCODED hack
        │
        ▼
readWasmFile → bytes from in-memory assetFiles (base64)
        │
        ▼
WebAssembly.instantiate(module)        ◄─ no importObject

After — two explicit branches, no generated-code patching:

 import './x.wasm'  (static / literal dynamic)     import(new URL('./x.wasm', import.meta.url).href)
        │  P1                                              │  P2  (runtime specifier)
        ▼                                                  ▼
 wasmLoader.mjs  (build-time)                       loadWasm  (runtime)
   self-contained JS module:                          readFile(source)
     readFileSync(<source path>)                      WebAssembly.instantiate(module)
     import * as glue from <specifier>                   └─ no importObject
       (relative | bare | alias, via this.getResolve())
     instantiate(bytes, { <specifier>: glue })
        │                                                  │
        ▼                                                  ▼
 named exports (no synthetic `default`)             named exports (no synthetic `default`)

Each wasm import-module is wired only if it resolves through rspack's normal-module resolver, so relative, bare, and aliased glue specifiers all work uniformly. Synthetic host modules that do not resolve (emscripten env, wasi_snapshot_preview1, GOT.*) are omitted from the importObject, so the build stays green; such a wasm only errors at runtime if it truly needs them — import the toolchain's JS glue instead (emscripten / wasm-bindgen --target web|nodejs).

Why the generated module is safe to review

The loader emits code by string templating, so it is worth stating what bounds it:

  • It never rewrites the wasm. It only reads the interface via WebAssembly.Module.imports/exports(...) and re-exports it; the on-disk .wasm bytes run unchanged. This non-invasiveness is exactly what makes the [Bug]: new URL('./file', import.meta.url) is rewritten to a bundled asset path, breaking runtime file resolution #1455 "run the source" guarantee hold. (Contrast: webpack's async-wasm rewrites the wasm's import section via @webassemblyjs/wasm-edit.)
  • The output is deterministic. It is a pure function of (wasm bytes, resolution results, resource path) — verified byte-identical across repeated runs.
  • Injection-safe. Every embedded import/export name and module specifier goes through JSON.stringify; export names bind to positional locals (__rstest_wasm_0__ as "exp"), so wasm export names that are not valid JS identifiers cannot break the emitted syntax.

A typical emitted module is small (glue import + instantiate + named re-exports); over a third of wasmLoader.mjs is explanatory comments, not generating logic.

Behavior notes

  • new URL(..., import.meta.url) now resolves against on-disk source for all asset types, not just wasm (the old rewrite was already broken at runtime, so this is a fix, not a regression). The url: false default is placed before the user-config spread, so user overrides still win.
  • Wasm module namespaces no longer expose a synthetic default ('default' in ns === false), matching Node's WebAssembly ESM namespace and Vitest. import init from './x.wasm' users should switch to named/namespace imports.
  • Watch-mode caveat (documented in the migration guide): because url: false keeps new URL(...) targets out of rspack's dependency graph, editing such a sibling asset in watch mode does not rerun the test that reads it. Wasm files, by contrast, stay in the graph (the loader emits a content-hash), so wasm byte edits do rerun.
  • Browser mode is untouched — it builds via a separate rsbuild that does not apply this plugin.

Related Links

Checklist

  • Tests updated (or not required).
    • e2e/new-url[Bug]: new URL('./file', import.meta.url) is rewritten to a bundled asset path, breaking runtime file resolution #1455 regression: new URL(..., import.meta.url) resolves a sibling on disk.
    • e2e/wasm — named exports, no synthetic default, and the P2 runtime new URL(...).href path.
    • e2e/wasm-imports — import-bearing wasm: circular wasm-bindgen-bundler shape links transparently; direct import resolves its importObject; non-resolvable env builds and only LinkErrors at runtime; alias/bare specifier resolves its glue via this.getResolve(); the glue the wasm links is the same instance the test holds (guards against a native auto-link "split-brain").
    • Covered by the e2e matrix (ESM/CJS output, isolate / no-isolate).
  • Documentation updated (or not required). — migration/vitest "Path resolution" section corrected in EN and ZH, including the watch-mode caveat.

https://claude.ai/code/session_01DWZvBGqkLP4X21iMa9Rhsq

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying rstest with  Cloudflare Pages  Cloudflare Pages

Latest commit: cc5cd9c
Status: ✅  Deploy successful!
Preview URL: https://634b88c2.rstest.pages.dev
Branch Preview URL: https://fix-core-wasm-url-loading.rstest.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3e7464b4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/core/plugins/wasmLoader.mjs
Comment thread packages/core/src/core/plugins/basic.ts
@fi3ework fi3ework force-pushed the fix-core-wasm-url-loading branch 2 times, most recently from fc45e25 to edb7c77 Compare June 23, 2026 11:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: edb7c77aca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/worker/loadEsModule.ts Outdated
@fi3ework fi3ework force-pushed the fix-core-wasm-url-loading branch from edb7c77 to 73fc3c4 Compare June 23, 2026 11:41

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73fc3c4e80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/core/plugins/basic.ts
@fi3ework fi3ework force-pushed the fix-core-wasm-url-loading branch 2 times, most recently from 1a5cf59 to 383855e Compare June 29, 2026 08:06
@fi3ework fi3ework force-pushed the fix-core-wasm-url-loading branch 2 times, most recently from 59416a4 to 48e436c Compare July 1, 2026 06:31
`new URL('./file', import.meta.url)` was rewritten by rspack into a hashed
bundled asset path, which does not exist at runtime in the vm worker -> ENOENT.
Set `module.parser.javascript.url = false` so these expressions stay
source-relative, matching Node.js and Vitest.

Rework wasm handling on top of that: disable rspack's `webassembly/async` and
turn each `.wasm` into a self-contained JS module via a build-time loader that
reads its on-disk source bytes and instantiates them. This removes the
hardcoded string-replace of rspack's generated `async_wasm_loading` runtime
(`readFile(` -> `readWasmFile(`) and transparently supports import-bearing wasm
(wasm-bindgen `--target bundler`) by wiring an importObject from statically
imported glue modules. Wasm namespaces no longer carry a synthetic `default`,
matching Node's WebAssembly ESM namespace.

Claude-Session: https://claude.ai/code/session_01DWZvBGqkLP4X21iMa9Rhsq
@fi3ework fi3ework force-pushed the fix-core-wasm-url-loading branch from 48e436c to cc5cd9c Compare July 1, 2026 07:06
@fi3ework fi3ework requested a review from 9aoy July 1, 2026 07:16
@fi3ework fi3ework merged commit 37148fb into main Jul 1, 2026
13 checks passed
@fi3ework fi3ework deleted the fix-core-wasm-url-loading branch July 1, 2026 07:40
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.

[Bug]: new URL('./file', import.meta.url) is rewritten to a bundled asset path, breaking runtime file resolution

2 participants