diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 55e8b5ef660..4eb70c19af1 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -668,10 +668,11 @@ export interface RawExternalItemFnResult { } export interface RawExternalItemValue { - type: "string" | "bool" | "array" + type: "string" | "bool" | "array" | "object" stringPayload?: string boolPayload?: boolean arrayPayload?: Array + objectPayload?: Record> } export interface RawExternalsPluginOptions { diff --git a/crates/rspack_binding_options/src/options/raw_external.rs b/crates/rspack_binding_options/src/options/raw_external.rs index 6f003e19813..23c0a09b8a8 100644 --- a/crates/rspack_binding_options/src/options/raw_external.rs +++ b/crates/rspack_binding_options/src/options/raw_external.rs @@ -62,11 +62,12 @@ impl Debug for RawExternalItem { #[serde(rename_all = "camelCase")] #[napi(object)] pub struct RawExternalItemValue { - #[napi(ts_type = r#""string" | "bool" | "array""#)] + #[napi(ts_type = r#""string" | "bool" | "array" | "object""#)] pub r#type: String, pub string_payload: Option, pub bool_payload: Option, pub array_payload: Option>, + pub object_payload: Option>>, } impl From for ExternalItemValue { @@ -87,6 +88,13 @@ impl From for ExternalItemValue { .array_payload .expect("should have a array_payload when RawExternalItemValue.type is \"array\""), ), + "object" => Self::Object( + value + .object_payload + .expect("should have a object_payload when RawExternalItemValue.type is \"object\"") + .into_iter() + .collect(), + ), _ => unreachable!(), } } diff --git a/crates/rspack_core/src/dependency/runtime_template.rs b/crates/rspack_core/src/dependency/runtime_template.rs index 054934c503a..fa9a9efd394 100644 --- a/crates/rspack_core/src/dependency/runtime_template.rs +++ b/crates/rspack_core/src/dependency/runtime_template.rs @@ -1,10 +1,9 @@ -use once_cell::sync::Lazy; -use regex::Regex; use swc_core::ecma::atoms::JsWord; use crate::{ - Compilation, DependencyId, ExportsType, FakeNamespaceObjectMode, InitFragmentStage, ModuleGraph, - ModuleIdentifier, NormalInitFragment, RuntimeGlobals, TemplateContext, + property_access, Compilation, DependencyId, ExportsType, FakeNamespaceObjectMode, + InitFragmentStage, ModuleGraph, ModuleIdentifier, NormalInitFragment, RuntimeGlobals, + TemplateContext, }; pub fn export_from_import( @@ -32,7 +31,7 @@ pub fn export_from_import( { match exports_type { ExportsType::Dynamic => { - return format!("{import_var}_default{}", property_access(&export_name, 1)); + return format!("{import_var}_default{}", property_access(export_name, 1)); } ExportsType::DefaultOnly | ExportsType::DefaultWithNamed => { export_name = export_name[1..].to_vec(); @@ -41,7 +40,7 @@ pub fn export_from_import( } } else if !export_name.is_empty() { if matches!(exports_type, ExportsType::DefaultOnly) { - return format!("/* non-default import from non-esm module */undefined\n{}", property_access(&export_name, 1)); + return format!("/* non-default import from non-esm module */undefined\n{}", property_access(export_name, 1)); } else if !matches!(exports_type, ExportsType::Namespace) && let Some(first_export_name) = export_name.get(0) && first_export_name == "__esModule" { return "/* __esModule */true".to_string(); } @@ -102,69 +101,6 @@ pub fn get_exports_type_with_strict( .get_exports_type(strict) } -static SAFE_IDENTIFIER_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^[_a-zA-Z$][_a-zA-Z$0-9]*$").expect("should init regex")); -const RESERVED_IDENTIFIER: [&str; 37] = [ - "break", - "case", - "catch", - "class", - "const", - "continue", - "debugger", - "default", - "delete", - "do", - "else", - "enum", - "export", - "extends", - "false", - "finally", - "for", - "function", - "if", - "import", - "in", - "instanceof", - "new", - "null", - "package", - "return", - "super", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "var", - "void", - "while", - "with", -]; - -fn property_access(o: &Vec, mut start: usize) -> String { - let mut str = String::default(); - while start < o.len() { - let property = &o[start]; - if SAFE_IDENTIFIER_REGEX.is_match(property) && !RESERVED_IDENTIFIER.contains(&property.as_ref()) - { - str.push_str(format!(".{property}").as_str()); - } else { - str.push_str( - format!( - "[{}]", - serde_json::to_string(property).expect("should render property") - ) - .as_str(), - ); - } - start += 1; - } - str -} - pub fn module_id_expr(request: &str, module_id: &str) -> String { format!( "/* {} */{}", diff --git a/crates/rspack_core/src/external_module.rs b/crates/rspack_core/src/external_module.rs index 6173ed53a26..3f5515d0487 100644 --- a/crates/rspack_core/src/external_module.rs +++ b/crates/rspack_core/src/external_module.rs @@ -1,12 +1,15 @@ use std::borrow::Cow; use std::hash::Hash; +use std::iter; use rspack_error::{internal_error, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; use rspack_hash::RspackHash; use rspack_identifier::{Identifiable, Identifier}; -use serde::{Serialize, Serializer}; +use rustc_hash::FxHashMap as HashMap; +use serde::Serialize; use crate::{ + property_access, rspack_sources::{BoxSource, RawSource, Source, SourceExt}, to_identifier, BuildContext, BuildInfo, BuildMetaExportsType, BuildResult, ChunkInitFragments, ChunkUkey, CodeGenerationDataUrl, CodeGenerationResult, Compilation, Context, ExternalType, @@ -17,46 +20,61 @@ use crate::{ static EXTERNAL_MODULE_JS_SOURCE_TYPES: &[SourceType] = &[SourceType::JavaScript]; static EXTERNAL_MODULE_CSS_SOURCE_TYPES: &[SourceType] = &[SourceType::Css]; +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum ExternalRequest { + Single(ExternalRequestValue), + Map(HashMap), +} + #[derive(Debug, Clone)] -pub struct ExternalRequest(pub Vec); +pub struct ExternalRequestValue { + primary: String, + rest: Option>, +} -impl Serialize for ExternalRequest { - fn serialize(&self, serializer: S) -> std::result::Result { - self.0.serialize(serializer) +impl Serialize for ExternalRequestValue { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + self.iter().collect::>().serialize(serializer) } } -impl ExternalRequest { - pub fn as_str(&self) -> &str { - // we're sure array have more than one element,because it is valid in js side - self.0.get(0).expect("should have at least element") +impl ExternalRequestValue { + pub fn new(primary: String, rest: Option>) -> Self { + Self { primary, rest } } - pub fn as_array(&self) -> &Vec { - &self.0 + + pub fn primary(&self) -> &str { + &self.primary } -} -pub fn property_access(o: &Vec, mut start: usize) -> String { - let mut str = String::default(); - while start < o.len() { - let property = &o[start]; - str.push_str(format!(r#"["{property}"]"#).as_str()); - start += 1; + + pub fn rest(&self) -> Option<&[String]> { + self.rest.as_deref() + } + + pub fn iter(&self) -> impl Iterator { + if let Some(rest) = &self.rest { + iter::once(&self.primary).chain(rest) + } else { + iter::once(&self.primary).chain(&[]) + } } - str } fn get_source_for_global_variable_external( - request: &ExternalRequest, + variable_names: &ExternalRequestValue, external_type: &ExternalType, ) -> String { - let object_lookup = property_access(request.as_array(), 0); + let object_lookup = property_access(variable_names.iter(), 0); format!("{external_type}{object_lookup}") } -fn get_source_for_default_case(_optional: bool, request: &ExternalRequest) -> String { - let request = request.as_array(); - let variable_name = request.get(0).expect("should have at least one element"); - let object_lookup = property_access(request, 1); +fn get_source_for_default_case(_optional: bool, request: &ExternalRequestValue) -> String { + let variable_name = request.primary(); + let object_lookup = property_access(request.iter(), 1); format!("{variable_name}{object_lookup}") } @@ -70,10 +88,10 @@ pub struct ExternalModule { } impl ExternalModule { - pub fn new(request: Vec, external_type: ExternalType, user_request: String) -> Self { + pub fn new(request: ExternalRequest, external_type: ExternalType, user_request: String) -> Self { Self { id: Identifier::from(format!("external {external_type} {request:?}")), - request: ExternalRequest(request), + request, external_type, user_request, } @@ -83,50 +101,59 @@ impl ExternalModule { &self.external_type } - fn get_source_for_commonjs(&self) -> String { - let request = &self.request.as_array(); - let module_name = request.get(0).expect("should have at least one element"); + fn get_request_and_external_type(&self) -> (Option<&ExternalRequestValue>, &ExternalType) { + match &self.request { + ExternalRequest::Single(request) => (Some(request), &self.external_type), + ExternalRequest::Map(map) => (map.get(&self.external_type), &self.external_type), + } + } + + fn get_source_for_commonjs(&self, module_and_specifiers: &ExternalRequestValue) -> String { + let module_name = module_and_specifiers.primary(); format!( "module.exports = require('{}'){}", module_name, - property_access(request, 1) + property_access(module_and_specifiers.iter(), 1) ) } - fn get_source_for_import(&self, compilation: &Compilation) -> String { + fn get_source_for_import( + &self, + module_and_specifiers: &ExternalRequestValue, + compilation: &Compilation, + ) -> String { format!( "module.exports = {}({})", compilation.options.output.import_function_name, - serde_json::to_string(&self.request).expect("invalid json to_string") + serde_json::to_string(module_and_specifiers.primary()).expect("invalid json to_string") ) } - pub fn get_source( + fn get_source( &self, compilation: &Compilation, + request: Option<&ExternalRequestValue>, + external_type: &ExternalType, ) -> (BoxSource, ChunkInitFragments, RuntimeGlobals) { let mut chunk_init_fragments: ChunkInitFragments = Default::default(); let mut runtime_requirements: RuntimeGlobals = Default::default(); let source = match self.external_type.as_str() { - "this" => format!( + "this" if let Some(request) = request => format!( "module.exports = (function() {{ return {}; }}())", - get_source_for_global_variable_external(&self.request, &self.external_type) + get_source_for_global_variable_external(request, external_type) ), - "window" | "self" => format!( + "window" | "self" if let Some(request) = request => format!( "module.exports = {}", - get_source_for_global_variable_external(&self.request, &self.external_type) + get_source_for_global_variable_external(request, external_type) ), - "global" => format!( + "global" if let Some(request) = request=> format!( "module.exports ={} ", - get_source_for_global_variable_external( - &self.request, - &compilation.options.output.global_object - ) + get_source_for_global_variable_external(request, &compilation.options.output.global_object) ), - "commonjs" | "commonjs2" | "commonjs-module" | "commonjs-static" => { - self.get_source_for_commonjs() + "commonjs" | "commonjs2" | "commonjs-module" | "commonjs-static" if let Some(request) = request => { + self.get_source_for_commonjs(request) } - "node-commonjs" => { + "node-commonjs" if let Some(request) = request => { if compilation.options.output.module { chunk_init_fragments .entry("external module node-commonjs".to_string()) @@ -138,10 +165,10 @@ impl ExternalModule { )); format!( "__WEBPACK_EXTERNAL_createRequire(import.meta.url)('{}')", - self.request.as_str() + request.primary() ) } else { - self.get_source_for_commonjs() + self.get_source_for_commonjs(request) } } "amd" | "amd-require" | "umd" | "umd2" | "system" | "jsonp" => { @@ -155,14 +182,14 @@ impl ExternalModule { to_identifier(id) ) } - "import" => self.get_source_for_import(compilation), - "var" | "promise" | "const" | "let" | "assign" => { + "import" if let Some(request) = request => self.get_source_for_import(request, compilation), + "var" | "promise" | "const" | "let" | "assign" if let Some(request) = request => { format!( "module.exports = {}", - get_source_for_default_case(false, &self.request) + get_source_for_default_case(false, request) ) } - "module" => { + "module" if let Some(request) = request => { if compilation.options.output.module { let id = compilation .module_graph @@ -175,7 +202,7 @@ impl ExternalModule { .or_insert(NormalInitFragment::new( format!( "import * as __WEBPACK_EXTERNAL_MODULE_{identifier}__ from '{}';\n", - self.request.as_str() + request.primary() ), InitFragmentStage::StageHarmonyImports, None, @@ -188,7 +215,7 @@ impl ExternalModule { RuntimeGlobals::DEFINE_PROPERTY_GETTERS, ) } else { - self.get_source_for_import(compilation) + self.get_source_for_import(request, compilation) } } // TODO "script" @@ -289,34 +316,34 @@ impl Module for ExternalModule { fn code_generation(&self, compilation: &Compilation) -> Result { let mut cgr = CodeGenerationResult::default(); + let (request, external_type) = self.get_request_and_external_type(); match self.external_type.as_str() { - "asset" => { + "asset" if let Some(request) = request => { cgr.add( SourceType::JavaScript, RawSource::from(format!( "module.exports = {};", - serde_json::to_string(&self.request.as_str()) - .map_err(|e| internal_error!(e.to_string()))? + serde_json::to_string(request.primary()).map_err(|e| internal_error!(e.to_string()))? )) .boxed(), ); - cgr.data.insert(CodeGenerationDataUrl::new( - self.request.as_str().to_string(), - )); + cgr + .data + .insert(CodeGenerationDataUrl::new(request.primary().to_string())); } - "css-import" => { + "css-import" if let Some(request) = request => { cgr.add( SourceType::Css, RawSource::from(format!( "@import url({});", - serde_json::to_string(&self.request.as_str()) - .map_err(|e| internal_error!(e.to_string()))? + serde_json::to_string(request.primary()).map_err(|e| internal_error!(e.to_string()))? )) .boxed(), ); } _ => { - let (source, chunk_init_fragments, runtime_requirements) = self.get_source(compilation); + let (source, chunk_init_fragments, runtime_requirements) = + self.get_source(compilation, request, external_type); cgr.add(SourceType::JavaScript, source); cgr.chunk_init_fragments = chunk_init_fragments; cgr.runtime_requirements.insert(runtime_requirements); diff --git a/crates/rspack_core/src/lib.rs b/crates/rspack_core/src/lib.rs index e226e09f5c1..cc1ae13c6f2 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -1,4 +1,5 @@ #![feature(let_chains)] +#![feature(if_let_guard)] #![feature(iter_intersperse)] #![feature(box_patterns)] #![feature(anonymous_lifetime_in_impl_trait)] diff --git a/crates/rspack_core/src/options/externals.rs b/crates/rspack_core/src/options/externals.rs index 3f1f2f418a2..11dc1e21ab4 100644 --- a/crates/rspack_core/src/options/externals.rs +++ b/crates/rspack_core/src/options/externals.rs @@ -10,8 +10,9 @@ pub type Externals = Vec; #[derive(Debug)] pub enum ExternalItemValue { String(String), + Array(Vec), Bool(bool), - Array(Vec), // TODO: Record + Object(HashMap>), } pub type ExternalItemObject = HashMap; diff --git a/crates/rspack_core/src/utils/mod.rs b/crates/rspack_core/src/utils/mod.rs index 2d265bc1ce3..a1603ba39be 100644 --- a/crates/rspack_core/src/utils/mod.rs +++ b/crates/rspack_core/src/utils/mod.rs @@ -6,6 +6,9 @@ use rustc_hash::FxHashMap as HashMap; mod identifier; pub use identifier::*; +mod property_access; +pub use property_access::*; + mod comment; pub use comment::*; diff --git a/crates/rspack_core/src/utils/property_access.rs b/crates/rspack_core/src/utils/property_access.rs new file mode 100644 index 00000000000..d54b64d05df --- /dev/null +++ b/crates/rspack_core/src/utils/property_access.rs @@ -0,0 +1,64 @@ +use once_cell::sync::Lazy; +use regex::Regex; + +static SAFE_IDENTIFIER_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^[_a-zA-Z$][_a-zA-Z$0-9]*$").expect("should init regex")); +const RESERVED_IDENTIFIER: [&str; 37] = [ + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "package", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", +]; + +pub fn property_access>(o: impl IntoIterator, start: usize) -> String { + o.into_iter() + .skip(start) + .fold(String::default(), |mut str, property| { + let property = property.as_ref(); + if SAFE_IDENTIFIER_REGEX.is_match(property) && !RESERVED_IDENTIFIER.contains(&property) { + str.push_str(format!(".{property}").as_str()); + } else { + str.push_str( + format!( + "[{}]", + serde_json::to_string(property).expect("should render property") + ) + .as_str(), + ); + } + str + }) +} diff --git a/crates/rspack_plugin_externals/src/plugin.rs b/crates/rspack_plugin_externals/src/plugin.rs index ecda6aabaf6..1cce3486b3d 100644 --- a/crates/rspack_plugin_externals/src/plugin.rs +++ b/crates/rspack_plugin_externals/src/plugin.rs @@ -3,9 +3,10 @@ use std::fmt::Debug; use once_cell::sync::Lazy; use regex::Regex; use rspack_core::{ - ExternalItem, ExternalItemFnCtx, ExternalItemValue, ExternalModule, ExternalType, FactorizeArgs, - ModuleDependency, ModuleExt, ModuleFactoryResult, NormalModuleFactoryContext, Plugin, - PluginContext, PluginFactorizeHookOutput, + ExternalItem, ExternalItemFnCtx, ExternalItemValue, ExternalModule, ExternalRequest, + ExternalRequestValue, ExternalType, FactorizeArgs, ModuleDependency, ModuleExt, + ModuleFactoryResult, NormalModuleFactoryContext, Plugin, PluginContext, + PluginFactorizeHookOutput, }; static UNSPECIFIED_EXTERNAL_TYPE_REGEXP: Lazy = @@ -36,32 +37,76 @@ impl ExternalsPlugin { r#type: Option, dependency: &dyn ModuleDependency, ) -> Option { - let mut external_module_config: Vec = match config { - ExternalItemValue::String(config) => vec![config.clone()], + let (external_module_config, external_module_type) = match config { + ExternalItemValue::String(config) => { + let (external_type, config) = + if let Some((external_type, new_config)) = parse_external_type_from_str(config) { + (external_type, new_config) + } else { + (self.r#type.clone(), config.to_owned()) + }; + ( + ExternalRequest::Single(ExternalRequestValue::new(config, None)), + external_type, + ) + } + ExternalItemValue::Array(arr) => { + let mut iter = arr.iter().peekable(); + let primary = iter.next()?; + let (external_type, primary) = + if let Some((external_type, new_primary)) = parse_external_type_from_str(primary) { + (external_type, new_primary) + } else { + (self.r#type.clone(), primary.to_owned()) + }; + let rest = iter.peek().is_some().then(|| iter.cloned().collect()); + ( + ExternalRequest::Single(ExternalRequestValue::new(primary, rest)), + external_type, + ) + } ExternalItemValue::Bool(config) => { if *config { - vec![dependency.request().to_string()] + ( + ExternalRequest::Single(ExternalRequestValue::new( + dependency.request().to_string(), + None, + )), + self.r#type.clone(), + ) } else { return None; } } - ExternalItemValue::Array(config) => config.to_vec(), + ExternalItemValue::Object(map) => ( + ExternalRequest::Map( + map + .iter() + .map(|(k, v)| { + let mut iter = v.iter().peekable(); + let primary = iter.next().expect("should have at least one value"); + let rest = iter.peek().is_some().then(|| iter.cloned().collect()); + ( + k.clone(), + ExternalRequestValue::new(primary.to_owned(), rest), + ) + }) + .collect(), + ), + self.r#type.clone(), + ), }; - let external_module_type = r#type.unwrap_or_else(|| { - let head = external_module_config - .get_mut(0) - .expect("should have at least one element"); - if UNSPECIFIED_EXTERNAL_TYPE_REGEXP.is_match(head.as_str()) - && let Some((t, c)) = head.clone().as_str().split_once(' ') { - *head = c.to_string(); - return t.to_owned(); + fn parse_external_type_from_str(v: &str) -> Option<(ExternalType, String)> { + if UNSPECIFIED_EXTERNAL_TYPE_REGEXP.is_match(v) && let Some((t, c)) = v.split_once(' ') { + return Some((t.to_owned(), c.to_owned())); } - self.r#type.clone() - }); + None + } + Some(ExternalModule::new( external_module_config, - external_module_type, + r#type.unwrap_or(external_module_type), dependency.request().to_owned(), )) } diff --git a/crates/rspack_plugin_library/src/amd_library_plugin.rs b/crates/rspack_plugin_library/src/amd_library_plugin.rs index 57e6bab1f21..6904ced6ca5 100644 --- a/crates/rspack_plugin_library/src/amd_library_plugin.rs +++ b/crates/rspack_plugin_library/src/amd_library_plugin.rs @@ -7,7 +7,7 @@ use rspack_core::{ PluginJsChunkHashHookOutput, PluginRenderHookOutput, RenderArgs, RuntimeGlobals, SourceType, }; -use super::utils::{external_arguments, external_dep_array}; +use super::utils::{external_arguments, externals_dep_array}; use crate::utils::normalize_name; #[derive(Debug)] @@ -70,7 +70,7 @@ impl Plugin for AmdLibraryPlugin { }) }) .collect::>(); - let external_deps_array = external_dep_array(&modules); + let externals_deps_array = externals_dep_array(&modules)?; let external_arguments = external_arguments(&modules, compilation); let mut fn_start = format!("function({external_arguments}){{\n"); if compilation.options.output.iife || !chunk.has_runtime(&compilation.chunk_group_by_ukey) { @@ -80,7 +80,7 @@ impl Plugin for AmdLibraryPlugin { let mut source = ConcatSource::default(); if self.require_as_wrapper { source.add(RawSource::from(format!( - "require({external_deps_array}, {fn_start}" + "require({externals_deps_array}, {fn_start}" ))); } else if let Some(name) = name { let normalize_name = compilation.get_path( @@ -93,13 +93,13 @@ impl Plugin for AmdLibraryPlugin { ), ); source.add(RawSource::from(format!( - "define('{normalize_name}', {external_deps_array}, {fn_start}" + "define('{normalize_name}', {externals_deps_array}, {fn_start}" ))); } else if modules.is_empty() { source.add(RawSource::from(format!("define({fn_start}"))); } else { source.add(RawSource::from(format!( - "define({external_deps_array}, {fn_start}" + "define({externals_deps_array}, {fn_start}" ))); } source.add(args.source.clone()); diff --git a/crates/rspack_plugin_library/src/assign_library_plugin.rs b/crates/rspack_plugin_library/src/assign_library_plugin.rs index 05ba332adcb..c23ebf3a41d 100644 --- a/crates/rspack_plugin_library/src/assign_library_plugin.rs +++ b/crates/rspack_plugin_library/src/assign_library_plugin.rs @@ -2,6 +2,7 @@ use std::hash::Hash; use once_cell::sync::Lazy; use regex::Regex; +use rspack_core::property_access; use rspack_core::tree_shaking::webpack_ext::ExportInfoExt; use rspack_core::{ rspack_sources::{ConcatSource, RawSource, SourceExt}, @@ -11,8 +12,6 @@ use rspack_core::{ }; use rspack_error::internal_error; -use crate::utils::property_access; - const COMMON_LIBRARY_NAME_MESSAGE: &str = "Common configuration options that specific library names are 'output.library[.name]', 'entry.xyz.library[.name]', 'ModuleFederationPlugin.name' and 'ModuleFederationPlugin.library[.name]'."; #[derive(Debug)] @@ -169,7 +168,7 @@ impl Plugin for AssignLibraryPlugin { .get(&args.module) { for info in analyze_results.ordered_exports() { - let name_access = property_access(&vec![info.name.to_string()]); + let name_access = property_access(&vec![info.name], 0); source.add(RawSource::from(format!( "{export_target}{name_access} = __webpack_exports__{export_access}{name_access};\n", ))); @@ -234,7 +233,7 @@ impl Plugin for AssignLibraryPlugin { fn property_library(library: &Option) -> String { if let Some(library) = library { if let Some(export) = &library.export { - return property_access(export); + return property_access(export, 0); } } String::default() @@ -256,7 +255,7 @@ fn access_with_init(accessor: &Vec, existing_length: usize, init_last: b if existing_length > i { props_so_far = accessor[1..existing_length].to_vec(); i = existing_length; - current.push_str(property_access(&props_so_far).as_str()); + current.push_str(property_access(&props_so_far, 0).as_str()); } let init_until = if init_last { @@ -269,8 +268,8 @@ fn access_with_init(accessor: &Vec, existing_length: usize, init_last: b props_so_far.push(accessor[i].clone()); current = format!( "({current}{} = {base}{} || {{}})", - property_access(&vec![accessor[i].clone()]), - property_access(&props_so_far) + property_access(&vec![&accessor[i]], 0), + property_access(&props_so_far, 0) ); i += 1; } @@ -278,7 +277,7 @@ fn access_with_init(accessor: &Vec, existing_length: usize, init_last: b if i < accessor.len() { current = format!( "{current}{}", - property_access(&vec![accessor[accessor.len() - 1].clone()]), + property_access([&accessor[accessor.len() - 1]], 0), ); } diff --git a/crates/rspack_plugin_library/src/export_property_plugin.rs b/crates/rspack_plugin_library/src/export_property_plugin.rs index ea95dc821ed..3dddf79f95e 100644 --- a/crates/rspack_plugin_library/src/export_property_plugin.rs +++ b/crates/rspack_plugin_library/src/export_property_plugin.rs @@ -1,10 +1,9 @@ use rspack_core::{ + property_access, rspack_sources::{ConcatSource, RawSource, SourceExt}, Plugin, PluginContext, PluginRenderStartupHookOutput, RenderStartupArgs, }; -use crate::utils::property_access; - #[derive(Debug, Default)] pub struct ExportPropertyLibraryPlugin; @@ -40,7 +39,7 @@ impl Plugin for ExportPropertyLibraryPlugin { s.add(args.source.clone()); s.add(RawSource::from(format!( "__webpack_exports__ = __webpack_exports__{};", - property_access(export) + property_access(export, 0) ))); return Ok(Some(s.boxed())); } diff --git a/crates/rspack_plugin_library/src/module_library_plugin.rs b/crates/rspack_plugin_library/src/module_library_plugin.rs index 9d5fcf84a44..62b976f033c 100644 --- a/crates/rspack_plugin_library/src/module_library_plugin.rs +++ b/crates/rspack_plugin_library/src/module_library_plugin.rs @@ -1,6 +1,7 @@ use std::hash::Hash; use rspack_core::{ + property_access, rspack_sources::{ConcatSource, RawSource, SourceExt}, to_identifier, tree_shaking::webpack_ext::ExportInfoExt, @@ -8,8 +9,6 @@ use rspack_core::{ PluginRenderStartupHookOutput, RenderStartupArgs, }; -use crate::utils::property_access; - #[derive(Debug, Default)] pub struct ModuleLibraryPlugin; @@ -46,7 +45,7 @@ impl Plugin for ModuleLibraryPlugin { let var_name = format!("__webpack_exports__{}", name); source.add(RawSource::from(format!( "var {var_name} = __webpack_exports__{};\n", - property_access(&vec![info.name.to_string()]) + property_access(&vec![&info.name], 0) ))); exports.push(format!("{var_name} as {}", info.name)); } diff --git a/crates/rspack_plugin_library/src/system_library_plugin.rs b/crates/rspack_plugin_library/src/system_library_plugin.rs index 0a6081a1275..d9461a43230 100644 --- a/crates/rspack_plugin_library/src/system_library_plugin.rs +++ b/crates/rspack_plugin_library/src/system_library_plugin.rs @@ -2,12 +2,12 @@ use std::hash::Hash; use rspack_core::{ rspack_sources::{ConcatSource, RawSource, SourceExt}, - AdditionalChunkRuntimeRequirementsArgs, ExternalModule, JsChunkHashArgs, Plugin, + AdditionalChunkRuntimeRequirementsArgs, ExternalModule, ExternalRequest, JsChunkHashArgs, Plugin, PluginAdditionalChunkRuntimeRequirementsOutput, PluginContext, PluginJsChunkHashHookOutput, PluginRenderHookOutput, RenderArgs, RuntimeGlobals, }; +use rspack_error::internal_error; -use super::utils::external_system_dep_array; use crate::utils::{external_module_names, normalize_name}; #[derive(Debug, Default)] @@ -65,7 +65,15 @@ impl Plugin for SystemLibraryPlugin { .and_then(|m| (m.get_external_type() == "system").then_some(m)) }) .collect::>(); - let external_deps_array = external_system_dep_array(&modules); + let external_deps_array = modules + .iter() + .map(|m| match &m.request { + ExternalRequest::Single(request) => Some(request.primary()), + ExternalRequest::Map(map) => map.get("amd").map(|request| request.primary()), + }) + .collect::>(); + let external_deps_array = + serde_json::to_string(&external_deps_array).map_err(|e| internal_error!(e.to_string()))?; let external_arguments = external_module_names(&modules, compilation); // The name of the variable provided by System for exporting diff --git a/crates/rspack_plugin_library/src/umd_library_plugin.rs b/crates/rspack_plugin_library/src/umd_library_plugin.rs index 8a7ab6bc8cc..6c1a5144160 100644 --- a/crates/rspack_plugin_library/src/umd_library_plugin.rs +++ b/crates/rspack_plugin_library/src/umd_library_plugin.rs @@ -2,13 +2,14 @@ use std::hash::Hash; use rspack_core::{ rspack_sources::{ConcatSource, RawSource, SourceExt}, - AdditionalChunkRuntimeRequirementsArgs, Chunk, Compilation, ExternalModule, Filename, - JsChunkHashArgs, LibraryAuxiliaryComment, PathData, Plugin, + AdditionalChunkRuntimeRequirementsArgs, Chunk, Compilation, ExternalModule, ExternalRequest, + Filename, JsChunkHashArgs, LibraryAuxiliaryComment, PathData, Plugin, PluginAdditionalChunkRuntimeRequirementsOutput, PluginContext, PluginJsChunkHashHookOutput, PluginRenderHookOutput, RenderArgs, RuntimeGlobals, SourceType, }; +use rspack_error::{internal_error, Result}; -use super::utils::{external_arguments, external_dep_array}; +use super::utils::{external_arguments, externals_dep_array}; #[derive(Debug)] pub struct UmdLibraryPlugin { @@ -104,12 +105,12 @@ impl Plugin for UmdLibraryPlugin { format!( "define({}, {}, {amd_factory});\n", library_name(&[amd.to_string()], chunk, compilation), - external_dep_array(&required_externals) + externals_dep_array(&required_externals)? ) } else { format!( "define({}, {amd_factory});\n", - external_dep_array(&required_externals) + externals_dep_array(&required_externals)? ) }; @@ -125,7 +126,7 @@ impl Plugin for UmdLibraryPlugin { .clone() .map(|root| library_name(&root, chunk, compilation))) .unwrap_or_default(), - externals_require_array("commonjs", &externals), + externals_require_array("commonjs", &externals)?, ); let root_code = format!( "{} @@ -142,7 +143,7 @@ impl Plugin for UmdLibraryPlugin { chunk, compilation, ), - external_root_array(&externals) + external_root_array(&externals)? ); format!( "}} else if(typeof exports === 'object'){{\n @@ -157,8 +158,8 @@ impl Plugin for UmdLibraryPlugin { } else { format!( "var a = typeof exports === 'object' ? factory({}) : factory({});\n", - externals_require_array("commonjs", &externals), - external_root_array(&externals) + externals_require_array("commonjs", &externals)?, + external_root_array(&externals)? ) }; format!( @@ -179,7 +180,7 @@ impl Plugin for UmdLibraryPlugin { module.exports = factory({}); }}"#, get_auxiliary_comment("commonjs2", auxiliary_comment), - externals_require_array("commonjs2", &externals) + externals_require_array("commonjs2", &externals)? ))); source.add(RawSource::from(format!( "else if(typeof define === 'function' && define.amd) {{ @@ -239,36 +240,61 @@ fn replace_keys(v: String, chunk: &Chunk, compilation: &Compilation) -> String { ) } -fn externals_require_array(_t: &str, externals: &[&ExternalModule]) -> String { - externals - .iter() - .map(|m| { - let request = &m.request; - // TODO: check if external module is optional - format!("require('{}')", request.as_str()) - }) - .collect::>() - .join(", ") +fn externals_require_array(typ: &str, externals: &[&ExternalModule]) -> Result { + Ok( + externals + .iter() + .map(|m| { + let request = match &m.request { + ExternalRequest::Single(r) => r, + ExternalRequest::Map(map) => map + .get(typ) + .ok_or_else(|| internal_error!("Missing external configuration for type: {typ}"))?, + }; + // TODO: check if external module is optional + let primary = + serde_json::to_string(request.primary()).map_err(|e| internal_error!(e.to_string()))?; + let expr = if let Some(rest) = request.rest() { + format!("require({}){}", primary, &accessor_to_object_access(rest)) + } else { + format!("require({})", primary) + }; + Ok(expr) + }) + .collect::>>()? + .join(", "), + ) } -fn external_root_array(modules: &[&ExternalModule]) -> String { - modules - .iter() - .map(|m| { - let request = &m.request; - format!( - "root{}", - accessor_to_object_access(&[request.as_str().to_owned()]) - ) - }) - .collect::>() - .join(", ") +fn external_root_array(modules: &[&ExternalModule]) -> Result { + Ok( + modules + .iter() + .map(|m| { + let typ = "root"; + let request = match &m.request { + ExternalRequest::Single(r) => r.primary(), + ExternalRequest::Map(map) => map + .get(typ) + .map(|r| r.primary()) + .ok_or_else(|| internal_error!("Missing external configuration for type: {typ}"))?, + }; + Ok(format!("root{}", accessor_to_object_access([request]))) + }) + .collect::>>()? + .join(", "), + ) } -fn accessor_to_object_access(accessor: &[String]) -> String { +fn accessor_to_object_access>(accessor: impl IntoIterator) -> String { accessor - .iter() - .map(|s| format!("['{s}']")) + .into_iter() + .map(|s| { + format!( + "[{}]", + serde_json::to_string(s.as_ref()).expect("failed to serde_json::to_string") + ) + }) .collect::>() .join("") } diff --git a/crates/rspack_plugin_library/src/utils.rs b/crates/rspack_plugin_library/src/utils.rs index 38498aeb7f0..b5a136ba559 100644 --- a/crates/rspack_plugin_library/src/utils.rs +++ b/crates/rspack_plugin_library/src/utils.rs @@ -1,30 +1,20 @@ -use rspack_core::{to_identifier, Compilation, ExternalModule, LibraryName, LibraryOptions}; -use rspack_error::Result; +use rspack_core::{ + to_identifier, Compilation, ExternalModule, ExternalRequest, LibraryName, LibraryOptions, +}; +use rspack_error::{internal_error, Result}; use rspack_identifier::Identifiable; -pub fn external_dep_array(modules: &[&ExternalModule]) -> String { - let value = modules - .iter() - .map(|m| serde_json::to_string(&m.request.as_str()).expect("invalid json to_string")) - .collect::>() - .join(", "); - format!("[{value}]") -} - -pub fn external_system_dep_array(modules: &[&ExternalModule]) -> String { +pub fn externals_dep_array(modules: &[&ExternalModule]) -> Result { let value = modules .iter() .map(|m| { - m.request - .0 - .iter() - .map(|r| format!("\"{r}\"")) - .collect::>() - .join(",") + Ok(match &m.request { + ExternalRequest::Single(s) => Some(s.primary()), + ExternalRequest::Map(map) => map.get("amd").map(|r| r.primary()), + }) }) - .collect::>() - .join(", "); - format!("[{value}]") + .collect::>>()?; + serde_json::to_string(&value).map_err(|e| internal_error!(e.to_string())) } fn inner_external_arguments(modules: &[&ExternalModule], compilation: &Compilation) -> Vec { @@ -71,11 +61,3 @@ pub fn normalize_name(o: &Option) -> Result> { } Ok(None) } - -pub fn property_access(o: &Vec) -> String { - let mut str = String::default(); - for property in o { - str.push_str(format!(r#"["{property}"]"#).as_str()); - } - str -} diff --git a/packages/rspack/src/builtin-plugin/ExternalsPlugin.ts b/packages/rspack/src/builtin-plugin/ExternalsPlugin.ts index a401e883a51..3577d3025df 100644 --- a/packages/rspack/src/builtin-plugin/ExternalsPlugin.ts +++ b/packages/rspack/src/builtin-plugin/ExternalsPlugin.ts @@ -75,6 +75,13 @@ function getRawExternalItemValue( type: "array", arrayPayload: value }; + } else if (typeof value === "object" && value !== null) { + return { + type: "object", + objectPayload: Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, Array.isArray(v) ? v : [v]]) + ) + }; } throw new Error("unreachable"); } diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index e0c0c142875..3ae4d88b6fe 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -635,7 +635,11 @@ export type ExternalsType = z.infer; //#endregion //#region Externals -const externalItemValue = z.string().or(z.boolean()).or(z.string().array()); +const externalItemValue = z + .string() + .or(z.boolean()) + .or(z.string().array().min(1)) + .or(z.record(z.string().or(z.string().array()))); export type ExternalItemValue = z.infer; const externalItemObjectUnknown = z.record(externalItemValue); diff --git a/packages/rspack/tests/configCases/externals/item-value-object/index.js b/packages/rspack/tests/configCases/externals/item-value-object/index.js new file mode 100644 index 00000000000..e34fd3e6a1b --- /dev/null +++ b/packages/rspack/tests/configCases/externals/item-value-object/index.js @@ -0,0 +1,5 @@ +var lodash = require("lodash"); + +it("should works", function () { + expect(lodash.add(1, 1)).toBe(2); +}); diff --git a/packages/rspack/tests/configCases/externals/item-value-object/lodash.js b/packages/rspack/tests/configCases/externals/item-value-object/lodash.js new file mode 100644 index 00000000000..a7b0f302b6c --- /dev/null +++ b/packages/rspack/tests/configCases/externals/item-value-object/lodash.js @@ -0,0 +1 @@ +exports.add = (a, b) => a + b; diff --git a/packages/rspack/tests/configCases/externals/item-value-object/webpack.config.js b/packages/rspack/tests/configCases/externals/item-value-object/webpack.config.js new file mode 100644 index 00000000000..b0b1155809a --- /dev/null +++ b/packages/rspack/tests/configCases/externals/item-value-object/webpack.config.js @@ -0,0 +1,18 @@ +const { CopyRspackPlugin } = require("../../../../"); + +module.exports = { + entry: "./index.js", + externals: { + lodash: { + root: "_", + commonjs: "./lodash.js", + amd: "./lodash.js" + } + }, + externalsType: "commonjs", + plugins: [ + new CopyRspackPlugin({ + patterns: ["./lodash.js"] + }) + ] +};