From c34411d1f7e810132e4a07a47a9a7f4bdc9e1ff4 Mon Sep 17 00:00:00 2001 From: Fy <1114550440@qq.com> Date: Sun, 7 Apr 2024 14:25:50 +0800 Subject: [PATCH] feat: mini-css-extract-plugin (#5072) --- Cargo.lock | 23 + crates/node_binding/binding.d.ts | 15 +- crates/rspack_binding_options/Cargo.toml | 1 + .../src/options/raw_builtins/mod.rs | 10 + .../options/raw_builtins/raw_css_extract.rs | 40 + .../src/options/raw_split_chunks/mod.rs | 8 +- .../rspack_binding_values/src/chunk_graph.rs | 3 +- crates/rspack_core/src/lib.rs | 34 +- crates/rspack_core/src/plugin/api.rs | 4 +- .../rspack_core/src/plugin/plugin_driver.rs | 6 +- .../src/plugin/impl_plugin_for_css_plugin.rs | 65 +- crates/rspack_plugin_css/src/plugin/mod.rs | 72 +- crates/rspack_plugin_extract_css/Cargo.toml | 24 + crates/rspack_plugin_extract_css/LICENSE | 22 + .../src/css_dependency.rs | 111 +++ .../src/css_module.rs | 221 ++++++ crates/rspack_plugin_extract_css/src/lib.rs | 6 + .../src/parser_and_generator.rs | 128 +++ .../rspack_plugin_extract_css/src/plugin.rs | 742 ++++++++++++++++++ .../rspack_plugin_extract_css/src/runtime.rs | 174 ++++ .../src/runtime/css_load.js | 58 ++ .../src/runtime/with_hmr.js | 41 + .../src/runtime/with_loading.js | 21 + crates/rspack_plugin_javascript/Cargo.toml | 1 + crates/rspack_plugin_runtime/src/lib.rs | 4 +- .../src/runtime_module/get_chunk_filename.rs | 34 +- .../src/runtime_plugin.rs | 14 +- .../__snapshots__/Defaults.unittest.js.snap | 5 + .../experiments/future-defaults-with-css.js | 12 +- packages/rspack/src/ExecuteModulePlugin.ts | 4 +- .../src/builtin-plugin/SplitChunksPlugin.ts | 2 + .../css-extract/hmr/hotModuleReplacement.js | 285 +++++++ .../css-extract/hmr/normalize-url.js | 46 ++ .../src/builtin-plugin/css-extract/index.ts | 124 +++ .../css-extract/loader-options.json | 32 + .../src/builtin-plugin/css-extract/loader.ts | 266 +++++++ .../css-extract/plugin-options.json | 79 ++ .../src/builtin-plugin/css-extract/utils.ts | 65 ++ packages/rspack/src/builtin-plugin/index.ts | 5 +- packages/rspack/src/config/adapterRuleUse.ts | 2 +- packages/rspack/src/config/defaults.ts | 18 +- packages/rspack/src/config/normalization.ts | 3 + packages/rspack/src/config/zod.ts | 2 + packages/rspack/src/exports.ts | 2 + packages/rspack/tsconfig.json | 6 +- 45 files changed, 2742 insertions(+), 98 deletions(-) create mode 100644 crates/rspack_binding_options/src/options/raw_builtins/raw_css_extract.rs create mode 100644 crates/rspack_plugin_extract_css/Cargo.toml create mode 100644 crates/rspack_plugin_extract_css/LICENSE create mode 100644 crates/rspack_plugin_extract_css/src/css_dependency.rs create mode 100644 crates/rspack_plugin_extract_css/src/css_module.rs create mode 100644 crates/rspack_plugin_extract_css/src/lib.rs create mode 100644 crates/rspack_plugin_extract_css/src/parser_and_generator.rs create mode 100644 crates/rspack_plugin_extract_css/src/plugin.rs create mode 100644 crates/rspack_plugin_extract_css/src/runtime.rs create mode 100644 crates/rspack_plugin_extract_css/src/runtime/css_load.js create mode 100644 crates/rspack_plugin_extract_css/src/runtime/with_hmr.js create mode 100644 crates/rspack_plugin_extract_css/src/runtime/with_loading.js create mode 100644 packages/rspack/src/builtin-plugin/css-extract/hmr/hotModuleReplacement.js create mode 100644 packages/rspack/src/builtin-plugin/css-extract/hmr/normalize-url.js create mode 100644 packages/rspack/src/builtin-plugin/css-extract/index.ts create mode 100644 packages/rspack/src/builtin-plugin/css-extract/loader-options.json create mode 100644 packages/rspack/src/builtin-plugin/css-extract/loader.ts create mode 100644 packages/rspack/src/builtin-plugin/css-extract/plugin-options.json create mode 100644 packages/rspack/src/builtin-plugin/css-extract/utils.ts diff --git a/Cargo.lock b/Cargo.lock index a67a4e2c8f7e..73848e4a5eff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,6 +3070,7 @@ dependencies = [ "rspack_plugin_ensure_chunk_conditions", "rspack_plugin_entry", "rspack_plugin_externals", + "rspack_plugin_extract_css", "rspack_plugin_hmr", "rspack_plugin_html", "rspack_plugin_javascript", @@ -3502,6 +3503,28 @@ dependencies = [ "rspack_regex", ] +[[package]] +name = "rspack_plugin_extract_css" +version = "0.1.0" +dependencies = [ + "async-trait", + "dashmap", + "once_cell", + "regex", + "rspack_core", + "rspack_error", + "rspack_hash", + "rspack_hook", + "rspack_identifier", + "rspack_plugin_css", + "rspack_plugin_runtime", + "rspack_util", + "rustc-hash", + "serde", + "serde_json", + "ustr-fxhash", +] + [[package]] name = "rspack_plugin_hmr" version = "0.1.0" diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index ceaa4c6495dd..3cb996bf5f22 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -154,7 +154,8 @@ export enum BuiltinPluginName { SwcJsMinimizerRspackPlugin = 'SwcJsMinimizerRspackPlugin', SwcCssMinimizerRspackPlugin = 'SwcCssMinimizerRspackPlugin', BundlerInfoRspackPlugin = 'BundlerInfoRspackPlugin', - JsLoaderRspackPlugin = 'JsLoaderRspackPlugin' + JsLoaderRspackPlugin = 'JsLoaderRspackPlugin', + CssExtractRspackPlugin = 'CssExtractRspackPlugin' } export function cleanupGlobalTrace(): void @@ -740,6 +741,17 @@ export interface RawCssAutoParserOptions { namedExports?: boolean } +export interface RawCssExtractPluginOption { + filename: string + chunkFilename: string + ignoreOrder: boolean + insert?: string + attributes: Record + linkType?: string + runtime: boolean + pathinfo: boolean +} + export interface RawCssGeneratorOptions { exportsConvention?: "as-is" | "camel-case" | "camel-case-only" | "dashes" | "dashes-only" exportsOnly?: boolean @@ -1235,6 +1247,7 @@ export interface RawSplitChunksOptions { automaticNameDelimiter?: string maxAsyncRequests?: number maxInitialRequests?: number + defaultSizeTypes: Array minChunks?: number hidePathInfo?: boolean minSize?: number diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index 0df870bae298..71cec7f6ef74 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -33,6 +33,7 @@ rspack_plugin_devtool = { path = "../rspack_plugin_devtool" } rspack_plugin_ensure_chunk_conditions = { path = "../rspack_plugin_ensure_chunk_conditions" } rspack_plugin_entry = { path = "../rspack_plugin_entry" } rspack_plugin_externals = { path = "../rspack_plugin_externals" } +rspack_plugin_extract_css = { path = "../rspack_plugin_extract_css" } rspack_plugin_hmr = { path = "../rspack_plugin_hmr" } rspack_plugin_html = { path = "../rspack_plugin_html" } rspack_plugin_javascript = { path = "../rspack_plugin_javascript" } diff --git a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs index 1693e780f883..cc46b68eab0f 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -1,6 +1,7 @@ mod raw_banner; mod raw_bundle_info; mod raw_copy; +mod raw_css_extract; mod raw_html; mod raw_limit_chunk_count; mod raw_mf; @@ -68,6 +69,7 @@ pub use self::{ }; use self::{ raw_bundle_info::{RawBundlerInfoModeWrapper, RawBundlerInfoPluginOptions}, + raw_css_extract::RawCssExtractPluginOption, raw_mf::{RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions}, }; use crate::{ @@ -144,6 +146,7 @@ pub enum BuiltinPluginName { // rspack js adapter plugins // naming format follow XxxRspackPlugin JsLoaderRspackPlugin, + CssExtractRspackPlugin, } #[napi(object)] @@ -408,6 +411,13 @@ impl BuiltinPlugin { JsLoaderResolverPlugin::new(downcast_into::(self.options)?).boxed(), ); } + BuiltinPluginName::CssExtractRspackPlugin => { + let plugin = rspack_plugin_extract_css::plugin::PluginCssExtract::new( + downcast_into::(self.options)?.into(), + ) + .boxed(); + plugins.push(plugin); + } } Ok(()) } diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_css_extract.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_css_extract.rs new file mode 100644 index 000000000000..766d06d68181 --- /dev/null +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_css_extract.rs @@ -0,0 +1,40 @@ +use std::collections::HashMap; + +use napi_derive::napi; +use rspack_plugin_extract_css::plugin::{CssExtractOptions, InsertType}; + +#[napi(object)] +pub struct RawCssExtractPluginOption { + pub filename: String, + pub chunk_filename: String, + pub ignore_order: bool, + pub insert: Option, + pub attributes: HashMap, + pub link_type: Option, + pub runtime: bool, + pub pathinfo: bool, +} + +impl From for CssExtractOptions { + fn from(value: RawCssExtractPluginOption) -> Self { + Self { + filename: value.filename, + chunk_filename: value.chunk_filename, + ignore_order: value.ignore_order, + insert: value + .insert + .map(|insert| { + if insert.starts_with("function") || insert.starts_with('(') { + InsertType::Fn(insert) + } else { + InsertType::Selector(insert) + } + }) + .unwrap_or(InsertType::Default), + attributes: value.attributes.into_iter().collect(), + link_type: value.link_type, + runtime: value.runtime, + pathinfo: value.pathinfo, + } + } +} diff --git a/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs b/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs index 70a21f84a046..8bd53b94c677 100644 --- a/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_split_chunks/mod.rs @@ -42,7 +42,7 @@ pub struct RawSplitChunksOptions { pub automatic_name_delimiter: Option, pub max_async_requests: Option, pub max_initial_requests: Option, - // pub default_size_types: Option>, + pub default_size_types: Vec, pub min_chunks: Option, pub hide_path_info: Option, pub min_size: Option, @@ -115,7 +115,11 @@ impl From for rspack_plugin_split_chunks::PluginOptions { normalize_raw_chunk_name(name) }); - let default_size_types = [SourceType::JavaScript, SourceType::Unknown]; + let default_size_types = raw_opts + .default_size_types + .into_iter() + .map(|size_type| SourceType::from(size_type.as_str())) + .collect::>(); let create_sizes = |size: Option| { size diff --git a/crates/rspack_binding_values/src/chunk_graph.rs b/crates/rspack_binding_values/src/chunk_graph.rs index 794fe33ee223..d450e9444628 100644 --- a/crates/rspack_binding_values/src/chunk_graph.rs +++ b/crates/rspack_binding_values/src/chunk_graph.rs @@ -64,8 +64,7 @@ pub fn get_chunk_modules_iterable_by_source_type( .chunk_graph .get_chunk_modules_iterable_by_source_type( &ChunkUkey::from(js_chunk_ukey as usize), - SourceType::try_from(source_type.as_str()) - .map_err(|e| napi::Error::from_reason(e.to_string()))?, + SourceType::from(source_type.as_str()), &compilation.get_module_graph(), ) .filter_map(|module| module.to_js_module().ok()) diff --git a/crates/rspack_core/src/lib.rs b/crates/rspack_core/src/lib.rs index fcf17ed60a74..5b33dc447ff6 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -112,6 +112,7 @@ pub enum SourceType { Remote, ShareInit, ConsumeShared, + Custom(Ustr), #[default] Unknown, CssImport, @@ -130,30 +131,25 @@ impl std::fmt::Display for SourceType { SourceType::ConsumeShared => write!(f, "consume-shared"), SourceType::Unknown => write!(f, "unknown"), SourceType::CssImport => write!(f, "css-import"), + SourceType::Custom(source_type) => f.write_str(source_type), } } } -impl TryFrom<&str> for SourceType { - type Error = rspack_error::Error; - - fn try_from(value: &str) -> Result { +impl From<&str> for SourceType { + fn from(value: &str) -> Self { match value { - "javascript" => Ok(Self::JavaScript), - "css" => Ok(Self::Css), - "wasm" => Ok(Self::Wasm), - "asset" => Ok(Self::Asset), - "expose" => Ok(Self::Expose), - "remote" => Ok(Self::Remote), - "share-init" => Ok(Self::ShareInit), - "consume-shared" => Ok(Self::ConsumeShared), - "unknown" => Ok(Self::Unknown), - "css-import" => Ok(Self::CssImport), - - _ => { - use rspack_error::error; - Err(error!("invalid source type: {value}")) - } + "javascript" => Self::JavaScript, + "css" => Self::Css, + "wasm" => Self::Wasm, + "asset" => Self::Asset, + "expose" => Self::Expose, + "remote" => Self::Remote, + "share-init" => Self::ShareInit, + "consume-shared" => Self::ConsumeShared, + "unknown" => Self::Unknown, + "css-import" => Self::CssImport, + other => SourceType::Custom(other.into()), } } } diff --git a/crates/rspack_core/src/plugin/api.rs b/crates/rspack_core/src/plugin/api.rs index 4a3cefed7d86..2475a8225cd3 100644 --- a/crates/rspack_core/src/plugin/api.rs +++ b/crates/rspack_core/src/plugin/api.rs @@ -4,7 +4,7 @@ use rspack_error::{IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; use rspack_hash::RspackHashDigest; use rspack_loader_runner::{Content, LoaderContext, ResourceData}; use rspack_sources::BoxSource; -use rustc_hash::FxHashMap; +use rspack_util::fx_dashmap::FxDashMap; use crate::{ AdditionalChunkRuntimeRequirementsArgs, AdditionalModuleRequirementsArgs, AssetInfo, BoxLoader, @@ -319,7 +319,7 @@ pub type BoxedParserAndGeneratorBuilder = Box< pub struct ApplyContext<'c> { pub(crate) registered_parser_and_generator_builder: - &'c mut FxHashMap, + &'c mut FxDashMap, pub compiler_hooks: &'c mut CompilerHooks, pub compilation_hooks: &'c mut CompilationHooks, pub normal_module_factory_hooks: &'c mut NormalModuleFactoryHooks, diff --git a/crates/rspack_core/src/plugin/plugin_driver.rs b/crates/rspack_core/src/plugin/plugin_driver.rs index 945abb824b11..f4f63078a087 100644 --- a/crates/rspack_core/src/plugin/plugin_driver.rs +++ b/crates/rspack_core/src/plugin/plugin_driver.rs @@ -5,6 +5,7 @@ use std::{ use rspack_error::{Diagnostic, Result, TWithDiagnosticArray}; use rspack_loader_runner::{LoaderContext, ResourceData}; +use rspack_util::fx_dashmap::FxDashMap; use rustc_hash::FxHashMap as HashMap; use tracing::instrument; @@ -28,7 +29,8 @@ pub struct PluginDriver { pub plugins: Vec>, pub resolver_factory: Arc, // pub registered_parser: HashMap, - pub registered_parser_and_generator_builder: HashMap, + pub registered_parser_and_generator_builder: + FxDashMap, /// Collecting error generated by plugin phase, e.g., `Syntax Error` pub diagnostics: Arc>>, pub compiler_hooks: CompilerHooks, @@ -59,7 +61,7 @@ impl PluginDriver { let mut compilation_hooks = Default::default(); let mut normal_module_factory_hooks = Default::default(); let mut context_module_factory_hooks = Default::default(); - let mut registered_parser_and_generator_builder = HashMap::default(); + let mut registered_parser_and_generator_builder = FxDashMap::default(); let mut apply_context = ApplyContext { registered_parser_and_generator_builder: &mut registered_parser_and_generator_builder, compiler_hooks: &mut compiler_hooks, diff --git a/crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs b/crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs index 697dd2f0aa0c..ef6f1030f21e 100644 --- a/crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs +++ b/crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs @@ -16,7 +16,7 @@ use rspack_core::{ LibIdentOptions, PluginContext, PluginRuntimeRequirementsInTreeOutput, PublicPath, RuntimeGlobals, RuntimeRequirementsInTreeArgs, }; -use rspack_error::{IntoTWithDiagnosticArray, Result}; +use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result}; use rspack_hash::RspackHash; use rspack_hook::{plugin_hook, AsyncSeries2}; use rspack_plugin_runtime::is_enabled_for_chunk; @@ -228,7 +228,7 @@ impl Plugin for CssPlugin { let compilation = &args.compilation; let chunk = compilation.chunk_by_ukey.expect_get(&args.chunk_ukey); let module_graph = compilation.get_module_graph(); - let ordered_modules = Self::get_ordered_chunk_css_modules( + let (ordered_modules, _) = Self::get_ordered_chunk_css_modules( chunk, &compilation.chunk_graph, &module_graph, @@ -270,7 +270,7 @@ impl Plugin for CssPlugin { return Ok(vec![].with_empty_diagnostic()); } let module_graph = compilation.get_module_graph(); - let ordered_css_modules = Self::get_ordered_chunk_css_modules( + let (ordered_css_modules, conflicts) = Self::get_ordered_chunk_css_modules( chunk, &compilation.chunk_graph, &module_graph, @@ -317,16 +317,55 @@ impl Plugin for CssPlugin { } else { source.boxed() }; - Ok( - vec![RenderManifestEntry::new( - source.boxed(), - output_path, - asset_info, - false, - false, - )] - .with_empty_diagnostic(), - ) + Ok({ + if let Some(conflicts) = conflicts { + vec![RenderManifestEntry::new( + source.boxed(), + output_path, + asset_info, + false, + false, + )] + .with_diagnostic( + conflicts + .into_iter() + .map(|conflict| { + let chunk = conflict.chunk.as_ref(&compilation.chunk_by_ukey); + let mg = compilation.get_module_graph(); + + let failed_module = mg + .module_by_identifier(&conflict.failed_module) + .expect("should have module"); + let selected_module = mg + .module_by_identifier(&conflict.selected_module) + .expect("should have module"); + + Diagnostic::warn( + "Conflicting order".into(), + format!( + "chunk {}\nConflicting order between {} and {}", + chunk + .name + .as_ref() + .unwrap_or(chunk.id.as_ref().expect("should have chunk id")), + failed_module.readable_identifier(&compilation.options.context), + selected_module.readable_identifier(&compilation.options.context) + ), + ) + }) + .collect(), + ) + } else { + vec![RenderManifestEntry::new( + source.boxed(), + output_path, + asset_info, + false, + false, + )] + .with_empty_diagnostic() + } + }) } async fn runtime_requirements_in_tree( diff --git a/crates/rspack_plugin_css/src/plugin/mod.rs b/crates/rspack_plugin_css/src/plugin/mod.rs index eb35ad18b6c2..45dc7b13f610 100644 --- a/crates/rspack_plugin_css/src/plugin/mod.rs +++ b/crates/rspack_plugin_css/src/plugin/mod.rs @@ -3,6 +3,7 @@ mod impl_plugin_for_css_plugin; use std::cmp::{self, Reverse}; use rspack_core::{Chunk, ChunkGraph, Compilation, Module, ModuleGraph, SourceType}; +use rspack_core::{ChunkUkey, ModuleIdentifier}; use rspack_hook::plugin; use rspack_identifier::IdentifierSet; @@ -10,22 +11,33 @@ use rspack_identifier::IdentifierSet; #[derive(Debug, Default)] pub struct CssPlugin; +#[derive(Debug)] +pub struct CssOrderConflicts { + pub chunk: ChunkUkey, + pub failed_module: ModuleIdentifier, + pub selected_module: ModuleIdentifier, +} + impl CssPlugin { pub(crate) fn get_ordered_chunk_css_modules<'chunk_graph>( chunk: &Chunk, chunk_graph: &'chunk_graph ChunkGraph, module_graph: &'chunk_graph ModuleGraph, compilation: &Compilation, - ) -> Vec<&'chunk_graph dyn Module> { - let mut external_css_modules = Self::get_ordered_chunk_css_modules_by_type( - chunk, - chunk_graph, - module_graph, - compilation, - SourceType::CssImport, - ); + ) -> ( + Vec<&'chunk_graph dyn Module>, + Option>, + ) { + let (mut external_css_modules, conflicts_external) = + Self::get_ordered_chunk_css_modules_by_type( + chunk, + chunk_graph, + module_graph, + compilation, + SourceType::CssImport, + ); - let mut css_modules = Self::get_ordered_chunk_css_modules_by_type( + let (mut css_modules, conflicts) = Self::get_ordered_chunk_css_modules_by_type( chunk, chunk_graph, module_graph, @@ -35,7 +47,17 @@ impl CssPlugin { external_css_modules.append(&mut css_modules); - external_css_modules + let conflicts = match (conflicts_external, conflicts) { + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + (Some(mut a), Some(mut b)) => { + a.append(&mut b); + Some(a) + } + (None, None) => None, + }; + + (external_css_modules, conflicts) } fn get_ordered_chunk_css_modules_by_type<'chunk_graph>( @@ -44,26 +66,29 @@ impl CssPlugin { module_graph: &'chunk_graph ModuleGraph, compilation: &Compilation, source_type: SourceType, - ) -> Vec<&'chunk_graph dyn Module> { + ) -> ( + Vec<&'chunk_graph dyn Module>, + Option>, + ) { // Align with https://github.com/webpack/webpack/blob/8241da7f1e75c5581ba535d127fa66aeb9eb2ac8/lib/css/CssModulesPlugin.js#L368 let mut css_modules = chunk_graph .get_chunk_modules_iterable_by_source_type(&chunk.ukey, source_type, module_graph) .collect::>(); css_modules.sort_unstable_by_key(|module| module.identifier()); - let css_modules = Self::get_modules_in_order(chunk, css_modules, compilation); + let (css_modules, conflicts) = Self::get_modules_in_order(chunk, css_modules, compilation); - css_modules + (css_modules, conflicts) } - pub(crate) fn get_modules_in_order<'module>( + pub fn get_modules_in_order<'module>( chunk: &Chunk, modules: Vec<&'module dyn Module>, compilation: &Compilation, - ) -> Vec<&'module dyn Module> { + ) -> (Vec<&'module dyn Module>, Option>) { // Align with https://github.com/webpack/webpack/blob/8241da7f1e75c5581ba535d127fa66aeb9eb2ac8/lib/css/CssModulesPlugin.js#L269 if modules.is_empty() { - return vec![]; + return (vec![], None); }; let modules_list = modules.clone(); @@ -112,12 +137,13 @@ impl CssPlugin { .expect("must have one") .list; ret.reverse(); - return ret; + return (ret, None); }; modules_by_chunk_group.sort_unstable_by(compare_module_lists); let mut final_modules: Vec<&'module dyn Module> = vec![]; + let mut conflicts: Option> = None; loop { let mut failed_modules: IdentifierSet = Default::default(); @@ -156,6 +182,16 @@ impl CssPlugin { // There is a not resolve-able conflict with the selectedModule // TODO(hyf0): we should emit a warning here tracing::warn!("Conflicting order between"); + let conflict = CssOrderConflicts { + chunk: chunk.ukey, + failed_module: has_failed.identifier(), + selected_module: selected_module.identifier(), + }; + if let Some(conflicts) = &mut conflicts { + conflicts.push(conflict); + } else { + conflicts = Some(vec![conflict]) + } // if (compilation) { // // TODO print better warning // compilation.warnings.push( @@ -191,7 +227,7 @@ impl CssPlugin { modules_by_chunk_group.sort_unstable_by(compare_module_lists); } - final_modules + (final_modules, conflicts) } } diff --git a/crates/rspack_plugin_extract_css/Cargo.toml b/crates/rspack_plugin_extract_css/Cargo.toml new file mode 100644 index 000000000000..02ed9b6735c1 --- /dev/null +++ b/crates/rspack_plugin_extract_css/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_plugin_extract_css" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.1.0" + +[dependencies] +async-trait = { workspace = true } +dashmap = { workspace = true } +once_cell = { workspace = true } +regex = { workspace = true } +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_hash = { path = "../rspack_hash" } +rspack_hook = { path = "../rspack_hook" } +rspack_identifier = { path = "../rspack_identifier" } +rspack_plugin_css = { path = "../rspack_plugin_css" } +rspack_plugin_runtime = { path = "../rspack_plugin_runtime" } +rspack_util = { path = "../rspack_util" } +rustc-hash = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +ustr = { workspace = true } diff --git a/crates/rspack_plugin_extract_css/LICENSE b/crates/rspack_plugin_extract_css/LICENSE new file mode 100644 index 000000000000..46310101ad8a --- /dev/null +++ b/crates/rspack_plugin_extract_css/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022-present Bytedance, Inc. and its affiliates. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/rspack_plugin_extract_css/src/css_dependency.rs b/crates/rspack_plugin_extract_css/src/css_dependency.rs new file mode 100644 index 000000000000..e68bbe69d431 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/css_dependency.rs @@ -0,0 +1,111 @@ +use std::path::PathBuf; + +use rspack_core::{ + AsContextDependency, AsDependencyTemplate, ConnectionState, Dependency, DependencyCategory, + DependencyId, ModuleDependency, ModuleGraph, ModuleIdentifier, +}; +use rustc_hash::FxHashSet; + +use crate::css_module::DEPENDENCY_TYPE; + +#[derive(Debug, Clone)] +pub struct CssDependency { + pub id: DependencyId, + pub identifier: String, + pub content: String, + pub context: String, + pub media: String, + pub supports: String, + pub source_map: String, + + // One module can be split apart by using `@import` in the middle of one module + pub identifier_index: u32, + + // determine module's postOrderIndex + pub order_index: u32, + + resource_identifier: String, + + pub filepath: PathBuf, +} + +impl CssDependency { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + identifier: String, + content: String, + context: String, + media: String, + supports: String, + source_map: String, + identifier_index: u32, + order_index: u32, + filepath: PathBuf, + ) -> Self { + let resource_identifier = format!("css-module-{}-{}", &identifier, identifier_index); + Self { + id: DependencyId::new(), + identifier, + content, + context, + media, + supports, + source_map, + identifier_index, + order_index, + resource_identifier, + filepath, + } + } +} + +impl AsDependencyTemplate for CssDependency {} +impl AsContextDependency for CssDependency {} + +impl Dependency for CssDependency { + fn dependency_debug_name(&self) -> &'static str { + "mini-extract-css-dependency" + } + + fn resource_identifier(&self) -> Option<&str> { + Some(&self.resource_identifier) + } + + fn id(&self) -> &DependencyId { + &self.id + } + + fn dependency_type(&self) -> &rspack_core::DependencyType { + &DEPENDENCY_TYPE + } + + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Unknown + } + + fn get_module_evaluation_side_effects_state( + &self, + _module_graph: &ModuleGraph, + _module_chain: &mut FxHashSet, + ) -> ConnectionState { + ConnectionState::TransitiveOnly + } + + // compare to Webpack, which has SortableSet to store + // the connections in order, if dependency has no span, + // it can keep the right order, but Rspack uses HashSet, + // when determining the postOrderIndex, Rspack uses + // dependency span to set correct order + fn span(&self) -> Option { + Some(rspack_core::ErrorSpan { + start: self.order_index, + end: self.order_index + 1, + }) + } +} + +impl ModuleDependency for CssDependency { + fn request(&self) -> &str { + &self.identifier + } +} diff --git a/crates/rspack_plugin_extract_css/src/css_module.rs b/crates/rspack_plugin_extract_css/src/css_module.rs new file mode 100644 index 000000000000..653c66da5106 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/css_module.rs @@ -0,0 +1,221 @@ +use std::hash::Hash; +use std::path::PathBuf; + +use once_cell::sync::Lazy; +use rspack_core::rspack_sources::Source; +use rspack_core::{ + impl_build_info_meta, impl_source_map_config, AsyncDependenciesBlockIdentifier, BuildContext, + BuildInfo, BuildMeta, BuildResult, CodeGenerationResult, Compilation, CompilerOptions, + ConcatenationScope, DependenciesBlock, DependencyId, DependencyType, Module, ModuleFactory, + ModuleFactoryCreateData, ModuleFactoryResult, RuntimeSpec, SourceType, +}; +use rspack_error::Result; +use rspack_error::{impl_empty_diagnosable_trait, Diagnostic}; +use rspack_hash::{RspackHash, RspackHashDigest}; +use rspack_identifier::{Identifiable, Identifier}; +use rustc_hash::FxHashSet; + +use crate::css_dependency::CssDependency; +use crate::plugin::{MODULE_TYPE, SOURCE_TYPE}; + +pub(crate) static DEPENDENCY_TYPE: Lazy = + Lazy::new(|| DependencyType::Custom("mini-extract-dep".into())); + +#[impl_source_map_config] +#[derive(Debug)] +pub(crate) struct CssModule { + pub(crate) identifier: String, + pub(crate) content: String, + pub(crate) context: String, + pub(crate) media: String, + pub(crate) supports: String, + pub(crate) source_map: String, + pub(crate) identifier_index: u32, + + pub build_info: Option, + pub build_meta: Option, + + blocks: Vec, + dependencies: Vec, + + identifier__: Identifier, + filepath: PathBuf, +} + +impl Hash for CssModule { + fn hash(&self, state: &mut H) { + self.identifier.hash(state); + } +} + +impl PartialEq for CssModule { + fn eq(&self, other: &Self) -> bool { + self.identifier == other.identifier + } +} + +impl Eq for CssModule {} + +impl CssModule { + pub fn new(dep: CssDependency) -> Self { + let identifier__ = format!( + "css|{}|{}|{}|{}}}", + dep.identifier, dep.identifier_index, dep.supports, dep.media, + ) + .into(); + + Self { + identifier: dep.identifier, + content: dep.content, + context: dep.context, + media: dep.media, + supports: dep.supports, + source_map: dep.source_map, + identifier_index: dep.identifier_index, + blocks: vec![], + dependencies: vec![], + build_info: None, + build_meta: None, + source_map_kind: rspack_util::source_map::SourceMapKind::None, + identifier__, + filepath: dep.filepath, + } + } + + fn compute_hash(&self, options: &CompilerOptions) -> RspackHashDigest { + let mut hasher = RspackHash::from(&options.output); + + self.content.hash(&mut hasher); + self.supports.hash(&mut hasher); + self.media.hash(&mut hasher); + self.context.hash(&mut hasher); + + hasher.digest(&options.output.hash_digest) + } +} + +#[async_trait::async_trait] +impl Module for CssModule { + impl_build_info_meta!(); + + fn readable_identifier(&self, context: &rspack_core::Context) -> std::borrow::Cow { + std::borrow::Cow::Owned(format!( + "css {}{}{}{}", + context.shorten(&self.identifier), + if self.identifier_index > 0 { + format!("({})", self.identifier_index) + } else { + "".into() + }, + if self.supports.is_empty() { + "".into() + } else { + format!(" (supports {})", self.supports) + }, + if self.media.is_empty() { + "".into() + } else { + format!(" (media {})", self.media) + } + )) + } + + fn name_for_condition(&self) -> Option> { + self + .identifier + .split('!') + .last() + .map(|resource| resource.split('?').next().unwrap_or(resource).into()) + } + + fn size(&self, _source_type: &SourceType) -> f64 { + self.content.len() as f64 + } + + fn original_source(&self) -> Option<&dyn Source> { + None + } + + fn module_type(&self) -> &rspack_core::ModuleType { + &MODULE_TYPE + } + + fn source_types(&self) -> &[SourceType] { + &*SOURCE_TYPE + } + + async fn build( + &mut self, + build_context: BuildContext<'_>, + _compilation: Option<&Compilation>, + ) -> Result { + let mut file_deps = FxHashSet::default(); + file_deps.insert(self.filepath.clone()); + + Ok(BuildResult { + build_info: BuildInfo { + hash: Some(self.compute_hash(build_context.compiler_options)), + file_dependencies: file_deps, + ..Default::default() + }, + ..Default::default() + }) + } + + fn code_generation( + &self, + _compilation: &Compilation, + _runtime: Option<&RuntimeSpec>, + _concatenation_scope: Option, + ) -> Result { + Ok(CodeGenerationResult::default()) + } + + fn get_diagnostics(&self) -> Vec { + vec![] + } +} + +impl Identifiable for CssModule { + fn identifier(&self) -> rspack_identifier::Identifier { + self.identifier__ + } +} + +impl DependenciesBlock for CssModule { + fn add_block_id(&mut self, block: AsyncDependenciesBlockIdentifier) { + self.blocks.push(block) + } + + fn get_blocks(&self) -> &[AsyncDependenciesBlockIdentifier] { + &self.blocks + } + + fn add_dependency_id(&mut self, dependency: DependencyId) { + self.dependencies.push(dependency) + } + + fn get_dependencies(&self) -> &[DependencyId] { + &self.dependencies + } +} + +#[derive(Debug)] +pub(crate) struct CssModuleFactory; + +#[async_trait::async_trait] +impl ModuleFactory for CssModuleFactory { + async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result { + let css_dep = data + .dependency + .downcast_ref::() + .expect("unreachable"); + + Ok(ModuleFactoryResult::new_with_module(Box::new( + CssModule::new(css_dep.clone()), + ))) + } +} + +impl_empty_diagnosable_trait!(CssModule); +impl_empty_diagnosable_trait!(CssModuleFactory); diff --git a/crates/rspack_plugin_extract_css/src/lib.rs b/crates/rspack_plugin_extract_css/src/lib.rs new file mode 100644 index 000000000000..ffab4474ce47 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/lib.rs @@ -0,0 +1,6 @@ +#![feature(let_chains)] +pub mod css_dependency; +mod css_module; +mod parser_and_generator; +pub mod plugin; +mod runtime; diff --git a/crates/rspack_plugin_extract_css/src/parser_and_generator.rs b/crates/rspack_plugin_extract_css/src/parser_and_generator.rs new file mode 100644 index 000000000000..52ad46b151aa --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/parser_and_generator.rs @@ -0,0 +1,128 @@ +use rspack_core::{ChunkGraph, Dependency, Module, ModuleGraph, ParserAndGenerator}; +use rspack_error::TWithDiagnosticArray; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use crate::css_dependency::CssDependency; + +#[derive(Deserialize)] +struct CssExtractJsonData { + #[serde(rename = "css-extract-rspack-plugin")] + value: String, +} + +#[derive(Debug)] +pub(crate) struct CssExtractParserAndGenerator { + orig_parser_generator: Box, + #[allow(clippy::vec_box)] + cache: FxHashMap>>, +} + +impl CssExtractParserAndGenerator { + pub(crate) fn new(orig_parser_generator: Box) -> Self { + Self { + orig_parser_generator, + cache: Default::default(), + } + } +} + +impl ParserAndGenerator for CssExtractParserAndGenerator { + fn source_types(&self) -> &[rspack_core::SourceType] { + self.orig_parser_generator.source_types() + } + + fn get_concatenation_bailout_reason( + &self, + _module: &dyn Module, + _mg: &ModuleGraph, + _cg: &ChunkGraph, + ) -> Option { + None + } + + #[allow(clippy::unwrap_used)] + fn parse( + &mut self, + parse_context: rspack_core::ParseContext, + ) -> rspack_error::Result> { + let deps = if let Some(additional_data) = parse_context.additional_data.get::() { + if let Some(deps) = self.cache.get(additional_data) { + deps.clone() + } else if let Ok(data) = serde_json::from_str::(additional_data) { + // parse the css data from js loader + // data: + // [identifier]__RSPACK_CSS_EXTRACT_SEP__ + // [content]__RSPACK_CSS_EXTRACT_SEP__ + // [context]__RSPACK_CSS_EXTRACT_SEP__ + // [media]__RSPACK_CSS_EXTRACT_SEP__ + // [supports]__RSPACK_CSS_EXTRACT_SEP__ + // [sourceMap]__RSPACK_CSS_EXTRACT_SEP__ + // [identifier]__RSPACK_CSS_EXTRACT_SEP__ ... repeated + // [content]__RSPACK_CSS_EXTRACT_SEP__ + let mut list = data.value.split("__RSPACK_CSS_EXTRACT_SEP__"); + + let mut deps = vec![]; + let mut idx = 0; + while let Some(identifier) = list.next() { + #[allow(clippy::unwrap_in_result)] + { + deps.push(Box::new(CssDependency::new( + identifier.into(), + list.next().unwrap().into(), + list.next().unwrap().into(), + list.next().unwrap().into(), + list.next().unwrap().into(), + list.next().unwrap().into(), + list + .next() + .unwrap() + .parse() + .expect("Cannot parse identifier_index, this should never happen"), + idx, + list.next().unwrap().into(), + ))); + } + idx += 1; + } + + self.cache.insert(data.value.clone(), deps.clone()); + + deps + } else { + vec![] + } + } else { + vec![] + }; + + let result = self.orig_parser_generator.parse(parse_context); + + if let Ok(result) = result { + let (mut res, diags) = result.split_into_parts(); + + res + .dependencies + .extend(deps.into_iter().map(|dep| dep as Box)); + + Ok(TWithDiagnosticArray::new(res, diags)) + } else { + result + } + } + + fn size(&self, module: &dyn rspack_core::Module, source_type: &rspack_core::SourceType) -> f64 { + self.orig_parser_generator.size(module, source_type) + } + + fn generate( + &self, + source: &rspack_core::rspack_sources::BoxSource, + module: &dyn rspack_core::Module, + generate_context: &mut rspack_core::GenerateContext, + ) -> rspack_error::Result { + self + .orig_parser_generator + .generate(source, module, generate_context) + } +} diff --git a/crates/rspack_plugin_extract_css/src/plugin.rs b/crates/rspack_plugin_extract_css/src/plugin.rs new file mode 100644 index 000000000000..14ea26f668c3 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/plugin.rs @@ -0,0 +1,742 @@ +use std::{borrow::Cow, cmp::max, hash::Hash, sync::Arc}; + +use dashmap::DashMap; +use once_cell::sync::Lazy; +use regex::Regex; +use rspack_core::{ + rspack_sources::{ConcatSource, RawSource, SourceMap, SourceMapSource, WithoutOriginalOptions}, + ApplyContext, AssetInfo, Chunk, ChunkGroupUkey, ChunkKind, ChunkUkey, Compilation, + CompilationParams, CompilerOptions, Filename, Module, ModuleGraph, ModuleIdentifier, ModuleType, + PathData, Plugin, PluginContext, PluginRenderManifestHookOutput, + PluginRuntimeRequirementsInTreeOutput, RenderManifestArgs, RenderManifestEntry, RuntimeGlobals, + RuntimeRequirementsInTreeArgs, SourceType, +}; +use rspack_error::{Diagnostic, Result}; +use rspack_error::{IntoTWithDiagnosticArray, TWithDiagnosticArray}; +use rspack_hash::RspackHash; +use rspack_hook::{plugin, plugin_hook, AsyncSeries2}; +use rspack_plugin_runtime::GetChunkFilenameRuntimeModule; +use rustc_hash::{FxHashMap, FxHashSet}; +use ustr::Ustr; + +use crate::{ + css_module::{CssModule, CssModuleFactory, DEPENDENCY_TYPE}, + parser_and_generator::CssExtractParserAndGenerator, + runtime::CssLoadingRuntimeModule, +}; +pub static PLUGIN_NAME: &str = "css-extract-rspack-plugin"; + +pub static MODULE_TYPE_STR: Lazy = Lazy::new(|| Ustr::from("css/mini-extract")); +pub static MODULE_TYPE: Lazy = Lazy::new(|| ModuleType::Custom(*MODULE_TYPE_STR)); +pub static SOURCE_TYPE: Lazy<[SourceType; 1]> = + Lazy::new(|| [SourceType::Custom(*MODULE_TYPE_STR)]); + +pub static AUTO_PUBLIC_PATH: &str = "__mini_css_extract_plugin_public_path_auto__"; +pub static AUTO_PUBLIC_PATH_RE: Lazy = + Lazy::new(|| Regex::new(AUTO_PUBLIC_PATH).expect("should compile")); + +pub static ABSOLUTE_PUBLIC_PATH: &str = "webpack:///mini-css-extract-plugin/"; +pub static ABSOLUTE_PUBLIC_PATH_RE: Lazy = + Lazy::new(|| Regex::new(ABSOLUTE_PUBLIC_PATH).expect("should compile")); + +pub static BASE_URI: &str = "webpack://"; +pub static BASE_URI_RE: Lazy = Lazy::new(|| Regex::new(BASE_URI).expect("should compile")); + +pub static SINGLE_DOT_PATH_SEGMENT: &str = "__mini_css_extract_plugin_single_dot_path_segment__"; +pub static SINGLE_DOT_PATH_SEGMENT_RE: Lazy = + Lazy::new(|| Regex::new(SINGLE_DOT_PATH_SEGMENT).expect("should compile")); + +static STARTS_WITH_AT_IMPORT_REGEX: Lazy = + Lazy::new(|| Regex::new("^@import url").expect("should compile")); + +struct CssOrderConflicts { + chunk: ChunkUkey, + fallback_module: ModuleIdentifier, + + // (module, failed chunkGroups, fulfilled chunkGroups) + reasons: Vec<(ModuleIdentifier, Option, Option)>, +} + +#[plugin] +#[derive(Debug)] +pub struct PluginCssExtract { + pub options: Arc, + sorted_module_cache: DashMap>, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct CssExtractOptions { + pub filename: String, + pub chunk_filename: String, + pub ignore_order: bool, + pub insert: InsertType, + pub attributes: FxHashMap, + pub link_type: Option, + pub runtime: bool, + pub pathinfo: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum InsertType { + Fn(String), + Selector(String), + Default, +} + +impl PluginCssExtract { + pub fn new(options: CssExtractOptions) -> Self { + Self::new_inner(Arc::new(options), Default::default()) + } + + // port from https://github.com/webpack-contrib/mini-css-extract-plugin/blob/d5e540baf8280442e523530ebbbe31c57a4c4336/src/index.js#L1127 + fn sort_modules<'comp>( + &self, + chunk: &Chunk, + modules: Vec<&dyn Module>, + compilation: &'comp Compilation, + module_graph: &'comp ModuleGraph<'comp>, + ) -> (Vec<&'comp dyn Module>, Option>) { + if let Some(used_modules) = self.sorted_module_cache.get(&chunk.ukey) { + return ( + used_modules + .iter() + .map(|id| { + module_graph + .module_by_identifier(id) + .expect("should have module") + .as_ref() + }) + .collect(), + None, + ); + } + + let mut module_deps_reasons: FxHashMap< + ModuleIdentifier, + FxHashMap>, + > = modules + .iter() + .map(|m| (m.identifier(), Default::default())) + .collect(); + + let mut module_dependencies: FxHashMap> = modules + .iter() + .map(|module| (module.identifier(), FxHashSet::default())) + .collect(); + + let mut groups = chunk.groups.iter().cloned().collect::>(); + groups.sort_unstable(); + + let mut modules_by_chunk_group = groups + .iter() + .map(|chunk_group| { + let chunk_group = compilation.chunk_group_by_ukey.expect_get(chunk_group); + let mut sorted_module = modules + .iter() + .map(|module| { + let identifier = module.identifier(); + (identifier, chunk_group.module_post_order_index(&identifier)) + }) + .filter_map(|(id, idx)| idx.map(|idx| (id, idx))) + .collect::>(); + + sorted_module.sort_by(|(_, idx1), (_, idx2)| idx2.cmp(idx1)); + + for (i, (module, _)) in sorted_module.iter().enumerate() { + let set = module_dependencies + .get_mut(module) + .expect("should have module before"); + + let reasons = module_deps_reasons + .get_mut(module) + .expect("should have module dep reason"); + + let mut j = i + 1; + while j < sorted_module.len() { + let (module, _) = sorted_module[j]; + set.insert(module); + + let reason = reasons.entry(module).or_default(); + reason.insert(chunk_group.ukey); + + j += 1; + } + } + + sorted_module + }) + .collect::>>(); + + let mut used_modules: FxHashSet = Default::default(); + let mut result: Vec<&dyn Module> = Default::default(); + let mut conflicts: Option> = None; + + while used_modules.len() < modules.len() { + let mut success = false; + let mut best_match: Option> = None; + let mut best_match_deps: Option> = None; + + for list in &mut modules_by_chunk_group { + // skip and remove already added modules + while !list.is_empty() + && used_modules.contains(&list.last().expect("should have list item").0) + { + list.pop(); + } + + // skip empty lists + if !list.is_empty() { + let module = list.last().expect("should have item").0; + let deps = module_dependencies.get(&module).expect("should have deps"); + let failed_deps = deps + .iter() + .filter(|dep| !used_modules.contains(dep)) + .cloned() + .collect::>(); + + let failed_count = failed_deps.len(); + + if best_match_deps.is_none() + || best_match_deps + .as_ref() + .expect("should have best match dep") + .len() + > failed_deps.len() + { + best_match = Some(list.iter().map(|(id, _)| *id).collect()); + best_match_deps = Some(failed_deps); + } + + if failed_count == 0 { + list.pop(); + used_modules.insert(module); + result.push( + module_graph + .module_by_identifier(&module) + .expect("should have module") + .as_ref(), + ); + success = true; + break; + } + } + } + + if !success { + // no module found => there is a conflict + // use list with fewest failed deps + // and emit a warning + let mut best_match = best_match.expect("should have best match"); + let best_match_deps = best_match_deps.expect("should have best match"); + let fallback_module = best_match.pop().expect("should have best match"); + if !self.options.ignore_order { + let reasons = module_deps_reasons + .get(&fallback_module) + .expect("should have dep reason"); + + let new_conflict = CssOrderConflicts { + chunk: chunk.ukey, + fallback_module, + reasons: best_match_deps + .into_iter() + .map(|m| { + let good_reasons_map = module_deps_reasons.get(&m); + let good_reasons = + good_reasons_map.and_then(|reasons| reasons.get(&fallback_module)); + + let failed_chunk_groups = reasons.get(&m).map(|reasons| { + reasons + .iter() + .filter_map(|cg| { + let chunk_group = compilation.chunk_group_by_ukey.expect_get(cg); + + chunk_group.name() + }) + .collect::>() + .join(",") + }); + + let good_chunk_groups = good_reasons.map(|reasons| { + reasons + .iter() + .filter_map(|cg| compilation.chunk_group_by_ukey.expect_get(cg).name()) + .collect::>() + .join(", ") + }); + + (m, failed_chunk_groups, good_chunk_groups) + }) + .collect(), + }; + if let Some(conflicts) = &mut conflicts { + conflicts.push(new_conflict); + } else { + conflicts = Some(vec![new_conflict]); + } + } + + used_modules.insert(fallback_module); + result.push( + module_graph + .module_by_identifier(&fallback_module) + .expect("should have fallback module") + .as_ref(), + ); + } + } + + self + .sorted_module_cache + .insert(chunk.ukey, result.iter().map(|m| m.identifier()).collect()); + + (result, conflicts) + } + + async fn render_content_asset<'comp>( + &self, + chunk: &Chunk, + rendered_modules: Vec<&dyn Module>, + filename_template: &Filename, + compilation: &'comp Compilation, + path_data: PathData<'comp>, + ) -> Result<(RenderManifestEntry, Option>)> { + let module_graph = compilation.get_module_graph(); + // mini-extract-plugin has different conflict order in some cases, + // for compatibility, we cannot use experiments.css sorting algorithm + let (used_modules, conflicts) = + self.sort_modules(chunk, rendered_modules, compilation, &module_graph); + + let used_modules = used_modules + .into_iter() + .filter_map(|module| module.downcast_ref::()); + + let mut source = ConcatSource::default(); + let mut external_source = ConcatSource::default(); + + let (filename, _) = compilation.get_path_with_info(filename_template, path_data)?; + + for module in used_modules { + let content = Cow::Borrowed(module.content.as_str()); + let readable_identifier = module.readable_identifier(&compilation.options.context); + let starts_with_at_import = STARTS_WITH_AT_IMPORT_REGEX.is_match(&content); + + let header = self.options.pathinfo.then(|| { + let req_str = readable_identifier.replace("*/", "*_/"); + let req_str_star = "*".repeat(req_str.len()); + RawSource::from(format!( + "/*!****{req_str_star}****!*\\\n !*** {req_str} ***!\n \\****{req_str_star}****/\n" + )) + }); + + if starts_with_at_import { + if let Some(header) = header { + external_source.add(header); + } + if !module.media.is_empty() { + static MEDIA_RE: Lazy = + Lazy::new(|| Regex::new(r#";|\s*$"#).expect("should compile")); + let new_content = MEDIA_RE.replace_all(content.as_ref(), &module.media); + external_source.add(RawSource::from(new_content.to_string() + "\n")); + } else { + external_source.add(RawSource::from(content.to_string() + "\n")); + } + } else { + if let Some(header) = header { + source.add(header); + } + if !module.supports.is_empty() { + source.add(RawSource::from(format!( + "@supports ({}) {{\n", + &module.supports + ))); + } + + if !module.media.is_empty() { + source.add(RawSource::from(format!("@media {} {{\n", &module.media))); + } + + // TODO: layer support + + let undo_path = get_undo_path( + &filename, + compilation + .options + .output + .path + .to_str() + .expect("should have output.path"), + false, + ); + + let content = ABSOLUTE_PUBLIC_PATH_RE.replace_all(&content, ""); + let content = SINGLE_DOT_PATH_SEGMENT_RE.replace_all(&content, "."); + let content = AUTO_PUBLIC_PATH_RE.replace_all(&content, &undo_path); + let content = BASE_URI_RE.replace_all( + &content, + chunk + .get_entry_options(&compilation.chunk_group_by_ukey) + .and_then(|entry_options| entry_options.base_uri.as_ref()) + .unwrap_or(&undo_path), + ); + + if !module.source_map.is_empty() { + source.add(SourceMapSource::new(WithoutOriginalOptions { + value: content.to_string(), + name: readable_identifier, + source_map: SourceMap::from_json(&module.source_map).expect("invalid sourcemap"), + })) + } else { + source.add(RawSource::from(content.to_string())); + } + + source.add(RawSource::from("\n")); + if !module.media.is_empty() { + source.add(RawSource::from("}\n")); + } + if !module.supports.is_empty() { + source.add(RawSource::from("}\n")); + } + } + } + + external_source.add(source); + Ok(( + RenderManifestEntry::new( + Arc::new(external_source), + filename, + AssetInfo::default(), + false, + false, + ), + conflicts, + )) + } +} + +#[plugin_hook(AsyncSeries2 for PluginCssExtract)] +async fn compilation( + &self, + compilation: &mut Compilation, + _params: &mut CompilationParams, +) -> Result<()> { + compilation.set_dependency_factory(DEPENDENCY_TYPE.clone(), Arc::new(CssModuleFactory)); + + let (_, parser_and_generator) = compilation + .plugin_driver + .registered_parser_and_generator_builder + .remove(&ModuleType::Js) + .expect("No JavaScript parser registered"); + + compilation + .plugin_driver + .registered_parser_and_generator_builder + .insert( + ModuleType::Js, + Box::new(move |parser_opt, generator_opt| { + let parser = parser_and_generator(parser_opt, generator_opt); + Box::new(CssExtractParserAndGenerator::new(parser)) + }), + ); + Ok(()) +} + +#[async_trait::async_trait] +impl Plugin for PluginCssExtract { + fn apply( + &self, + ctx: PluginContext<&mut ApplyContext>, + _options: &mut CompilerOptions, + ) -> Result<()> { + ctx + .context + .compiler_hooks + .compilation + .tap(compilation::new(self)); + + Ok(()) + } + + async fn content_hash( + &self, + _ctx: rspack_core::PluginContext, + args: &rspack_core::ContentHashArgs<'_>, + ) -> rspack_core::PluginContentHashHookOutput { + let compilation = args.compilation; + let chunk_ukey = args.chunk_ukey; + let module_graph = compilation.get_module_graph(); + + let rendered_modules = compilation + .chunk_graph + .get_chunk_modules_iterable_by_source_type(&chunk_ukey, SOURCE_TYPE[0], &module_graph) + .collect::>(); + + if rendered_modules.is_empty() { + return Ok(None); + } + let chunk = compilation.chunk_by_ukey.expect_get(&chunk_ukey); + + let used_modules = + rspack_plugin_css::CssPlugin::get_modules_in_order(chunk, rendered_modules, compilation) + .0 + .into_iter() + .filter_map(|module| module.downcast_ref::()); + + let mut hasher = RspackHash::from(&compilation.options.output); + + used_modules + .map(|m| { + m.build_info() + .expect("css module built") + .hash + .as_ref() + .expect("css module should have hash") + }) + .for_each(|current| { + current.hash(&mut hasher); + }); + + return Ok(Some(( + SOURCE_TYPE[0], + hasher.digest(&compilation.options.output.hash_digest), + ))); + } + + async fn render_manifest( + &self, + _ctx: PluginContext, + args: RenderManifestArgs<'_>, + ) -> PluginRenderManifestHookOutput { + let compilation = args.compilation; + let module_graph = compilation.get_module_graph(); + let chunk_ukey = args.chunk_ukey; + let chunk = compilation.chunk_by_ukey.expect_get(&chunk_ukey); + + if matches!(chunk.kind, ChunkKind::HotUpdate) { + return Ok(vec![].with_empty_diagnostic()); + } + + let rendered_modules = compilation + .chunk_graph + .get_chunk_modules_iterable_by_source_type(&chunk_ukey, SOURCE_TYPE[0], &module_graph) + .collect::>(); + + if rendered_modules.is_empty() { + return Ok(vec![].with_empty_diagnostic()); + } + + let filename_template = if chunk.can_be_initial(&compilation.chunk_group_by_ukey) { + Filename::from(self.options.filename.clone()) + } else { + Filename::from(self.options.chunk_filename.clone()) + }; + + let (render_result, conflicts) = self + .render_content_asset( + chunk, + rendered_modules, + &filename_template, + compilation, + PathData::default().chunk(chunk).content_hash_optional( + chunk + .content_hash + .get(&SOURCE_TYPE[0]) + .map(|hash| hash.encoded()), + ), + ) + .await?; + + let diagnostics = if let Some(conflicts) = conflicts { + conflicts + .into_iter() + .map(|conflict| { + let chunk = compilation.chunk_by_ukey.expect_get(&conflict.chunk); + let fallback_module = module_graph + .module_by_identifier(&conflict.fallback_module) + .expect("should have module"); + + Diagnostic::warn( + "".into(), + format!( + "chunk {} [{PLUGIN_NAME}]\nConflicting order. Following module has been added:\n * {} +despite it was not able to fulfill desired ordering with these modules:\n{}", + chunk + .name + .as_deref() + .unwrap_or(chunk.id.as_deref().unwrap_or_default()), + fallback_module.readable_identifier(&compilation.options.context), + conflict + .reasons + .iter() + .map(|(m, failed_reasons, good_reasons)| { + let m = module_graph + .module_by_identifier(m) + .expect("should have module"); + + format!( + " * {}\n - couldn't fulfill desired order of chunk group(s) {}{}", + m.readable_identifier(&compilation.options.context), + failed_reasons + .as_ref() + .map(|s| s.as_str()) + .unwrap_or_default(), + good_reasons + .as_ref() + .map(|s| format!( + "\n - while fulfilling desired order of chunk group(s) {}", + s.as_str() + )) + .unwrap_or_default(), + ) + }) + .collect::>() + .join("\n") + ), + ) + }) + .collect() + } else { + vec![] + }; + + Ok(TWithDiagnosticArray::new(vec![render_result], diagnostics)) + } + + async fn runtime_requirements_in_tree( + &self, + _ctx: PluginContext, + args: &mut RuntimeRequirementsInTreeArgs, + ) -> PluginRuntimeRequirementsInTreeOutput { + if !self.options.runtime { + return Ok(()); + } + + let with_loading = args + .runtime_requirements + .contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) + && { + let chunk = args.compilation.chunk_by_ukey.expect_get(args.chunk); + + chunk + .get_all_async_chunks(&args.compilation.chunk_group_by_ukey) + .iter() + .any(|chunk| { + !args + .compilation + .chunk_graph + .get_chunk_modules_by_source_type( + chunk, + SOURCE_TYPE[0], + &args.compilation.get_module_graph(), + ) + .is_empty() + }) + }; + + let with_hmr = args + .runtime_requirements + .contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS); + + if with_loading || with_hmr { + if self.options.chunk_filename.contains("hash") { + args + .runtime_requirements_mut + .insert(RuntimeGlobals::GET_FULL_HASH); + } + args + .runtime_requirements_mut + .insert(RuntimeGlobals::PUBLIC_PATH); + + let filename = self.options.filename.clone(); + let chunk_filename = self.options.chunk_filename.clone(); + + args + .compilation + .add_runtime_module( + args.chunk, + Box::new(GetChunkFilenameRuntimeModule::new( + "css", + "mini-css", + SOURCE_TYPE[0], + "__webpack_require__.miniCssF".into(), + |_| false, + move |chunk, compilation| { + chunk.content_hash.contains_key(&SOURCE_TYPE[0]).then(|| { + if chunk.can_be_initial(&compilation.chunk_group_by_ukey) { + Filename::from(filename.clone()) + } else { + Filename::from(chunk_filename.clone()) + } + }) + }, + )), + ) + .await?; + + args + .compilation + .add_runtime_module( + args.chunk, + Box::new(CssLoadingRuntimeModule::new( + *args.chunk, + self.options.clone(), + with_loading, + with_hmr, + )), + ) + .await?; + } + + Ok(()) + } +} + +#[allow(clippy::unwrap_used)] +fn get_undo_path(filename: &str, output_path: &str, enforce_relative: bool) -> String { + let mut depth: isize = -1; + let mut append = "".into(); + + // eslint-disable-next-line no-param-reassign + let output_path = output_path.strip_suffix('\\').unwrap_or(output_path); + let mut output_path = output_path + .strip_suffix('/') + .unwrap_or(output_path) + .to_string(); + + static PATH_SEP: Lazy = Lazy::new(|| Regex::new(r#"[\\/]+"#).expect("should compile")); + + for part in PATH_SEP.split(filename) { + if part == ".." { + if depth > -1 { + depth -= 1; + } else { + let i = output_path.find('/'); + let j = output_path.find('\\'); + let pos = if i.is_none() { + j + } else if j.is_none() { + i + } else { + max(i, j) + }; + + if pos.is_none() { + return format!("{output_path}/"); + } + + append = format!("{}/{append}", &output_path[pos.unwrap() + 1..]); + + output_path = output_path[0..pos.unwrap()].to_string(); + } + } else if part != "." { + depth += 1; + } + } + + if depth > 0 { + format!("{}{append}", "../".repeat(depth as usize)) + } else if enforce_relative { + format!("./{append}") + } else { + append + } +} diff --git a/crates/rspack_plugin_extract_css/src/runtime.rs b/crates/rspack_plugin_extract_css/src/runtime.rs new file mode 100644 index 000000000000..3ce068fbdd10 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/runtime.rs @@ -0,0 +1,174 @@ +use std::sync::Arc; + +use rspack_core::{ + impl_runtime_module, rspack_sources::RawSource, ChunkUkey, Compilation, CrossOriginLoading, + RuntimeGlobals, RuntimeModule, RuntimeModuleStage, +}; +use rspack_error::Result; +use rustc_hash::FxHashSet; + +use crate::plugin::{CssExtractOptions, InsertType, SOURCE_TYPE}; + +static RUNTIME_CODE: &str = include_str!("./runtime/css_load.js"); +static WITH_LOADING: &str = include_str!("./runtime/with_loading.js"); +static WITH_HMR: &str = include_str!("./runtime/with_hmr.js"); + +#[impl_runtime_module] +#[derive(Debug, Eq)] +pub(crate) struct CssLoadingRuntimeModule { + chunk: ChunkUkey, + options: Arc, + loading: bool, + hmr: bool, +} + +impl CssLoadingRuntimeModule { + pub(crate) fn new( + chunk: ChunkUkey, + options: Arc, + loading: bool, + hmr: bool, + ) -> Self { + Self { + chunk, + options, + loading, + hmr, + source_map_kind: rspack_util::source_map::SourceMapKind::None, + custom_source: None, + } + } + + fn get_css_chunks(&self, compilation: &Compilation) -> FxHashSet { + let mut set: FxHashSet = Default::default(); + let module_graph = compilation.get_module_graph(); + + let chunk = compilation.chunk_by_ukey.expect_get(&self.chunk); + + for chunk in chunk.get_all_async_chunks(&compilation.chunk_group_by_ukey) { + let modules = compilation + .chunk_graph + .get_chunk_modules_iterable_by_source_type(&chunk, SOURCE_TYPE[0], &module_graph); + + if modules.count() > 0 { + set.insert(chunk); + } + } + + set + } +} + +impl RuntimeModule for CssLoadingRuntimeModule { + fn name(&self) -> rspack_identifier::Identifier { + "webpack/runtime/css loading".into() + } + + fn stage(&self) -> RuntimeModuleStage { + RuntimeModuleStage::Trigger + } + + fn generate( + &self, + compilation: &rspack_core::Compilation, + ) -> Result { + let runtime = RUNTIME_CODE; + + let mut attr = String::default(); + for (attr_key, attr_value) in &self.options.attributes { + attr += &format!("linkTag.setAttribute({}, {});\n", attr_key, attr_value); + } + let runtime = runtime.replace("__SET_ATTRIBUTES__", &attr); + + let runtime = if let Some(link_type) = &self.options.link_type { + runtime.replace("__SET_LINKTYPE__", &format!("linkTag.type={};", link_type)) + } else { + runtime.replace("__SET_LINKTYPE__", "") + }; + + let runtime = if let CrossOriginLoading::Enable(cross_origin_loading) = + &compilation.options.output.cross_origin_loading + { + runtime.replace( + "__CROSS_ORIGIN_LOADING__", + &format!( + "if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {{ + linkTag.crossOrigin = \"{}\"; +}}", + cross_origin_loading + ), + ) + } else { + runtime.replace("__CROSS_ORIGIN_LOADING__", "") + }; + + let runtime = match &self.options.insert { + InsertType::Fn(f) => runtime.replace("__INSERT__", &format!("({f})(linkTag);")), + InsertType::Selector(sel) => runtime.replace( + "__INSERT__", + &format!("var target = document.querySelector({sel});\ntarget.parentNode.insertBefore(linkTag, target.nextSibling);"), + ), + InsertType::Default => runtime.replace( + "__INSERT__", + "if (oldTag) { + oldTag.parentNode.insertBefore(linkTag, oldTag.nextSibling); +} else { + document.head.appendChild(linkTag); +}", + ), + }; + + let runtime = if self.loading { + let chunks = self.get_css_chunks(compilation); + if chunks.is_empty() { + runtime.replace("__WITH_LOADING__", "// no chunk loading") + } else { + let chunk = compilation.chunk_by_ukey.expect_get(&self.chunk); + let with_loading = WITH_LOADING.replace( + "__INSTALLED_CHUNKS__", + &chunk.ids.iter().fold(String::default(), |output, id| { + format!("{output}\"{id}\": 0,\n") + }), + ); + + let with_loading = with_loading.replace( + "__ENSURE_CHUNK_HANDLERS__", + &RuntimeGlobals::ENSURE_CHUNK_HANDLERS.to_string(), + ); + + let with_loading = with_loading.replace( + "__CSS_CHUNKS__", + &format!( + "{{\n{}\n}}", + chunks + .iter() + .filter_map(|id| { + let chunk = compilation.chunk_by_ukey.expect_get(id); + + chunk.id.as_ref().map(|id| format!("\"{}\": 1,\n", id)) + }) + .collect::() + ), + ); + + runtime.replace("__WITH_LOADING__", &with_loading) + } + } else { + runtime.replace("__WITH_LOADING__", "// no chunk loading") + }; + + let runtime = if self.hmr { + runtime.replace( + "__WITH_HMT__", + &WITH_HMR.replace( + "__HMR_DOWNLOAD__", + &RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS.to_string(), + ), + ) + } else { + runtime.replace("__WITH_HMT__", "// no hmr") + }; + + Ok(Arc::new(RawSource::from(runtime))) + } +} diff --git a/crates/rspack_plugin_extract_css/src/runtime/css_load.js b/crates/rspack_plugin_extract_css/src/runtime/css_load.js new file mode 100644 index 000000000000..5b898c697197 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/runtime/css_load.js @@ -0,0 +1,58 @@ +if (typeof document === "undefined") return; +var createStylesheet = function ( + chunkId, fullhref, oldTag, resolve, reject +) { + var linkTag = document.createElement("link"); + __SET_ATTRIBUTES__ + linkTag.rel = "stylesheet"; + __SET_LINKTYPE__ + var onLinkComplete = function (event) { + // avoid mem leaks. + linkTag.onerror = linkTag.onload = null; + if (event.type === 'load') { + resolve(); + } else { + var errorType = event && (event.type === 'load' ? 'missing' : event.type); + var realHref = event && event.target && event.target.href || fullhref; + var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + realHref + ")"); + err.code = "CSS_CHUNK_LOAD_FAILED"; + err.type = errorType; + err.request = realHref; + if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag) + reject(err); + } + } + + linkTag.onerror = linkTag.onload = onLinkComplete; + linkTag.href = fullhref; + __CROSS_ORIGIN_LOADING__ + __INSERT__ + return linkTag; +} +var findStylesheet = function (href, fullhref) { + var existingLinkTags = document.getElementsByTagName("link"); + for(var i = 0; i < existingLinkTags.length; i++) { + var tag = existingLinkTags[i]; + var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href"); + if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return tag; + } + + var existingStyleTags = document.getElementsByTagName("style"); + for(var i = 0; i < existingStyleTags.length; i++) { + var tag = existingStyleTags[i]; + var dataHref = tag.getAttribute("data-href"); + if(dataHref === href || dataHref === fullhref) return tag; + } +} + +var loadStylesheet = function (chunkId) { + return new Promise(function (resolve, reject) { + var href = __webpack_require__.miniCssF(chunkId); + var fullhref = __webpack_require__.p + href; + if(findStylesheet(href, fullhref)) return resolve(); + createStylesheet(chunkId, fullhref, null, resolve, reject); + }) +} + +__WITH_LOADING__ +__WITH_HMT__ diff --git a/crates/rspack_plugin_extract_css/src/runtime/with_hmr.js b/crates/rspack_plugin_extract_css/src/runtime/with_hmr.js new file mode 100644 index 000000000000..b4fe9cb4b4b4 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/runtime/with_hmr.js @@ -0,0 +1,41 @@ +var oldTags = []; +var newTags = []; +var applyHandler = function (options) { + return { + dispose: function () { + for (var i = 0; i < oldTags.length; i++) { + var oldTag = oldTags[i]; + if (oldTag.parentNode) oldTag.parentNode.removeChild(oldTag); + } + oldTags.length = 0; + }, + apply: function () { + for (var i = 0; i < newTags.length; i++) newTags[i].rel = "stylesheet"; + newTags.length = 0; + } + } +} +__HMR_DOWNLOAD__.miniCss = function (chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList) { + applyHandlers.push(applyHandler); + chunkIds.forEach(function (chunkId) { + var href = __webpack_require__.miniCssF(chunkId); + var fullhref = __webpack_require__.p + href; + var oldTag = findStylesheet(href, fullhref); + if (!oldTag) return; + promises.push(new Promise(function (resolve, reject) { + var tag = createStylesheet( + chunkId, + fullhref, + oldTag, + function () { + tag.as = "style"; + tag.rel = "preload"; + resolve(); + }, + reject + ); + oldTags.push(oldTag); + newTags.push(tag); + })) + }); +} diff --git a/crates/rspack_plugin_extract_css/src/runtime/with_loading.js b/crates/rspack_plugin_extract_css/src/runtime/with_loading.js new file mode 100644 index 000000000000..f925b02e1c42 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/runtime/with_loading.js @@ -0,0 +1,21 @@ +// object to store loaded CSS chunks +var installedCssChunks = { + __INSTALLED_CHUNKS__ +}; + +__ENSURE_CHUNK_HANDLERS__.miniCss = function(chunkId, promises) { + var cssChunks = __CSS_CHUNKS__; + if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]) + else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) + promises.push( + installedCssChunks[chunkId] = loadStylesheet(chunkId).then( + function() { + installedCssChunks[chunkId] = 0; + }, + function(e) { + delete installedCssChunks[chunkId]; + throw e; + } + ) + ) +} diff --git a/crates/rspack_plugin_javascript/Cargo.toml b/crates/rspack_plugin_javascript/Cargo.toml index 884eae3503e8..f8a0506282a8 100644 --- a/crates/rspack_plugin_javascript/Cargo.toml +++ b/crates/rspack_plugin_javascript/Cargo.toml @@ -44,6 +44,7 @@ swc_core = { workspace = true, features = [ "ecma_transforms_compat", "ecma_transforms_proposal", "ecma_transforms_typescript", + "base", "ecma_quote", ] } swc_node_comments = { workspace = true } diff --git a/crates/rspack_plugin_runtime/src/lib.rs b/crates/rspack_plugin_runtime/src/lib.rs index 4f0b52f64692..a07497beb1d8 100644 --- a/crates/rspack_plugin_runtime/src/lib.rs +++ b/crates/rspack_plugin_runtime/src/lib.rs @@ -21,7 +21,9 @@ pub use module_chunk_loading::ModuleChunkLoadingPlugin; mod import_scripts_chunk_loading; pub use import_scripts_chunk_loading::ImportScriptsChunkLoadingPlugin; mod runtime_module; -pub use runtime_module::{chunk_has_css, is_enabled_for_chunk, stringify_chunks}; +pub use runtime_module::{ + chunk_has_css, is_enabled_for_chunk, stringify_chunks, GetChunkFilenameRuntimeModule, +}; mod startup_chunk_dependencies; pub use startup_chunk_dependencies::StartupChunkDependenciesPlugin; mod chunk_prefetch_preload; diff --git a/crates/rspack_plugin_runtime/src/runtime_module/get_chunk_filename.rs b/crates/rspack_plugin_runtime/src/runtime_module/get_chunk_filename.rs index 3411a7d92c4c..3e458263c665 100644 --- a/crates/rspack_plugin_runtime/src/runtime_module/get_chunk_filename.rs +++ b/crates/rspack_plugin_runtime/src/runtime_module/get_chunk_filename.rs @@ -18,8 +18,7 @@ use super::stringify_static_chunk_map; use crate::{get_chunk_runtime_requirements, runtime_module::unquoted_stringify}; type GetChunkFilenameAllChunks = Box bool + Sync + Send>; -type GetFilenameForChunk = - Box Fn(&'me Chunk, &'me Compilation) -> Option<&'me Filename> + Sync + Send>; +type GetFilenameForChunk = Box Option + Sync + Send>; #[impl_runtime_module] pub struct GetChunkFilenameRuntimeModule { @@ -52,7 +51,7 @@ impl Eq for GetChunkFilenameRuntimeModule {} impl GetChunkFilenameRuntimeModule { pub fn new< F: Fn(&RuntimeGlobals) -> bool + Sync + Send + 'static, - T: for<'me> Fn(&'me Chunk, &'me Compilation) -> Option<&'me Filename> + Sync + Send + 'static, + T: Fn(&Chunk, &Compilation) -> Option + Sync + Send + 'static, >( content_type: &'static str, name: &'static str, @@ -119,10 +118,10 @@ impl RuntimeModule for GetChunkFilenameRuntimeModule { } }); - let mut dynamic_filename: Option<&str> = None; + let mut dynamic_filename: Option = None; let mut max_chunk_set_size = 0; - let mut chunk_filenames = Vec::<(&Filename, &ChunkUkey)>::new(); - let mut chunk_set_sizes_by_filenames = HashMap::<&str, usize>::default(); + let mut chunk_filenames = Vec::<(Filename, &ChunkUkey)>::new(); + let mut chunk_set_sizes_by_filenames = HashMap::::default(); let mut chunk_map = IndexMap::new(); if let Some(chunks) = chunks { @@ -135,42 +134,43 @@ impl RuntimeModule for GetChunkFilenameRuntimeModule { if let Some(filename) = filename { chunk_map.insert(&chunk.ukey, chunk); - chunk_filenames.push((filename, &chunk.ukey)); + chunk_filenames.push((filename.clone(), &chunk.ukey)); if let Some(filename_template) = filename.template() { let chunk_set_size = chunk_set_sizes_by_filenames - .entry(filename_template) + .entry(filename_template.to_owned()) .or_insert(0); *chunk_set_size += 1; let chunk_set_size = *chunk_set_size; let should_update = match dynamic_filename { - Some(dynamic_filename) => match chunk_set_size.cmp(&max_chunk_set_size) { + Some(ref dynamic_filename) => match chunk_set_size.cmp(&max_chunk_set_size) { Ordering::Less => false, Ordering::Greater => true, Ordering::Equal => match filename_template.len().cmp(&dynamic_filename.len()) { Ordering::Less => false, Ordering::Greater => true, - Ordering::Equal => { - !matches!(filename_template.cmp(dynamic_filename), Ordering::Less) - } + Ordering::Equal => !matches!( + filename_template.cmp(dynamic_filename.as_str()), + Ordering::Less + ), }, }, None => true, }; if should_update { max_chunk_set_size = chunk_set_size; - dynamic_filename = Some(filename_template); + dynamic_filename = Some(filename_template.to_owned()); } }; } }); } - let dynamic_url = dynamic_filename.map(|dynamic_filename| { + let dynamic_url = dynamic_filename.as_ref().map(|dynamic_filename| { let chunks = chunk_filenames .iter() .filter_map(|(filename, chunk)| { - if filename.template() == Some(dynamic_filename) { + if filename.template() == Some(dynamic_filename.as_str()) { Some(*chunk) } else { None @@ -248,9 +248,9 @@ impl RuntimeModule for GetChunkFilenameRuntimeModule { for (filename_template, chunk_ukey) in chunk_filenames .iter() - .filter(|(filename, _)| match dynamic_filename { + .filter(|(filename, _)| match &dynamic_filename { None => true, - Some(dynamic_filename) => filename.template() != Some(dynamic_filename), + Some(dynamic_filename) => filename.template() != Some(dynamic_filename.as_str()), }) { if let Some(chunk) = chunk_map.get(chunk_ukey) { diff --git a/crates/rspack_plugin_runtime/src/runtime_plugin.rs b/crates/rspack_plugin_runtime/src/runtime_plugin.rs index 396a7b479252..5c717804415f 100644 --- a/crates/rspack_plugin_runtime/src/runtime_plugin.rs +++ b/crates/rspack_plugin_runtime/src/runtime_plugin.rs @@ -319,11 +319,14 @@ impl Plugin for RuntimePlugin { RuntimeGlobals::GET_CHUNK_SCRIPT_FILENAME.to_string(), |_| false, |chunk, compilation| { - Some(get_js_chunk_filename_template( - chunk, - &compilation.options.output, - &compilation.chunk_group_by_ukey, - )) + Some( + get_js_chunk_filename_template( + chunk, + &compilation.options.output, + &compilation.chunk_group_by_ukey, + ) + .clone(), + ) }, ) .boxed(), @@ -349,6 +352,7 @@ impl Plugin for RuntimePlugin { &compilation.options.output, &compilation.chunk_group_by_ukey, ) + .clone() }) }, ) diff --git a/packages/rspack-test-tools/tests/__snapshots__/Defaults.unittest.js.snap b/packages/rspack-test-tools/tests/__snapshots__/Defaults.unittest.js.snap index 2dfb06f550a0..e6ffb31318b7 100644 --- a/packages/rspack-test-tools/tests/__snapshots__/Defaults.unittest.js.snap +++ b/packages/rspack-test-tools/tests/__snapshots__/Defaults.unittest.js.snap @@ -243,6 +243,11 @@ Object { }, }, "chunks": "async", + "defaultSizeTypes": Array [ + "javascript", + "css", + "unknown", + ], "hidePathInfo": false, "maxAsyncRequests": Infinity, "maxInitialRequests": Infinity, diff --git a/packages/rspack-test-tools/tests/defaultsCases/experiments/future-defaults-with-css.js b/packages/rspack-test-tools/tests/defaultsCases/experiments/future-defaults-with-css.js index 95fba7a88ea2..2a1f24f22417 100644 --- a/packages/rspack-test-tools/tests/defaultsCases/experiments/future-defaults-with-css.js +++ b/packages/rspack-test-tools/tests/defaultsCases/experiments/future-defaults-with-css.js @@ -24,15 +24,15 @@ module.exports = { - }, - "test": /\\.css$/i, - "type": "css/auto", - - }, - - Object { + @@ ... @@ - "mimetype": "text/css+module", - "resolve": Object { - "fullySpecified": true, - "preferRelative": true, - }, - "type": "css/module", - @@ ... @@ + - }, + - Object { - "mimetype": "text/css", - "resolve": Object { - "fullySpecified": true, @@ -64,13 +64,15 @@ module.exports = { - }, - "css": Object { - "namedExports": true, - - }, + @@ ... @@ - "css/auto": Object { - "namedExports": true, - @@ ... @@ + - }, - "css/module": Object { - "namedExports": true, @@ ... @@ + - "css", + @@ ... @@ - "hashDigestLength": 20, - "hashFunction": "md4", + "hashDigestLength": 16, diff --git a/packages/rspack/src/ExecuteModulePlugin.ts b/packages/rspack/src/ExecuteModulePlugin.ts index aff8066892de..1697dae23df0 100644 --- a/packages/rspack/src/ExecuteModulePlugin.ts +++ b/packages/rspack/src/ExecuteModulePlugin.ts @@ -14,7 +14,7 @@ export default class ExecuteModulePlugin { const source = options.codeGenerationResult.get("javascript"); try { const fn = vm.runInThisContext( - `(function(module, __webpack_exports__, ${RuntimeGlobals.require}) {\n${source}\n})`, + `(function(module, __webpack_module__, __webpack_exports__, exports, ${RuntimeGlobals.require}) {\n${source}\n})`, { filename: moduleObject.id } @@ -23,6 +23,8 @@ export default class ExecuteModulePlugin { fn.call( moduleObject.exports, moduleObject, + moduleObject, + moduleObject.exports, moduleObject.exports, context.__webpack_require__ ); diff --git a/packages/rspack/src/builtin-plugin/SplitChunksPlugin.ts b/packages/rspack/src/builtin-plugin/SplitChunksPlugin.ts index ef7aec537f0f..5ffd87cdd48b 100644 --- a/packages/rspack/src/builtin-plugin/SplitChunksPlugin.ts +++ b/packages/rspack/src/builtin-plugin/SplitChunksPlugin.ts @@ -89,6 +89,7 @@ function toRawSplitChunksOptions( const { name, chunks, + defaultSizeTypes, cacheGroups = {}, fallbackCacheGroup, ...passThrough @@ -97,6 +98,7 @@ function toRawSplitChunksOptions( return { name: getName(name), chunks: getChunks(chunks), + defaultSizeTypes: defaultSizeTypes || ["javascript", "unknown"], cacheGroups: Object.entries(cacheGroups) .filter(([_key, group]) => group !== false) .map(([key, group]) => { diff --git a/packages/rspack/src/builtin-plugin/css-extract/hmr/hotModuleReplacement.js b/packages/rspack/src/builtin-plugin/css-extract/hmr/hotModuleReplacement.js new file mode 100644 index 000000000000..6fda8200b5cb --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/hmr/hotModuleReplacement.js @@ -0,0 +1,285 @@ +/* eslint-env browser */ +/* + eslint-disable + no-console, + func-names +*/ + +/** @typedef {any} TODO */ + +const normalizeUrl = require("./normalize-url"); + +const srcByModuleId = Object.create(null); + +const noDocument = typeof document === "undefined"; + +const { forEach } = Array.prototype; + +/** + * @param {function} fn + * @param {number} time + * @returns {(function(): void)|*} + */ +function debounce(fn, time) { + let timeout = 0; + + return function () { + // @ts-ignore + const self = this; + // eslint-disable-next-line prefer-rest-params + const args = arguments; + + const functionCall = function functionCall() { + return fn.apply(self, args); + }; + + clearTimeout(timeout); + + // @ts-ignore + timeout = setTimeout(functionCall, time); + }; +} + +function noop() {} + +/** + * @param {TODO} moduleId + * @returns {TODO} + */ +function getCurrentScriptUrl(moduleId) { + let src = srcByModuleId[moduleId]; + + if (!src) { + if (document.currentScript) { + ({ src } = /** @type {HTMLScriptElement} */ (document.currentScript)); + } else { + const scripts = document.getElementsByTagName("script"); + const lastScriptTag = scripts[scripts.length - 1]; + + if (lastScriptTag) { + ({ src } = lastScriptTag); + } + } + + srcByModuleId[moduleId] = src; + } + + /** + * @param {string} fileMap + * @returns {null | string[]} + */ + return function (fileMap) { + if (!src) { + return null; + } + + const splitResult = src.split(/([^\\/]+)\.js$/); + const filename = splitResult && splitResult[1]; + + if (!filename) { + return [src.replace(".js", ".css")]; + } + + if (!fileMap) { + return [src.replace(".js", ".css")]; + } + + return fileMap.split(",").map(mapRule => { + const reg = new RegExp(`${filename}\\.js$`, "g"); + + return normalizeUrl( + src.replace(reg, `${mapRule.replace(/{fileName}/g, filename)}.css`) + ); + }); + }; +} + +/** + * @param {TODO} el + * @param {string} [url] + */ +function updateCss(el, url) { + if (!url) { + if (!el.href) { + return; + } + + // eslint-disable-next-line + url = el.href.split("?")[0]; + } + + if (!isUrlRequest(/** @type {string} */ (url))) { + return; + } + + if (el.isLoaded === false) { + // We seem to be about to replace a css link that hasn't loaded yet. + // We're probably changing the same file more than once. + return; + } + + if (!url || !(url.indexOf(".css") > -1)) { + return; + } + + // eslint-disable-next-line no-param-reassign + el.visited = true; + + const newEl = el.cloneNode(); + + newEl.isLoaded = false; + + newEl.addEventListener("load", () => { + if (newEl.isLoaded) { + return; + } + + newEl.isLoaded = true; + el.parentNode.removeChild(el); + }); + + newEl.addEventListener("error", () => { + if (newEl.isLoaded) { + return; + } + + newEl.isLoaded = true; + el.parentNode.removeChild(el); + }); + + newEl.href = `${url}?${Date.now()}`; + + if (el.nextSibling) { + el.parentNode.insertBefore(newEl, el.nextSibling); + } else { + el.parentNode.appendChild(newEl); + } +} + +/** + * @param {string} href + * @param {TODO} src + * @returns {TODO} + */ +function getReloadUrl(href, src) { + let ret; + + // eslint-disable-next-line no-param-reassign + href = normalizeUrl(href); + + src.some( + /** + * @param {string} url + */ + // eslint-disable-next-line array-callback-return + url => { + if (href.indexOf(src) > -1) { + ret = url; + } + } + ); + + return ret; +} + +/** + * @param {string} [src] + * @returns {boolean} + */ +function reloadStyle(src) { + if (!src) { + return false; + } + + const elements = document.querySelectorAll("link"); + let loaded = false; + + forEach.call(elements, el => { + if (!el.href) { + return; + } + + const url = getReloadUrl(el.href, src); + + if (!isUrlRequest(url)) { + return; + } + + if (el.visited === true) { + return; + } + + if (url) { + updateCss(el, url); + + loaded = true; + } + }); + + return loaded; +} + +function reloadAll() { + const elements = document.querySelectorAll("link"); + + forEach.call(elements, el => { + if (el.visited === true) { + return; + } + + updateCss(el); + }); +} + +/** + * @param {string} url + * @returns {boolean} + */ +function isUrlRequest(url) { + // An URL is not an request if + + // It is not http or https + if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { + return false; + } + + return true; +} + +/** + * @param {TODO} moduleId + * @param {TODO} options + * @returns {TODO} + */ +module.exports = function (moduleId, options) { + if (noDocument) { + console.log("no window.document found, will not HMR CSS"); + + return noop; + } + + const getScriptSrc = getCurrentScriptUrl(moduleId); + + function update() { + const src = getScriptSrc(options.filename); + const reloaded = reloadStyle(src); + + if (options.locals) { + console.log("[HMR] Detected local css modules. Reload all css"); + + reloadAll(); + + return; + } + + if (reloaded) { + console.log("[HMR] css reload %s", src.join(" ")); + } else { + console.log("[HMR] Reload all css"); + + reloadAll(); + } + } + + return debounce(update, 50); +}; diff --git a/packages/rspack/src/builtin-plugin/css-extract/hmr/normalize-url.js b/packages/rspack/src/builtin-plugin/css-extract/hmr/normalize-url.js new file mode 100644 index 000000000000..04fb0969aeca --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/hmr/normalize-url.js @@ -0,0 +1,46 @@ +/* eslint-disable */ + +/** + * @param {string[]} pathComponents + * @returns {string} + */ +function normalizeUrl(pathComponents) { + return pathComponents + .reduce(function (accumulator, item) { + switch (item) { + case "..": + accumulator.pop(); + break; + case ".": + break; + default: + accumulator.push(item); + } + + return accumulator; + }, /** @type {string[]} */ ([])) + .join("/"); +} + +/** + * @param {string} urlString + * @returns {string} + */ +module.exports = function (urlString) { + urlString = urlString.trim(); + + if (/^data:/i.test(urlString)) { + return urlString; + } + + var protocol = + urlString.indexOf("//") !== -1 ? urlString.split("//")[0] + "//" : ""; + var components = urlString.replace(new RegExp(protocol, "i"), "").split("/"); + var host = components[0].toLowerCase().replace(/\.$/, ""); + + components[0] = ""; + + var path = normalizeUrl(components); + + return protocol + host + path; +}; diff --git a/packages/rspack/src/builtin-plugin/css-extract/index.ts b/packages/rspack/src/builtin-plugin/css-extract/index.ts new file mode 100644 index 000000000000..c4dec7ea1eca --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/index.ts @@ -0,0 +1,124 @@ +import type { RawCssExtractPluginOption } from "@rspack/binding"; +import { Compiler } from "../.."; +import { MODULE_TYPE } from "./loader"; + +export * from "./loader"; + +const DEFAULT_FILENAME = "[name].css"; +const LOADER_PATH = require.resolve("./loader"); + +export interface PluginOptions { + filename?: string; + chunkFilename?: string; + ignoreOrder?: boolean; + insert?: string | ((linkTag: HTMLLinkElement) => void); + attributes?: Record; + linkType?: string | "text/css" | false; + runtime?: boolean; + + // workaround for pathinto, deprecate this when rspack supports pathinfo + pathinfo?: boolean; +} + +export class CssExtractRspackPlugin { + static pluginName: string = "css-extract-rspack-plugin"; + static loader: string = LOADER_PATH; + + options: PluginOptions; + + constructor(options?: PluginOptions) { + this.options = options || {}; + } + + apply(compiler: Compiler) { + const { splitChunks } = compiler.options.optimization; + + if (splitChunks) { + if ( + /** @type {string[]} */ splitChunks.defaultSizeTypes!.includes("...") + ) { + /** @type {string[]} */ + splitChunks.defaultSizeTypes!.push(MODULE_TYPE); + } + } + + if ( + // @ts-expect-error rspack don't support pathinfo for now + compiler.options.output.pathinfo && + this.options.pathinfo === undefined + ) { + this.options.pathinfo = true; + } + + compiler.__internal__registerBuiltinPlugin({ + // @ts-expect-error CssExtractPlugin is a constant value of BuiltinPlugin + name: "CssExtractPlugin", + options: this.normalizeOptions(this.options) + }); + } + + normalizeOptions(options: PluginOptions): RawCssExtractPluginOption { + let chunkFilename = options.chunkFilename; + + if (!chunkFilename) { + const filename = options.filename || DEFAULT_FILENAME; + + if (typeof filename !== "function") { + const hasName = /** @type {string} */ filename.includes("[name]"); + const hasId = /** @type {string} */ filename.includes("[id]"); + const hasChunkHash = + /** @type {string} */ + filename.includes("[chunkhash]"); + const hasContentHash = + /** @type {string} */ + filename.includes("[contenthash]"); + + // Anything changing depending on chunk is fine + if (hasChunkHash || hasContentHash || hasName || hasId) { + chunkFilename = filename; + } else { + // Otherwise prefix "[id]." in front of the basename to make it changing + chunkFilename = + /** @type {string} */ + filename.replace(/(^|\/)([^/]*(?:\?|$))/, "$1[id].$2"); + } + } else { + chunkFilename = "[id].css"; + } + } + + const normalzedOptions: RawCssExtractPluginOption = { + filename: options.filename || DEFAULT_FILENAME, + chunkFilename: chunkFilename!, + ignoreOrder: options.ignoreOrder ?? false, + runtime: options.runtime ?? true, + insert: + typeof options.insert === "function" + ? options.insert.toString() + : JSON.stringify(options.insert), + linkType: + typeof options.linkType === "undefined" + ? JSON.stringify("text/css") + : options.linkType === false + ? undefined + : JSON.stringify(options.linkType), + attributes: options.attributes + ? (Reflect.ownKeys(options.attributes) + .map(k => [ + JSON.stringify(k), + JSON.stringify(options.attributes![k as string]) + ]) + .reduce((obj, [k, v]) => { + // @ts-expect-error + obj[k] = v; + return obj; + }, {}) as Record) + : {}, + pathinfo: options.pathinfo ?? false + }; + + return normalzedOptions; + } +} + +export default CssExtractRspackPlugin; diff --git a/packages/rspack/src/builtin-plugin/css-extract/loader-options.json b/packages/rspack/src/builtin-plugin/css-extract/loader-options.json new file mode 100644 index 000000000000..6d10fec261ef --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/loader-options.json @@ -0,0 +1,32 @@ +{ + "title": "Mini CSS Extract Plugin Loader options", + "type": "object", + "additionalProperties": false, + "properties": { + "publicPath": { + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ], + "description": "Specifies a custom public path for the external resources like images, files, etc inside CSS.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#publicpath" + }, + "emit": { + "type": "boolean", + "description": "If true, emits a file (writes a file to the filesystem). If false, the plugin will extract the CSS but will not emit the file", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#emit" + }, + "esModule": { + "type": "boolean", + "description": "Generates JS modules that use the ES modules syntax.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#esmodule" + }, + "layer": { + "type": "string" + } + } +} diff --git a/packages/rspack/src/builtin-plugin/css-extract/loader.ts b/packages/rspack/src/builtin-plugin/css-extract/loader.ts new file mode 100644 index 000000000000..0aabcc355abf --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/loader.ts @@ -0,0 +1,266 @@ +import schema from "./loader-options.json"; +import { CssExtractRspackPlugin } from "./index"; +import path from "path"; +import { stringifyLocal, stringifyRequest } from "./utils"; + +import type { LoaderContext, LoaderDefinition } from "../.."; + +export const MODULE_TYPE = "css/mini-extract"; +export const AUTO_PUBLIC_PATH = "__mini_css_extract_plugin_public_path_auto__"; +export const ABSOLUTE_PUBLIC_PATH = "webpack:///mini-css-extract-plugin/"; +export const BASE_URI = "webpack://"; +export const SINGLE_DOT_PATH_SEGMENT = + "__mini_css_extract_plugin_single_dot_path_segment__"; + +const SERIALIZE_SEP = "__RSPACK_CSS_EXTRACT_SEP__"; + +interface DependencyDescription { + identifier: string; + content: string; + context: string; + media?: string; + supports?: string; + layer?: string; + sourceMap?: string; + identifierIndex: number; + filepath: string; +} + +export interface LoaderOptions { + publicPath?: string | ((resourcePath: string, context: string) => string); + emit?: boolean; + esModule?: boolean; + + // TODO: support layer + layer?: boolean; +} + +function hotLoader( + content: string, + context: { + loaderContext: LoaderContext; + options: LoaderOptions; + locals: Record; + } +) { + const accept = context.locals + ? "" + : "module.hot.accept(undefined, cssReload);"; + return `${content} + if(module.hot) { + // ${Date.now()} + var cssReload = require(${stringifyRequest( + context.loaderContext, + path.join(__dirname, "./hmr/hotModuleReplacement.js") + )})(module.id, ${JSON.stringify({ + ...context.options, + locals: !!context.locals + })}); + module.hot.dispose(cssReload); + ${accept} + } + `; +} + +// mini-css-extract-plugin +const loader: LoaderDefinition = function loader(content) { + if ( + this._compiler && + this._compiler.options && + this._compiler.options.experiments && + this._compiler.options.experiments.css + ) { + return content; + } +}; + +export const pitch: LoaderDefinition["pitch"] = function (request, _, data) { + if ( + this._compiler && + this._compiler.options && + this._compiler.options.experiments && + this._compiler.options.experiments.css + ) { + this.emitWarning( + new Error( + "You can't use `experiments.css` and `mini-css-extract-plugin` together, please set `experiments.css` to `false`" + ) + ); + + return; + } + + const options = this.getOptions(schema) as LoaderOptions; + const emit = typeof options.emit !== "undefined" ? options.emit : true; + const callback = this.async(); + const filepath = this.resourcePath; + + let { publicPath } = + /** @type {Compilation} */ + this._compilation!.outputOptions; + + if (typeof options.publicPath === "string") { + // eslint-disable-next-line prefer-destructuring + publicPath = options.publicPath; + } else if (typeof options.publicPath === "function") { + publicPath = options.publicPath(this.resourcePath, this.rootContext); + } + + if (publicPath === "auto") { + publicPath = AUTO_PUBLIC_PATH; + } + + let publicPathForExtract: string | undefined; + + if (typeof publicPath === "string") { + const isAbsolutePublicPath = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(publicPath); + + publicPathForExtract = isAbsolutePublicPath + ? publicPath + : `${ABSOLUTE_PUBLIC_PATH}${publicPath.replace( + /\./g, + SINGLE_DOT_PATH_SEGMENT + )}`; + } else { + publicPathForExtract = publicPath; + } + + const handleExports = ( + originalExports: + | { default: Record; __esModule: true } + | Record + ) => { + /** @type {Locals | undefined} */ + let locals: Record; + let namedExport; + + const esModule = + typeof options.esModule !== "undefined" ? options.esModule : true; + let dependencies: DependencyDescription[] = []; + + try { + // eslint-disable-next-line no-underscore-dangle + const exports = originalExports.__esModule + ? originalExports.default + : originalExports; + + namedExport = + // eslint-disable-next-line no-underscore-dangle + originalExports.__esModule && + (!originalExports.default || !("locals" in originalExports.default)); + + if (namedExport) { + Object.keys(originalExports).forEach(key => { + if (key !== "default") { + if (!locals) { + locals = {}; + } + + /** @type {Locals} */ locals[key] = ( + originalExports as Record + )[key]; + } + }); + } else { + locals = exports && exports.locals; + } + + if (Array.isArray(exports) && emit) { + const identifierCountMap = new Map(); + + dependencies = exports.map( + ([id, content, media, sourceMap, supports, layer]) => { + let identifier = id; + let context = this.rootContext; + + const count = identifierCountMap.get(identifier) || 0; + + identifierCountMap.set(identifier, count + 1); + + return { + identifier, + context, + content, + media, + supports, + layer, + identifierIndex: count, + sourceMap: sourceMap + ? JSON.stringify(sourceMap) + : // eslint-disable-next-line no-undefined + undefined, + filepath + }; + } + ); + } + } catch (e) { + callback(e as Error); + + return; + } + + const result = locals! + ? namedExport + ? Object.keys(locals) + .map( + key => + `\nexport var ${key} = ${stringifyLocal( + /** @type {Locals} */ locals[key] + )};` + ) + .join("") + : `\n${ + esModule ? "export default" : "module.exports =" + } ${JSON.stringify(locals)};` + : esModule + ? `\nexport {};` + : ""; + + let resultSource = `// extracted by ${CssExtractRspackPlugin.pluginName}`; + + // only attempt hotreloading if the css is actually used for something other than hash values + resultSource += + this.hot && emit + ? hotLoader(result, { loaderContext: this, options, locals: locals! }) + : result; + + const additionalData: Record = { ...data }; + if (dependencies.length > 0) { + additionalData[CssExtractRspackPlugin.pluginName] = dependencies + .map(dep => { + return [ + dep.identifier, + dep.content, + dep.context, + dep.media, + dep.supports, + dep.sourceMap, + dep.identifierIndex, + dep.filepath + ].join(SERIALIZE_SEP); + }) + .join(SERIALIZE_SEP); + } + callback(null, resultSource, undefined, additionalData); + }; + + this.importModule( + `${this.resourcePath}.webpack[javascript/auto]!=!!!${request}`, + { + publicPath: /** @type {string} */ publicPathForExtract, + baseUri: `${BASE_URI}/` + }, + (error, exports) => { + if (error) { + callback(error); + + return; + } + + handleExports(exports); + } + ); +}; + +export default loader; diff --git a/packages/rspack/src/builtin-plugin/css-extract/plugin-options.json b/packages/rspack/src/builtin-plugin/css-extract/plugin-options.json new file mode 100644 index 000000000000..eea6cfd1bfde --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/plugin-options.json @@ -0,0 +1,79 @@ +{ + "title": "Mini CSS Extract Plugin options", + "type": "object", + "additionalProperties": false, + "properties": { + "filename": { + "anyOf": [ + { + "type": "string", + "absolutePath": false, + "minLength": 1 + }, + { + "instanceof": "Function" + } + ], + "description": "This option determines the name of each output CSS file.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#filename" + }, + "chunkFilename": { + "anyOf": [ + { + "type": "string", + "absolutePath": false, + "minLength": 1 + }, + { + "instanceof": "Function" + } + ], + "description": "This option determines the name of non-entry chunk files.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#chunkfilename" + }, + "experimentalUseImportModule": { + "type": "boolean", + "description": "Enable the experimental importModule approach instead of using child compilers. This uses less memory and is faster.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#experimentaluseimportmodule" + }, + "ignoreOrder": { + "type": "boolean", + "description": "Remove Order Warnings.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#ignoreorder" + }, + "insert": { + "description": "Inserts the `link` tag at the given position for non-initial (async) (https://webpack.js.org/concepts/under-the-hood/#chunks) CSS chunks.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#insert", + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + }, + "attributes": { + "description": "Adds custom attributes to the `link` tag for non-initial (async) (https://webpack.js.org/concepts/under-the-hood/#chunks) CSS chunks.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#attributes", + "type": "object" + }, + "linkType": { + "anyOf": [ + { + "enum": ["text/css"] + }, + { + "type": "boolean" + } + ], + "description": "This option allows loading asynchronous chunks with a custom link type", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#linktype" + }, + "runtime": { + "type": "boolean", + "description": "Enabled/Disables runtime generation. CSS will be still extracted and can be used for a custom loading methods.", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#noRuntime" + } + } +} diff --git a/packages/rspack/src/builtin-plugin/css-extract/utils.ts b/packages/rspack/src/builtin-plugin/css-extract/utils.ts new file mode 100644 index 000000000000..2d52f19f0fa3 --- /dev/null +++ b/packages/rspack/src/builtin-plugin/css-extract/utils.ts @@ -0,0 +1,65 @@ +import { LoaderContext } from "../.."; +import path from "path"; + +export function isAbsolutePath(str: string) { + return path.posix.isAbsolute(str) || path.win32.isAbsolute(str); +} + +const RELATIVE_PATH_REGEXP = /^\.\.?[/\\]/; + +export function isRelativePath(str: string) { + return RELATIVE_PATH_REGEXP.test(str); +} + +export function stringifyRequest( + loaderContext: LoaderContext, + request: string +) { + if ( + typeof loaderContext.utils !== "undefined" && + typeof loaderContext.utils.contextify === "function" + ) { + return JSON.stringify( + loaderContext.utils.contextify( + loaderContext.context || loaderContext.rootContext, + request + ) + ); + } + + const splitted = request.split("!"); + const { context } = loaderContext; + + return JSON.stringify( + splitted + .map(part => { + // First, separate singlePath from query, because the query might contain paths again + const splittedPart = part.match(/^(.*?)(\?.*)/); + const query = splittedPart ? splittedPart[2] : ""; + let singlePath = splittedPart ? splittedPart[1] : part; + + if (isAbsolutePath(singlePath) && context) { + singlePath = path.relative(context, singlePath); + + if (isAbsolutePath(singlePath)) { + // If singlePath still matches an absolute path, singlePath was on a different drive than context. + // In this case, we leave the path platform-specific without replacing any separators. + // @see https://github.com/webpack/loader-utils/pull/14 + return singlePath + query; + } + + if (isRelativePath(singlePath) === false) { + // Ensure that the relative path starts at least with ./ otherwise it would be a request into the modules directory (like node_modules). + singlePath = `./${singlePath}`; + } + } + + return singlePath.replace(/\\/g, "/") + query; + }) + .join("!") + ); +} + +export function stringifyLocal(value: any) { + return typeof value === "function" ? value.toString() : JSON.stringify(value); +} diff --git a/packages/rspack/src/builtin-plugin/index.ts b/packages/rspack/src/builtin-plugin/index.ts index 0fac13178852..b24f39856efa 100644 --- a/packages/rspack/src/builtin-plugin/index.ts +++ b/packages/rspack/src/builtin-plugin/index.ts @@ -55,6 +55,7 @@ export * from "./SwcJsMinimizerPlugin"; export * from "./SwcCssMinimizerPlugin"; export * from "./JsLoaderRspackPlugin"; +export * from "./css-extract"; ///// DEPRECATED ///// import { RawBuiltins } from "@rspack/binding"; @@ -67,8 +68,8 @@ function resolveTreeShaking( return treeShaking !== undefined ? treeShaking.toString() : production - ? "true" - : "false"; + ? "true" + : "false"; } export interface Builtins { diff --git a/packages/rspack/src/config/adapterRuleUse.ts b/packages/rspack/src/config/adapterRuleUse.ts index 8803f30bd16e..fb89e04020b5 100644 --- a/packages/rspack/src/config/adapterRuleUse.ts +++ b/packages/rspack/src/config/adapterRuleUse.ts @@ -142,7 +142,7 @@ export interface LoaderContext { addBuildDependency(file: string): void; importModule( request: string, - options: { publicPath: string; baseUri: string }, + options: { publicPath?: string; baseUri?: string }, callback: (err?: Error, res?: any) => void ): void; fs: any; diff --git a/packages/rspack/src/config/defaults.ts b/packages/rspack/src/config/defaults.ts index 1f4ac941f418..1aeb9f2ce8f0 100644 --- a/packages/rspack/src/config/defaults.ts +++ b/packages/rspack/src/config/defaults.ts @@ -120,7 +120,11 @@ export const applyRspackOptionsDefaults = ( applyNodeDefaults(options.node, { targetProperties }); - applyOptimizationDefaults(options.optimization, { production, development }); + applyOptimizationDefaults(options.optimization, { + production, + development, + css: options.experiments.css! + }); options.resolve = cleverMerge( getResolveDefaults({ @@ -745,7 +749,11 @@ const applyNodeDefaults = ( const applyOptimizationDefaults = ( optimization: Optimization, - { production, development }: { production: boolean; development: boolean } + { + production, + development, + css + }: { production: boolean; development: boolean; css: boolean } ) => { D(optimization, "removeAvailableModules", true); D(optimization, "removeEmptyChunks", true); @@ -779,9 +787,9 @@ const applyOptimizationDefaults = ( }); const { splitChunks } = optimization; if (splitChunks) { - // A(splitChunks, "defaultSizeTypes", () => - // css ? ["javascript", "css", "unknown"] : ["javascript", "unknown"] - // ); + A(splitChunks, "defaultSizeTypes", () => + css ? ["javascript", "css", "unknown"] : ["javascript", "unknown"] + ); D(splitChunks, "hidePathInfo", production); D(splitChunks, "chunks", "async"); // D(splitChunks, "usedExports", optimization.usedExports === true); diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index 5a46ef1f454f..9d3a815bdab3 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -280,6 +280,9 @@ export const getNormalizedRspackOptions = ( splitChunks => splitChunks && { ...splitChunks, + defaultSizeTypes: splitChunks.defaultSizeTypes + ? [...splitChunks.defaultSizeTypes] + : ["..."], cacheGroups: cloneObject(splitChunks.cacheGroups) } ) diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index dbb0f403e0bf..b3afe203fb91 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1085,8 +1085,10 @@ const optimizationSplitChunksChunks = z .or(z.instanceof(RegExp)) .or(z.function().args(z.instanceof(Chunk)).returns(z.boolean())); const optimizationSplitChunksSizes = z.number(); +const optimizationSplitChunksDefaultSizeTypes = z.array(z.string()); const sharedOptimizationSplitChunksCacheGroup = { chunks: optimizationSplitChunksChunks.optional(), + defaultSizeTypes: optimizationSplitChunksDefaultSizeTypes.optional(), minChunks: z.number().min(1).optional(), name: optimizationSplitChunksName.optional(), minSize: optimizationSplitChunksSizes.optional(), diff --git a/packages/rspack/src/exports.ts b/packages/rspack/src/exports.ts index c342de196540..88eba9b94acd 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -200,3 +200,5 @@ export type { SourceMapDevToolPluginOptions } from "./builtin-plugin"; export { EvalDevToolModulePlugin } from "./builtin-plugin"; export type { EvalDevToolModulePluginOptions } from "./builtin-plugin"; + +export { CssExtractRspackPlugin } from "./builtin-plugin"; diff --git a/packages/rspack/tsconfig.json b/packages/rspack/tsconfig.json index 06ab00e8a9b2..455f6f3c4396 100644 --- a/packages/rspack/tsconfig.json +++ b/packages/rspack/tsconfig.json @@ -2,10 +2,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "resolveJsonModule": true }, "include": [ - "src" + "src", + "src/**/*.json" ], "exclude": [ "src/config/schema.check.js"