Skip to content

Commit 932ac95

Browse files
authored
feat: add VirtualModulesPlugin (#11021)
1 parent fbd7c21 commit 932ac95

File tree

35 files changed

+929
-11
lines changed

35 files changed

+929
-11
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ strip = "debuginfo"
254254
[profile.release]
255255
codegen-units = 1
256256
debug = false
257-
# Performs fat LTO which attempts to perform optimizations across all crates within the dependency graph.
257+
# Performs "fat" LTO which attempts to perform optimizations across all crates within the dependency graph.
258258
lto = "fat"
259259
opt-level = 3
260260
panic = "abort"

crates/node_binding/napi-binding.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export declare class JsCompiler {
328328
/** Rebuild with the given option passed to the constructor */
329329
rebuild(changed_files: string[], removed_files: string[], callback: (err: null | Error) => void): void
330330
close(): Promise<void>
331+
getVirtualFileStore(): VirtualFileStore | null
331332
}
332333

333334
export declare class JsContextModuleFactoryAfterResolveData {
@@ -443,6 +444,7 @@ export declare class ModuleGraphConnection {
443444
export declare class NativeWatcher {
444445
constructor(options: NativeWatcherOptions)
445446
watch(files: [Array<string>, Array<string>], directories: [Array<string>, Array<string>], missing: [Array<string>, Array<string>], callback: (err: Error | null, result: NativeWatchResult) => void, callbackUndelayed: (path: string) => void): void
447+
triggerEvent(kind: 'change' | 'remove' | 'create', path: string): void
446448
/**
447449
* # Safety
448450
*
@@ -487,6 +489,12 @@ export declare class Sources {
487489
_get(sourceType: string): JsCompatSourceOwned | null
488490
}
489491

492+
export declare class VirtualFileStore {
493+
writeVirtualFileSync(path: string, content: string): void
494+
batchWriteVirtualFilesSync(files: Array<JsVirtualFile>): void
495+
}
496+
export type JsVirtualFileStore = VirtualFileStore
497+
490498
export declare function async(path: string, request: string): Promise<ResolveResult>
491499

492500
export interface BuiltinPlugin {
@@ -1479,6 +1487,11 @@ export interface JsTap {
14791487
stage: number
14801488
}
14811489

1490+
export interface JsVirtualFile {
1491+
path: string
1492+
content: string
1493+
}
1494+
14821495
export interface KnownAssetInfo {
14831496
/** if the asset can be long term cached forever (contains a hash) */
14841497
immutable?: boolean
@@ -2535,6 +2548,7 @@ export interface RawOptions {
25352548
amd?: string
25362549
bail: boolean
25372550
__references: Record<string, any>
2551+
__virtual_files?: Array<JsVirtualFile>
25382552
}
25392553

25402554
export interface RawOutputOptions {

crates/node_binding/rspack.wasi-browser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export const RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx
102102
export const ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData
103103
export const ResolverFactory = __napiModule.exports.ResolverFactory
104104
export const Sources = __napiModule.exports.Sources
105+
export const VirtualFileStore = __napiModule.exports.VirtualFileStore
106+
export const JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore
105107
export const async = __napiModule.exports.async
106108
export const BuiltinPluginName = __napiModule.exports.BuiltinPluginName
107109
export const cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace

crates/node_binding/rspack.wasi.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ module.exports.RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx
147147
module.exports.ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData
148148
module.exports.ResolverFactory = __napiModule.exports.ResolverFactory
149149
module.exports.Sources = __napiModule.exports.Sources
150+
module.exports.VirtualFileStore = __napiModule.exports.VirtualFileStore
151+
module.exports.JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore
150152
module.exports.async = __napiModule.exports.async
151153
module.exports.BuiltinPluginName = __napiModule.exports.BuiltinPluginName
152154
module.exports.cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace

crates/rspack_binding_api/src/lib.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ mod stats;
9797
mod swc;
9898
mod trace_event;
9999
mod utils;
100+
mod virtual_modules;
100101

101-
use std::{cell::RefCell, sync::Arc};
102+
use std::{
103+
cell::RefCell,
104+
sync::{Arc, RwLock},
105+
};
102106

103107
use napi::{CallContext, bindgen_prelude::*};
104108
pub use raw_options::{CustomPluginBuilder, register_custom_plugin};
@@ -137,6 +141,9 @@ use crate::{
137141
resolver_factory::JsResolverFactory,
138142
trace_event::RawTraceEvent,
139143
utils::callbackify,
144+
virtual_modules::{
145+
JsVirtualFileStore, TrieVirtualFileStore, VirtualFileStore, VirtualFileSystem,
146+
},
140147
};
141148

142149
// Export expected @rspack/core version
@@ -166,6 +173,7 @@ struct JsCompiler {
166173
include_dependencies_map: FxHashMap<String, FxHashMap<EntryOptions, BoxDependency>>,
167174
entry_dependencies_map: FxHashMap<String, FxHashMap<EntryOptions, BoxDependency>>,
168175
compiler_context: Arc<CompilerContext>,
176+
virtual_file_store: Option<Arc<RwLock<dyn VirtualFileStore>>>,
169177
}
170178

171179
#[napi]
@@ -218,12 +226,14 @@ impl JsCompiler {
218226
bp.append_to(env, &mut this, &mut plugins)?;
219227
}
220228

229+
let pnp = options.resolve.pnp.unwrap_or(false);
230+
let virtual_files = options.__virtual_files.take();
221231
let use_input_fs = options.experiments.use_input_file_system.take();
222232
let compiler_options: rspack_core::CompilerOptions = options.try_into().to_napi_result()?;
223233

224234
tracing::debug!(name:"normalized_options", options=?&compiler_options);
225235

226-
let input_file_system: Option<Arc<dyn ReadableFileSystem>> =
236+
let mut input_file_system: Option<Arc<dyn ReadableFileSystem>> =
227237
input_filesystem.and_then(|fs| {
228238
use_input_fs.and_then(|use_input_file_system| {
229239
let node_fs = NodeFileSystem::new(fs).expect("Failed to create readable filesystem");
@@ -245,6 +255,31 @@ impl JsCompiler {
245255
})
246256
});
247257

258+
let mut virtual_file_store: Option<Arc<RwLock<dyn VirtualFileStore>>> = None;
259+
if let Some(list) = virtual_files {
260+
let store = {
261+
let mut store = TrieVirtualFileStore::new();
262+
for f in list {
263+
store.write_file(f.path.as_str().into(), f.content.into());
264+
}
265+
Arc::new(RwLock::new(store))
266+
};
267+
input_file_system = input_file_system
268+
.map(|real_fs| {
269+
let binding: Arc<dyn ReadableFileSystem> =
270+
Arc::new(VirtualFileSystem::new(real_fs, store.clone()));
271+
binding
272+
})
273+
.or_else(|| {
274+
let binding: Arc<dyn ReadableFileSystem> = Arc::new(VirtualFileSystem::new(
275+
Arc::new(NativeFileSystem::new(pnp)),
276+
store.clone(),
277+
));
278+
Some(binding)
279+
});
280+
virtual_file_store = Some(store);
281+
}
282+
248283
resolver_factory_reference.update_options(
249284
input_file_system.clone(),
250285
compiler_options.resolve.clone(),
@@ -288,6 +323,7 @@ impl JsCompiler {
288323
include_dependencies_map: Default::default(),
289324
entry_dependencies_map: Default::default(),
290325
compiler_context,
326+
virtual_file_store,
291327
})
292328
})
293329
}
@@ -368,6 +404,14 @@ impl JsCompiler {
368404
})?;
369405
Ok(())
370406
}
407+
408+
#[napi]
409+
pub fn get_virtual_file_store(&self) -> Option<JsVirtualFileStore> {
410+
self
411+
.virtual_file_store
412+
.as_ref()
413+
.map(|store| JsVirtualFileStore::new(store.clone()))
414+
}
371415
}
372416

373417
struct RunGuard {

crates/rspack_binding_api/src/native_watcher.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use std::{boxed::Box, path::PathBuf};
1+
use std::{
2+
boxed::Box,
3+
path::{Path, PathBuf},
4+
};
25

36
use napi::bindgen_prelude::*;
47
use napi_derive::*;
5-
use rspack_fs::{FsWatcher, FsWatcherIgnored, FsWatcherOptions};
8+
use rspack_fs::{FsEventKind, FsWatcher, FsWatcherIgnored, FsWatcherOptions};
69
use rspack_paths::ArcPath;
710
use rspack_regex::RspackRegex;
811

@@ -106,6 +109,20 @@ impl NativeWatcher {
106109
Ok(())
107110
}
108111

112+
#[napi(ts_type = "(kind: 'change' | 'remove' | 'create', path: string): void")]
113+
pub fn trigger_event(&self, kind: String, path: String) {
114+
if let Some(kind) = match kind.as_str() {
115+
"change" => Some(FsEventKind::Change),
116+
"remove" => Some(FsEventKind::Remove),
117+
"create" => Some(FsEventKind::Create),
118+
_ => None,
119+
} {
120+
self
121+
.watcher
122+
.trigger_event(&ArcPath::from(AsRef::<Path>::as_ref(&path)), kind);
123+
}
124+
}
125+
109126
#[napi]
110127
/// # Safety
111128
///

crates/rspack_binding_api/src/raw_options/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub use raw_split_chunks::*;
3939
pub use raw_stats::*;
4040

4141
pub use crate::options::raw_resolve::*;
42+
use crate::virtual_modules::JsVirtualFile;
4243

4344
#[derive(Debug)]
4445
#[napi(object, object_to_js = false)]
@@ -61,6 +62,8 @@ pub struct RawOptions {
6162
pub bail: bool,
6263
#[napi(js_name = "__references", ts_type = "Record<string, any>")]
6364
pub __references: References,
65+
#[napi(js_name = "__virtual_files")]
66+
pub __virtual_files: Option<Vec<JsVirtualFile>>,
6467
}
6568

6669
impl TryFrom<RawOptions> for CompilerOptions {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use std::{
2+
fmt::Debug,
3+
sync::{Arc, RwLock},
4+
};
5+
6+
use rspack_fs::{FileMetadata, FilePermissions, ReadableFileSystem, Result};
7+
use rspack_paths::{Utf8Path, Utf8PathBuf};
8+
9+
use crate::virtual_modules::VirtualFileStore;
10+
11+
pub struct VirtualFileSystem {
12+
real_fs: Arc<dyn ReadableFileSystem>,
13+
virtual_file_store: Arc<RwLock<dyn VirtualFileStore>>,
14+
}
15+
16+
impl VirtualFileSystem {
17+
pub fn new(
18+
real_fs: Arc<dyn ReadableFileSystem>,
19+
virtual_file_store: Arc<RwLock<dyn VirtualFileStore>>,
20+
) -> Self {
21+
Self {
22+
real_fs,
23+
virtual_file_store,
24+
}
25+
}
26+
}
27+
28+
impl Debug for VirtualFileSystem {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
f.debug_struct("VirtualFileSystem")
31+
.field("virtual_file_store", &"<VirtualFileStore>")
32+
.field("real_fs", &"<ReadableFileSystem>")
33+
.finish()
34+
}
35+
}
36+
37+
#[async_trait::async_trait]
38+
impl ReadableFileSystem for VirtualFileSystem {
39+
async fn read(&self, path: &Utf8Path) -> Result<Vec<u8>> {
40+
if let Ok(store) = self.virtual_file_store.read()
41+
&& let Some(content) = store.get_file_content(path)
42+
{
43+
return Ok(content.clone());
44+
}
45+
46+
self.real_fs.read(path).await
47+
}
48+
49+
fn read_sync(&self, path: &Utf8Path) -> Result<Vec<u8>> {
50+
if let Ok(store) = self.virtual_file_store.read()
51+
&& let Some(content) = store.get_file_content(path)
52+
{
53+
return Ok(content.clone());
54+
}
55+
56+
self.real_fs.read_sync(path)
57+
}
58+
59+
async fn metadata(&self, path: &Utf8Path) -> Result<FileMetadata> {
60+
if let Ok(store) = self.virtual_file_store.read()
61+
&& let Some(metadata) = store.get_file_metadata(path)
62+
{
63+
return Ok(metadata.clone());
64+
}
65+
66+
self.real_fs.metadata(path).await
67+
}
68+
69+
fn metadata_sync(&self, path: &Utf8Path) -> Result<FileMetadata> {
70+
if let Ok(store) = self.virtual_file_store.read()
71+
&& let Some(metadata) = store.get_file_metadata(path)
72+
{
73+
return Ok(metadata.clone());
74+
}
75+
76+
self.real_fs.metadata_sync(path)
77+
}
78+
79+
async fn symlink_metadata(&self, path: &Utf8Path) -> Result<FileMetadata> {
80+
if let Ok(store) = self.virtual_file_store.read()
81+
&& let Some(metadata) = store.get_file_metadata(path)
82+
{
83+
return Ok(metadata.clone());
84+
}
85+
86+
self.real_fs.symlink_metadata(path).await
87+
}
88+
89+
async fn canonicalize(&self, path: &Utf8Path) -> Result<Utf8PathBuf> {
90+
if let Ok(store) = self.virtual_file_store.read()
91+
&& store.contains(path)
92+
{
93+
return Ok(path.to_path_buf());
94+
}
95+
96+
self.real_fs.canonicalize(path).await
97+
}
98+
99+
async fn read_dir(&self, dir: &Utf8Path) -> Result<Vec<String>> {
100+
if let Some(mut vlist) = self
101+
.virtual_file_store
102+
.read()
103+
.ok()
104+
.and_then(|store| store.read_dir(dir))
105+
{
106+
let mut list = self.real_fs.read_dir(dir).await.unwrap_or_default();
107+
list.append(&mut vlist);
108+
Ok(list)
109+
} else {
110+
self.real_fs.read_dir(dir).await
111+
}
112+
}
113+
114+
fn read_dir_sync(&self, dir: &Utf8Path) -> Result<Vec<String>> {
115+
if let Some(mut vlist) = self
116+
.virtual_file_store
117+
.read()
118+
.ok()
119+
.and_then(|store| store.read_dir(dir))
120+
{
121+
let mut list = self.real_fs.read_dir_sync(dir).unwrap_or_default();
122+
list.append(&mut vlist);
123+
Ok(list)
124+
} else {
125+
self.real_fs.read_dir_sync(dir)
126+
}
127+
}
128+
129+
async fn permissions(&self, path: &Utf8Path) -> Result<Option<FilePermissions>> {
130+
if let Ok(store) = self.virtual_file_store.read()
131+
&& store.contains(path)
132+
{
133+
return Ok(Some(FilePermissions::from_mode(0o700)));
134+
}
135+
136+
self.real_fs.permissions(path).await
137+
}
138+
}

0 commit comments

Comments
 (0)