diff --git a/crates/binding/src/js_hook.rs b/crates/binding/src/js_hook.rs index e36f21c82..444b47681 100644 --- a/crates/binding/src/js_hook.rs +++ b/crates/binding/src/js_hook.rs @@ -58,12 +58,15 @@ pub struct JsHooks { pub _on_generate_file: Option, #[napi(ts_type = "() => Promise;")] pub build_start: Option, + #[napi(ts_type = "(source: string, importer: string) => Promise<{ id: string }>;")] + pub resolve_id: Option, } pub struct TsFnHooks { pub build_start: Option>, pub generate_end: Option>, pub load: Option>>, + pub resolve_id: Option>>, pub _on_generate_file: Option>, } @@ -79,6 +82,9 @@ impl TsFnHooks { load: hooks.load.as_ref().map(|hook| unsafe { ThreadsafeFunction::from_napi_value(env.raw(), hook.raw()).unwrap() }), + resolve_id: hooks.resolve_id.as_ref().map(|hook| unsafe { + ThreadsafeFunction::from_napi_value(env.raw(), hook.raw()).unwrap() + }), _on_generate_file: hooks._on_generate_file.as_ref().map(|hook| unsafe { ThreadsafeFunction::from_napi_value(env.raw(), hook.raw()).unwrap() }), @@ -99,3 +105,9 @@ pub struct LoadResult { #[napi(js_name = "type")] pub content_type: String, } + +#[napi(object, use_nullable = true)] +pub struct ResolveIdResult { + pub id: String, + pub external: Option, +} diff --git a/crates/binding/src/js_plugin.rs b/crates/binding/src/js_plugin.rs index 83a7792a0..6d6b47ee5 100644 --- a/crates/binding/src/js_plugin.rs +++ b/crates/binding/src/js_plugin.rs @@ -1,6 +1,7 @@ +use std::path::PathBuf; use std::sync::Arc; -use crate::js_hook::{LoadResult, TsFnHooks, WriteFile}; +use crate::js_hook::{LoadResult, ResolveIdResult, TsFnHooks, WriteFile}; pub struct JsPlugin { pub hooks: TsFnHooks, @@ -9,6 +10,7 @@ use anyhow::{anyhow, Result}; use mako::ast::file::{Content, JsContent}; use mako::compiler::Context; use mako::plugin::{Plugin, PluginGenerateEndParams, PluginLoadParam}; +use mako::resolve::{ExternalResource, Resolution, ResolvedResource, ResolverResource}; impl Plugin for JsPlugin { fn name(&self) -> &str { @@ -47,6 +49,36 @@ impl Plugin for JsPlugin { Ok(None) } + fn resolve_id( + &self, + source: &str, + importer: &str, + _context: &Arc, + ) -> Result> { + if let Some(hook) = &self.hooks.resolve_id { + let x: Option = + hook.call((source.to_string(), importer.to_string()))?; + if let Some(x) = x { + if let Some(true) = x.external { + return Ok(Some(ResolverResource::External(ExternalResource { + source: source.to_string(), + external: source.to_string(), + script: None, + }))); + } + return Ok(Some(ResolverResource::Resolved(ResolvedResource( + Resolution { + path: PathBuf::from(x.id), + query: None, + fragment: None, + package_json: None, + }, + )))); + } + } + Ok(None) + } + fn generate_end(&self, param: &PluginGenerateEndParams, _context: &Arc) -> Result<()> { if let Some(hook) = &self.hooks.generate_end { hook.call(serde_json::to_value(param)?)? diff --git a/crates/mako/src/config.rs b/crates/mako/src/config.rs index 51be8ae37..1dbc8c3ef 100644 --- a/crates/mako/src/config.rs +++ b/crates/mako/src/config.rs @@ -1,3 +1,488 @@ -#[allow(clippy::module_inception)] -mod config; -pub use config::*; +mod analyze; +mod code_splitting; +mod dev_server; +mod devtool; +mod duplicate_package_checker; +mod experimental; +mod external; +mod generic_usize; +mod hmr; +mod inline_css; +mod macros; +mod manifest; +mod minifish; +mod mode; +mod module_id_strategy; +mod optimization; +mod output; +mod progress; +mod provider; +mod px2rem; +mod react; +mod resolve; +mod rsc_client; +mod rsc_server; +mod stats; +mod transform_import; +mod tree_shaking; +mod umd; +mod watch; + +use std::collections::HashMap; +use std::fmt; +use std::path::{Path, PathBuf}; + +pub use analyze::AnalyzeConfig; +use anyhow::{anyhow, Result}; +pub use code_splitting::*; +use colored::Colorize; +use config; +pub use dev_server::{deserialize_dev_server, DevServerConfig}; +pub use devtool::{deserialize_devtool, DevtoolConfig}; +pub use duplicate_package_checker::{ + deserialize_check_duplicate_package, DuplicatePackageCheckerConfig, +}; +use experimental::ExperimentalConfig; +pub use external::{ + ExternalAdvanced, ExternalAdvancedSubpath, ExternalAdvancedSubpathConverter, + ExternalAdvancedSubpathRule, ExternalAdvancedSubpathTarget, ExternalConfig, +}; +pub use generic_usize::GenericUsizeDefault; +pub use hmr::{deserialize_hmr, HmrConfig}; +pub use inline_css::{deserialize_inline_css, InlineCssConfig}; +pub use manifest::{deserialize_manifest, ManifestConfig}; +use miette::{miette, ByteOffset, Diagnostic, NamedSource, SourceOffset, SourceSpan}; +pub use minifish::{deserialize_minifish, MinifishConfig}; +pub use mode::Mode; +pub use module_id_strategy::ModuleIdStrategy; +pub use optimization::{deserialize_optimization, OptimizationConfig}; +use output::get_default_chunk_loading_global; +pub use output::{CrossOriginLoading, OutputConfig, OutputMode}; +pub use progress::{deserialize_progress, ProgressConfig}; +pub use provider::Providers; +pub use px2rem::{deserialize_px2rem, Px2RemConfig}; +pub use react::{ReactConfig, ReactRuntimeConfig}; +pub use resolve::ResolveConfig; +pub use rsc_client::{deserialize_rsc_client, LogServerComponent, RscClientConfig}; +pub use rsc_server::{deserialize_rsc_server, RscServerConfig}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +pub use stats::{deserialize_stats, StatsConfig}; +use thiserror::Error; +pub use transform_import::{TransformImportConfig, TransformImportStyle}; +pub use tree_shaking::{deserialize_tree_shaking, TreeShakingStrategy}; +pub use umd::{deserialize_umd, Umd}; +pub use watch::WatchConfig; + +use crate::features::node::Node; + +#[derive(Debug, Diagnostic)] +#[diagnostic(code("mako.config.json parsed failed"))] +struct ConfigParseError { + #[source_code] + src: NamedSource, + #[label("Error here.")] + span: SourceSpan, + message: String, +} + +impl std::error::Error for ConfigParseError {} + +impl fmt::Display for ConfigParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +fn validate_mako_config(abs_config_file: String) -> miette::Result<()> { + if Path::new(&abs_config_file).exists() { + let content = std::fs::read_to_string(abs_config_file.clone()) + .map_err(|e| miette!("Failed to read file '{}': {}", &abs_config_file, e))?; + let result: Result = serde_json::from_str(&content); + if let Err(e) = result { + let line = e.line(); + let column = e.column(); + let start = SourceOffset::from_location(&content, line, column); + let span = SourceSpan::new(start, (1 as ByteOffset).into()); + return Err(ConfigParseError { + src: NamedSource::new("mako.config.json", content), + span, + message: e.to_string(), + } + .into()); + } + } + Ok(()) +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] +pub enum Platform { + #[serde(rename = "browser")] + Browser, + #[serde(rename = "node")] + Node, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Config { + pub entry: HashMap, + pub output: OutputConfig, + pub resolve: ResolveConfig, + #[serde(deserialize_with = "deserialize_manifest", default)] + pub manifest: Option, + pub mode: Mode, + pub minify: bool, + #[serde(deserialize_with = "deserialize_devtool")] + pub devtool: Option, + pub externals: HashMap, + pub providers: Providers, + pub copy: Vec, + pub public_path: String, + pub inline_limit: usize, + pub inline_excludes_extensions: Vec, + pub targets: HashMap, + pub platform: Platform, + pub module_id_strategy: ModuleIdStrategy, + pub define: HashMap, + pub analyze: Option, + pub stats: Option, + pub mdx: bool, + #[serde(deserialize_with = "deserialize_hmr")] + pub hmr: Option, + #[serde(deserialize_with = "deserialize_dev_server")] + pub dev_server: Option, + #[serde(deserialize_with = "deserialize_code_splitting", default)] + pub code_splitting: Option, + #[serde(deserialize_with = "deserialize_px2rem", default)] + pub px2rem: Option, + #[serde(deserialize_with = "deserialize_progress", default)] + pub progress: Option, + pub hash: bool, + #[serde(rename = "_treeShaking", deserialize_with = "deserialize_tree_shaking")] + pub _tree_shaking: Option, + #[serde(rename = "autoCSSModules")] + pub auto_css_modules: bool, + #[serde(rename = "ignoreCSSParserErrors")] + pub ignore_css_parser_errors: bool, + pub dynamic_import_to_require: bool, + #[serde(deserialize_with = "deserialize_umd", default)] + pub umd: Option, + pub cjs: bool, + pub write_to_disk: bool, + pub transform_import: Vec, + pub chunk_parallel: bool, + pub clean: bool, + pub node_polyfill: bool, + pub ignores: Vec, + #[serde( + rename = "_minifish", + deserialize_with = "deserialize_minifish", + default + )] + pub _minifish: Option, + #[serde(rename = "optimizePackageImports")] + pub optimize_package_imports: bool, + pub emotion: bool, + pub flex_bugs: bool, + #[serde(deserialize_with = "deserialize_optimization")] + pub optimization: Option, + pub react: ReactConfig, + pub emit_assets: bool, + #[serde(rename = "cssModulesExportOnlyLocales")] + pub css_modules_export_only_locales: bool, + #[serde( + rename = "inlineCSS", + deserialize_with = "deserialize_inline_css", + default + )] + pub inline_css: Option, + #[serde( + rename = "rscServer", + deserialize_with = "deserialize_rsc_server", + default + )] + pub rsc_server: Option, + #[serde( + rename = "rscClient", + deserialize_with = "deserialize_rsc_client", + default + )] + pub rsc_client: Option, + pub experimental: ExperimentalConfig, + pub watch: WatchConfig, + pub use_define_for_class_fields: bool, + pub emit_decorator_metadata: bool, + #[serde( + rename = "duplicatePackageChecker", + deserialize_with = "deserialize_check_duplicate_package", + default + )] + pub check_duplicate_package: Option, +} + +const CONFIG_FILE: &str = "mako.config.json"; +const DEFAULT_CONFIG: &str = include_str!("./config/mako.config.default.json"); + +impl Config { + pub fn new( + root: &Path, + default_config: Option<&str>, + cli_config: Option<&str>, + ) -> Result { + let abs_config_file = root.join(CONFIG_FILE); + let abs_config_file = abs_config_file.to_str().unwrap(); + let c = config::Config::builder(); + // default config + let c = c.add_source(config::File::from_str( + DEFAULT_CONFIG, + config::FileFormat::Json5, + )); + // default config from args + let c = if let Some(default_config) = default_config { + c.add_source(config::File::from_str( + default_config, + config::FileFormat::Json5, + )) + } else { + c + }; + // validate user config + validate_mako_config(abs_config_file.to_string()).map_err(|e| anyhow!("{}", e))?; + // user config + let c = c.add_source(config::File::with_name(abs_config_file).required(false)); + // cli config + let c = if let Some(cli_config) = cli_config { + c.add_source(config::File::from_str( + cli_config, + config::FileFormat::Json5, + )) + } else { + c + }; + + let c = c.build()?; + let mut ret = c.try_deserialize::(); + // normalize & check + if let Ok(config) = &mut ret { + // normalize output + if config.output.path.is_relative() { + config.output.path = root.join(config.output.path.to_string_lossy().to_string()); + } + + if config.output.chunk_loading_global.is_empty() { + config.output.chunk_loading_global = + get_default_chunk_loading_global(config.umd.clone(), root); + } + + let node_env_config_opt = config.define.get("NODE_ENV"); + if let Some(node_env_config) = node_env_config_opt { + if node_env_config.as_str() != Some(config.mode.to_string().as_str()) { + let warn_message = format!( + "{}: The configuration of {} conflicts with current {} and will be overwritten as {} ", + "warning".to_string().yellow(), + "NODE_ENV".to_string().yellow(), + "mode".to_string().yellow(), + config.mode.to_string().red() + ); + println!("{}", warn_message); + } + } + + if config.cjs && config.umd.is_some() { + return Err(anyhow!("cjs and umd cannot be used at the same time",)); + } + + if config.hmr.is_some() && config.dev_server.is_none() { + return Err(anyhow!("hmr can only be used with devServer",)); + } + + if config.inline_css.is_some() && config.umd.is_none() { + return Err(anyhow!("inlineCSS can only be used with umd",)); + } + + let mode = format!("\"{}\"", config.mode); + config + .define + .insert("NODE_ENV".to_string(), serde_json::Value::String(mode)); + + if config.public_path != "runtime" && !config.public_path.ends_with('/') { + return Err(anyhow!("public_path must end with '/' or be 'runtime'")); + } + + // 暂不支持 remote external + // 如果 config.externals 中有值是以「script 」开头,则 panic 报错 + let basic_external_values = config + .externals + .values() + .filter_map(|v| match v { + ExternalConfig::Basic(b) => Some(b), + _ => None, + }) + .collect::>(); + for v in basic_external_values { + if v.starts_with("script ") { + return Err(anyhow!( + "remote external is not supported yet, but we found {}", + v.to_string().red() + )); + } + } + + // support default entries + if config.entry.is_empty() { + let file_paths = vec!["src/index.tsx", "src/index.ts", "index.tsx", "index.ts"]; + for file_path in file_paths { + let file_path = root.join(file_path); + if file_path.exists() { + config.entry.insert("index".to_string(), file_path); + break; + } + } + if config.entry.is_empty() { + return Err(anyhow!("Entry is empty")); + } + } + + // normalize entry + let entry_tuples = config + .entry + .clone() + .into_iter() + .map(|(k, v)| { + if let Ok(entry_path) = root.join(v).canonicalize() { + Ok((k, entry_path)) + } else { + Err(anyhow!("entry:{} not found", k,)) + } + }) + .collect::>>()?; + config.entry = entry_tuples.into_iter().collect(); + + // support relative alias + config.resolve.alias = config + .resolve + .alias + .clone() + .into_iter() + .map(|(k, v)| { + let v = if v.starts_with('.') { + root.join(v).to_string_lossy().to_string() + } else { + v + }; + (k, v) + }) + .collect(); + + // dev 环境下不产生 hash, prod 环境下根据用户配置 + if config.mode == Mode::Development { + config.hash = false; + } + + // configure node platform + Node::modify_config(config); + } + ret.map_err(|e| anyhow!("{}: {}", "config error".red(), e.to_string().red())) + } +} + +impl Default for Config { + fn default() -> Self { + let c = config::Config::builder(); + let c = c.add_source(config::File::from_str( + DEFAULT_CONFIG, + config::FileFormat::Json5, + )); + let c = c.build().unwrap(); + c.try_deserialize::().unwrap() + } +} + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("define value '{0}' is not an Expression")] + InvalidateDefineConfig(String), +} + +#[cfg(test)] +mod tests { + use crate::config::{Config, Mode, Platform}; + + #[test] + fn test_config() { + let current_dir = std::env::current_dir().unwrap(); + let config = Config::new(¤t_dir.join("test/config/normal"), None, None).unwrap(); + println!("{:?}", config); + assert_eq!(config.platform, Platform::Node); + } + + #[test] + fn test_config_args_default() { + let current_dir = std::env::current_dir().unwrap(); + let config = Config::new( + ¤t_dir.join("test/config/normal"), + Some(r#"{"mode":"production"}"#), + None, + ) + .unwrap(); + println!("{:?}", config); + assert_eq!(config.mode, Mode::Production); + } + + #[test] + fn test_config_cli_args() { + let current_dir = std::env::current_dir().unwrap(); + let config = Config::new( + ¤t_dir.join("test/config/normal"), + None, + Some(r#"{"platform":"browser"}"#), + ) + .unwrap(); + println!("{:?}", config); + assert_eq!(config.platform, Platform::Browser); + } + + #[test] + fn test_node_env_conflicts_with_mode() { + let current_dir = std::env::current_dir().unwrap(); + let config = Config::new( + ¤t_dir.join("test/config/node-env"), + None, + Some(r#"{"mode":"development"}"#), + ) + .unwrap(); + assert_eq!( + config.define.get("NODE_ENV"), + Some(&serde_json::Value::String("\"development\"".to_string())) + ); + } + + #[test] + #[should_panic(expected = "public_path must end with '/' or be 'runtime'")] + fn test_config_invalid_public_path() { + let current_dir = std::env::current_dir().unwrap(); + Config::new( + ¤t_dir.join("test/config/normal"), + None, + Some(r#"{"publicPath":"abc"}"#), + ) + .unwrap(); + } + + #[test] + fn test_node_platform() { + let current_dir = std::env::current_dir().unwrap(); + let config = + Config::new(¤t_dir.join("test/config/node-platform"), None, None).unwrap(); + assert_eq!( + config.targets.get("node"), + Some(&14.0), + "use node targets by default if platform is node", + ); + assert!( + config.ignores.iter().any(|i| i.contains("|fs|")), + "ignore Node.js standard library by default if platform is node", + ); + } +} diff --git a/crates/mako/src/config/analyze.rs b/crates/mako/src/config/analyze.rs new file mode 100644 index 000000000..1a4655123 --- /dev/null +++ b/crates/mako/src/config/analyze.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct AnalyzeConfig {} diff --git a/crates/mako/src/config/code_splitting.rs b/crates/mako/src/config/code_splitting.rs new file mode 100644 index 000000000..73e973f98 --- /dev/null +++ b/crates/mako/src/config/code_splitting.rs @@ -0,0 +1,145 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use super::generic_usize::GenericUsizeDefault; +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub enum OptimizeAllowChunks { + #[serde(rename = "all")] + All, + #[serde(rename = "entry")] + Entry, + #[serde(rename = "async")] + #[default] + Async, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct CodeSplitting { + pub strategy: CodeSplittingStrategy, + pub options: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub enum CodeSplittingStrategy { + #[serde(rename = "auto")] + Auto, + #[serde(rename = "granular")] + Granular, + #[serde(rename = "advanced")] + Advanced, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(untagged)] +pub enum CodeSplittingStrategyOptions { + Granular(CodeSplittingGranularOptions), + Advanced(CodeSplittingAdvancedOptions), +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CodeSplittingGranularOptions { + pub framework_packages: Vec, + #[serde(default = "GenericUsizeDefault::<160000>::value")] + pub lib_min_size: usize, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CodeSplittingAdvancedOptions { + #[serde(default = "GenericUsizeDefault::<20000>::value")] + pub min_size: usize, + pub groups: Vec, +} + +impl Default for CodeSplittingAdvancedOptions { + fn default() -> Self { + CodeSplittingAdvancedOptions { + min_size: GenericUsizeDefault::<20000>::value(), + groups: vec![], + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub enum OptimizeChunkNameSuffixStrategy { + #[serde(rename = "packageName")] + PackageName, + #[serde(rename = "dependentsHash")] + DependentsHash, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OptimizeChunkGroup { + pub name: String, + #[serde(default)] + pub name_suffix: Option, + #[serde(default)] + pub allow_chunks: OptimizeAllowChunks, + #[serde(default = "GenericUsizeDefault::<1>::value")] + pub min_chunks: usize, + #[serde(default = "GenericUsizeDefault::<20000>::value")] + pub min_size: usize, + #[serde(default = "GenericUsizeDefault::<5000000>::value")] + pub max_size: usize, + #[serde(default)] + pub min_module_size: Option, + #[serde(default)] + pub priority: i8, + #[serde(default, with = "optimize_test_format")] + pub test: Option, +} + +impl Default for OptimizeChunkGroup { + fn default() -> Self { + Self { + allow_chunks: OptimizeAllowChunks::default(), + min_chunks: GenericUsizeDefault::<1>::value(), + min_size: GenericUsizeDefault::<20000>::value(), + max_size: GenericUsizeDefault::<5000000>::value(), + name: String::default(), + name_suffix: None, + min_module_size: None, + test: None, + priority: i8::default(), + } + } +} + +/** + * custom formatter for convert string to regex + * @see https://serde.rs/custom-date-format.html + */ +mod optimize_test_format { + use regex::Regex; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(v: &Option, serializer: S) -> Result + where + S: Serializer, + { + if let Some(v) = v { + serializer.serialize_str(&v.to_string()) + } else { + serializer.serialize_none() + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let v = String::deserialize(deserializer)?; + + if v.is_empty() { + Ok(None) + } else { + Ok(Regex::new(v.as_str()).ok()) + } + } +} + +create_deserialize_fn!(deserialize_code_splitting, CodeSplitting); diff --git a/crates/mako/src/config/config.rs b/crates/mako/src/config/config.rs deleted file mode 100644 index 0309648c7..000000000 --- a/crates/mako/src/config/config.rs +++ /dev/null @@ -1,1093 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, Result}; -use clap::ValueEnum; -use colored::Colorize; -use miette::{miette, ByteOffset, Diagnostic, NamedSource, SourceOffset, SourceSpan}; -use regex::Regex; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use swc_core::ecma::ast::EsVersion; -use thiserror::Error; -use {clap, config, thiserror}; - -use crate::features::node::Node; -use crate::{plugins, visitors}; - -#[derive(Debug, Diagnostic)] -#[diagnostic(code("mako.config.json parsed failed"))] -struct ConfigParseError { - #[source_code] - src: NamedSource, - #[label("Error here.")] - span: SourceSpan, - message: String, -} - -impl std::error::Error for ConfigParseError {} - -impl fmt::Display for ConfigParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -fn validate_mako_config(abs_config_file: String) -> miette::Result<()> { - if Path::new(&abs_config_file).exists() { - let content = std::fs::read_to_string(abs_config_file.clone()) - .map_err(|e| miette!("Failed to read file '{}': {}", &abs_config_file, e))?; - let result: Result = serde_json::from_str(&content); - if let Err(e) = result { - let line = e.line(); - let column = e.column(); - let start = SourceOffset::from_location(&content, line, column); - let span = SourceSpan::new(start, (1 as ByteOffset).into()); - return Err(ConfigParseError { - src: NamedSource::new("mako.config.json", content), - span, - message: e.to_string(), - } - .into()); - } - } - Ok(()) -} - -/** - * a macro to create deserialize function that allow false value for optional struct - */ -macro_rules! create_deserialize_fn { - ($fn_name:ident, $struct_type:ty) => { - pub fn $fn_name<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; - - match value { - // allow false value for optional struct - serde_json::Value::Bool(false) => Ok(None), - // try deserialize - serde_json::Value::Object(obj) => Ok(Some( - serde_json::from_value::<$struct_type>(serde_json::Value::Object(obj)) - .map_err(serde::de::Error::custom)?, - )), - serde_json::Value::String(s) => Ok(Some( - serde_json::from_value::<$struct_type>(serde_json::Value::String(s.clone())) - .map_err(serde::de::Error::custom)?, - )), - _ => Err(serde::de::Error::custom(format!( - "invalid `{}` value: {}", - stringify!($fn_name).replace("deserialize_", ""), - value - ))), - } - } - }; -} -create_deserialize_fn!(deserialize_hmr, HmrConfig); -create_deserialize_fn!(deserialize_dev_server, DevServerConfig); -create_deserialize_fn!(deserialize_manifest, ManifestConfig); -create_deserialize_fn!(deserialize_code_splitting, CodeSplitting); -create_deserialize_fn!(deserialize_px2rem, Px2RemConfig); -create_deserialize_fn!(deserialize_progress, ProgressConfig); -create_deserialize_fn!( - deserialize_check_duplicate_package, - DuplicatePackageCheckerConfig -); -create_deserialize_fn!(deserialize_umd, String); -create_deserialize_fn!(deserialize_devtool, DevtoolConfig); -create_deserialize_fn!(deserialize_tree_shaking, TreeShakingStrategy); -create_deserialize_fn!(deserialize_optimization, OptimizationConfig); -create_deserialize_fn!(deserialize_minifish, MinifishConfig); -create_deserialize_fn!(deserialize_inline_css, InlineCssConfig); -create_deserialize_fn!(deserialize_rsc_client, RscClientConfig); -create_deserialize_fn!(deserialize_rsc_server, RscServerConfig); -create_deserialize_fn!(deserialize_stats, StatsConfig); -create_deserialize_fn!(deserialize_detect_loop, DetectCircularDependence); -create_deserialize_fn!(deserialize_cross_origin_loading, CrossOriginLoading); - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct OutputConfig { - pub path: PathBuf, - pub mode: OutputMode, - pub es_version: EsVersion, - pub meta: bool, - pub chunk_loading_global: String, - pub preserve_modules: bool, - pub preserve_modules_root: PathBuf, - pub skip_write: bool, - #[serde(deserialize_with = "deserialize_cross_origin_loading")] - pub cross_origin_loading: Option, - pub global_module_registry: bool, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub enum CrossOriginLoading { - #[serde(rename = "anonymous")] - Anonymous, - #[serde(rename = "use-credentials")] - UseCredentials, -} - -impl fmt::Display for CrossOriginLoading { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CrossOriginLoading::Anonymous => write!(f, "anonymous"), - CrossOriginLoading::UseCredentials => write!(f, "use-credentials"), - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct ManifestConfig { - #[serde( - rename(deserialize = "fileName"), - default = "plugins::manifest::default_manifest_file_name" - )] - pub file_name: String, - #[serde(rename(deserialize = "basePath"), default)] - pub base_path: String, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct ResolveConfig { - pub alias: Vec<(String, String)>, - pub extensions: Vec, -} - -// format: HashMap -// e.g. -// { "process": ("process", "") } -// { "Buffer": ("buffer", "Buffer") } -pub type Providers = HashMap; - -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] -pub enum Mode { - #[serde(rename = "development")] - Development, - #[serde(rename = "production")] - Production, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] -pub enum OutputMode { - #[serde(rename = "bundle")] - Bundle, - #[serde(rename = "bundless")] - Bundless, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] -pub enum Platform { - #[serde(rename = "browser")] - Browser, - #[serde(rename = "node")] - Node, -} - -impl std::fmt::Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.to_possible_value().unwrap().get_name().fmt(f) - } -} - -#[derive(Deserialize, Serialize, Debug)] -pub enum DevtoolConfig { - /// Generate separate sourcemap file - #[serde(rename = "source-map")] - SourceMap, - /// Generate inline sourcemap - #[serde(rename = "inline-source-map")] - InlineSourceMap, -} - -#[derive(Deserialize, Serialize, Clone, Copy, Debug)] -pub enum ModuleIdStrategy { - #[serde(rename = "hashed")] - Hashed, - #[serde(rename = "named")] - Named, - #[serde(rename = "numeric")] - Numeric, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CodeSplittingGranularOptions { - pub framework_packages: Vec, - #[serde(default = "GenericUsizeDefault::<160000>::value")] - pub lib_min_size: usize, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct StatsConfig { - pub modules: bool, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct AnalyzeConfig {} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub enum CodeSplittingStrategy { - #[serde(rename = "auto")] - Auto, - #[serde(rename = "granular")] - Granular, - #[serde(rename = "advanced")] - Advanced, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(untagged)] -pub enum CodeSplittingStrategyOptions { - Granular(CodeSplittingGranularOptions), - Advanced(CodeSplittingAdvancedOptions), -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct CodeSplitting { - pub strategy: CodeSplittingStrategy, - pub options: Option, -} - -#[derive(Deserialize, Serialize, Clone, Copy, Debug)] -pub enum TreeShakingStrategy { - #[serde(rename = "basic")] - Basic, - #[serde(rename = "advanced")] - Advanced, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct Px2RemConfig { - #[serde(default = "visitors::css_px2rem::default_root")] - pub root: f64, - #[serde(rename = "propBlackList", default)] - pub prop_blacklist: Vec, - #[serde(rename = "propWhiteList", default)] - pub prop_whitelist: Vec, - #[serde(rename = "selectorBlackList", default)] - pub selector_blacklist: Vec, - #[serde(rename = "selectorWhiteList", default)] - pub selector_whitelist: Vec, - #[serde(rename = "selectorDoubleList", default)] - pub selector_doublelist: Vec, - #[serde(rename = "minPixelValue", default)] - pub min_pixel_value: f64, - #[serde(rename = "mediaQuery", default)] - pub media_query: bool, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct ProgressConfig { - #[serde(rename = "progressChars", default)] - pub progress_chars: String, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct DuplicatePackageCheckerConfig { - #[serde(rename = "verbose", default)] - pub verbose: bool, - #[serde(rename = "emitError", default)] - pub emit_error: bool, - #[serde(rename = "showHelp", default)] - pub show_help: bool, -} - -impl Default for Px2RemConfig { - fn default() -> Self { - Px2RemConfig { - root: visitors::css_px2rem::default_root(), - prop_blacklist: vec![], - prop_whitelist: vec![], - selector_blacklist: vec![], - selector_whitelist: vec![], - selector_doublelist: vec![], - min_pixel_value: 0.0, - media_query: false, - } - } -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(untagged)] -pub enum TransformImportStyle { - Built(String), - Source(bool), -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TransformImportConfig { - pub library_name: String, - pub library_directory: Option, - pub style: Option, -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -pub enum ExternalAdvancedSubpathConverter { - PascalCase, -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -#[serde(untagged)] -pub enum ExternalAdvancedSubpathTarget { - Empty, - Tpl(String), -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -pub struct ExternalAdvancedSubpathRule { - pub regex: String, - #[serde(with = "external_target_format")] - pub target: ExternalAdvancedSubpathTarget, - #[serde(rename = "targetConverter")] - pub target_converter: Option, -} - -/** - * custom formatter for convert $EMPTY to enum, because rename is not supported for $ symbol - * @see https://serde.rs/custom-date-format.html - */ -mod external_target_format { - use serde::{self, Deserialize, Deserializer, Serializer}; - - use super::ExternalAdvancedSubpathTarget; - - pub fn serialize(v: &ExternalAdvancedSubpathTarget, serializer: S) -> Result - where - S: Serializer, - { - match v { - ExternalAdvancedSubpathTarget::Empty => serializer.serialize_str("$EMPTY"), - ExternalAdvancedSubpathTarget::Tpl(s) => serializer.serialize_str(s), - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let v = String::deserialize(deserializer)?; - - if v == "$EMPTY" { - Ok(ExternalAdvancedSubpathTarget::Empty) - } else { - Ok(ExternalAdvancedSubpathTarget::Tpl(v)) - } - } -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -pub struct ExternalAdvancedSubpath { - pub exclude: Option>, - pub rules: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -pub struct ExternalAdvanced { - pub root: String, - #[serde(rename = "type")] - pub module_type: Option, - pub script: Option, - pub subpath: Option, -} - -#[derive(Deserialize, Serialize, Debug, Hash)] -#[serde(untagged)] -pub enum ExternalConfig { - Basic(String), - Advanced(ExternalAdvanced), -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct InjectItem { - pub from: String, - pub named: Option, - pub namespace: Option, - pub exclude: Option, - pub include: Option, - pub prefer_require: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub enum ReactRuntimeConfig { - #[serde(rename = "automatic")] - Automatic, - #[serde(rename = "classic")] - Classic, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct ReactConfig { - pub pragma: String, - #[serde(rename = "importSource")] - pub import_source: String, - pub runtime: ReactRuntimeConfig, - #[serde(rename = "pragmaFrag")] - pub pragma_frag: String, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct MinifishConfig { - pub mapping: HashMap, - pub meta_path: Option, - pub inject: Option>, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct OptimizationConfig { - pub skip_modules: Option, - pub concatenate_modules: Option, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct InlineCssConfig {} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RscServerConfig { - pub client_component_tpl: String, - #[serde(rename = "emitCSS")] - pub emit_css: bool, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] -pub enum LogServerComponent { - #[serde(rename = "error")] - Error, - #[serde(rename = "ignore")] - Ignore, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RscClientConfig { - pub log_server_component: LogServerComponent, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DetectCircularDependence { - pub ignores: Vec, - pub graphviz: bool, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ExperimentalConfig { - pub webpack_syntax_validate: Vec, - pub require_context: bool, - #[serde(deserialize_with = "deserialize_detect_loop")] - pub detect_circular_dependence: Option, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct WatchConfig { - pub ignore_paths: Option>, - #[serde(rename = "_nodeModulesRegexes")] - pub node_modules_regexes: Option>, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct HmrConfig {} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DevServerConfig { - pub host: String, - pub port: u16, -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Config { - pub entry: HashMap, - pub output: OutputConfig, - pub resolve: ResolveConfig, - #[serde(deserialize_with = "deserialize_manifest", default)] - pub manifest: Option, - pub mode: Mode, - pub minify: bool, - #[serde(deserialize_with = "deserialize_devtool")] - pub devtool: Option, - pub externals: HashMap, - pub providers: Providers, - pub copy: Vec, - pub public_path: String, - pub inline_limit: usize, - pub inline_excludes_extensions: Vec, - pub targets: HashMap, - pub platform: Platform, - pub module_id_strategy: ModuleIdStrategy, - pub define: HashMap, - pub analyze: Option, - pub stats: Option, - pub mdx: bool, - #[serde(deserialize_with = "deserialize_hmr")] - pub hmr: Option, - #[serde(deserialize_with = "deserialize_dev_server")] - pub dev_server: Option, - #[serde(deserialize_with = "deserialize_code_splitting", default)] - pub code_splitting: Option, - #[serde(deserialize_with = "deserialize_px2rem", default)] - pub px2rem: Option, - #[serde(deserialize_with = "deserialize_progress", default)] - pub progress: Option, - pub hash: bool, - #[serde(rename = "_treeShaking", deserialize_with = "deserialize_tree_shaking")] - pub _tree_shaking: Option, - #[serde(rename = "autoCSSModules")] - pub auto_css_modules: bool, - #[serde(rename = "ignoreCSSParserErrors")] - pub ignore_css_parser_errors: bool, - pub dynamic_import_to_require: bool, - #[serde(deserialize_with = "deserialize_umd", default)] - pub umd: Option, - pub cjs: bool, - pub write_to_disk: bool, - pub transform_import: Vec, - pub chunk_parallel: bool, - pub clean: bool, - pub node_polyfill: bool, - pub ignores: Vec, - #[serde( - rename = "_minifish", - deserialize_with = "deserialize_minifish", - default - )] - pub _minifish: Option, - #[serde(rename = "optimizePackageImports")] - pub optimize_package_imports: bool, - pub emotion: bool, - pub flex_bugs: bool, - #[serde(deserialize_with = "deserialize_optimization")] - pub optimization: Option, - pub react: ReactConfig, - pub emit_assets: bool, - #[serde(rename = "cssModulesExportOnlyLocales")] - pub css_modules_export_only_locales: bool, - #[serde( - rename = "inlineCSS", - deserialize_with = "deserialize_inline_css", - default - )] - pub inline_css: Option, - #[serde( - rename = "rscServer", - deserialize_with = "deserialize_rsc_server", - default - )] - pub rsc_server: Option, - #[serde( - rename = "rscClient", - deserialize_with = "deserialize_rsc_client", - default - )] - pub rsc_client: Option, - pub experimental: ExperimentalConfig, - pub watch: WatchConfig, - pub use_define_for_class_fields: bool, - pub emit_decorator_metadata: bool, - #[serde( - rename = "duplicatePackageChecker", - deserialize_with = "deserialize_check_duplicate_package", - default - )] - pub check_duplicate_package: Option, -} - -#[derive(Deserialize, Serialize, Clone, Debug, Default)] -pub enum OptimizeAllowChunks { - #[serde(rename = "all")] - All, - #[serde(rename = "entry")] - Entry, - #[serde(rename = "async")] - #[default] - Async, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CodeSplittingAdvancedOptions { - #[serde(default = "GenericUsizeDefault::<20000>::value")] - pub min_size: usize, - pub groups: Vec, -} - -impl Default for CodeSplittingAdvancedOptions { - fn default() -> Self { - CodeSplittingAdvancedOptions { - min_size: GenericUsizeDefault::<20000>::value(), - groups: vec![], - } - } -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub enum OptimizeChunkNameSuffixStrategy { - #[serde(rename = "packageName")] - PackageName, - #[serde(rename = "dependentsHash")] - DependentsHash, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct OptimizeChunkGroup { - pub name: String, - #[serde(default)] - pub name_suffix: Option, - #[serde(default)] - pub allow_chunks: OptimizeAllowChunks, - #[serde(default = "GenericUsizeDefault::<1>::value")] - pub min_chunks: usize, - #[serde(default = "GenericUsizeDefault::<20000>::value")] - pub min_size: usize, - #[serde(default = "GenericUsizeDefault::<5000000>::value")] - pub max_size: usize, - #[serde(default)] - pub min_module_size: Option, - #[serde(default)] - pub priority: i8, - #[serde(default, with = "optimize_test_format")] - pub test: Option, -} - -impl Default for OptimizeChunkGroup { - fn default() -> Self { - Self { - allow_chunks: OptimizeAllowChunks::default(), - min_chunks: GenericUsizeDefault::<1>::value(), - min_size: GenericUsizeDefault::<20000>::value(), - max_size: GenericUsizeDefault::<5000000>::value(), - name: String::default(), - name_suffix: None, - min_module_size: None, - test: None, - priority: i8::default(), - } - } -} - -/** - * custom formatter for convert string to regex - * @see https://serde.rs/custom-date-format.html - */ -mod optimize_test_format { - use regex::Regex; - use serde::{self, Deserialize, Deserializer, Serializer}; - - pub fn serialize(v: &Option, serializer: S) -> Result - where - S: Serializer, - { - if let Some(v) = v { - serializer.serialize_str(&v.to_string()) - } else { - serializer.serialize_none() - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let v = String::deserialize(deserializer)?; - - if v.is_empty() { - Ok(None) - } else { - Ok(Regex::new(v.as_str()).ok()) - } - } -} - -const CONFIG_FILE: &str = "mako.config.json"; -const DEFAULT_CONFIG: &str = r#" -{ - "entry": {}, - "output": { - "path": "dist", - "mode": "bundle", - "esVersion": "es2022", - "meta": false, - "chunkLoadingGlobal": "", - "preserveModules": false, - "preserveModulesRoot": "", - "skipWrite": false, - "crossOriginLoading": false, - "globalModuleRegistry": false, - }, - "resolve": { "alias": [], "extensions": ["js", "jsx", "ts", "tsx"] }, - "mode": "development", - "minify": true, - "devtool": "source-map", - "externals": {}, - "copy": ["public"], - "providers": {}, - "publicPath": "/", - "inlineLimit": 10000, - "inlineExcludesExtensions": [], - "targets": { "chrome": 80 }, - "less": { "theme": {}, "lesscPath": "", javascriptEnabled: true }, - "define": {}, - "mdx": false, - "platform": "browser", - "hmr": {}, - "moduleIdStrategy": "named", - "hash": false, - "_treeShaking": "basic", - "autoCSSModules": false, - "ignoreCSSParserErrors": false, - "dynamicImportToRequire": false, - "writeToDisk": true, - "transformImport": [], - "chunkParallel": true, - "clean": true, - "nodePolyfill": true, - "ignores": [], - "optimizePackageImports": false, - "emotion": false, - "flexBugs": false, - "cjs": false, - "optimization": { "skipModules": true, "concatenateModules": true }, - "react": { - "pragma": "React.createElement", - "importSource": "react", - "runtime": "automatic", - "pragmaFrag": "React.Fragment" - }, - "progress": { - "progressChars": "▨▨" - }, - "duplicatePackageChecker": { - "verbose": false, - "showHelp": false, - "emitError": false, - }, - "emitAssets": true, - "cssModulesExportOnlyLocales": false, - "inlineCSS": false, - "rscServer": false, - "rscClient": false, - "experimental": { - "webpackSyntaxValidate": [], - "requireContext": true, - "detectCircularDependence": { "ignores": ["node_modules"], "graphviz": false } - }, - "useDefineForClassFields": true, - "emitDecoratorMetadata": false, - "watch": { "ignorePaths": [], "_nodeModulesRegexes": [] }, - "devServer": { "host": "127.0.0.1", "port": 3000 } -} -"#; - -impl Config { - pub fn new( - root: &Path, - default_config: Option<&str>, - cli_config: Option<&str>, - ) -> Result { - let abs_config_file = root.join(CONFIG_FILE); - let abs_config_file = abs_config_file.to_str().unwrap(); - let c = config::Config::builder(); - // default config - let c = c.add_source(config::File::from_str( - DEFAULT_CONFIG, - config::FileFormat::Json5, - )); - // default config from args - let c = if let Some(default_config) = default_config { - c.add_source(config::File::from_str( - default_config, - config::FileFormat::Json5, - )) - } else { - c - }; - // validate user config - validate_mako_config(abs_config_file.to_string()) - .map_err(|e| anyhow!("{}", format!("{:?}", e)))?; - // user config - let c = c.add_source(config::File::with_name(abs_config_file).required(false)); - // cli config - let c = if let Some(cli_config) = cli_config { - c.add_source(config::File::from_str( - cli_config, - config::FileFormat::Json5, - )) - } else { - c - }; - - let c = c.build()?; - let mut ret = c.try_deserialize::(); - // normalize & check - if let Ok(config) = &mut ret { - // normalize output - if config.output.path.is_relative() { - config.output.path = root.join(config.output.path.to_string_lossy().to_string()); - } - - if config.output.chunk_loading_global.is_empty() { - config.output.chunk_loading_global = - get_default_chunk_loading_global(config.umd.clone(), root); - } - - let node_env_config_opt = config.define.get("NODE_ENV"); - if let Some(node_env_config) = node_env_config_opt { - if node_env_config.as_str() != Some(config.mode.to_string().as_str()) { - let warn_message = format!( - "{}: The configuration of {} conflicts with current {} and will be overwritten as {} ", - "warning".to_string().yellow(), - "NODE_ENV".to_string().yellow(), - "mode".to_string().yellow(), - config.mode.to_string().red() - ); - println!("{}", warn_message); - } - } - - if config.cjs && config.umd.is_some() { - return Err(anyhow!("cjs and umd cannot be used at the same time",)); - } - - if config.hmr.is_some() && config.dev_server.is_none() { - return Err(anyhow!("hmr can only be used with devServer",)); - } - - if config.inline_css.is_some() && config.umd.is_none() { - return Err(anyhow!("inlineCSS can only be used with umd",)); - } - - let mode = format!("\"{}\"", config.mode); - config - .define - .insert("NODE_ENV".to_string(), serde_json::Value::String(mode)); - - if config.public_path != "runtime" && !config.public_path.ends_with('/') { - return Err(anyhow!("public_path must end with '/' or be 'runtime'")); - } - - // 暂不支持 remote external - // 如果 config.externals 中有值是以「script 」开头,则 panic 报错 - let basic_external_values = config - .externals - .values() - .filter_map(|v| match v { - ExternalConfig::Basic(b) => Some(b), - _ => None, - }) - .collect::>(); - for v in basic_external_values { - if v.starts_with("script ") { - return Err(anyhow!( - "remote external is not supported yet, but we found {}", - v.to_string().red() - )); - } - } - - // support default entries - if config.entry.is_empty() { - let file_paths = vec!["src/index.tsx", "src/index.ts", "index.tsx", "index.ts"]; - for file_path in file_paths { - let file_path = root.join(file_path); - if file_path.exists() { - config.entry.insert("index".to_string(), file_path); - break; - } - } - if config.entry.is_empty() { - return Err(anyhow!("Entry is empty")); - } - } - - // normalize entry - let entry_tuples = config - .entry - .clone() - .into_iter() - .map(|(k, v)| { - if let Ok(entry_path) = root.join(v).canonicalize() { - Ok((k, entry_path)) - } else { - Err(anyhow!("entry:{} not found", k,)) - } - }) - .collect::>>()?; - config.entry = entry_tuples.into_iter().collect(); - - // support relative alias - config.resolve.alias = config - .resolve - .alias - .clone() - .into_iter() - .map(|(k, v)| { - let v = if v.starts_with('.') { - root.join(v).to_string_lossy().to_string() - } else { - v - }; - (k, v) - }) - .collect(); - - // dev 环境下不产生 hash, prod 环境下根据用户配置 - if config.mode == Mode::Development { - config.hash = false; - } - - // configure node platform - Node::modify_config(config); - } - ret.map_err(|e| anyhow!("{}: {}", "config error".red(), e.to_string().red())) - } -} - -impl Default for Config { - fn default() -> Self { - let c = config::Config::builder(); - let c = c.add_source(config::File::from_str( - DEFAULT_CONFIG, - config::FileFormat::Json5, - )); - let c = c.build().unwrap(); - c.try_deserialize::().unwrap() - } -} - -pub(crate) fn get_pkg_name(root: &Path) -> Option { - let pkg_json_path = root.join("package.json"); - - if pkg_json_path.exists() { - let pkg_json = std::fs::read_to_string(pkg_json_path).unwrap(); - let pkg_json: serde_json::Value = serde_json::from_str(&pkg_json).unwrap(); - - pkg_json - .get("name") - .map(|name| name.as_str().unwrap().to_string()) - } else { - None - } -} - -fn get_default_chunk_loading_global(umd: Option, root: &Path) -> String { - let unique_name = umd.unwrap_or_else(|| get_pkg_name(root).unwrap_or("global".to_string())); - - format!("makoChunk_{}", unique_name) -} - -#[derive(Error, Debug)] -pub enum ConfigError { - #[error("define value '{0}' is not an Expression")] - InvalidateDefineConfig(String), -} - -pub struct GenericUsizeDefault; - -impl GenericUsizeDefault { - pub fn value() -> usize { - U - } -} - -#[cfg(test)] -mod tests { - use crate::config::config::GenericUsizeDefault; - use crate::config::{Config, Mode, Platform}; - - #[test] - fn test_config() { - let current_dir = std::env::current_dir().unwrap(); - let config = Config::new(¤t_dir.join("test/config/normal"), None, None).unwrap(); - println!("{:?}", config); - assert_eq!(config.platform, Platform::Node); - } - - #[test] - fn test_config_args_default() { - let current_dir = std::env::current_dir().unwrap(); - let config = Config::new( - ¤t_dir.join("test/config/normal"), - Some(r#"{"mode":"production"}"#), - None, - ) - .unwrap(); - println!("{:?}", config); - assert_eq!(config.mode, Mode::Production); - } - - #[test] - fn test_config_cli_args() { - let current_dir = std::env::current_dir().unwrap(); - let config = Config::new( - ¤t_dir.join("test/config/normal"), - None, - Some(r#"{"platform":"browser"}"#), - ) - .unwrap(); - println!("{:?}", config); - assert_eq!(config.platform, Platform::Browser); - } - - #[test] - fn test_node_env_conflicts_with_mode() { - let current_dir = std::env::current_dir().unwrap(); - let config = Config::new( - ¤t_dir.join("test/config/node-env"), - None, - Some(r#"{"mode":"development"}"#), - ) - .unwrap(); - assert_eq!( - config.define.get("NODE_ENV"), - Some(&serde_json::Value::String("\"development\"".to_string())) - ); - } - - #[test] - #[should_panic(expected = "public_path must end with '/' or be 'runtime'")] - fn test_config_invalid_public_path() { - let current_dir = std::env::current_dir().unwrap(); - Config::new( - ¤t_dir.join("test/config/normal"), - None, - Some(r#"{"publicPath":"abc"}"#), - ) - .unwrap(); - } - - #[test] - fn test_node_platform() { - let current_dir = std::env::current_dir().unwrap(); - let config = - Config::new(¤t_dir.join("test/config/node-platform"), None, None).unwrap(); - assert_eq!( - config.targets.get("node"), - Some(&14.0), - "use node targets by default if platform is node", - ); - assert!( - config.ignores.iter().any(|i| i.contains("|fs|")), - "ignore Node.js standard library by default if platform is node", - ); - } - - #[test] - fn test_generic_usize_default() { - assert!(GenericUsizeDefault::<100>::value() == 100usize) - } -} diff --git a/crates/mako/src/config/dev_server.rs b/crates/mako/src/config/dev_server.rs new file mode 100644 index 000000000..928fb7b8b --- /dev/null +++ b/crates/mako/src/config/dev_server.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DevServerConfig { + pub host: String, + pub port: u16, +} + +create_deserialize_fn!(deserialize_dev_server, DevServerConfig); diff --git a/crates/mako/src/config/devtool.rs b/crates/mako/src/config/devtool.rs new file mode 100644 index 000000000..367d4b307 --- /dev/null +++ b/crates/mako/src/config/devtool.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +pub enum DevtoolConfig { + /// Generate separate sourcemap file + #[serde(rename = "source-map")] + SourceMap, + /// Generate inline sourcemap + #[serde(rename = "inline-source-map")] + InlineSourceMap, +} + +create_deserialize_fn!(deserialize_devtool, DevtoolConfig); diff --git a/crates/mako/src/config/duplicate_package_checker.rs b/crates/mako/src/config/duplicate_package_checker.rs new file mode 100644 index 000000000..14eedc9b3 --- /dev/null +++ b/crates/mako/src/config/duplicate_package_checker.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct DuplicatePackageCheckerConfig { + #[serde(rename = "verbose", default)] + pub verbose: bool, + #[serde(rename = "emitError", default)] + pub emit_error: bool, + #[serde(rename = "showHelp", default)] + pub show_help: bool, +} + +create_deserialize_fn!( + deserialize_check_duplicate_package, + DuplicatePackageCheckerConfig +); diff --git a/crates/mako/src/config/experimental.rs b/crates/mako/src/config/experimental.rs new file mode 100644 index 000000000..a3bee9264 --- /dev/null +++ b/crates/mako/src/config/experimental.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExperimentalConfig { + pub webpack_syntax_validate: Vec, + pub require_context: bool, + #[serde(deserialize_with = "deserialize_detect_loop")] + pub detect_circular_dependence: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DetectCircularDependence { + pub ignores: Vec, + pub graphviz: bool, +} + +create_deserialize_fn!(deserialize_detect_loop, DetectCircularDependence); diff --git a/crates/mako/src/config/external.rs b/crates/mako/src/config/external.rs new file mode 100644 index 000000000..922be0616 --- /dev/null +++ b/crates/mako/src/config/external.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Hash)] +#[serde(untagged)] +pub enum ExternalConfig { + Basic(String), + Advanced(ExternalAdvanced), +} + +#[derive(Deserialize, Serialize, Debug, Hash)] +pub struct ExternalAdvancedSubpath { + pub exclude: Option>, + pub rules: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Hash)] +pub struct ExternalAdvanced { + pub root: String, + #[serde(rename = "type")] + pub module_type: Option, + pub script: Option, + pub subpath: Option, +} + +#[derive(Deserialize, Serialize, Debug, Hash)] +pub struct ExternalAdvancedSubpathRule { + pub regex: String, + #[serde(with = "external_target_format")] + pub target: ExternalAdvancedSubpathTarget, + #[serde(rename = "targetConverter")] + pub target_converter: Option, +} + +#[derive(Deserialize, Serialize, Debug, Hash)] +pub enum ExternalAdvancedSubpathConverter { + PascalCase, +} + +#[derive(Deserialize, Serialize, Debug, Hash)] +#[serde(untagged)] +pub enum ExternalAdvancedSubpathTarget { + Empty, + Tpl(String), +} + +/** + * custom formatter for convert $EMPTY to enum, because rename is not supported for $ symbol + * @see https://serde.rs/custom-date-format.html + */ +mod external_target_format { + use serde::{self, Deserialize, Deserializer, Serializer}; + + use super::ExternalAdvancedSubpathTarget; + + pub fn serialize(v: &ExternalAdvancedSubpathTarget, serializer: S) -> Result + where + S: Serializer, + { + match v { + ExternalAdvancedSubpathTarget::Empty => serializer.serialize_str("$EMPTY"), + ExternalAdvancedSubpathTarget::Tpl(s) => serializer.serialize_str(s), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let v = String::deserialize(deserializer)?; + + if v == "$EMPTY" { + Ok(ExternalAdvancedSubpathTarget::Empty) + } else { + Ok(ExternalAdvancedSubpathTarget::Tpl(v)) + } + } +} diff --git a/crates/mako/src/config/generic_usize.rs b/crates/mako/src/config/generic_usize.rs new file mode 100644 index 000000000..8aa1074d7 --- /dev/null +++ b/crates/mako/src/config/generic_usize.rs @@ -0,0 +1,17 @@ +pub struct GenericUsizeDefault; + +impl GenericUsizeDefault { + pub fn value() -> usize { + U + } +} + +#[cfg(test)] +mod tests { + use crate::config::GenericUsizeDefault; + + #[test] + fn test_generic_usize_default() { + assert!(GenericUsizeDefault::<100>::value() == 100usize) + } +} diff --git a/crates/mako/src/config/hmr.rs b/crates/mako/src/config/hmr.rs new file mode 100644 index 000000000..005be10ee --- /dev/null +++ b/crates/mako/src/config/hmr.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct HmrConfig {} + +create_deserialize_fn!(deserialize_hmr, HmrConfig); diff --git a/crates/mako/src/config/inline_css.rs b/crates/mako/src/config/inline_css.rs new file mode 100644 index 000000000..af3c4ec94 --- /dev/null +++ b/crates/mako/src/config/inline_css.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +pub struct InlineCssConfig {} + +create_deserialize_fn!(deserialize_inline_css, InlineCssConfig); diff --git a/crates/mako/src/config/macros.rs b/crates/mako/src/config/macros.rs new file mode 100644 index 000000000..4acdd112a --- /dev/null +++ b/crates/mako/src/config/macros.rs @@ -0,0 +1,33 @@ +/** + * a macro to create deserialize function that allow false value for optional struct + */ +#[macro_export] +macro_rules! create_deserialize_fn { + ($fn_name:ident, $struct_type:ty) => { + pub fn $fn_name<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; + + match value { + // allow false value for optional struct + serde_json::Value::Bool(false) => Ok(None), + // try deserialize + serde_json::Value::Object(obj) => Ok(Some( + serde_json::from_value::<$struct_type>(serde_json::Value::Object(obj)) + .map_err(serde::de::Error::custom)?, + )), + serde_json::Value::String(s) => Ok(Some( + serde_json::from_value::<$struct_type>(serde_json::Value::String(s.clone())) + .map_err(serde::de::Error::custom)?, + )), + _ => Err(serde::de::Error::custom(format!( + "invalid `{}` value: {}", + stringify!($fn_name).replace("deserialize_", ""), + value + ))), + } + } + }; +} diff --git a/crates/mako/src/config/mako.config.default.json b/crates/mako/src/config/mako.config.default.json new file mode 100644 index 000000000..bfbce56dd --- /dev/null +++ b/crates/mako/src/config/mako.config.default.json @@ -0,0 +1,79 @@ +{ + "entry": {}, + "output": { + "path": "dist", + "mode": "bundle", + "esVersion": "es2022", + "meta": false, + "chunkLoadingGlobal": "", + "preserveModules": false, + "preserveModulesRoot": "", + "skipWrite": false, + "crossOriginLoading": false, + "globalModuleRegistry": false + }, + "resolve": { "alias": [], "extensions": ["js", "jsx", "ts", "tsx"] }, + "mode": "development", + "minify": true, + "devtool": "source-map", + "externals": {}, + "copy": ["public"], + "providers": {}, + "publicPath": "/", + "inlineLimit": 10000, + "inlineExcludesExtensions": [], + "targets": { "chrome": 80 }, + "less": { "theme": {}, "lesscPath": "", "javascriptEnabled": true }, + "define": {}, + "mdx": false, + "platform": "browser", + "hmr": {}, + "moduleIdStrategy": "named", + "hash": false, + "_treeShaking": "basic", + "autoCSSModules": false, + "ignoreCSSParserErrors": false, + "dynamicImportToRequire": false, + "writeToDisk": true, + "transformImport": [], + "chunkParallel": true, + "clean": true, + "nodePolyfill": true, + "ignores": [], + "optimizePackageImports": false, + "emotion": false, + "flexBugs": false, + "cjs": false, + "optimization": { "skipModules": true, "concatenateModules": true }, + "react": { + "pragma": "React.createElement", + "importSource": "react", + "runtime": "automatic", + "pragmaFrag": "React.Fragment" + }, + "progress": { + "progressChars": "▨▨" + }, + "duplicatePackageChecker": { + "verbose": false, + "showHelp": false, + "emitError": false + }, + "emitAssets": true, + "cssModulesExportOnlyLocales": false, + "inlineCSS": false, + "rscServer": false, + "rscClient": false, + "experimental": { + "webpackSyntaxValidate": [], + "requireContext": true, + "detectCircularDependence": { + "ignores": ["node_modules"], + "graphviz": false + } + }, + "useDefineForClassFields": true, + "emitDecoratorMetadata": false, + "watch": { "ignorePaths": [], "_nodeModulesRegexes": [] }, + "devServer": { "host": "127.0.0.1", "port": 3000 } +} diff --git a/crates/mako/src/config/manifest.rs b/crates/mako/src/config/manifest.rs new file mode 100644 index 000000000..5581b8ee7 --- /dev/null +++ b/crates/mako/src/config/manifest.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +use crate::{create_deserialize_fn, plugins}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct ManifestConfig { + #[serde( + rename(deserialize = "fileName"), + default = "plugins::manifest::default_manifest_file_name" + )] + pub file_name: String, + #[serde(rename(deserialize = "basePath"), default)] + pub base_path: String, +} + +create_deserialize_fn!(deserialize_manifest, ManifestConfig); diff --git a/crates/mako/src/config/minifish.rs b/crates/mako/src/config/minifish.rs new file mode 100644 index 000000000..7e25adbe1 --- /dev/null +++ b/crates/mako/src/config/minifish.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MinifishConfig { + pub mapping: HashMap, + pub meta_path: Option, + pub inject: Option>, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InjectItem { + pub from: String, + pub named: Option, + pub namespace: Option, + pub exclude: Option, + pub include: Option, + pub prefer_require: Option, +} + +create_deserialize_fn!(deserialize_minifish, MinifishConfig); diff --git a/crates/mako/src/config/mode.rs b/crates/mako/src/config/mode.rs new file mode 100644 index 000000000..bf18c8d3c --- /dev/null +++ b/crates/mako/src/config/mode.rs @@ -0,0 +1,16 @@ +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] +pub enum Mode { + #[serde(rename = "development")] + Development, + #[serde(rename = "production")] + Production, +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value().unwrap().get_name().fmt(f) + } +} diff --git a/crates/mako/src/config/module_id_strategy.rs b/crates/mako/src/config/module_id_strategy.rs new file mode 100644 index 000000000..801c8afb8 --- /dev/null +++ b/crates/mako/src/config/module_id_strategy.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Copy, Debug)] +pub enum ModuleIdStrategy { + #[serde(rename = "hashed")] + Hashed, + #[serde(rename = "named")] + Named, + #[serde(rename = "numeric")] + Numeric, +} diff --git a/crates/mako/src/config/optimization.rs b/crates/mako/src/config/optimization.rs new file mode 100644 index 000000000..8e57ca69e --- /dev/null +++ b/crates/mako/src/config/optimization.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OptimizationConfig { + pub skip_modules: Option, + pub concatenate_modules: Option, +} + +create_deserialize_fn!(deserialize_optimization, OptimizationConfig); diff --git a/crates/mako/src/config/output.rs b/crates/mako/src/config/output.rs new file mode 100644 index 000000000..da3edb041 --- /dev/null +++ b/crates/mako/src/config/output.rs @@ -0,0 +1,58 @@ +use core::fmt; +use std::path::{Path, PathBuf}; + +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use swc_core::ecma::ast::EsVersion; + +use crate::create_deserialize_fn; +use crate::utils::get_pkg_name; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OutputConfig { + pub path: PathBuf, + pub mode: OutputMode, + pub es_version: EsVersion, + pub meta: bool, + pub chunk_loading_global: String, + pub preserve_modules: bool, + pub preserve_modules_root: PathBuf, + pub skip_write: bool, + #[serde(deserialize_with = "deserialize_cross_origin_loading")] + pub cross_origin_loading: Option, + pub global_module_registry: bool, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] +pub enum OutputMode { + #[serde(rename = "bundle")] + Bundle, + #[serde(rename = "bundless")] + Bundless, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum CrossOriginLoading { + #[serde(rename = "anonymous")] + Anonymous, + #[serde(rename = "use-credentials")] + UseCredentials, +} + +impl fmt::Display for CrossOriginLoading { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CrossOriginLoading::Anonymous => write!(f, "anonymous"), + CrossOriginLoading::UseCredentials => write!(f, "use-credentials"), + } + } +} + +pub fn get_default_chunk_loading_global(umd: Option, root: &Path) -> String { + let unique_name = umd.unwrap_or_else(|| get_pkg_name(root).unwrap_or("global".to_string())); + + format!("makoChunk_{}", unique_name) +} + +create_deserialize_fn!(deserialize_cross_origin_loading, CrossOriginLoading); diff --git a/crates/mako/src/config/progress.rs b/crates/mako/src/config/progress.rs new file mode 100644 index 000000000..37588b740 --- /dev/null +++ b/crates/mako/src/config/progress.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ProgressConfig { + #[serde(rename = "progressChars", default)] + pub progress_chars: String, +} + +create_deserialize_fn!(deserialize_progress, ProgressConfig); diff --git a/crates/mako/src/config/provider.rs b/crates/mako/src/config/provider.rs new file mode 100644 index 000000000..21f35463f --- /dev/null +++ b/crates/mako/src/config/provider.rs @@ -0,0 +1,7 @@ +use std::collections::HashMap; + +// format: HashMap +// e.g. +// { "process": ("process", "") } +// { "Buffer": ("buffer", "Buffer") } +pub type Providers = HashMap; diff --git a/crates/mako/src/config/px2rem.rs b/crates/mako/src/config/px2rem.rs new file mode 100644 index 000000000..3e3437710 --- /dev/null +++ b/crates/mako/src/config/px2rem.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use crate::{create_deserialize_fn, visitors}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Px2RemConfig { + #[serde(default = "visitors::css_px2rem::default_root")] + pub root: f64, + #[serde(rename = "propBlackList", default)] + pub prop_blacklist: Vec, + #[serde(rename = "propWhiteList", default)] + pub prop_whitelist: Vec, + #[serde(rename = "selectorBlackList", default)] + pub selector_blacklist: Vec, + #[serde(rename = "selectorWhiteList", default)] + pub selector_whitelist: Vec, + #[serde(rename = "selectorDoubleList", default)] + pub selector_doublelist: Vec, + #[serde(rename = "minPixelValue", default)] + pub min_pixel_value: f64, + #[serde(rename = "mediaQuery", default)] + pub media_query: bool, +} + +impl Default for Px2RemConfig { + fn default() -> Self { + Px2RemConfig { + root: visitors::css_px2rem::default_root(), + prop_blacklist: vec![], + prop_whitelist: vec![], + selector_blacklist: vec![], + selector_whitelist: vec![], + selector_doublelist: vec![], + min_pixel_value: 0.0, + media_query: false, + } + } +} + +create_deserialize_fn!(deserialize_px2rem, Px2RemConfig); diff --git a/crates/mako/src/config/react.rs b/crates/mako/src/config/react.rs new file mode 100644 index 000000000..38cfa12c7 --- /dev/null +++ b/crates/mako/src/config/react.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct ReactConfig { + pub pragma: String, + #[serde(rename = "importSource")] + pub import_source: String, + pub runtime: ReactRuntimeConfig, + #[serde(rename = "pragmaFrag")] + pub pragma_frag: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum ReactRuntimeConfig { + #[serde(rename = "automatic")] + Automatic, + #[serde(rename = "classic")] + Classic, +} diff --git a/crates/mako/src/config/resolve.rs b/crates/mako/src/config/resolve.rs new file mode 100644 index 000000000..dbe1b5029 --- /dev/null +++ b/crates/mako/src/config/resolve.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct ResolveConfig { + pub alias: Vec<(String, String)>, + pub extensions: Vec, +} diff --git a/crates/mako/src/config/rsc_client.rs b/crates/mako/src/config/rsc_client.rs new file mode 100644 index 000000000..e3517c446 --- /dev/null +++ b/crates/mako/src/config/rsc_client.rs @@ -0,0 +1,20 @@ +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RscClientConfig { + pub log_server_component: LogServerComponent, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, ValueEnum, Clone)] +pub enum LogServerComponent { + #[serde(rename = "error")] + Error, + #[serde(rename = "ignore")] + Ignore, +} + +create_deserialize_fn!(deserialize_rsc_client, RscClientConfig); diff --git a/crates/mako/src/config/rsc_server.rs b/crates/mako/src/config/rsc_server.rs new file mode 100644 index 000000000..980216bde --- /dev/null +++ b/crates/mako/src/config/rsc_server.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RscServerConfig { + pub client_component_tpl: String, + #[serde(rename = "emitCSS")] + pub emit_css: bool, +} + +create_deserialize_fn!(deserialize_rsc_server, RscServerConfig); diff --git a/crates/mako/src/config/stats.rs b/crates/mako/src/config/stats.rs new file mode 100644 index 000000000..9a3dcc04e --- /dev/null +++ b/crates/mako/src/config/stats.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct StatsConfig { + pub modules: bool, +} + +create_deserialize_fn!(deserialize_stats, StatsConfig); diff --git a/crates/mako/src/config/transform_import.rs b/crates/mako/src/config/transform_import.rs new file mode 100644 index 000000000..91384f621 --- /dev/null +++ b/crates/mako/src/config/transform_import.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(untagged)] +pub enum TransformImportStyle { + Built(String), + Source(bool), +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TransformImportConfig { + pub library_name: String, + pub library_directory: Option, + pub style: Option, +} diff --git a/crates/mako/src/config/tree_shaking.rs b/crates/mako/src/config/tree_shaking.rs new file mode 100644 index 000000000..e716201fa --- /dev/null +++ b/crates/mako/src/config/tree_shaking.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::create_deserialize_fn; + +#[derive(Deserialize, Serialize, Clone, Copy, Debug)] +pub enum TreeShakingStrategy { + #[serde(rename = "basic")] + Basic, + #[serde(rename = "advanced")] + Advanced, +} + +create_deserialize_fn!(deserialize_tree_shaking, TreeShakingStrategy); diff --git a/crates/mako/src/config/umd.rs b/crates/mako/src/config/umd.rs new file mode 100644 index 000000000..634c2c41a --- /dev/null +++ b/crates/mako/src/config/umd.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +use crate::create_deserialize_fn; + +pub type Umd = String; + +create_deserialize_fn!(deserialize_umd, Umd); diff --git a/crates/mako/src/config/watch.rs b/crates/mako/src/config/watch.rs new file mode 100644 index 000000000..3587c5024 --- /dev/null +++ b/crates/mako/src/config/watch.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WatchConfig { + pub ignore_paths: Option>, + #[serde(rename = "_nodeModulesRegexes")] + pub node_modules_regexes: Option>, +} diff --git a/crates/mako/src/generate/chunk_pot/util.rs b/crates/mako/src/generate/chunk_pot/util.rs index ae15de5b9..b0c0966d2 100644 --- a/crates/mako/src/generate/chunk_pot/util.rs +++ b/crates/mako/src/generate/chunk_pot/util.rs @@ -21,10 +21,11 @@ use twox_hash::XxHash64; use crate::ast::sourcemap::build_source_map_to_buf; use crate::compiler::Context; -use crate::config::{get_pkg_name, Mode}; +use crate::config::Mode; use crate::generate::chunk_pot::ChunkPot; use crate::generate::runtime::AppRuntimeTemplate; use crate::module::{relative_to_root, Module, ModuleAst}; +use crate::utils::get_pkg_name; pub(crate) fn render_module_js( ast: &SwcModule, diff --git a/crates/mako/src/generate/generate_chunks.rs b/crates/mako/src/generate/generate_chunks.rs index 68fe12e3e..e14946990 100644 --- a/crates/mako/src/generate/generate_chunks.rs +++ b/crates/mako/src/generate/generate_chunks.rs @@ -40,24 +40,7 @@ pub struct ChunkFile { impl ChunkFile { pub fn disk_name(&self) -> String { - // fixed os error 63 file name too long, reserve 48 bytes for _js-async、extension、.map and others - let reserve_file_name_length = 207; - let file_path = Path::new(&self.file_name); - let mut format_file_name = self.file_name.clone(); - if self.file_name.len() > reserve_file_name_length { - let mut hasher: XxHash64 = Default::default(); - hasher.write_str(self.file_name.as_str()); - let file_extension = file_path.extension().unwrap(); - let file_stem = file_path.file_stem().unwrap().to_string_lossy().to_string(); - let (_, reserve_file_path) = - file_stem.split_at(file_stem.len() - reserve_file_name_length); - format_file_name = format!( - "{}.{}.{}", - reserve_file_path, - &hasher.finish().to_string()[0..8], - file_extension.to_str().unwrap() - ); - } + let format_file_name = hash_too_long_file_name(&self.file_name); if let Some(hash) = &self.hash { hash_file_name(&format_file_name, hash) @@ -191,7 +174,7 @@ impl Compiler { hash_file_name(&js_filename, &placeholder), ); } else { - let js_filename = chunk_pot.js_name; + let js_filename = hash_too_long_file_name(&chunk_pot.js_name); if chunk_pot.stylesheet.is_some() { let css_filename = get_css_chunk_filename(&js_filename); @@ -408,3 +391,26 @@ fn hash_file_name(file_name: &String, hash: &String) -> String { format!("{}.{}.{}", file_stem, hash, file_extension) } + +fn hash_too_long_file_name(file_name: &String) -> String { + // fixed os error 63 file name too long, reserve 48 bytes for _js-async、extension、.map and others + let reserve_file_name_length = 207; + let file_path = Path::new(&file_name); + + let mut format_file_name = file_name.to_string(); + let file_stem = file_path.file_stem().unwrap().to_string_lossy().to_string(); + if file_stem.len() > reserve_file_name_length { + let mut hasher: XxHash64 = Default::default(); + hasher.write_str(file_name.as_str()); + let file_extension = file_path.extension().unwrap(); + let (_, reserve_file_path) = file_stem.split_at(file_stem.len() - reserve_file_name_length); + format_file_name = format!( + "{}.{}.{}", + reserve_file_path, + &hasher.finish().to_string()[0..8], + file_extension.to_str().unwrap() + ); + } + + format_file_name.to_string() +} diff --git a/crates/mako/src/lib.rs b/crates/mako/src/lib.rs index bf00a30b0..5bc0ede9a 100644 --- a/crates/mako/src/lib.rs +++ b/crates/mako/src/lib.rs @@ -14,7 +14,7 @@ mod module; mod module_graph; pub mod plugin; mod plugins; -mod resolve; +pub mod resolve; pub mod share; pub mod stats; pub mod utils; diff --git a/crates/mako/src/plugin.rs b/crates/mako/src/plugin.rs index f33b568fc..1b4d27883 100644 --- a/crates/mako/src/plugin.rs +++ b/crates/mako/src/plugin.rs @@ -53,6 +53,15 @@ pub trait Plugin: Any + Send + Sync { Ok(None) } + fn resolve_id( + &self, + _source: &str, + _importer: &str, + _context: &Arc, + ) -> Result> { + Ok(None) + } + fn next_build(&self, _next_build_param: &NextBuildParam) -> bool { true } @@ -208,7 +217,6 @@ impl PluginDriver { Ok(None) } - #[allow(dead_code)] pub fn transform_js( &self, param: &PluginTransformJsParam, @@ -233,7 +241,6 @@ impl PluginDriver { Ok(()) } - #[allow(dead_code)] pub fn before_resolve( &self, param: &mut Vec, @@ -245,6 +252,21 @@ impl PluginDriver { Ok(()) } + pub fn resolve_id( + &self, + source: &str, + importer: &str, + context: &Arc, + ) -> Result> { + for plugin in &self.plugins { + let ret = plugin.resolve_id(source, importer, context)?; + if ret.is_some() { + return Ok(ret); + } + } + Ok(None) + } + pub fn before_generate(&self, context: &Arc) -> Result<()> { for plugin in &self.plugins { plugin.generate_begin(context)?; diff --git a/crates/mako/src/plugins/duplicate_package_checker.rs b/crates/mako/src/plugins/duplicate_package_checker.rs index 2b3ee1b80..4f283a60b 100644 --- a/crates/mako/src/plugins/duplicate_package_checker.rs +++ b/crates/mako/src/plugins/duplicate_package_checker.rs @@ -99,17 +99,19 @@ impl DuplicatePackageCheckerPlugin { .as_ref() .and_then(|info| info.resolved_resource.as_ref()) { - let package_json = resource.0.package_json().unwrap(); - let raw_json = package_json.raw_json(); - if let Some(name) = package_json.name.clone() { - if let Some(version) = raw_json.as_object().unwrap().get("version") { - let package_info = PackageInfo { - name, - version: semver::Version::parse(version.as_str().unwrap()).unwrap(), - path: package_json.path.clone(), - }; - - packages.push(package_info); + if let Some(package_json) = resource.0.package_json() { + let raw_json = package_json.raw_json(); + if let Some(name) = package_json.name.clone() { + if let Some(version) = raw_json.as_object().unwrap().get("version") { + let package_info = PackageInfo { + name, + version: semver::Version::parse(version.as_str().unwrap()) + .unwrap(), + path: package_json.path.clone(), + }; + + packages.push(package_info); + } } } } diff --git a/crates/mako/src/resolve.rs b/crates/mako/src/resolve.rs index 19a62d2fe..4a74f4462 100644 --- a/crates/mako/src/resolve.rs +++ b/crates/mako/src/resolve.rs @@ -10,8 +10,10 @@ use regex::Captures; use thiserror::Error; use tracing::debug; +mod resolution; mod resource; -pub(crate) use resource::{ExternalResource, ResolvedResource, ResolverResource}; +pub use resolution::Resolution; +pub use resource::{ExternalResource, ResolvedResource, ResolverResource}; use crate::ast::file::parse_path; use crate::compiler::Context; @@ -49,6 +51,14 @@ pub fn resolve( crate::mako_profile_function!(); crate::mako_profile_scope!("resolve", &dep.source); + // plugin first + if let Some(resolved) = context + .plugin_driver + .resolve_id(&dep.source, path, context)? + { + return Ok(resolved); + } + if dep.source.starts_with("virtual:") { return Ok(ResolverResource::Virtual(PathBuf::from(&dep.source))); } @@ -244,7 +254,12 @@ fn do_resolve( // TODO: 临时方案,需要改成删除文件时删 resolve cache 里的内容 // 比如把 util.ts 改名为 util.tsx,目前应该是还有问题的 if resolution.path().exists() { - Ok(ResolverResource::Resolved(ResolvedResource(resolution))) + Ok(ResolverResource::Resolved(ResolvedResource(Resolution { + package_json: resolution.package_json().cloned(), + path: resolution.clone().into_path_buf(), + query: resolution.query().map(|q| q.to_string()), + fragment: resolution.fragment().map(|f| f.to_string()), + }))) } else { Err(anyhow!(ResolveError { path: source.to_string(), diff --git a/crates/mako/src/resolve/resolution.rs b/crates/mako/src/resolve/resolution.rs new file mode 100644 index 000000000..52c93dfd1 --- /dev/null +++ b/crates/mako/src/resolve/resolution.rs @@ -0,0 +1,63 @@ +use std::fmt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use oxc_resolver::PackageJson; + +#[derive(Clone)] +pub struct Resolution { + pub path: PathBuf, + pub query: Option, + pub fragment: Option, + pub package_json: Option>, +} + +impl Resolution { + /// Returns the path without query and fragment + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the path without query and fragment + pub fn into_path_buf(self) -> PathBuf { + self.path + } + + /// Returns the path query `?query`, contains the leading `?` + pub fn query(&self) -> Option<&str> { + self.query.as_deref() + } + + /// Returns the path fragment `#fragment`, contains the leading `#` + pub fn fragment(&self) -> Option<&str> { + self.fragment.as_deref() + } + + /// Returns serialized package_json + pub fn package_json(&self) -> Option<&Arc> { + self.package_json.as_ref() + } + + /// Returns the full path with query and fragment + pub fn full_path(&self) -> PathBuf { + let mut path = self.path.clone().into_os_string(); + if let Some(query) = &self.query { + path.push(query); + } + if let Some(fragment) = &self.fragment { + path.push(fragment); + } + PathBuf::from(path) + } +} + +impl fmt::Debug for Resolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Resolution") + .field("path", &self.path) + .field("query", &self.query) + .field("fragment", &self.fragment) + .field("package_json", &self.package_json.as_ref().map(|p| &p.path)) + .finish() + } +} diff --git a/crates/mako/src/resolve/resource.rs b/crates/mako/src/resolve/resource.rs index 7de6e08af..6e9219481 100644 --- a/crates/mako/src/resolve/resource.rs +++ b/crates/mako/src/resolve/resource.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use oxc_resolver::Resolution; +use crate::resolve::Resolution; #[derive(Debug, Clone)] pub struct ExternalResource { diff --git a/crates/mako/src/utils.rs b/crates/mako/src/utils.rs index fb3778fdf..afde8941a 100644 --- a/crates/mako/src/utils.rs +++ b/crates/mako/src/utils.rs @@ -7,6 +7,8 @@ pub(crate) mod test_helper; pub mod thread_pool; pub mod tokio_runtime; +use std::path::Path; + use anyhow::{anyhow, Result}; use base64::engine::general_purpose; use base64::Engine; @@ -51,6 +53,21 @@ pub fn process_req_url(public_path: &str, req_url: &str) -> Result { Ok(req_url.to_string()) } +pub(crate) fn get_pkg_name(root: &Path) -> Option { + let pkg_json_path = root.join("package.json"); + + if pkg_json_path.exists() { + let pkg_json = std::fs::read_to_string(pkg_json_path).unwrap(); + let pkg_json: serde_json::Value = serde_json::from_str(&pkg_json).unwrap(); + + pkg_json + .get("name") + .map(|name| name.as_str().unwrap().to_string()) + } else { + None + } +} + #[cached(key = "String", convert = r#"{ re.to_string() }"#)] pub fn create_cached_regex(re: &str) -> Regex { Regex::new(re).unwrap() diff --git a/docs/config.md b/docs/config.md index 44205841e..869cc77d6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -544,6 +544,7 @@ Specify the plugins to use. }; }) => void; load?: (filePath: string) => Promise<{ content: string, type: 'css'|'js'|'jsx'|'ts'|'tsx' }>; + resolveId?: (id: string, importer: string) => Promise<{ id: string, external: bool }>; } ``` diff --git a/docs/config.zh-CN.md b/docs/config.zh-CN.md index 152377c31..4e497031c 100644 --- a/docs/config.zh-CN.md +++ b/docs/config.zh-CN.md @@ -544,6 +544,7 @@ e.g. }; }) => void; load?: (filePath: string) => Promise<{ content: string, type: 'css'|'js'|'jsx'|'ts'|'tsx' }>; + resolveId?: (id: string, importer: string) => Promise<{ id: string, external: bool }>; } ``` diff --git a/e2e/fixtures/plugins/expect.js b/e2e/fixtures/plugins/expect.js index 848af158a..433942d89 100644 --- a/e2e/fixtures/plugins/expect.js +++ b/e2e/fixtures/plugins/expect.js @@ -8,3 +8,7 @@ assert(content.includes(`children: "foo.bar"`), `jsx in foo.bar works`); assert(content.includes(`children: ".bar"`), `jsx in hoo.bar works`); assert(content.includes(`children: ".haha"`), `plugin in node_modules works`); assert(content.includes(`children: ".hoo"`), `relative plugin works`); + +// resolve_id hook +assert(content.includes(`resolve_id mocked`), `resolve_id hook works`); +assert(content.includes(`module.exports = resolve_id_external;`), `resolve_id hook with external works`); diff --git a/e2e/fixtures/plugins/plugins.config.js b/e2e/fixtures/plugins/plugins.config.js index 5331b5b42..81e88e0bb 100644 --- a/e2e/fixtures/plugins/plugins.config.js +++ b/e2e/fixtures/plugins/plugins.config.js @@ -19,5 +19,17 @@ module.exports = [ }; } } - } + }, + { + async resolveId(source, importer) { + console.log('resolveId', source, importer); + if (source === 'resolve_id') { + return { id: require('path').join(__dirname, 'resolve_id_mock.js'), external: false }; + } + if (source === 'resolve_id_external') { + return { id: 'resolve_id_external', external: true }; + } + return null; + } + }, ]; diff --git a/e2e/fixtures/plugins/resolve_id_mock.js b/e2e/fixtures/plugins/resolve_id_mock.js new file mode 100644 index 000000000..39e08b0a5 --- /dev/null +++ b/e2e/fixtures/plugins/resolve_id_mock.js @@ -0,0 +1 @@ +console.log('resolve_id mocked'); diff --git a/e2e/fixtures/plugins/src/index.tsx b/e2e/fixtures/plugins/src/index.tsx index a936a668a..b53b90ae7 100644 --- a/e2e/fixtures/plugins/src/index.tsx +++ b/e2e/fixtures/plugins/src/index.tsx @@ -2,3 +2,5 @@ console.log(require('./foo.bar')); console.log(require('./hoo.bar')); console.log(require('./foo.haha')); console.log(require('./foo.hoo')); +console.log(require('resolve_id')); +console.log(require('resolve_id_external')); diff --git a/examples/dead-simple/app.tsx b/examples/dead-simple/app.tsx new file mode 100644 index 000000000..1a05af128 --- /dev/null +++ b/examples/dead-simple/app.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function App() { + return
Hello, world!
; +} diff --git a/examples/dead-simple/index.ts b/examples/dead-simple/index.ts index e53c31d05..400ee215a 100644 --- a/examples/dead-simple/index.ts +++ b/examples/dead-simple/index.ts @@ -1,6 +1,9 @@ -import { foo } from './foo'; -import './foo/foo'; +// import { foo } from './foo'; +// import './foo/foo'; + +import App from './app'; + /** * abcd */ -console.log(foo); +console.log(App); diff --git a/packages/bundler-mako/index.js b/packages/bundler-mako/index.js index 318932b14..43ff235df 100644 --- a/packages/bundler-mako/index.js +++ b/packages/bundler-mako/index.js @@ -636,6 +636,8 @@ async function getMakoConfig(opts) { plugins: opts.config.lessLoader?.plugins, }, analyze: analyze || process.env.ANALYZE ? {} : undefined, + sass: sassLoader, + ...mako, experimental: { webpackSyntaxValidate: [], requireContext: true, @@ -643,9 +645,8 @@ async function getMakoConfig(opts) { ignores: ['node_modules', '\\.umi'], graphviz: false, }, + ...mako.experimental, }, - sass: sassLoader, - ...mako, }; return makoConfig; diff --git a/packages/mako/binding.d.ts b/packages/mako/binding.d.ts index 1972f583e..138b79371 100644 --- a/packages/mako/binding.d.ts +++ b/packages/mako/binding.d.ts @@ -51,6 +51,7 @@ export interface JsHooks { }) => void; onGenerateFile?: (path: string, content: Buffer) => Promise; buildStart?: () => Promise; + resolveId?: (source: string, importer: string) => Promise<{ id: string }>; } export interface WriteFile { path: string; @@ -60,6 +61,10 @@ export interface LoadResult { content: string; type: string; } +export interface ResolveIdResult { + id: string; + external: boolean | null; +} export interface BuildParams { root: string; config: {