Skip to content

Commit 98ae5fc

Browse files
authored
refactor(core): pre-resolve child imports before V8 module registration (#34050)
When a child import returned `ModuleResolveResponse::Async` from the loader, `find_imports_in_compiled_module` used to discard the future, place a placeholder URL on the request with `needs_resolve = true`, and let `RecursiveModuleLoad` issue a second resolve later. That doubled every async resolve round-trip (the first reply was sent to a dropped `oneshot::Receiver` and silently lost) and required the synth-placeholder fallback that was necessary because `base.join(spec)` fails for `cannot-be-a-base` referrers like `blob:` and `data:` — the original bug this PR was opened for: lume's MDX plugin compiling MDX into a blob: module that imports `lume/jsx-runtime`, plus the related vite/rollup case where any transitive `module.register()` (e.g. tailwindcss) flipped on `resolve_active` and broke `node:path` imports from inside npm packages.
1 parent 7f44ddd commit 98ae5fc

13 files changed

Lines changed: 570 additions & 161 deletions

File tree

libs/core/modules/map.rs

Lines changed: 247 additions & 51 deletions
Large diffs are not rendered by default.

libs/core/modules/mod.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -723,10 +723,6 @@ pub(crate) struct ModuleRequest {
723723
/// None if this is a root request.
724724
pub referrer_source_offset: Option<i32>,
725725
pub phase: ModuleImportPhase,
726-
/// If true, the specifier in `reference` is a best-effort parse and
727-
/// needs to be properly resolved asynchronously during module loading.
728-
#[serde(default)]
729-
pub needs_resolve: bool,
730726
}
731727

732728
#[derive(Debug, PartialEq, Serialize, Deserialize)]

libs/core/modules/recursive_load.rs

Lines changed: 129 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ use crate::modules::RequestedModuleType;
3535
use crate::modules::ResolutionKind;
3636
use crate::modules::loaders::ModuleLoadReferrer;
3737
use crate::modules::map::ModuleMap;
38+
use crate::modules::map::NewModuleResult;
39+
use crate::modules::map::PendingModule;
3840
use crate::modules::module_map_data::ModuleSourceKind;
3941
use crate::source_map::SourceMapApplication;
4042
use crate::source_map::SourceMapper;
@@ -83,7 +85,7 @@ pub(crate) struct RecursiveModuleLoad {
8385
root_module_id: Option<ModuleId>,
8486
init: LoadInit,
8587
state: LoadState,
86-
module_map_rc: Rc<ModuleMap>,
88+
pub(crate) module_map_rc: Rc<ModuleMap>,
8789
pending: FuturesUnordered<Pin<Box<ModuleLoadFuture>>>,
8890
/// Pending async root resolution future.
8991
pending_root_resolve: Option<Pin<Box<ModuleResolveFuture>>>,
@@ -325,11 +327,20 @@ impl RecursiveModuleLoad {
325327
}
326328

327329
pub(crate) fn register_and_recurse(
330+
&mut self,
331+
scope: &mut v8::PinScope,
332+
module_request: &ModuleRequest,
333+
module_source: ModuleSource,
334+
) -> Result<RegisterOutcome, ModuleError> {
335+
self.register_and_recurse_impl(scope, module_request, module_source)
336+
}
337+
338+
fn register_and_recurse_impl(
328339
&mut self,
329340
scope: &mut v8::PinScope,
330341
module_request: &ModuleRequest,
331342
mut module_source: ModuleSource,
332-
) -> Result<(), ModuleError> {
343+
) -> Result<RegisterOutcome, ModuleError> {
333344
// Synthetic_esm specifiers carry a sentinel `ModuleSource` from
334345
// the load step. Divert to building the synthetic module from the
335346
// backing script's cached exports instead of going through
@@ -348,19 +359,8 @@ impl RecursiveModuleLoad {
348359
let exception = e.to_v8_error(scope);
349360
ModuleError::Exception(v8::Global::new(scope, exception))
350361
})?;
351-
self.register_and_recurse_inner(
352-
module_id,
353-
&module_request.reference,
354-
None,
355-
);
356-
if self.state == LoadState::LoadingRoot {
357-
self.root_module_id = Some(module_id);
358-
self.state = LoadState::LoadingImports;
359-
}
360-
if self.pending.is_empty() {
361-
self.state = LoadState::Done;
362-
}
363-
return Ok(());
362+
self.finalize_module(module_id, &module_request.reference, None);
363+
return Ok(RegisterOutcome::Done);
364364
}
365365

366366
if let Some(source_kind) =
@@ -379,7 +379,7 @@ impl RecursiveModuleLoad {
379379
if self.pending.is_empty() {
380380
self.state = LoadState::Done;
381381
}
382-
return Ok(());
382+
return Ok(RegisterOutcome::Done);
383383
}
384384
} else if module_request.phase == ModuleImportPhase::Source {
385385
let message = v8::String::new(
@@ -396,28 +396,51 @@ impl RecursiveModuleLoad {
396396

397397
let code = module_source.cheap_copy_code();
398398

399-
let module_id = self.module_map_rc.new_module(
399+
match self.module_map_rc.new_module_with_pending(
400400
scope,
401401
self.is_currently_loading_main_module(),
402402
self.is_dynamic_import(),
403403
module_source,
404-
)?;
404+
)? {
405+
NewModuleResult::Ready(module_id) => {
406+
self.finalize_module(module_id, &module_request.reference, Some(&code));
407+
Ok(RegisterOutcome::Done)
408+
}
409+
NewModuleResult::Pending(pending) => {
410+
Ok(RegisterOutcome::PendingFinalize {
411+
pending: Box::new(pending),
412+
reference: module_request.reference.clone(),
413+
code: Some(code),
414+
})
415+
}
416+
}
417+
}
405418

406-
self.register_and_recurse_inner(
407-
module_id,
408-
&module_request.reference,
409-
Some(&code),
410-
);
419+
/// Continue `register_and_recurse` after the caller has driven a
420+
/// [`PendingModule`] to completion via `ModuleMap::finalize_pending_module`.
421+
pub(crate) fn finalize_after_pending(
422+
&mut self,
423+
module_id: ModuleId,
424+
reference: &ModuleReference,
425+
code: Option<&ModuleSourceCode>,
426+
) {
427+
self.finalize_module(module_id, reference, code);
428+
}
411429

430+
fn finalize_module(
431+
&mut self,
432+
module_id: ModuleId,
433+
reference: &ModuleReference,
434+
code: Option<&ModuleSourceCode>,
435+
) {
436+
self.register_and_recurse_inner(module_id, reference, code);
412437
if self.state == LoadState::LoadingRoot {
413438
self.root_module_id = Some(module_id);
414439
self.state = LoadState::LoadingImports;
415440
}
416441
if self.pending.is_empty() {
417442
self.state = LoadState::Done;
418443
}
419-
420-
Ok(())
421444
}
422445

423446
fn register_and_recurse_inner(
@@ -490,9 +513,6 @@ impl RecursiveModuleLoad {
490513
let is_synchronous = self.is_synchronous();
491514
let requested_module_type =
492515
request.reference.requested_module_type.clone();
493-
let needs_resolve = request.needs_resolve;
494-
let raw_specifier = request.specifier_key.clone();
495-
let referrer_str = referrer.to_string();
496516
let module_map_rc = self.module_map_rc.clone();
497517
let fut = async move {
498518
// `visited_as_alias` unlike `visited` is checked as late as
@@ -506,49 +526,9 @@ impl RecursiveModuleLoad {
506526
return Ok(None);
507527
}
508528

509-
// If the import needs async resolution, resolve it now.
510-
let (resolved_specifier, request) = if needs_resolve {
511-
let raw = raw_specifier
512-
.as_deref()
513-
.unwrap_or(request.reference.specifier.as_str());
514-
let kind = if is_dynamic_import {
515-
ResolutionKind::DynamicImport
516-
} else {
517-
ResolutionKind::Import
518-
};
519-
let resolved = module_map_rc
520-
.resolve_async(raw, &referrer_str, kind)
521-
.await
522-
.map_err(|e| JsErrorBox::generic(e.to_string()))?;
523-
// The async resolver may map the raw specifier to a URL
524-
// that's already registered in the module map (e.g. a
525-
// bare `"path"` from an npm package resolving to
526-
// `node:path`, which was lazy-loaded earlier). In that
527-
// case the load step would try `take_lazy_esm_source`,
528-
// miss because the source was already consumed, and fall
529-
// through to `loader.load(node:path)` which doesn't
530-
// recognize the scheme. Short-circuit here so
531-
// `register_and_recurse_inner` later picks up the
532-
// existing module id via `get_id`.
533-
if module_map_rc
534-
.get_id(
535-
resolved.as_str(),
536-
&request.reference.requested_module_type,
537-
)
538-
.is_some()
539-
{
540-
visited_as_alias
541-
.borrow_mut()
542-
.insert(resolved.as_str().to_string());
543-
return Ok(None);
544-
}
545-
let mut resolved_request = request;
546-
resolved_request.reference.specifier = resolved.clone();
547-
resolved_request.needs_resolve = false;
548-
(resolved, resolved_request)
549-
} else {
550-
(request.reference.specifier.clone(), request)
551-
};
529+
// Imports are pre-resolved in `new_module_from_js_source` --
530+
// `request.reference.specifier` already holds the final URL.
531+
let resolved_specifier = request.reference.specifier.clone();
552532

553533
// First check if this module is a lazy-loaded ESM source
554534
// (embedded in binary but not snapshotted).
@@ -611,20 +591,93 @@ impl RecursiveModuleLoad {
611591
/// loaded module. Returns the root module ID once the full graph is loaded.
612592
pub(crate) async fn run_to_completion(
613593
mut self,
614-
mut on_module: impl FnMut(
594+
mut step: impl FnMut(
615595
&mut Self,
616-
&ModuleRequest,
617-
ModuleSource,
618-
) -> Result<(), CoreError>,
596+
RegisterStep<'_>,
597+
) -> Result<RegisterOutcome, CoreError>,
619598
) -> Result<ModuleId, CoreError> {
620599
while let Some(load_result) = self.next().await {
621600
let (request, source) = load_result?;
622-
on_module(&mut self, &request, source)?;
601+
match step(
602+
&mut self,
603+
RegisterStep::Register {
604+
request: &request,
605+
source,
606+
},
607+
)? {
608+
RegisterOutcome::Done => {}
609+
RegisterOutcome::PendingFinalize {
610+
pending,
611+
reference,
612+
code,
613+
} => {
614+
let module_map = self.module_map_rc.clone();
615+
let module_id = module_map
616+
.finalize_pending_module(*pending)
617+
.await
618+
.map_err(|e| match e {
619+
ModuleError::Core(core) => core,
620+
// Concrete / Exception variants shouldn't surface from the
621+
// finalize step (the only failure mode here is the resolve
622+
// hook erroring), but map them defensively.
623+
other => CoreError::from(JsErrorBox::generic(format!(
624+
"module finalize failed: {other:?}"
625+
))),
626+
})?;
627+
step(
628+
&mut self,
629+
RegisterStep::Finalize {
630+
module_id,
631+
reference: &reference,
632+
code: code.as_ref(),
633+
},
634+
)?;
635+
}
636+
}
623637
}
624638
Ok(self.root_module_id.expect("Root module should be loaded"))
625639
}
626640
}
627641

642+
/// Action requested by [`RecursiveModuleLoad::run_to_completion`] of its
643+
/// driver. A single `FnMut` closure handles both — passing one closure rather
644+
/// than two side-steps the borrow checker (both phases need `&mut isolate`,
645+
/// but they're never active simultaneously).
646+
pub(crate) enum RegisterStep<'a> {
647+
/// Compile a freshly-loaded module's source, resolve its imports, and
648+
/// register it. Returns [`RegisterOutcome`].
649+
Register {
650+
request: &'a ModuleRequest,
651+
source: ModuleSource,
652+
},
653+
/// Continue after `ModuleMap::finalize_pending_module` resolved the
654+
/// async-resolved imports of a previous `Register` step. Must return
655+
/// `RegisterOutcome::Done`.
656+
Finalize {
657+
module_id: ModuleId,
658+
reference: &'a ModuleReference,
659+
code: Option<&'a ModuleSourceCode>,
660+
},
661+
}
662+
663+
/// Outcome of a single `register_and_recurse` call. `PendingFinalize` lets the
664+
/// driver loop drop the V8 scope, await async resolves, then re-enter scope to
665+
/// complete registration -- without that detour we'd be stuck holding a scope
666+
/// across an `.await`.
667+
pub(crate) enum RegisterOutcome {
668+
/// Module was registered synchronously; no further work for this entry.
669+
Done,
670+
/// Module compiled but had child imports whose resolution returned
671+
/// `ModuleResolveResponse::Async`. The driver awaits
672+
/// `ModuleMap::finalize_pending_module(pending)` and then calls the
673+
/// `on_finalize` continuation with the resulting [`ModuleId`].
674+
PendingFinalize {
675+
pending: Box<PendingModule>,
676+
reference: ModuleReference,
677+
code: Option<ModuleSourceCode>,
678+
},
679+
}
680+
628681
impl RecursiveModuleLoad {
629682
/// Shared logic for Init and ResolvingRoot states once the root specifier
630683
/// has been resolved.
@@ -647,7 +700,6 @@ impl RecursiveModuleLoad {
647700
specifier_key: None,
648701
referrer_source_offset: None,
649702
phase,
650-
needs_resolve: false,
651703
};
652704
inner.root_module_reference = Some(module_request.reference.clone());
653705
let load_fut = if phase == ModuleImportPhase::Evaluation

0 commit comments

Comments
 (0)