diff --git a/Cargo.lock b/Cargo.lock index ad65a4ced1a7..23eba73985e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5157,6 +5157,7 @@ name = "rspack_plugin_rstest" version = "0.2.0" dependencies = [ "async-trait", + "regex", "rspack_cacheable", "rspack_core", "rspack_error", diff --git a/crates/rspack_core/src/dependency/dependency_type.rs b/crates/rspack_core/src/dependency/dependency_type.rs index e46e8b3b1a0d..e6a7099418fe 100644 --- a/crates/rspack_core/src/dependency/dependency_type.rs +++ b/crates/rspack_core/src/dependency/dependency_type.rs @@ -117,7 +117,9 @@ pub enum DependencyType { DllEntry, DelegatedSource, ExtractCSS, - Rstest, + RstestModulePath, + RstestMockModuleId, + RstestHoistMock, } impl DependencyType { @@ -191,7 +193,9 @@ impl DependencyType { DependencyType::ModuleDecorator => "module decorator", DependencyType::DelegatedSource => "delegated source", DependencyType::ExtractCSS => "extract css", - DependencyType::Rstest => "rstest", + DependencyType::RstestModulePath => "rstest module path", + DependencyType::RstestMockModuleId => "rstest mock module id", + DependencyType::RstestHoistMock => "rstest hoist mock", } } } diff --git a/crates/rspack_core/src/init_fragment.rs b/crates/rspack_core/src/init_fragment.rs index b6337017f107..c186d9394ad8 100644 --- a/crates/rspack_core/src/init_fragment.rs +++ b/crates/rspack_core/src/init_fragment.rs @@ -40,7 +40,6 @@ pub enum InitFragmentKey { ModuleDecorator(String /* module_id */), ESMFakeNamespaceObjectFragment(String), Const(String), - // ModuleName(String), } impl InitFragmentKey { diff --git a/crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs index 280e7c8ba27d..5523d270fc53 100644 --- a/crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs @@ -375,6 +375,11 @@ async fn compilation( RuntimeRequirementsDependencyTemplate::template_type(), Arc::new(RuntimeRequirementsDependencyTemplate::default()), ); + // Rstest + compilation.set_dependency_factory( + DependencyType::RstestMockModuleId, + params.normal_module_factory.clone(), + ); Ok(()) } diff --git a/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs b/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs index 3fec11b0c44f..9309fba651ad 100644 --- a/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs +++ b/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs @@ -254,7 +254,7 @@ pub struct JavascriptParser<'parser> { pub(crate) current_tag_info: Option, pub(crate) local_modules: Vec, // ===== scope info ======= - pub(crate) in_try: bool, + pub in_try: bool, pub(crate) in_short_hand: bool, pub(super) definitions: ScopeInfoId, pub(crate) top_level_scope: TopLevelScope, diff --git a/crates/rspack_plugin_rstest/Cargo.toml b/crates/rspack_plugin_rstest/Cargo.toml index ce5fa10448cb..18327fb261a0 100644 --- a/crates/rspack_plugin_rstest/Cargo.toml +++ b/crates/rspack_plugin_rstest/Cargo.toml @@ -9,6 +9,7 @@ version = "0.2.0" [dependencies] async-trait = { workspace = true } +regex = { workspace = true } rspack_cacheable = { workspace = true } rspack_core = { workspace = true } rspack_error = { workspace = true } diff --git a/crates/rspack_plugin_rstest/src/lib.rs b/crates/rspack_plugin_rstest/src/lib.rs index 70896438c06f..fe29409aa721 100644 --- a/crates/rspack_plugin_rstest/src/lib.rs +++ b/crates/rspack_plugin_rstest/src/lib.rs @@ -1,4 +1,6 @@ #![feature(let_chains)] +mod mock_hoist_dependency; +mod mock_module_id_dependency; mod module_path_name_dependency; mod parser_plugin; mod plugin; diff --git a/crates/rspack_plugin_rstest/src/mock_hoist_dependency.rs b/crates/rspack_plugin_rstest/src/mock_hoist_dependency.rs new file mode 100644 index 000000000000..52d3900435a8 --- /dev/null +++ b/crates/rspack_plugin_rstest/src/mock_hoist_dependency.rs @@ -0,0 +1,93 @@ +use rspack_cacheable::{cacheable, cacheable_dyn, with::Skip}; +use rspack_core::{ + AsContextDependency, AsModuleDependency, DependencyCodeGeneration, DependencyRange, + DependencyTemplate, DependencyTemplateType, DependencyType, InitFragmentExt, InitFragmentKey, + InitFragmentStage, NormalInitFragment, TemplateContext, TemplateReplaceSource, +}; +use swc_core::common::Span; + +#[cacheable] +#[derive(Debug, Clone)] +pub struct MockHoistDependency { + #[cacheable(with=Skip)] + call_expr_span: Span, + #[cacheable(with=Skip)] + callee_span: Span, + request: String, +} + +impl MockHoistDependency { + pub fn new(call_expr_span: Span, callee_span: Span, request: String) -> Self { + Self { + call_expr_span, + callee_span, + request, + } + } +} + +#[cacheable_dyn] +impl DependencyCodeGeneration for MockHoistDependency { + fn dependency_template(&self) -> Option { + Some(MockHoistDependencyTemplate::template_type()) + } +} + +impl AsModuleDependency for MockHoistDependency {} +impl AsContextDependency for MockHoistDependency {} + +#[cacheable] +#[derive(Debug, Clone, Default)] +pub struct MockHoistDependencyTemplate; + +impl MockHoistDependencyTemplate { + pub fn template_type() -> DependencyTemplateType { + DependencyTemplateType::Dependency(DependencyType::RstestHoistMock) + } +} + +impl DependencyTemplate for MockHoistDependencyTemplate { + fn render( + &self, + dep: &dyn DependencyCodeGeneration, + source: &mut TemplateReplaceSource, + code_generatable_context: &mut TemplateContext, + ) { + let TemplateContext { init_fragments, .. } = code_generatable_context; + + let dep = dep + .as_any() + .downcast_ref::() + .expect("ModulePathNameDependencyTemplate can only be applied to ModulePathNameDependency"); + let request = &dep.request; + + // Placeholder of hoist target. + let init = NormalInitFragment::new( + format!("/* RSTEST:MOCK_PLACEHOLDER:{request} */;"), + InitFragmentStage::StageConstants, + 0, + InitFragmentKey::Const(format!("retest mock_hoist {request}")), + None, + ); + + // Start before hoist. + let callee_range: DependencyRange = dep.callee_span.into(); + source.replace( + callee_range.start, + callee_range.end, + format!("/* RSTEST:MOCK_HOIST_START:{request} */__webpack_require__.set_mock").as_ref(), + None, + ); + + // End before hoist. + let range: DependencyRange = dep.call_expr_span.into(); + source.replace( + range.end, // count the trailing semicolon + range.end, + format!("/* RSTEST:MOCK_HOIST_END:{request} */").as_ref(), + None, + ); + + init_fragments.push(init.boxed()); + } +} diff --git a/crates/rspack_plugin_rstest/src/mock_module_id_dependency.rs b/crates/rspack_plugin_rstest/src/mock_module_id_dependency.rs new file mode 100644 index 000000000000..a46d37841967 --- /dev/null +++ b/crates/rspack_plugin_rstest/src/mock_module_id_dependency.rs @@ -0,0 +1,147 @@ +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::{ + module_id, AsContextDependency, Dependency, DependencyCategory, DependencyCodeGeneration, + DependencyId, DependencyRange, DependencyTemplate, DependencyTemplateType, DependencyType, + ExtendedReferencedExport, FactorizeInfo, ModuleDependency, ModuleGraph, RuntimeSpec, + TemplateContext, TemplateReplaceSource, +}; + +#[cacheable] +#[derive(Debug, Clone)] +pub struct MockModuleIdDependency { + pub id: DependencyId, + pub request: String, + pub weak: bool, + range: DependencyRange, + optional: bool, + factorize_info: FactorizeInfo, + category: DependencyCategory, +} + +impl MockModuleIdDependency { + pub fn new( + request: String, + range: DependencyRange, + weak: bool, + optional: bool, + category: DependencyCategory, + ) -> Self { + Self { + range, + request, + weak, + optional, + id: DependencyId::new(), + factorize_info: Default::default(), + category, + } + } +} + +#[cacheable_dyn] +impl Dependency for MockModuleIdDependency { + fn id(&self) -> &DependencyId { + &self.id + } + + fn category(&self) -> &DependencyCategory { + &self.category + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::RstestMockModuleId + } + + fn range(&self) -> Option<&DependencyRange> { + Some(&self.range) + } + + fn get_referenced_exports( + &self, + _module_graph: &ModuleGraph, + _runtime: Option<&RuntimeSpec>, + ) -> Vec { + vec![] + } + + fn could_affect_referencing_module(&self) -> rspack_core::AffectType { + rspack_core::AffectType::True + } +} + +#[cacheable_dyn] +impl ModuleDependency for MockModuleIdDependency { + fn request(&self) -> &str { + &self.request + } + + fn user_request(&self) -> &str { + &self.request + } + + fn weak(&self) -> bool { + self.weak + } + + fn get_optional(&self) -> bool { + self.optional + } + + fn set_request(&mut self, request: String) { + self.request = request; + } + + fn factorize_info(&self) -> &FactorizeInfo { + &self.factorize_info + } + + fn factorize_info_mut(&mut self) -> &mut FactorizeInfo { + &mut self.factorize_info + } +} + +#[cacheable_dyn] +impl DependencyCodeGeneration for MockModuleIdDependency { + fn dependency_template(&self) -> Option { + Some(MockModuleIdDependencyTemplate::template_type()) + } +} + +impl AsContextDependency for MockModuleIdDependency {} + +#[cacheable] +#[derive(Debug, Clone, Default)] +pub struct MockModuleIdDependencyTemplate; + +impl MockModuleIdDependencyTemplate { + pub fn template_type() -> DependencyTemplateType { + DependencyTemplateType::Dependency(DependencyType::RstestMockModuleId) + } +} + +impl DependencyTemplate for MockModuleIdDependencyTemplate { + fn render( + &self, + dep: &dyn DependencyCodeGeneration, + source: &mut TemplateReplaceSource, + code_generatable_context: &mut TemplateContext, + ) { + let dep = dep + .as_any() + .downcast_ref::() + .expect("MockModuleIdDependencyTemplate should only be used for MockModuleIdDependency"); + + source.replace( + dep.range.start, + dep.range.end, + module_id( + code_generatable_context.compilation, + &dep.id, + &dep.request, + dep.weak, + ) + .as_str(), + None, + ); + } +} diff --git a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs index 6fa625f905b5..600f29c67024 100644 --- a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs +++ b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs @@ -40,7 +40,7 @@ pub struct ModulePathNameDependencyTemplate; impl ModulePathNameDependencyTemplate { pub fn template_type() -> DependencyTemplateType { - DependencyTemplateType::Dependency(DependencyType::Rstest) + DependencyTemplateType::Dependency(DependencyType::RstestModulePath) } } diff --git a/crates/rspack_plugin_rstest/src/parser_plugin.rs b/crates/rspack_plugin_rstest/src/parser_plugin.rs index 15008e3902b3..f06f08dacf0f 100644 --- a/crates/rspack_plugin_rstest/src/parser_plugin.rs +++ b/crates/rspack_plugin_rstest/src/parser_plugin.rs @@ -1,15 +1,25 @@ use rspack_core::{ConstDependency, SpanExt}; use rspack_plugin_javascript::{ - utils, utils::eval, visitors::JavascriptParser, JavascriptParserPlugin, + utils::{self, eval}, + visitors::JavascriptParser, + JavascriptParserPlugin, +}; +use swc_core::{ + common::Spanned, + ecma::ast::{CallExpr, Ident, MemberExpr, UnaryExpr}, }; -use swc_core::common::Spanned; -use crate::module_path_name_dependency::{ModulePathNameDependency, NameType}; +use crate::{ + mock_hoist_dependency::MockHoistDependency, + mock_module_id_dependency::MockModuleIdDependency, + module_path_name_dependency::{ModulePathNameDependency, NameType}, +}; const DIR_NAME: &str = "__dirname"; const FILE_NAME: &str = "__filename"; const IMPORT_META_DIRNAME: &str = "import.meta.dirname"; const IMPORT_META_FILENAME: &str = "import.meta.filename"; +const RS_MOCK: &str = "rs.mock"; #[derive(PartialEq)] enum ModulePathType { @@ -21,7 +31,50 @@ enum ModulePathType { pub struct RstestParserPlugin; impl RstestParserPlugin { - fn import_meta(&self, parser: &mut JavascriptParser, r#type: ModulePathType) -> String { + fn process_hoist_mock(&self, parser: &mut JavascriptParser, call_expr: &CallExpr) { + match call_expr.args.len() { + // TODO: mock a module to __mocks__ + 1 => {} + // mock a module + 2 => { + let first_arg = &call_expr.args[0]; + let second_arg = &call_expr.args[1]; + + if first_arg.spread.is_some() || second_arg.spread.is_some() { + return; + } + + if let Some(lit) = first_arg.expr.as_lit() { + if let Some(lit) = lit.as_str() { + parser + .presentational_dependencies + .push(Box::new(MockHoistDependency::new( + call_expr.span(), + call_expr.callee.span(), + lit.value.to_string(), + ))); + + parser + .dependencies + .push(Box::new(MockModuleIdDependency::new( + lit.value.to_string(), + first_arg.span().into(), + false, + parser.in_try, + rspack_core::DependencyCategory::Esm, + ))); + } else { + panic!("`rs.mock` function expects a string literal as the first argument"); + } + } + } + _ => { + panic!("`rs.mock` function expects 1 or 2 arguments, got more than 2"); + } + } + } + + fn process_import_meta(&self, parser: &mut JavascriptParser, r#type: ModulePathType) -> String { if r#type == ModulePathType::FileName { if let Some(resource_path) = &parser.resource_data.resource_path { format!("'{}'", resource_path.clone().into_string()) @@ -42,10 +95,24 @@ impl RstestParserPlugin { } impl JavascriptParserPlugin for RstestParserPlugin { + fn call( + &self, + parser: &mut rspack_plugin_javascript::visitors::JavascriptParser, + call_expr: &CallExpr, + for_name: &str, + ) -> Option { + if for_name == RS_MOCK { + self.process_hoist_mock(parser, call_expr); + Some(false) + } else { + None + } + } + fn identifier( &self, parser: &mut rspack_plugin_javascript::visitors::JavascriptParser, - ident: &swc_core::ecma::ast::Ident, + ident: &Ident, _for_name: &str, ) -> Option { let str = ident.sym.as_str(); @@ -73,7 +140,7 @@ impl JavascriptParserPlugin for RstestParserPlugin { fn evaluate_typeof<'a>( &self, _parser: &mut JavascriptParser, - expr: &'a swc_core::ecma::ast::UnaryExpr, + expr: &'a UnaryExpr, for_name: &str, ) -> Option> { let mut evaluated = None; @@ -92,13 +159,13 @@ impl JavascriptParserPlugin for RstestParserPlugin { ) -> Option> { if ident == IMPORT_META_DIRNAME { Some(eval::evaluate_to_string( - self.import_meta(parser, ModulePathType::DirName), + self.process_import_meta(parser, ModulePathType::DirName), start, end, )) } else if ident == IMPORT_META_FILENAME { Some(eval::evaluate_to_string( - self.import_meta(parser, ModulePathType::FileName), + self.process_import_meta(parser, ModulePathType::FileName), start, end, )) @@ -110,7 +177,7 @@ impl JavascriptParserPlugin for RstestParserPlugin { fn r#typeof( &self, parser: &mut JavascriptParser, - unary_expr: &swc_core::ecma::ast::UnaryExpr, + unary_expr: &UnaryExpr, for_name: &str, ) -> Option { if for_name == IMPORT_META_DIRNAME || for_name == IMPORT_META_FILENAME { @@ -130,11 +197,11 @@ impl JavascriptParserPlugin for RstestParserPlugin { fn member( &self, parser: &mut JavascriptParser, - member_expr: &swc_core::ecma::ast::MemberExpr, + member_expr: &MemberExpr, for_name: &str, ) -> Option { if for_name == IMPORT_META_DIRNAME { - let result = self.import_meta(parser, ModulePathType::DirName); + let result = self.process_import_meta(parser, ModulePathType::DirName); parser .presentational_dependencies .push(Box::new(ConstDependency::new( @@ -144,7 +211,7 @@ impl JavascriptParserPlugin for RstestParserPlugin { ))); Some(true) } else if for_name == IMPORT_META_FILENAME { - let result = self.import_meta(parser, ModulePathType::FileName); + let result = self.process_import_meta(parser, ModulePathType::FileName); parser .presentational_dependencies .push(Box::new(ConstDependency::new( diff --git a/crates/rspack_plugin_rstest/src/plugin.rs b/crates/rspack_plugin_rstest/src/plugin.rs index b045ff5dbfce..a6c739278366 100644 --- a/crates/rspack_plugin_rstest/src/plugin.rs +++ b/crates/rspack_plugin_rstest/src/plugin.rs @@ -5,8 +5,10 @@ use std::{ use async_trait::async_trait; use rspack_core::{ - ApplyContext, Compilation, CompilationParams, CompilerCompilation, CompilerOptions, ModuleType, - NormalModuleFactoryParser, ParserAndGenerator, ParserOptions, Plugin, PluginContext, + rspack_sources::{BoxSource, ReplaceSource, SourceExt}, + ApplyContext, Compilation, CompilationParams, CompilationProcessAssets, CompilerCompilation, + CompilerOptions, ModuleType, NormalModuleFactoryParser, ParserAndGenerator, ParserOptions, + Plugin, PluginContext, }; use rspack_error::Result; use rspack_hook::{plugin, plugin_hook}; @@ -15,6 +17,8 @@ use rspack_plugin_javascript::{ }; use crate::{ + mock_hoist_dependency::MockHoistDependencyTemplate, + mock_module_id_dependency::MockModuleIdDependencyTemplate, module_path_name_dependency::ModulePathNameDependencyTemplate, parser_plugin::RstestParserPlugin, }; @@ -40,6 +44,49 @@ impl RstestPlugin { pub fn new(options: RstestPluginOptions) -> Self { Self::new_inner(options) } + + fn update_source( + &self, + old: BoxSource, + replace_map: &std::collections::HashMap, + ) -> BoxSource { + let old_source = old.to_owned(); + let mut replace = ReplaceSource::new(old_source); + + for (mocked_id, pos) in replace_map { + if let ( + Some(content_start), + Some(content_end), + Some(placeholder_start), + Some(placeholder_end), + Some(content_with_flag_start), + Some(content_with_flag_end), + ) = ( + pos.content_start, + pos.content_end, + pos.placeholder_start, + pos.placeholder_end, + pos.content_with_flag_start, + pos.content_with_flag_end, + ) { + let content = old.source()[content_start..content_end].to_string(); + replace.replace( + placeholder_start as u32, + placeholder_end as u32 + 1, // consider the trailing semicolon + &format! {"// [Rstest mock hoist] \"{mocked_id}\"\n{content};\n\n"}, + None, + ); + replace.replace( + content_with_flag_start as u32, + content_with_flag_end as u32, + "", + None, + ); + } + } + + replace.boxed() + } } #[plugin_hook(NormalModuleFactoryParser for RstestPlugin)] @@ -68,6 +115,87 @@ async fn compilation( ModulePathNameDependencyTemplate::template_type(), Arc::new(ModulePathNameDependencyTemplate::default()), ); + + compilation.set_dependency_template( + MockHoistDependencyTemplate::template_type(), + Arc::new(MockHoistDependencyTemplate::default()), + ); + + compilation.set_dependency_template( + MockModuleIdDependencyTemplate::template_type(), + Arc::new(MockModuleIdDependencyTemplate::default()), + ); + Ok(()) +} + +#[derive(Debug)] +struct MockFlagPos { + content_start: Option, + content_with_flag_start: Option, + content_end: Option, + content_with_flag_end: Option, + placeholder_start: Option, + placeholder_end: Option, +} + +#[plugin_hook(CompilationProcessAssets for RstestPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_ADDITIONAL)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + let mut files = vec![]; + + for chunk in compilation.chunk_by_ukey.values() { + for file in chunk.files() { + files.push(file.clone()); + } + } + + let regex = + regex::Regex::new(r"\/\* RSTEST:MOCK_(.*?):(.*?) \*\/").expect("should initialize `Regex`"); + let mut pos_map: std::collections::HashMap = + std::collections::HashMap::new(); + + for file in files { + let _res = compilation.update_asset(file.as_str(), |old, info| { + let content = old.source().to_string(); + let captures: Vec<_> = regex.captures_iter(&content).collect(); + + for c in captures { + let [Some(full), Some(t), Some(request)] = [c.get(0), c.get(1), c.get(2)] else { + continue; + }; + + let entry = pos_map + .entry(request.as_str().to_string()) + .or_insert_with(|| MockFlagPos { + content_start: None, + content_end: None, + content_with_flag_start: None, + content_with_flag_end: None, + placeholder_start: None, + placeholder_end: None, + }); + + if t.as_str() == "HOIST_START" { + entry.content_with_flag_start = Some(full.start()); + entry.content_start = Some(full.end()); + } else if t.as_str() == "HOIST_END" { + entry.content_with_flag_end = Some(full.end()); + entry.content_end = Some(full.start()); + } else if t.as_str() == "PLACEHOLDER" { + entry.placeholder_start = Some(full.start()); + entry.placeholder_end = Some(full.end()); + } else { + panic!( + "Unknown rstest mock type: {}", + c.get(1).map_or("", |m| m.as_str()) + ); + } + } + + let new = self.update_source(old, &pos_map); + Ok((new, info)) + }); + } + Ok(()) } @@ -90,6 +218,12 @@ impl Plugin for RstestPlugin { .normal_module_factory_hooks .parser .tap(nmf_parser::new(self)); + + ctx + .context + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); } Ok(()) diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/bar.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/bar.js new file mode 100644 index 000000000000..2f142159ee83 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/bar.js @@ -0,0 +1 @@ +export const value = 'bar'; diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/barrel.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/barrel.js new file mode 100644 index 000000000000..86615c0b8fe5 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/barrel.js @@ -0,0 +1,2 @@ +export { value as foo } from './foo.js'; +export { value as bar } from './bar.js'; diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/foo.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/foo.js new file mode 100644 index 000000000000..481f5a3fb18f --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/foo.js @@ -0,0 +1 @@ +export const value = 'foo'; diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/index.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/index.js new file mode 100644 index 000000000000..c7a167132381 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/index.js @@ -0,0 +1,17 @@ +const TOP_OF_FILE = 1 + +import { foo, bar } from './barrel' + +rs.mock('./foo', () => { + return { value: 'mockedFoo' } +}) + +rs.mock('./bar', () => { + return { value: 'mockedBar' } +}) + + +it('should mock modules', () => { + expect(foo).toBe('mockedFoo') + expect(bar).toBe('mockedBar') +}) diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/rspack.config.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/rspack.config.js new file mode 100644 index 000000000000..cbee1aec52aa --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/rspack.config.js @@ -0,0 +1,68 @@ +const { RstestPlugin } = require("@rspack/core"); + +class RstestSimpleRuntimePlugin { + constructor() {} + + apply(compiler) { + const { RuntimeModule, RuntimeGlobals } = compiler.rspack; + class RetestImportRuntimeModule extends RuntimeModule { + constructor() { + super("rstest runtime"); + } + + generate() { + return ` +__webpack_require__.set_mock = (id, modFactory) => { + __webpack_module_cache__[id] = { exports: modFactory() } +};`; + } + } + + compiler.hooks.thisCompilation.tap( + "RstestSimpleRuntimePlugin", + compilation => { + compilation.hooks.additionalTreeRuntimeRequirements.tap( + "RstestSimpleRuntimePlugin", + chunk => { + compilation.addRuntimeModule( + chunk, + new RetestImportRuntimeModule() + ); + } + ); + } + ); + } +} + +/** @type {import("@rspack/core").Configuration} */ +module.exports = [ + { + entry: "./index.js", + target: "node", + node: { + __filename: false, + __dirname: false + }, + optimization: { + mangleExports: false + }, + plugins: [ + new RstestSimpleRuntimePlugin(), + new RstestPlugin({ + injectModulePathName: true + }) + ] + }, + { + entry: "./test.js", + target: "node", + node: { + __filename: false, + __dirname: false + }, + optimization: { + mangleExports: false + } + } +]; diff --git a/packages/rspack-test-tools/tests/configCases/rstest/mock/test.js b/packages/rspack-test-tools/tests/configCases/rstest/mock/test.js new file mode 100644 index 000000000000..af900eabb66e --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/rstest/mock/test.js @@ -0,0 +1,10 @@ +const path = require('path'); +const fs = require('fs'); + +const file = path.resolve(__dirname, 'bundle0.js') +const content = fs.readFileSync(file, 'utf-8'); + +it ('mocked modules should be hoisted', () => { + const afterTopOfFile = content.indexOf('TOP_OF_FILE'); + expect(afterTopOfFile).toBeGreaterThan(content.lastIndexOf('__webpack_require__.set_mock')); +}) diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/index.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/index.js similarity index 71% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/index.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/index.js index 6c0732acdc30..aae15b861a9d 100644 --- a/packages/rspack-test-tools/tests/configCases/plugins/rstest/index.js +++ b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/index.js @@ -1,30 +1,30 @@ const fs = require('fs'); const path = require('path'); +const sourceDir = path.resolve(__dirname, "../../../../configCases/rstest/module-path-names/src"); + it("Insert module path names with concatenateModules", () => { - const sourceDir = path.resolve(__dirname, "../../../../"); const content = fs.readFileSync(path.resolve(__dirname, "modulePathName.js"), "utf-8"); // __dirname and __filename renamed after concatenation - expect(content).toContain(`const foo_filename = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src/foo.js")}'`); - expect(content).toMatch(`const foo_dirname = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src")}'`); + expect(content).toContain(`const foo_filename = '${path.resolve(sourceDir, "./foo.js")}'`); + expect(content).toMatch(`const foo_dirname = '${path.resolve(sourceDir, ".")}'`); - expect(content).toMatch(`const metaFile1 = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src/foo.js")}'`) - expect(content).toMatch(`const metaDir1 = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src")}'`) + expect(content).toMatch(`const metaFile1 = '${path.resolve(sourceDir, "./foo.js")}'`) + expect(content).toMatch(`const metaDir1 = '${path.resolve(sourceDir, ".")}'`) expect(content).toContain(`const typeofMetaDir = 'string'`); expect(content).toContain(`const typeofMetaFile = 'string'`); }); it("Insert module path names without concatenateModules", () => { - const sourceDir = path.resolve(__dirname, "../../../../"); const content = fs.readFileSync(path.resolve(__dirname, "modulePathNameWithoutConcatenate.js"), "utf-8"); - expect(content).toContain(`const __filename = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src/foo.js")}'`); - expect(content).toMatch(`const __dirname = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src")}'`); + expect(content).toContain(`const __filename = '${path.resolve(sourceDir, "./foo.js")}'`); + expect(content).toMatch(`const __dirname = '${path.resolve(sourceDir, ".")}'`); expect(content.match(/const __dirname = /g).length).toBe(2); expect(content.match(/const __filename = /g).length).toBe(2); - expect(content).toMatch(`const metaFile1 = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src/foo.js")}'`) - expect(content).toMatch(`const metaDir1 = '${path.resolve(sourceDir, "./configCases/plugins/rstest/src")}'`) + expect(content).toMatch(`const metaFile1 = '${path.resolve(sourceDir, "./foo.js")}'`) + expect(content).toMatch(`const metaDir1 = '${path.resolve(sourceDir, ".")}'`) expect(content).toContain(`const typeofMetaDir = 'string'`); expect(content).toContain(`const typeofMetaFile = 'string'`); diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/rspack.config.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/rspack.config.js similarity index 93% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/rspack.config.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/rspack.config.js index 82e3451a5d19..8543e1a8ecab 100644 --- a/packages/rspack-test-tools/tests/configCases/plugins/rstest/rspack.config.js +++ b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/rspack.config.js @@ -1,5 +1,6 @@ const { RstestPlugin } = require("@rspack/core"); +/** @type {import("@rspack/core").Configuration} */ module.exports = [ { entry: "./src/index.js", diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/src/bar.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/bar.js similarity index 100% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/src/bar.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/bar.js diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/src/foo.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/foo.js similarity index 100% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/src/foo.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/foo.js diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/src/index.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/index.js similarity index 100% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/src/index.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/src/index.js diff --git a/packages/rspack-test-tools/tests/configCases/plugins/rstest/test.config.js b/packages/rspack-test-tools/tests/configCases/rstest/module-path-names/test.config.js similarity index 100% rename from packages/rspack-test-tools/tests/configCases/plugins/rstest/test.config.js rename to packages/rspack-test-tools/tests/configCases/rstest/module-path-names/test.config.js