Skip to content

Commit 2b0ec54

Browse files
committed
feat: allow rerouting ESM dependencies in succeedModule
1 parent 5b66bef commit 2b0ec54

15 files changed

Lines changed: 312 additions & 26 deletions

File tree

crates/node_binding/napi-binding.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export declare class Dependency {
216216
get type(): string
217217
get category(): string
218218
get request(): string | undefined
219+
set request(request: string)
219220
get attributes(): Record<string, string> | undefined
220221
get critical(): boolean
221222
set critical(val: boolean)

crates/rspack_binding_api/src/dependency.rs

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
use std::{cell::RefCell, ptr::NonNull};
1+
use std::{
2+
cell::RefCell,
3+
ptr::NonNull,
4+
sync::{
5+
Arc,
6+
atomic::{AtomicBool, Ordering},
7+
},
8+
};
29

310
use napi::{
411
Either, Env,
512
bindgen_prelude::{Array, Object, ToNapiValue},
613
};
714
use napi_derive::napi;
8-
use rspack_core::{Compilation, CompilationId, DependencyId, internal};
15+
use rspack_core::{BoxDependency, Compilation, CompilationId, DependencyId, internal};
916
use rspack_napi::OneShotInstanceRef;
1017
use rspack_plugin_javascript::dependency::{
1118
CommonJsExportRequireDependency, ESMExportImportedSpecifierDependency,
@@ -22,6 +29,7 @@ pub struct Dependency {
2229
pub(crate) compilation_id: Option<CompilationId>,
2330
pub(crate) dependency_id: DependencyId,
2431
pub(crate) dependency: NonNull<dyn rspack_core::Dependency>,
32+
mutable_access: Option<Arc<AtomicBool>>,
2533
}
2634

2735
impl Dependency {
@@ -43,12 +51,41 @@ impl Dependency {
4351
}
4452
})
4553
} else {
54+
if self
55+
.mutable_access
56+
.as_ref()
57+
.is_some_and(|access| !access.load(Ordering::Acquire))
58+
{
59+
return Err(napi::Error::from_reason(
60+
"Unable to access dependency outside the succeedModule hook callback",
61+
));
62+
}
4663
// SAFETY:
4764
// We need to make users aware in the documentation that values obtained within the JS hook callback should not be used outside the scope of the callback.
4865
// We do not guarantee that the memory pointed to by the pointer remains valid when used outside the scope.
4966
f(unsafe { self.dependency.as_ref() }, None)
5067
}
5168
}
69+
70+
fn with_mut<R>(
71+
&mut self,
72+
f: impl FnOnce(&mut dyn rspack_core::Dependency) -> napi::Result<R>,
73+
) -> napi::Result<R> {
74+
let Some(mutable_access) = &self.mutable_access else {
75+
return Err(napi::Error::from_reason(
76+
"Dependency.request can only be changed inside the succeedModule hook callback",
77+
));
78+
};
79+
if !mutable_access.load(Ordering::Acquire) {
80+
return Err(napi::Error::from_reason(
81+
"Dependency.request cannot be changed after the succeedModule hook callback",
82+
));
83+
}
84+
85+
// SAFETY: The succeedModule hook exclusively borrows the pending dependencies until the
86+
// JavaScript callback settles. `mutable_access` revokes this pointer before that borrow ends.
87+
f(unsafe { self.dependency.as_mut() })
88+
}
5289
}
5390

5491
#[napi]
@@ -92,6 +129,25 @@ impl Dependency {
92129
})
93130
}
94131

132+
#[napi(setter)]
133+
pub fn set_request(&mut self, request: String) -> napi::Result<()> {
134+
self.with_mut(|dependency| {
135+
if let Some(dependency) = dependency.downcast_mut::<ESMImportSpecifierDependency>() {
136+
dependency.set_request(request.clone().into());
137+
return Ok(());
138+
}
139+
if let Some(dependency) = dependency.downcast_mut::<ESMExportImportedSpecifierDependency>() {
140+
dependency.set_request(request.into());
141+
return Ok(());
142+
}
143+
144+
Err(napi::Error::from_reason(format!(
145+
"Dependency.request cannot be changed for dependency type '{}'",
146+
dependency.dependency_type().as_str()
147+
)))
148+
})
149+
}
150+
95151
#[napi(getter, ts_return_type = "Record<string, string> | undefined")]
96152
pub fn attributes<'a>(&mut self, env: &'a Env) -> napi::Result<Either<Object<'a>, ()>> {
97153
self.with_ref(|dependency, _| {
@@ -164,7 +220,28 @@ impl Dependency {
164220
Either::B(())
165221
}
166222
}
167-
None => Either::B(()),
223+
None => {
224+
if let Some(dependency) =
225+
dependency.downcast_ref::<ESMExportImportedSpecifierDependency>()
226+
{
227+
let ids = dependency.ids();
228+
let mut arr = env.create_array(ids.len() as u32)?;
229+
for (i, v) in ids.iter().enumerate() {
230+
arr.set(i as u32, v.as_str())?;
231+
}
232+
Either::A(arr)
233+
} else if let Some(dependency) = dependency.downcast_ref::<ESMImportSpecifierDependency>()
234+
{
235+
let ids = dependency.ids();
236+
let mut arr = env.create_array(ids.len() as u32)?;
237+
for (i, v) in ids.iter().enumerate() {
238+
arr.set(i as u32, v.as_str())?;
239+
}
240+
Either::A(arr)
241+
} else {
242+
Either::B(())
243+
}
244+
}
168245
})
169246
})
170247
}
@@ -189,6 +266,7 @@ pub struct DependencyWrapper {
189266
dependency: NonNull<dyn rspack_core::Dependency>,
190267
compilation_id: CompilationId,
191268
registered_compilation_id: Option<CompilationId>,
269+
mutable_access: Option<Arc<AtomicBool>>,
192270
}
193271

194272
impl DependencyWrapper {
@@ -220,6 +298,22 @@ impl DependencyWrapper {
220298
dependency,
221299
compilation_id,
222300
registered_compilation_id: compilation.map(Compilation::id),
301+
mutable_access: None,
302+
}
303+
}
304+
305+
fn new_pending(
306+
dependency_id: DependencyId,
307+
dependency: NonNull<dyn rspack_core::Dependency>,
308+
compilation_id: CompilationId,
309+
mutable_access: Arc<AtomicBool>,
310+
) -> Self {
311+
Self {
312+
dependency_id,
313+
dependency,
314+
compilation_id,
315+
registered_compilation_id: None,
316+
mutable_access: Some(mutable_access),
223317
}
224318
}
225319

@@ -254,6 +348,7 @@ impl ToNapiValue for DependencyWrapper {
254348
let instance = &mut **r;
255349
instance.compilation_id = val.registered_compilation_id;
256350
instance.dependency = val.dependency;
351+
instance.mutable_access = val.mutable_access;
257352

258353
ToNapiValue::to_napi_value(env, r)
259354
}
@@ -262,6 +357,7 @@ impl ToNapiValue for DependencyWrapper {
262357
compilation_id: val.registered_compilation_id,
263358
dependency_id: val.dependency_id,
264359
dependency: val.dependency,
360+
mutable_access: val.mutable_access,
265361
};
266362
let r = vacant_entry.insert(OneShotInstanceRef::new(env, js_dependency)?);
267363
ToNapiValue::to_napi_value(env, r)
@@ -271,3 +367,80 @@ impl ToNapiValue for DependencyWrapper {
271367
}
272368
}
273369
}
370+
371+
#[derive(Debug, Clone, Copy)]
372+
struct PendingDependency {
373+
id: DependencyId,
374+
dependency: NonNull<dyn rspack_core::Dependency>,
375+
}
376+
377+
#[derive(Debug, Clone)]
378+
pub(crate) struct PendingDependencies {
379+
dependencies: Vec<PendingDependency>,
380+
compilation_id: CompilationId,
381+
mutable_access: Arc<AtomicBool>,
382+
}
383+
384+
pub(crate) struct PendingDependenciesGuard(Arc<AtomicBool>);
385+
386+
impl Drop for PendingDependenciesGuard {
387+
fn drop(&mut self) {
388+
self.0.store(false, Ordering::Release);
389+
}
390+
}
391+
392+
// SAFETY: The succeedModule hook owns the pending dependency slice while the JavaScript callback
393+
// runs. The callback is awaited, and `mutable_access` is revoked before that borrow is released.
394+
unsafe impl Send for PendingDependencies {}
395+
unsafe impl Sync for PendingDependencies {}
396+
397+
impl PendingDependencies {
398+
pub(crate) fn new(dependencies: &mut [BoxDependency], compilation_id: CompilationId) -> Self {
399+
let dependencies = dependencies
400+
.iter_mut()
401+
.map(|dependency| {
402+
let id = *dependency.id();
403+
let dependency = dependency.as_mut() as *mut dyn rspack_core::Dependency;
404+
// SAFETY: The pointer is only exposed while the hook retains the source slice.
405+
let dependency = unsafe {
406+
std::mem::transmute::<
407+
*mut (dyn rspack_core::Dependency + '_),
408+
*mut (dyn rspack_core::Dependency + 'static),
409+
>(dependency)
410+
};
411+
// SAFETY: `dependency` came from a valid mutable reference.
412+
let dependency = unsafe { NonNull::new_unchecked(dependency) };
413+
PendingDependency { id, dependency }
414+
})
415+
.collect();
416+
417+
Self {
418+
dependencies,
419+
compilation_id,
420+
mutable_access: Arc::new(AtomicBool::new(true)),
421+
}
422+
}
423+
424+
pub(crate) fn is_active(&self) -> bool {
425+
self.mutable_access.load(Ordering::Acquire)
426+
}
427+
428+
pub(crate) fn guard(&self) -> PendingDependenciesGuard {
429+
PendingDependenciesGuard(self.mutable_access.clone())
430+
}
431+
432+
pub(crate) fn wrappers(&self) -> Vec<DependencyWrapper> {
433+
self
434+
.dependencies
435+
.iter()
436+
.map(|dependency| {
437+
DependencyWrapper::new_pending(
438+
dependency.id,
439+
dependency.dependency,
440+
self.compilation_id,
441+
self.mutable_access.clone(),
442+
)
443+
})
444+
.collect()
445+
}
446+
}

crates/rspack_binding_api/src/module.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::{
2929
codegen_result::JsCodegenerationResults,
3030
compiler_scoped_tsfn::CompilerScopedTsFnHandle,
3131
define_symbols,
32-
dependency::DependencyWrapper,
32+
dependency::{DependencyWrapper, PendingDependencies},
3333
modules::{ConcatenatedModule, ContextModule, ExternalModule, NormalModule},
3434
source::{JsSourceFromJs, JsSourceToJs},
3535
};
@@ -297,6 +297,7 @@ struct OriginalSourceNapiRef {
297297
pub struct Module {
298298
pub(crate) identifier: ModuleIdentifier,
299299
ptr: Option<NonNull<dyn rspack_core::Module>>,
300+
pending_dependencies: Option<PendingDependencies>,
300301
compiler_id: CompilerId,
301302
original_source_ref: Option<OriginalSourceNapiRef>,
302303
pub(crate) build_info_ref: Option<WeakRef>,
@@ -496,6 +497,12 @@ impl Module {
496497

497498
#[napi(getter, ts_return_type = "Dependency[]")]
498499
pub fn dependencies(&mut self) -> napi::Result<Vec<DependencyWrapper>> {
500+
if let Some(pending_dependencies) = &self.pending_dependencies
501+
&& pending_dependencies.is_active()
502+
{
503+
return Ok(pending_dependencies.wrappers());
504+
}
505+
499506
self.with_ref(|compilation, module| {
500507
let Some(module_graph) = compilation.try_get_module_graph() else {
501508
return Err(napi::Error::from_reason(
@@ -624,6 +631,7 @@ pub struct ModuleObject {
624631
type_id: TypeId,
625632
identifier: ModuleIdentifier,
626633
ptr: Option<NonNull<dyn rspack_core::Module>>,
634+
pending_dependencies: Option<PendingDependencies>,
627635
compiler_id: CompilerId,
628636
}
629637

@@ -636,6 +644,7 @@ impl ModuleObject {
636644
type_id: module.as_any().type_id(),
637645
identifier: module.identifier(),
638646
ptr: None,
647+
pending_dependencies: None,
639648
compiler_id,
640649
}
641650
}
@@ -647,6 +656,23 @@ impl ModuleObject {
647656
type_id: module.as_any().type_id(),
648657
identifier: module.identifier(),
649658
ptr: Some(module_ptr),
659+
pending_dependencies: None,
660+
compiler_id,
661+
}
662+
}
663+
664+
pub(crate) fn with_ptr_and_dependencies(
665+
module_ptr: NonNull<dyn rspack_core::Module>,
666+
compiler_id: CompilerId,
667+
pending_dependencies: PendingDependencies,
668+
) -> Self {
669+
let module = unsafe { module_ptr.as_ref() };
670+
671+
Self {
672+
type_id: module.as_any().type_id(),
673+
identifier: module.identifier(),
674+
ptr: Some(module_ptr),
675+
pending_dependencies: Some(pending_dependencies),
650676
compiler_id,
651677
}
652678
}
@@ -710,6 +736,7 @@ impl ToNapiValue for ModuleObject {
710736
Either5::E(module) => &mut **module,
711737
};
712738
instance.ptr = val.ptr;
739+
instance.pending_dependencies = val.pending_dependencies;
713740
match instance_ref {
714741
Either5::A(r) => ToNapiValue::to_napi_value(env, r),
715742
Either5::B(r) => ToNapiValue::to_napi_value(env, r),
@@ -723,6 +750,7 @@ impl ToNapiValue for ModuleObject {
723750
identifier: val.identifier,
724751
compiler_id: val.compiler_id,
725752
ptr: val.ptr,
753+
pending_dependencies: val.pending_dependencies,
726754
original_source_ref: None,
727755
build_info_ref: Default::default(),
728756
};
@@ -781,30 +809,35 @@ impl FromNapiValue for ModuleObject {
781809
type_id: TypeId::of::<rspack_core::NormalModule>(),
782810
identifier: normal_module.module.identifier,
783811
ptr: normal_module.module.ptr,
812+
pending_dependencies: normal_module.module.pending_dependencies.clone(),
784813
compiler_id: normal_module.module.compiler_id,
785814
},
786815
Either5::B(concatenated_module) => Self {
787816
type_id: TypeId::of::<rspack_core::ConcatenatedModule>(),
788817
identifier: concatenated_module.module.identifier,
789818
ptr: concatenated_module.module.ptr,
819+
pending_dependencies: concatenated_module.module.pending_dependencies.clone(),
790820
compiler_id: concatenated_module.module.compiler_id,
791821
},
792822
Either5::C(context_module) => Self {
793823
type_id: TypeId::of::<rspack_core::ContextModule>(),
794824
identifier: context_module.module.identifier,
795825
ptr: context_module.module.ptr,
826+
pending_dependencies: context_module.module.pending_dependencies.clone(),
796827
compiler_id: context_module.module.compiler_id,
797828
},
798829
Either5::D(external_module) => Self {
799830
type_id: TypeId::of::<rspack_core::ExternalModule>(),
800831
identifier: external_module.module.identifier,
801832
ptr: external_module.module.ptr,
833+
pending_dependencies: external_module.module.pending_dependencies.clone(),
802834
compiler_id: external_module.module.compiler_id,
803835
},
804836
Either5::E(module) => Self {
805837
type_id: TypeId::of::<dyn rspack_core::Module>(),
806838
identifier: module.identifier,
807839
ptr: module.ptr,
840+
pending_dependencies: module.pending_dependencies.clone(),
808841
compiler_id: module.compiler_id,
809842
},
810843
})

0 commit comments

Comments
 (0)