Visitar URL original
fix(new-url): emit the asset when a plugin loads a `new URL()` target as code by theRizwan · Pull Request #11418 · rolldown/rolldown · GitHub
Skip to content

fix(new-url): emit the asset when a plugin loads a new URL() target as code - #11418

Open
theRizwan wants to merge 1 commit into
rolldown:mainfrom
theRizwan:fix/new-url-asset-loaded-by-plugin
Open

theRizwan wants to merge 1 commit into
rolldown:mainfrom
theRizwan:fix/new-url-asset-loaded-by-plugin

Conversation

@theRizwan

Copy link
Copy Markdown
Contributor

Fixes case 1 of #11325. Cases 2 (percent-encoded paths) and 3 (query and hash) are separate and not touched here.

Problem

With experimental.resolveNewUrlToAsset, the new URL() import record asserts ModuleType::Asset. The builtin asset-module plugin runs its load as Post, so a user plugin can answer first. When that plugin returns code without a moduleType, as unwasm does for every .wasm file, load_source applied the asserted asset type to the plugin's code and parsing failed:

[UNHANDLEABLE_ERROR] ... Encountered a module with type `asset` during AST parsing.

This breaks every Nitro build that uses unwasm with this option (nitrojs/nitro#4930).

Fix

In load_source, when the asserted type is Asset and a load hook returned code but no module type:

  • the code is kept as a JS module, as it would be without the assertion;
  • the file itself is read and emitted through the FileEmitter (same assetFileNames and sanitizeFileName handling as emit_file_async), associated with the module, and watched.

The new URL() finalizer then finds the asset through file_ref_for_module as usual, so the output is new URL("assets/m-[hash].wasm", import.meta.url) and the asset is emitted with the original bytes. A plugin that returns a moduleType is still trusted as before.

The issue suggested either a clear error or ignoring the plugin's result for the asset. I went with emitting the asset while keeping the plugin's module, because the new URL() caller wants the file, and the plugin's code stays available to any import of the same id.

Not covered

The asserted type comes from whichever record reaches the module first. If a module is first reached by a plain import, it has no asserted type and its new URL() is left unrewritten. That is the existing behavior with or without plugins and is unchanged here. I noted it in internal-docs/plugin-asset-module/implementation.md.

Tests

New Rust integration test plugin/new_url_asset_loaded_by_plugin: a plugin loads .wasm as JS without a module type, and the test checks that the wasm bytes are emitted as an asset and that new URL() points at it. It fails on main with the UNHANDLEABLE_ERROR above.

Also checked the issue's JS reproduction against the built package: src/load.js now produces new URL("assets/m-BYieBQW-.wasm", import.meta.url) plus the asset.

cargo test -p rolldown passes except test262_module_code, which fails the same way on main locally because the test262 submodule is not checked out. cargo fmt --check and cargo clippy -p rolldown --all-targets are clean.

AI disclosure: I used Claude Code to help trace the code path and write the change and test. I reviewed the change and ran the tests above myself.

… as code

With `experimental.resolveNewUrlToAsset`, a user plugin whose `load` hook
returns code for the referenced file without a `moduleType` (for example a
wasm loader) left the asserted `asset` type on the module, and parsing failed
with an internal error. Keep the plugin code as the module, and emit the file
as an asset so `new URL()` is rewritten to it.

Refs rolldown#11325
@codspeed

codspeed Bot commented Nov 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 4 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing theRizwan:fix/new-url-asset-loaded-by-plugin (a3c8220) with main (d87a31e)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@hyfdev hyfdev self-assigned this Nov 2, 2026
@hyfdev

hyfdev commented Nov 2, 2026

Copy link
Copy Markdown
Member

The asset is emitted only when the plugin returns no moduleType; a plugin that returns moduleType: 'js' for the same file still leaves new URL('./m.wasm', import.meta.url) unrewritten with no asset emitted, as on main. Could the check be "asserted asset and no file reference associated after load", so both cases produce the asset?

hyfdev added a commit that referenced this pull request Nov 3, 2026
…11420)

Fixes case 3 of #11325. Case 1 is #11418 and case 2 is #11419; this one
is independent of both.

## Problem

With `experimental.resolveNewUrlToAsset`, the query and fragment were
dropped when `new URL()` was rewritten:

```js
new URL("./sprite.svg?v=1", import.meta.url)  // -> new URL("assets/sprite-[hash].svg", import.meta.url)
new URL("./sprite.svg#icon", import.meta.url) // -> new URL("assets/sprite-[hash].svg", import.meta.url)
```

so `url.search` and `url.hash` were lost, which breaks uses like SVG
sprite fragments.

## Fix

In the `new URL()` finalizer, after computing the asset path, append the
part of the original specifier that follows the path (everything from
the first `?` or `#`, using `clean_url`). This matches Vite's
`vite:asset-import-meta-url`, which keeps the postfix when it rewrites
the URL. Resolution and the emitted asset are unchanged: all of `?v=1`,
`#icon`, `?v=1#icon` and the plain path still share one emitted file.

## Tests

New fixture `topics/new_url/keep_query_and_hash` covering a query, a
fragment, both, and the plain path. With snapshot updates disabled
(`INSTA_UPDATE=no CI=true`) it fails on `main` and passes here. No
existing snapshot changed.

`cargo test -p rolldown` passes except `test262_module_code`, which
fails the same way on `main` locally because the `test262` submodule is
not checked out. `cargo fmt --check` and `cargo clippy -p rolldown
--all-targets` are clean.

AI disclosure: I used Claude Code to help trace the code path and write
the change and test. I reviewed the change and ran the tests above
myself.

Co-authored-by: Yunfei He <i.heyunfei@gmail.com>

This branch has not been deployed

No deployments
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