From 53f7655feded16ad6233e126459799797c54737e Mon Sep 17 00:00:00 2001 From: jserfeng <1114550440@qq.com> Date: Tue, 19 Dec 2023 20:46:08 +0800 Subject: [PATCH] feat: mini-css-extract-plugin --- Cargo.lock | 30 ++ crates/node_binding/binding.d.ts | 13 +- crates/rspack_binding_options/Cargo.toml | 1 + .../src/options/raw_builtins/mod.rs | 16 +- .../options/raw_builtins/raw_css_extract.rs | 38 ++ .../rspack_core/src/compiler/compilation.rs | 2 +- crates/rspack_core/src/lib.rs | 7 +- crates/rspack_core/src/plugin/api.rs | 3 +- .../rspack_core/src/plugin/plugin_driver.rs | 6 +- crates/rspack_plugin_extract_css/Cargo.toml | 42 +++ crates/rspack_plugin_extract_css/LICENSE | 22 ++ .../src/css_dependency.rs | 74 ++++ .../src/css_module.rs | 121 ++++++ crates/rspack_plugin_extract_css/src/lib.rs | 6 + .../src/parser_and_generator.rs | 105 ++++++ .../rspack_plugin_extract_css/src/plugin.rs | 348 ++++++++++++++++++ .../rspack_plugin_extract_css/src/runtime.rs | 175 +++++++++ .../src/runtime/css_load.js | 58 +++ .../src/runtime/with_hmr.js | 41 +++ .../src/runtime/with_loading.js | 21 ++ crates/rspack_plugin_runtime/src/lib.rs | 2 +- examples/basic-mini-css-extract/index.html | 12 + examples/basic-mini-css-extract/package.json | 21 ++ .../basic-mini-css-extract/rspack.config.js | 33 ++ examples/basic-mini-css-extract/src/async.js | 2 + .../basic-mini-css-extract/src/change.css | 3 + examples/basic-mini-css-extract/src/index.js | 11 + examples/basic-mini-css-extract/src/other.css | 3 + .../src/style.module.css | 7 + examples/monaco-editor-js/index.js | 39 +- examples/monaco-editor-js/rspack.config.js | 8 +- .../hmr/hotModuleReplacement.js | 285 ++++++++++++++ .../hmr/normalize-url.js | 46 +++ .../jest.config.js | 22 ++ .../package.json | 50 +++ .../src/index.ts | 85 +++++ .../src/loader-options.json | 32 ++ .../src/loader.ts | 248 +++++++++++++ .../src/plugin-options.json | 79 ++++ .../src/utils.ts | 65 ++++ .../tsconfig.json | 22 ++ packages/rspack/src/Compiler.ts | 8 +- packages/rspack/src/builtin-plugin/base.ts | 3 +- packages/rspack/src/config/adapterRuleUse.ts | 2 +- pnpm-lock.yaml | 101 ++++- 45 files changed, 2274 insertions(+), 44 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 examples/basic-mini-css-extract/index.html create mode 100644 examples/basic-mini-css-extract/package.json create mode 100644 examples/basic-mini-css-extract/rspack.config.js create mode 100644 examples/basic-mini-css-extract/src/async.js create mode 100644 examples/basic-mini-css-extract/src/change.css create mode 100644 examples/basic-mini-css-extract/src/index.js create mode 100644 examples/basic-mini-css-extract/src/other.css create mode 100644 examples/basic-mini-css-extract/src/style.module.css create mode 100644 packages/rspack-plugin-mini-css-extract/hmr/hotModuleReplacement.js create mode 100644 packages/rspack-plugin-mini-css-extract/hmr/normalize-url.js create mode 100644 packages/rspack-plugin-mini-css-extract/jest.config.js create mode 100644 packages/rspack-plugin-mini-css-extract/package.json create mode 100644 packages/rspack-plugin-mini-css-extract/src/index.ts create mode 100644 packages/rspack-plugin-mini-css-extract/src/loader-options.json create mode 100644 packages/rspack-plugin-mini-css-extract/src/loader.ts create mode 100644 packages/rspack-plugin-mini-css-extract/src/plugin-options.json create mode 100644 packages/rspack-plugin-mini-css-extract/src/utils.ts create mode 100644 packages/rspack-plugin-mini-css-extract/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 4cf0ec91d691..e050ef31a1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2476,6 +2476,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", @@ -2978,6 +2979,35 @@ dependencies = [ "rspack_regex", ] +[[package]] +name = "rspack_plugin_extract_css" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 1.3.2", + "heck", + "indexmap 1.9.3", + "once_cell", + "rayon", + "regex", + "rkyv", + "rspack_core", + "rspack_error", + "rspack_hash", + "rspack_identifier", + "rspack_plugin_runtime", + "rustc-hash", + "serde", + "serde_json", + "sugar_path", + "swc_core", + "tokio", + "tracing", + "urlencoding", + "ustr", +] + [[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 b36b652488fd..b2f431c11e90 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -193,7 +193,8 @@ export const enum BuiltinPluginName { CopyRspackPlugin = 'CopyRspackPlugin', HtmlRspackPlugin = 'HtmlRspackPlugin', SwcJsMinimizerRspackPlugin = 'SwcJsMinimizerRspackPlugin', - SwcCssMinimizerRspackPlugin = 'SwcCssMinimizerRspackPlugin' + SwcCssMinimizerRspackPlugin = 'SwcCssMinimizerRspackPlugin', + CssExtractPlugin = 'CssExtractPlugin' } export function cleanupGlobalTrace(): void @@ -742,6 +743,16 @@ export interface RawCrossOriginLoading { boolPayload?: boolean } +export interface RawCssExtractPluginOption { + filename: string + chunkFilename: string + ignoreOrder: boolean + insert?: string + attributes: Record + linkType?: string + runtime: boolean +} + export interface RawCssModulesConfig { localsConvention: "asIs" | "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly" localIdentName: string diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index 58ed9ad45dcf..36a7a5445ae2 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -26,6 +26,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 c1fce92bf549..22d939f8a3ff 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -1,5 +1,6 @@ mod raw_banner; mod raw_copy; +mod raw_css_extract; mod raw_html; mod raw_limit_chunk_count; mod raw_mf; @@ -57,15 +58,16 @@ use rspack_plugin_wasm::{enable_wasm_loading_plugin, AsyncWasmPlugin}; use rspack_plugin_web_worker_template::web_worker_template_plugin; use rspack_plugin_worker::WorkerPlugin; -use self::raw_mf::{ - RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions, -}; pub use self::{ raw_banner::RawBannerPluginOptions, raw_copy::RawCopyRspackPluginOptions, raw_html::RawHtmlRspackPluginOptions, raw_limit_chunk_count::RawLimitChunkCountPluginOptions, raw_mf::RawContainerPluginOptions, raw_progress::RawProgressPluginOptions, raw_swc_js_minimizer::RawSwcJsMinimizerRspackPluginOptions, }; +use self::{ + raw_css_extract::RawCssExtractPluginOption, + raw_mf::{RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions}, +}; use crate::{ RawEntryPluginOptions, RawExternalItemWrapper, RawExternalsPluginOptions, RawHttpExternalsRspackPluginOptions, RawSourceMapDevToolPluginOptions, RawSplitChunksOptions, @@ -131,6 +133,7 @@ pub enum BuiltinPluginName { HtmlRspackPlugin, SwcJsMinimizerRspackPlugin, SwcCssMinimizerRspackPlugin, + CssExtractPlugin, } #[napi(object)] @@ -358,6 +361,13 @@ impl BuiltinPlugin { .boxed(); plugins.push(plugin); } + BuiltinPluginName::CssExtractPlugin => { + 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..44904ef1501f --- /dev/null +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_css_extract.rs @@ -0,0 +1,38 @@ +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, +} + +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, + } + } +} diff --git a/crates/rspack_core/src/compiler/compilation.rs b/crates/rspack_core/src/compiler/compilation.rs index f2726b29a682..26c0a3b9c33a 100644 --- a/crates/rspack_core/src/compiler/compilation.rs +++ b/crates/rspack_core/src/compiler/compilation.rs @@ -2192,7 +2192,6 @@ impl Compilation { // restore code_generation_results and chunk_graph let mut codegen_results = std::mem::replace(&mut self.code_generation_results, old_codegen_results); - self.chunk_graph = old_chunk_graph; for runtime_id in &runtime_modules { let runtime_module = self @@ -2210,6 +2209,7 @@ impl Compilation { } // clear side effects stuff we caused + self.chunk_graph = old_chunk_graph; self.chunk_by_ukey = old_chunk_by_ukey; self.chunk_group_by_ukey.remove(&entry_ukey); self.bailout_module_identifiers = old_bailout_module_identifiers; diff --git a/crates/rspack_core/src/lib.rs b/crates/rspack_core/src/lib.rs index 7dc83dd7bc70..6644f26f97d7 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -107,6 +107,7 @@ pub enum SourceType { Remote, ShareInit, ConsumeShared, + Custom(Ustr), #[default] Unknown, } @@ -123,6 +124,7 @@ impl std::fmt::Display for SourceType { SourceType::ShareInit => write!(f, "share-init"), SourceType::ConsumeShared => write!(f, "consume-shared"), SourceType::Unknown => write!(f, "unknown"), + SourceType::Custom(source_type) => f.write_str(source_type), } } } @@ -142,10 +144,7 @@ impl TryFrom<&str> for SourceType { "consume-shared" => Ok(Self::ConsumeShared), "unknown" => Ok(Self::Unknown), - _ => { - use rspack_error::internal_error; - Err(internal_error!("invalid source type: {value}")) - } + other => Ok(SourceType::Custom(other.into())), } } } diff --git a/crates/rspack_core/src/plugin/api.rs b/crates/rspack_core/src/plugin/api.rs index c8192cbdb9be..e6a0235e5e8a 100644 --- a/crates/rspack_core/src/plugin/api.rs +++ b/crates/rspack_core/src/plugin/api.rs @@ -596,8 +596,7 @@ pub type BoxedParserAndGeneratorBuilder = #[derive(Default)] pub struct ApplyContext { - pub(crate) registered_parser_and_generator_builder: - DashMap, + pub registered_parser_and_generator_builder: DashMap, } impl ApplyContext { diff --git a/crates/rspack_core/src/plugin/plugin_driver.rs b/crates/rspack_core/src/plugin/plugin_driver.rs index 08a02cb9f1b3..f4c9297d3181 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}; use rspack_loader_runner::ResourceData; +use rspack_util::fx_dashmap::FxDashMap; use rustc_hash::FxHashMap as HashMap; use tokio::sync::mpsc::UnboundedSender; use tracing::instrument; @@ -34,7 +35,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>>, } @@ -75,7 +77,7 @@ impl PluginDriver { .into_iter() .collect::>() }) - .collect::>(); + .collect::>(); let options = Arc::new(options); diff --git a/crates/rspack_plugin_extract_css/Cargo.toml b/crates/rspack_plugin_extract_css/Cargo.toml new file mode 100644 index 000000000000..978e7a914946 --- /dev/null +++ b/crates/rspack_plugin_extract_css/Cargo.toml @@ -0,0 +1,42 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_plugin_extract_css" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.1.0" + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bitflags = { workspace = true } +heck = "0.4.1" +indexmap = { workspace = true } +once_cell = { workspace = true } +rayon = { workspace = true } +regex = { workspace = true } +rkyv = { workspace = true, features = ["indexmap", "validation"] } +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_hash = { path = "../rspack_hash" } +rspack_identifier = { path = "../rspack_identifier" } +rspack_plugin_runtime = { path = "../rspack_plugin_runtime" } +rustc-hash = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sugar_path = { workspace = true } +swc_core = { workspace = true, features = [ + "css_ast", + "css_codegen", + "css_parser", + "css_utils", + "css_visit", + "css_visit_path", + "css_compat", + "css_modules", + "css_prefixer", + "css_minifier", +] } +tokio = { workspace = true } +tracing = { workspace = true } +urlencoding = "2.1.2" +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..bfe93b655104 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/css_dependency.rs @@ -0,0 +1,74 @@ +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, +} + +impl CssDependency { + pub(crate) fn new( + identifier: String, + content: String, + context: String, + media: String, + supports: String, + source_map: String, + ) -> Self { + Self { + id: DependencyId::new(), + identifier, + content, + context, + media, + supports, + source_map, + } + } +} + +impl AsDependencyTemplate for CssDependency {} +impl AsContextDependency for CssDependency {} + +impl Dependency for CssDependency { + fn dependency_debug_name(&self) -> &'static str { + "mini-extract-css-dependency" + } + + 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 + } +} + +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..4d1cd1029e70 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/css_module.rs @@ -0,0 +1,121 @@ +use once_cell::sync::Lazy; +use rspack_core::{ + rspack_sources::Source, AsyncDependenciesBlockIdentifier, CodeGenerationResult, Compilation, + DependenciesBlock, DependencyId, Module, RuntimeSpec, SourceType, +}; +use rspack_core::{DependencyType, ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult}; +use rspack_error::Result; +use rspack_error::{impl_empty_diagnosable_trait, TWithDiagnosticArray}; +use rspack_identifier::Identifiable; + +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())); + +#[derive(Debug, PartialEq, Eq, Hash)] +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, + + blocks: Vec, + dependencies: Vec, +} + +impl CssModule { + pub fn new(dep: CssDependency) -> Self { + Self { + identifier: format!("css|{}", dep.identifier), + content: dep.content, + context: dep.context, + media: dep.media, + supports: dep.supports, + source_map: dep.source_map, + blocks: vec![], + dependencies: vec![], + } + } +} + +#[async_trait::async_trait] +impl Module for CssModule { + fn readable_identifier(&self, _context: &rspack_core::Context) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(&self.identifier) + } + + 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 + } + + fn code_generation( + &self, + _compilation: &Compilation, + _runtime: Option<&RuntimeSpec>, + ) -> Result { + Ok(CodeGenerationResult::default()) + } +} + +impl Identifiable for CssModule { + fn identifier(&self) -> rspack_identifier::Identifier { + self.identifier.as_str().into() + } +} + +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 + } +} + +impl_empty_diagnosable_trait!(CssModule); + +#[derive(Debug)] +pub(crate) struct CssModuleFactory; + +#[async_trait::async_trait] +impl ModuleFactory for CssModuleFactory { + async fn create( + &self, + data: ModuleFactoryCreateData, + ) -> Result> { + let css_dep = data + .dependency + .downcast_ref::() + .expect("unreachable"); + + Ok(TWithDiagnosticArray::new( + ModuleFactoryResult::new(Box::new(CssModule::new(css_dep.clone()))), + vec![], + )) + } +} 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..a948e92c47d9 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/parser_and_generator.rs @@ -0,0 +1,105 @@ +use rspack_core::{Dependency, ParserAndGenerator}; +use rspack_error::TWithDiagnosticArray; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use crate::css_dependency::CssDependency; + +#[derive(Deserialize)] +struct CssExtractJsonData { + #[serde(rename = "rspack-mini-css-extract-plugin")] + value: String, +} + +#[derive(Debug)] +pub(crate) struct CssExtractParserAndGenerator { + orig_parser_generator: 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 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![]; + while let Some(identifier) = list.next() { + 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(), + ))); + } + + 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..7ade68305db1 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/plugin.rs @@ -0,0 +1,348 @@ +use std::{borrow::Cow, cmp::max, str::FromStr, sync::Arc}; + +use once_cell::sync::Lazy; +use regex::Regex; +use rspack_core::{ + rspack_sources::{ConcatSource, RawSource, SourceMap, SourceMapSource, WithoutOriginalOptions}, + ApplyContext, AssetInfo, Chunk, ChunkKind, Compilation, CompilationArgs, CompilationParams, + CompilerOptions, Filename, ModuleType, PathData, Plugin, PluginCompilationHookOutput, + PluginContext, PluginRenderManifestHookOutput, PluginRuntimeRequirementsInTreeOutput, + RenderManifestArgs, RenderManifestEntry, RuntimeGlobals, RuntimeRequirementsInTreeArgs, + SourceType, +}; +use rspack_error::Result; +use rustc_hash::FxHashMap; +use ustr::Ustr; + +use crate::{ + css_module::{CssModule, CssModuleFactory, DEPENDENCY_TYPE}, + parser_and_generator::CssExtractParserAndGenerator, + runtime::CssLoadingRuntimeModule, +}; +pub static PLUGIN_NAME: &str = "rspack-mini-css-extract-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.clone())); +pub static SOURCE_TYPE: Lazy<[SourceType; 1]> = + Lazy::new(|| [SourceType::Custom(MODULE_TYPE_STR.clone())]); + +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")); + +#[derive(Debug)] +pub struct PluginCssExtract { + pub options: Arc, +} + +#[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, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum InsertType { + Fn(String), + Selector(String), + Default, +} + +impl PluginCssExtract { + pub fn new(options: CssExtractOptions) -> Self { + Self { + options: Arc::new(options), + } + } + + fn render_content_asset( + &self, + chunk: &Chunk, + rendered_modules: Vec<&CssModule>, + filename_template: &str, + compilation: &Compilation, + path_data: PathData, + ) -> RenderManifestEntry { + // TODO: sort modules + let used_modules = rendered_modules; + + let mut source = ConcatSource::default(); + let mut external_source = ConcatSource::default(); + + let (filename, _) = compilation.get_path_with_info( + &Filename::from_str(filename_template).expect("should convert to filename template"), + path_data, + ); + + for module in used_modules { + let content = Cow::Borrowed(module.content.as_str()); + let readable_identifier = module.identifier.clone(); + let starts_with_at_import = STARTS_WITH_AT_IMPORT_REGEX.is_match(&content); + + if starts_with_at_import { + 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 !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().unwrap(), + 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); + RenderManifestEntry::new( + Arc::new(external_source), + filename, + AssetInfo::default(), + false, + false, + ) + } +} + +#[async_trait::async_trait] +impl Plugin for PluginCssExtract { + fn apply( + &self, + _ctx: PluginContext<&mut ApplyContext>, + _options: &mut CompilerOptions, + ) -> Result<()> { + Ok(()) + } + + async fn compilation( + &self, + args: CompilationArgs<'_>, + _params: &CompilationParams, + ) -> PluginCompilationHookOutput { + let compilation = args.compilation; + + 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 || { + let parser = parser_and_generator(); + Box::new(CssExtractParserAndGenerator::new(parser)) + }), + ); + Ok(()) + } + + async fn render_manifest( + &self, + _ctx: PluginContext, + args: RenderManifestArgs<'_>, + ) -> PluginRenderManifestHookOutput { + let compilation = args.compilation; + let chunk_ukey = args.chunk_ukey; + let chunk = compilation + .chunk_by_ukey + .get(&chunk_ukey) + .expect("should have chunk"); + + if matches!(chunk.kind, ChunkKind::HotUpdate) { + return Ok(vec![]); + } + + let rendered_modules = compilation + .chunk_graph + .get_chunk_modules_iterable_by_source_type( + &chunk_ukey, + SOURCE_TYPE[0], + &compilation.module_graph, + ) + .into_iter() + .map(|m| m.downcast_ref::().expect("should be css module")) + .collect::>(); + + if rendered_modules.is_empty() { + return Ok(vec![]); + } + + let filename_template = if chunk.can_be_initial(&compilation.chunk_group_by_ukey) { + &self.options.filename + } else { + &self.options.chunk_filename + }; + + Ok(vec![self.render_content_asset( + chunk, + rendered_modules, + filename_template, + compilation, + PathData::default().chunk(chunk), + )]) + } + + fn runtime_requirements_in_tree( + &self, + _ctx: PluginContext, + args: &mut RuntimeRequirementsInTreeArgs, + ) -> PluginRuntimeRequirementsInTreeOutput { + if args + .runtime_requirements + .contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) + || args + .runtime_requirements + .contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS) + { + if self.options.chunk_filename.contains("hash") { + args + .runtime_requirements_mut + .insert(RuntimeGlobals::GET_FULL_HASH); + } + args + .runtime_requirements_mut + .insert(RuntimeGlobals::PUBLIC_PATH); + args + .runtime_requirements_mut + .insert(RuntimeGlobals::GET_CHUNK_CSS_FILENAME); + + args.compilation.add_runtime_module( + args.chunk, + Box::new(CssLoadingRuntimeModule::new( + *args.chunk, + self.options.clone(), + args + .runtime_requirements + .contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS), + args + .runtime_requirements + .contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS), + )), + ); + } + + Ok(()) + } +} + +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..].to_string()); + + output_path = output_path[0..pos.unwrap()].to_string(); + } + } else if part != "." { + depth += 1; + } + } + + return 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..ab77585251f8 --- /dev/null +++ b/crates/rspack_plugin_extract_css/src/runtime.rs @@ -0,0 +1,175 @@ +use std::sync::Arc; + +use rspack_core::{ + impl_runtime_module, rspack_sources::RawSource, ChunkUkey, Compilation, CrossOriginLoading, + RuntimeGlobals, RuntimeModule, +}; +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"); + +#[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, + } + } + + fn get_css_chunks(&self, compilation: &Compilation) -> FxHashSet { + let mut set: FxHashSet = Default::default(); + + let chunk = compilation + .chunk_by_ukey + .get(&self.chunk) + .expect("should have 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], + &compilation.module_graph, + ); + + if modules.count() > 0 { + set.insert(chunk); + } + } + + set + } +} + +impl RuntimeModule for CssLoadingRuntimeModule { + fn name(&self) -> rspack_identifier::Identifier { + "css-extract-runtime".into() + } + + fn generate( + &self, + compilation: &rspack_core::Compilation, + ) -> rspack_core::rspack_sources::BoxSource { + 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 + .get(&self.chunk) + .expect("should have chunk"); + let with_loading = WITH_LOADING.replace( + "__INSTALLED_CHUNKS__", + &chunk + .ids + .iter() + .map(|id| format!("\"{id}\": 0,\n")) + .collect::(), + ); + + 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() + .map(|id| format!("\"{}\": 0,\n", id.as_usize())) + .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") + }; + + Arc::new(RawSource::from(runtime)) + } +} + +impl_runtime_module!(CssLoadingRuntimeModule); 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..95b6083b94de --- /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__.k(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..a1420844b7ca --- /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_runtime/src/lib.rs b/crates/rspack_plugin_runtime/src/lib.rs index 9517dfc735f4..3738b54dc323 100644 --- a/crates/rspack_plugin_runtime/src/lib.rs +++ b/crates/rspack_plugin_runtime/src/lib.rs @@ -22,7 +22,7 @@ mod module_chunk_loading; pub use module_chunk_loading::ModuleChunkLoadingPlugin; mod import_scripts_chunk_loading; pub use import_scripts_chunk_loading::ImportScriptsChunkLoadingPlugin; -mod runtime_module; +pub mod runtime_module; mod startup_chunk_dependencies; pub use startup_chunk_dependencies::StartupChunkDependenciesPlugin; mod chunk_prefetch_preload; diff --git a/examples/basic-mini-css-extract/index.html b/examples/basic-mini-css-extract/index.html new file mode 100644 index 000000000000..3520faac9b0a --- /dev/null +++ b/examples/basic-mini-css-extract/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + + + + diff --git a/examples/basic-mini-css-extract/package.json b/examples/basic-mini-css-extract/package.json new file mode 100644 index 000000000000..4b498131a6ab --- /dev/null +++ b/examples/basic-mini-css-extract/package.json @@ -0,0 +1,21 @@ +{ + "name": "example-basic", + "version": "1.0.0", + "description": "", + "main": "index.js", + "private": true, + "scripts": { + "dev": "rspack serve", + "build": "rspack build" + }, + "devDependencies": { + "@rspack/cli": "workspace:*", + "@rspack/core": "workspace:*", + "@rspack/plugin-mini-css-extract": "workspace:*", + "css-loader": "^6.8.1" + }, + "sideEffects": false, + "keywords": [], + "author": "", + "license": "MIT" +} \ No newline at end of file diff --git a/examples/basic-mini-css-extract/rspack.config.js b/examples/basic-mini-css-extract/rspack.config.js new file mode 100644 index 000000000000..ef3f7e149ec6 --- /dev/null +++ b/examples/basic-mini-css-extract/rspack.config.js @@ -0,0 +1,33 @@ +const rspack = require("@rspack/core"); +const MiniCssExtractPlugin = require("@rspack/plugin-mini-css-extract").default; +/** @type {import('@rspack/cli').Configuration} */ +const config = { + mode: "development", + entry: "./src/index.js", + plugins: [ + new rspack.HtmlRspackPlugin({ + template: "./index.html" + }), + new MiniCssExtractPlugin({}) + ], + module: { + rules: [ + { + test: /\.css/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + } + ] + }, + optimization: { + moduleIds: "named", + minimize: false, + runtimeChunk: true + }, + experiments: { + css: false, + rspackFuture: { + newTreeshaking: true + } + } +}; +module.exports = config; diff --git a/examples/basic-mini-css-extract/src/async.js b/examples/basic-mini-css-extract/src/async.js new file mode 100644 index 000000000000..cd1659eb7a49 --- /dev/null +++ b/examples/basic-mini-css-extract/src/async.js @@ -0,0 +1,2 @@ +import css from "./change.css"; +console.log(css); diff --git a/examples/basic-mini-css-extract/src/change.css b/examples/basic-mini-css-extract/src/change.css new file mode 100644 index 000000000000..42617e9114d0 --- /dev/null +++ b/examples/basic-mini-css-extract/src/change.css @@ -0,0 +1,3 @@ +body { + background-color: rgb(183, 111, 231); +} diff --git a/examples/basic-mini-css-extract/src/index.js b/examples/basic-mini-css-extract/src/index.js new file mode 100644 index 000000000000..a2e36c8dafed --- /dev/null +++ b/examples/basic-mini-css-extract/src/index.js @@ -0,0 +1,11 @@ +import names from "./style.module.css"; + +import("./async"); + +function render() { + const button = document.getElementById("root"); + button.className = names.btn; + + button.innerText = "Hello World"; +} +render(); diff --git a/examples/basic-mini-css-extract/src/other.css b/examples/basic-mini-css-extract/src/other.css new file mode 100644 index 000000000000..2712b1fb091e --- /dev/null +++ b/examples/basic-mini-css-extract/src/other.css @@ -0,0 +1,3 @@ +body { + background-color: lightblue; +} diff --git a/examples/basic-mini-css-extract/src/style.module.css b/examples/basic-mini-css-extract/src/style.module.css new file mode 100644 index 000000000000..69293f7d26b2 --- /dev/null +++ b/examples/basic-mini-css-extract/src/style.module.css @@ -0,0 +1,7 @@ +@import url("./other.css"); + +.btn { + border-radius: 5px; + color: rgb(109, 109, 109); + box-shadow: 0 0 20px rgba(117, 117, 117, 0.1); +} diff --git a/examples/monaco-editor-js/index.js b/examples/monaco-editor-js/index.js index b0ad0d6a9c41..1729d27f920e 100644 --- a/examples/monaco-editor-js/index.js +++ b/examples/monaco-editor-js/index.js @@ -1,24 +1,31 @@ -import * as monaco from 'monaco-editor'; +import * as monaco from "monaco-editor"; self.MonacoEnvironment = { getWorker: function (moduleId, label) { - if (label === 'json') { - return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url)); + // if (label === 'json') { + // return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url)); + // } + // if (label === 'css' || label === 'scss' || label === 'less') { + // return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url)); + // } + // if (label === 'html' || label === 'handlebars' || label === 'razor') { + // return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url)); + // } + if (label === "typescript" || label === "javascript") { + return new Worker( + new URL( + "monaco-editor/esm/vs/language/typescript/ts.worker", + import.meta.url + ) + ); } - if (label === 'css' || label === 'scss' || label === 'less') { - return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url)); - } - if (label === 'html' || label === 'handlebars' || label === 'razor') { - return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url)); - } - if (label === 'typescript' || label === 'javascript') { - return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url)); - } - return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url)); + return new Worker( + new URL("monaco-editor/esm/vs/editor/editor.worker", import.meta.url) + ); } }; -monaco.editor.create(document.getElementById('container'), { - value: ['function x() {', '\tconsole.log("Hello world!");', '}'].join('\n'), - language: 'javascript' +monaco.editor.create(document.getElementById("container"), { + value: ["function x() {", '\tconsole.log("Hello world!");', "}"].join("\n"), + language: "javascript" }); diff --git a/examples/monaco-editor-js/rspack.config.js b/examples/monaco-editor-js/rspack.config.js index 50271d676259..30b8a26c93a1 100644 --- a/examples/monaco-editor-js/rspack.config.js +++ b/examples/monaco-editor-js/rspack.config.js @@ -10,6 +10,11 @@ module.exports = { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist") }, + optimization: { + splitChunks: false, + minimize: false, + moduleIds: "named" + }, module: { rules: [ { @@ -22,5 +27,6 @@ module.exports = { new rspack.HtmlRspackPlugin({ template: "./index.html" }) - ] + ], + devtool: false }; diff --git a/packages/rspack-plugin-mini-css-extract/hmr/hotModuleReplacement.js b/packages/rspack-plugin-mini-css-extract/hmr/hotModuleReplacement.js new file mode 100644 index 000000000000..6fda8200b5cb --- /dev/null +++ b/packages/rspack-plugin-mini-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-plugin-mini-css-extract/hmr/normalize-url.js b/packages/rspack-plugin-mini-css-extract/hmr/normalize-url.js new file mode 100644 index 000000000000..04fb0969aeca --- /dev/null +++ b/packages/rspack-plugin-mini-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-plugin-mini-css-extract/jest.config.js b/packages/rspack-plugin-mini-css-extract/jest.config.js new file mode 100644 index 000000000000..e0038fdc1a4b --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/jest.config.js @@ -0,0 +1,22 @@ +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +const config = { + testEnvironment: "../../scripts/test/patch-node-env.cjs", + testMatch: [ + "/test/*.test.js", + "/test/*.basictest.js", + "/test/*.longtest.js", + "/test/*.unittest.js" + ], + testTimeout: process.env.CI ? 60000 : 30000, + cache: false, + transform: { + "^.+\\.(t|j)sx?$": "@swc/jest" + }, + globals: { + "ts-jest": { + tsconfig: "/tests/tsconfig.json" + } + } +}; + +module.exports = config; diff --git a/packages/rspack-plugin-mini-css-extract/package.json b/packages/rspack-plugin-mini-css-extract/package.json new file mode 100644 index 000000000000..5f4ccd738fe3 --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/package.json @@ -0,0 +1,50 @@ +{ + "name": "@rspack/plugin-mini-css-extract", + "version": "0.4.3", + "license": "MIT", + "description": "port of webpack mini-css-extract-plugin", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "exports": { + ".": { + "default": "./dist/index.js" + }, + "./loader": { + "default": "./dist/loader.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsc --build --force", + "dev": "tsc -w", + "test": "cross-env NODE_ENV=test jest" + }, + "files": [ + "dist", + "hmr" + ], + "keywords": [ + "rspack", + "html" + ], + "homepage": "https://rspack.dev", + "bugs": "https://github.com/web-infra-dev/rspack/issues", + "repository": { + "type": "git", + "url": "https://github.com/web-infra-dev/rspack", + "directory": "packages/rspack-plugin-mini-css-extract" + }, + "peerDependencies": { + "@rspack/core": "workspace:*" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + } + }, + "devDependencies": { + "@rspack/core": "workspace:*", + "@rspack/binding": "workspace:*", + "@swc/jest": "^0.2.29" + } +} diff --git a/packages/rspack-plugin-mini-css-extract/src/index.ts b/packages/rspack-plugin-mini-css-extract/src/index.ts new file mode 100644 index 000000000000..3084c78b85be --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/src/index.ts @@ -0,0 +1,85 @@ +import type { RawCssExtractPluginOption } from "@rspack/binding"; +import { Compiler } from "@rspack/core"; + +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; + attributes?: Record; + linkType?: string | "text/css" | false; + runtime?: boolean; +} + +export class MiniCssExtractPlugin { + static pluginName: string = "rspack-mini-css-extract-plugin"; + static loader: string = LOADER_PATH; + + options: PluginOptions; + + constructor(options: PluginOptions) { + this.options = options; + } + + apply(compiler: Compiler) { + 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, + linkType: + typeof options.linkType === "undefined" + ? "text/css" + : options.linkType === false + ? undefined + : options.linkType, + attributes: options.attributes ?? {} + }; + + return normalzedOptions; + } +} + +export default MiniCssExtractPlugin; diff --git a/packages/rspack-plugin-mini-css-extract/src/loader-options.json b/packages/rspack-plugin-mini-css-extract/src/loader-options.json new file mode 100644 index 000000000000..6d10fec261ef --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/src/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-plugin-mini-css-extract/src/loader.ts b/packages/rspack-plugin-mini-css-extract/src/loader.ts new file mode 100644 index 000000000000..6d52581ceb3f --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/src/loader.ts @@ -0,0 +1,248 @@ +import schema from "./loader-options.json"; +import { MiniCssExtractPlugin } from "./index"; +import path from "path"; +import { stringifyLocal, stringifyRequest } from "./utils"; + +import type { LoaderContext, LoaderDefinition } from "@rspack/core"; + +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; +} + +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 +export default function () {} + +export const pitch: LoaderDefinition["pitch"] = function (request, _, data) { + if ( + this._compiler && + this._compiler.options && + this._compiler.options.experiments && + this._compiler.options.experiments.css + ) { + this.emitError( + new Error( + 'You can\'t use `experiments.css` (`experiments.futureDefaults` enable built-in CSS support by default) and `mini-css-extract-plugin` together, please set `experiments.css` to `false` or set `{ type: "javascript/auto" }` for rules with `mini-css-extract-plugin` in your webpack config (now `mini-css-extract-plugin` does nothing).' + ) + ); + + return; + } + + const options = this.getOptions(schema) as LoaderOptions; + const emit = typeof options.emit !== "undefined" ? options.emit : true; + const callback = this.async(); + + 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)) { + callback( + new Error( + "Now rspack-mini-css-extract-plugin only supports `exportsType: 'array'` in css-loader" + ) + ); + return; + } else { + dependencies = exports.map( + ([id, content, media, sourceMap, supports, layer]) => { + let identifier = id; + let context = this.rootContext; + + return { + identifier, + context, + content, + media, + supports, + layer, + sourceMap: sourceMap + ? JSON.stringify(sourceMap) + : // eslint-disable-next-line no-undefined + undefined + }; + } + ); + } + } 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 ${MiniCssExtractPlugin.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; + + callback(null, resultSource, undefined, { + [MiniCssExtractPlugin.pluginName]: dependencies + .map(dep => { + return [ + dep.identifier, + dep.content, + dep.context, + dep.media, + dep.supports, + dep.sourceMap + ].join(SERIALIZE_SEP); + }) + .join(SERIALIZE_SEP), + ...data + }); + }; + + this.importModule( + `${this.resourcePath}.webpack[javascript/auto]!=!!!${request}`, + { + publicPath: /** @type {string} */ publicPathForExtract, + baseUri: `${BASE_URI}/` + }, + (error, exports) => { + if (error) { + callback(error); + + return; + } + + handleExports(exports); + } + ); +}; diff --git a/packages/rspack-plugin-mini-css-extract/src/plugin-options.json b/packages/rspack-plugin-mini-css-extract/src/plugin-options.json new file mode 100644 index 000000000000..eea6cfd1bfde --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/src/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-plugin-mini-css-extract/src/utils.ts b/packages/rspack-plugin-mini-css-extract/src/utils.ts new file mode 100644 index 000000000000..f4fb3e77eb46 --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/src/utils.ts @@ -0,0 +1,65 @@ +import { LoaderContext } from "@rspack/core"; +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-plugin-mini-css-extract/tsconfig.json b/packages/rspack-plugin-mini-css-extract/tsconfig.json new file mode 100644 index 000000000000..5a3f2df18b06 --- /dev/null +++ b/packages/rspack-plugin-mini-css-extract/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + // FIXME: If we use Node16, it will be a segmentation fault in unit test, so we can only use CommonJS here + // related issue: https://github.com/nodejs/node/issues/35889 + "module": "CommonJS", + "moduleResolution": "Node10", + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src" + }, + "include": [ + "src", + "src/loader-options.json", + "src/plugin-options.json", + ], + "references": [ + { + "path": "../rspack" + } + ] +} \ No newline at end of file diff --git a/packages/rspack/src/Compiler.ts b/packages/rspack/src/Compiler.ts index dc31ba186e18..aad2916b9a7d 100644 --- a/packages/rspack/src/Compiler.ts +++ b/packages/rspack/src/Compiler.ts @@ -933,15 +933,15 @@ class Compiler { __webpack_require__(runtimeModule); } - exports = __webpack_require__(entry); + const executeExports = __webpack_require__(entry); this.#moduleExecutionResultsMap.set( request + options.publicPath + options.baseUri, - exports + executeExports ); // convert to Rust side - return JSON.stringify(exports); + return JSON.stringify(executeExports); } #compilation(native: binding.JsCompilation) { @@ -1154,7 +1154,7 @@ class Compiler { } __internal__getModuleExecutionResult(entry: string) { - this.#moduleExecutionResultsMap.get(entry); + return this.#moduleExecutionResultsMap.get(entry); } } diff --git a/packages/rspack/src/builtin-plugin/base.ts b/packages/rspack/src/builtin-plugin/base.ts index a5a5a5da8fc6..13d17be1f1b1 100644 --- a/packages/rspack/src/builtin-plugin/base.ts +++ b/packages/rspack/src/builtin-plugin/base.ts @@ -56,7 +56,8 @@ export enum BuiltinPluginName { SideEffectsFlagPlugin = "SideEffectsFlagPlugin", FlagDependencyExportsPlugin = "FlagDependencyExportsPlugin", FlagDependencyUsagePlugin = "FlagDependencyUsagePlugin", - MangleExportsPlugin = "MangleExportsPlugin" + MangleExportsPlugin = "MangleExportsPlugin", + MiniCssExtractPlugin = "MiniCssExtractPlugin" } type AffectedHooks = keyof Compiler["hooks"]; diff --git a/packages/rspack/src/config/adapterRuleUse.ts b/packages/rspack/src/config/adapterRuleUse.ts index 19cab0665c64..c134cb717269 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/pnpm-lock.yaml b/pnpm-lock.yaml index 10c9973851cd..4ec7224ace4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,6 +394,21 @@ importers: specifier: workspace:* version: link:../../packages/rspack + examples/basic-mini-css-extract: + devDependencies: + '@rspack/cli': + specifier: workspace:* + version: link:../../packages/rspack-cli + '@rspack/core': + specifier: workspace:* + version: link:../../packages/rspack + '@rspack/plugin-mini-css-extract': + specifier: workspace:* + version: link:../../packages/rspack-plugin-mini-css-extract + css-loader: + specifier: ^6.8.1 + version: 6.8.1 + examples/basic-ts: dependencies: typescript: @@ -1858,6 +1873,18 @@ importers: specifier: ^3.0.2 version: 3.0.2 + packages/rspack-plugin-mini-css-extract: + devDependencies: + '@rspack/binding': + specifier: workspace:* + version: link:../../crates/node_binding + '@rspack/core': + specifier: workspace:* + version: link:../rspack + '@swc/jest': + specifier: ^0.2.29 + version: 0.2.29 + packages/rspack-plugin-minify: dependencies: esbuild: @@ -11447,6 +11474,16 @@ packages: tslib: 2.5.0 dev: true + /@swc/jest@0.2.29: + resolution: {integrity: sha512-8reh5RvHBsSikDC3WGCd5ZTd2BXKkyOdK7QwynrCH58jk2cQFhhHhFBg/jvnWZehUQe/EoOImLENc9/DwbBFow==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + jsonc-parser: 3.2.0 + dev: true + /@swc/jest@0.2.29(@swc/core@1.3.99): resolution: {integrity: sha512-8reh5RvHBsSikDC3WGCd5ZTd2BXKkyOdK7QwynrCH58jk2cQFhhHhFBg/jvnWZehUQe/EoOImLENc9/DwbBFow==} engines: {npm: '>= 7.0.0'} @@ -16438,14 +16475,14 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.23) - postcss: 8.4.23 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.23) - postcss-modules-local-by-default: 4.0.3(postcss@8.4.23) - postcss-modules-scope: 3.0.0(postcss@8.4.23) - postcss-modules-values: 4.0.0(postcss@8.4.23) + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.31) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.31) + postcss-modules-scope: 3.0.0(postcss@8.4.31) + postcss-modules-values: 4.0.0(postcss@8.4.31) postcss-value-parser: 4.2.0 - semver: 7.5.1 + semver: 7.5.4 dev: true /css-loader@6.8.1(webpack@5.89.0): @@ -19639,6 +19676,15 @@ packages: postcss: 8.4.23 dev: true + /icss-utils@5.1.0(postcss@8.4.31): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.31 + dev: true + /idb-wrapper@1.7.2: resolution: {integrity: sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==} dev: false @@ -23788,6 +23834,15 @@ packages: postcss: 8.4.23 dev: true + /postcss-modules-extract-imports@3.0.0(postcss@8.4.31): + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.31 + dev: true + /postcss-modules-local-by-default@4.0.0(postcss@8.4.21): resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} engines: {node: ^10 || ^12 || >= 14} @@ -23824,6 +23879,18 @@ packages: postcss-value-parser: 4.2.0 dev: true + /postcss-modules-local-by-default@4.0.3(postcss@8.4.31): + resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: true + /postcss-modules-scope@3.0.0(postcss@8.4.21): resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} @@ -23844,6 +23911,16 @@ packages: postcss-selector-parser: 6.0.11 dev: true + /postcss-modules-scope@3.0.0(postcss@8.4.31): + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.31 + postcss-selector-parser: 6.0.11 + dev: true + /postcss-modules-values@4.0.0(postcss@8.4.21): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} @@ -23864,6 +23941,16 @@ packages: postcss: 8.4.23 dev: true + /postcss-modules-values@4.0.0(postcss@8.4.31): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + dev: true + /postcss-nested@6.0.0(postcss@8.4.21): resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} engines: {node: '>=12.0'}