Skip to content

fix(ext/node): apply Deno's resolver inside loader-hook defaultResolve#33964

Merged
bartlomieju merged 4 commits into
mainfrom
fix/loader-hook-default-resolve-import-map
May 12, 2026
Merged

fix(ext/node): apply Deno's resolver inside loader-hook defaultResolve#33964
bartlomieju merged 4 commits into
mainfrom
fix/loader-hook-default-resolve-import-map

Conversation

@bartlomieju

@bartlomieju bartlomieju commented May 11, 2026

Copy link
Copy Markdown
Member

When any package calls module.register() -- @tailwindcss/postcss
does, transitively, on Fresh apps that use Tailwind -- the global
resolve_active flag flips on and every subsequent bare-specifier
resolution gets routed through the hooks-worker bridge. The worker's
defaultResolve used new URL(spec, parentURL), which bypasses Deno's
import map / jsr / npm resolution. That turned import "fresh" into
file:///.../fresh and produced Loading unprepared module errors for
any mapped specifier, which broke things like deno task dev on the
dotcom repo.

This pre-computes Deno's default-resolved URL on the Rust side and
passes it through to the hooks worker so defaultResolve() returns the
real resolution, letting user hooks that chain next() (the common
passthrough pattern) see the same URL Deno would have produced. It also
falls back to inner_resolve synchronously when V8's
module_resolve_callback hits a cache miss -- recursive_load can skip
the async resolve when the placeholder URL coincidentally matches a
registered module, which left the cache empty and panicked during
instantiation.

Closes #33989

When any package calls module.register() (e.g. @tailwindcss/postcss),
every subsequent bare-specifier resolution gets routed through the
hooks-worker bridge. The worker's defaultResolve used
new URL(spec, parentURL), which bypasses Deno's import map / jsr /
npm resolution and produced "Loading unprepared module" errors for
mapped specifiers like "fresh".

Pre-compute Deno's default-resolved URL on the Rust side and pass it
through to the hooks worker so defaultResolve() returns the real
resolution. Also fall back to inner_resolve synchronously when V8's
module_resolve_callback hits a cache miss (recursive_load can skip
the async resolve when the placeholder URL coincidentally matches a
registered module, leaving the cache empty).
When a module load redirects (e.g. `npm:foo@^1` aliased to its
`file:///.../node_modules/...` location), `register_and_recurse_inner`
was still using the original request specifier as the referrer for the
module's child imports. With the resolve-hook bridge in the path, that
caused `resolve_async` to run with an `npm:` URL as the referrer, where
Deno's resolver can't locate the parent package's `package.json` and
fails any bare intra-package import. Switch to the module's canonical
post-redirect URL via `get_name_by_id`.
Add a spec test that registers a passthrough hook and dynamically
imports `npm:@denotest/dual-cjs-esm-dep`, whose ESM entry does a bare
intra-package dependency import. Without the canonical-referrer fix
in `recursive_load`, the resolver sees the `npm:` request URL as the
referrer and can't find the parent package's package.json, so the
intra-package import fails.

Also expand the sync-fallback comment in `cli/module_loader.rs` to be
explicit that the user hook chain is bypassed for that single resolve,
and why that's safe in practice.
@lunadogbot

Copy link
Copy Markdown
Contributor

specs::install::global::allow_scripts failed on windows-aarch64 after 4 in-runner retries — unrelated to this PR's loader-hook resolver changes. Re-running the job.

@bartlomieju
bartlomieju merged commit 0f47a9e into main May 12, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the fix/loader-hook-default-resolve-import-map branch May 12, 2026 14:30
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
denoland#33964)

When any package calls `module.register()` -- `@tailwindcss/postcss`
does, transitively, on Fresh apps that use Tailwind -- the global
`resolve_active` flag flips on and every subsequent bare-specifier
resolution gets routed through the hooks-worker bridge. The worker's
`defaultResolve` used `new URL(spec, parentURL)`, which bypasses Deno's
import map / jsr / npm resolution. That turned `import "fresh"` into
`file:///.../fresh` and produced `Loading unprepared module` errors for
any mapped specifier, which broke things like `deno task dev` on the
`dotcom` repo.

This pre-computes Deno's default-resolved URL on the Rust side and
passes it through to the hooks worker so `defaultResolve()` returns the
real resolution, letting user hooks that chain `next()` (the common
passthrough pattern) see the same URL Deno would have produced. It also
falls back to `inner_resolve` synchronously when V8's
`module_resolve_callback` hits a cache miss -- `recursive_load` can skip
the async resolve when the placeholder URL coincidentally matches a
registered module, which left the cache empty and panicked during
instantiation.

Closes denoland#33989
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.

SvelteKit build fails. Unable to load compiler (ERR_MODULE_NOT_FOUND)

2 participants