diff --git a/Cargo.lock b/Cargo.lock index fc00bd24bb9..e62f5f6830f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3628,6 +3628,7 @@ dependencies = [ "swc_html", "swc_html_minifier", "tracing", + "urlencoding", ] [[package]] diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 883f67afbaf..602144a9f4f 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -1244,12 +1244,13 @@ export interface RawHtmlRspackPluginOptions { scriptLoading: "blocking" | "defer" | "module" /** entry_chunk_name (only entry chunks are supported) */ chunks?: Array - excludedChunks?: Array + excludeChunks?: Array sri?: "sha256" | "sha384" | "sha512" minify?: boolean title?: string favicon?: string meta?: Record> + hash?: boolean } export interface RawHttpExternalsRspackPluginOptions { diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs index 51ac3b92dc9..180b932f078 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs @@ -33,13 +33,14 @@ pub struct RawHtmlRspackPluginOptions { /// entry_chunk_name (only entry chunks are supported) pub chunks: Option>, - pub excluded_chunks: Option>, + pub exclude_chunks: Option>, #[napi(ts_type = "\"sha256\" | \"sha384\" | \"sha512\"")] pub sri: Option, pub minify: Option, pub title: Option, pub favicon: Option, pub meta: Option>>, + pub hash: Option, } impl From for HtmlRspackPluginOptions { @@ -62,12 +63,13 @@ impl From for HtmlRspackPluginOptions { public_path: value.public_path, script_loading, chunks: value.chunks, - excluded_chunks: value.excluded_chunks, + exclude_chunks: value.exclude_chunks, sri, minify: value.minify.unwrap_or_default(), title: value.title, favicon: value.favicon, meta: value.meta, + hash: value.hash.unwrap_or_default(), } } } diff --git a/crates/rspack_plugin_html/Cargo.toml b/crates/rspack_plugin_html/Cargo.toml index 8f8d942c388..92dd1accc41 100644 --- a/crates/rspack_plugin_html/Cargo.toml +++ b/crates/rspack_plugin_html/Cargo.toml @@ -30,6 +30,7 @@ swc_core = { workspace = true } swc_html = { workspace = true } swc_html_minifier = { workspace = true } tracing = { workspace = true } +urlencoding = { workspace = true } [package.metadata.cargo-shear] ignored = ["tracing"] diff --git a/crates/rspack_plugin_html/src/config.rs b/crates/rspack_plugin_html/src/config.rs index f5892bda760..0fff316e9f3 100644 --- a/crates/rspack_plugin_html/src/config.rs +++ b/crates/rspack_plugin_html/src/config.rs @@ -84,7 +84,7 @@ pub struct HtmlRspackPluginOptions { /// entry_chunk_name (only entry chunks are supported) pub chunks: Option>, - pub excluded_chunks: Option>, + pub exclude_chunks: Option>, /// hash func that used in subsource integrity /// sha384, sha256 or sha512 @@ -94,6 +94,7 @@ pub struct HtmlRspackPluginOptions { pub title: Option, pub favicon: Option, pub meta: Option>>, + pub hash: bool, } fn default_filename() -> String { @@ -119,12 +120,13 @@ impl Default for HtmlRspackPluginOptions { public_path: None, script_loading: default_script_loading(), chunks: None, - excluded_chunks: None, + exclude_chunks: None, sri: None, minify: false, title: None, favicon: None, meta: None, + hash: false, } } } diff --git a/crates/rspack_plugin_html/src/plugin.rs b/crates/rspack_plugin_html/src/plugin.rs index 63baaf380e6..2a7376143c9 100644 --- a/crates/rspack_plugin_html/src/plugin.rs +++ b/crates/rspack_plugin_html/src/plugin.rs @@ -7,6 +7,7 @@ use std::{ use anyhow::Context; use dojang::dojang::Dojang; +use itertools::Itertools; use rayon::prelude::*; use rspack_core::{ parse_to_url, @@ -22,7 +23,10 @@ use crate::{ config::{HtmlInject, HtmlRspackPluginOptions}, parser::HtmlCompiler, sri::{add_sri, create_digest_from_asset}, - visitors::asset::{AssetWriter, HTMLPluginTag}, + visitors::{ + asset::{AssetWriter, HTMLPluginTag}, + utils::append_hash, + }, }; #[plugin] @@ -100,8 +104,8 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { if let Some(included_chunks) = &config.chunks { included = included_chunks.iter().any(|c| c.eq(entry_name)); } - if let Some(excluded_chunks) = &config.excluded_chunks { - included = included && !excluded_chunks.iter().any(|c| c.eq(entry_name)); + if let Some(exclude_chunks) = &config.exclude_chunks { + included = included && !exclude_chunks.iter().any(|c| c.eq(entry_name)); } included }) @@ -121,11 +125,19 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { // if inject is 'false', don't do anything if !matches!(config.inject, HtmlInject::False) { for (asset_name, asset) in included_assets { - if let Some(extension) = Path::new(&asset_name).extension() { - let asset_uri = format!( - "{}{asset_name}", + if let Some(extension) = + Path::new(asset_name.split("?").next().unwrap_or_default()).extension() + { + let mut asset_uri = format!( + "{}{}", config.get_public_path(compilation, &self.config.filename), + url_encode_path(&asset_name) ); + if config.hash { + if let Some(hash) = compilation.get_hash() { + asset_uri = append_hash(&asset_uri, hash); + } + } let mut tag: Option = None; if extension.eq_ignore_ascii_case("css") { tag = Some(HTMLPluginTag::create_style(&asset_uri, HtmlInject::Head)); @@ -160,7 +172,9 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { let mut visitor = AssetWriter::new(config, &tags, compilation); current_ast.visit_mut_with(&mut visitor); - let source = parser.codegen(&mut current_ast)?; + let source = parser + .codegen(&mut current_ast)? + .replace("$$RSPACK_URL_AMP$$", "&"); let hash = hash_for_source(&source); let html_file_name = FilenameTemplate::from(config.filename.clone()); // Use the same filename as template @@ -243,3 +257,27 @@ fn hash_for_source(source: &str) -> String { source.hash(&mut hasher); format!("{:016x}", hasher.finish()) } + +fn url_encode_path(file_path: &str) -> String { + let query_string_start = file_path.find('?'); + let url_path = if let Some(query_string_start) = query_string_start { + file_path[..query_string_start].to_string() + } else { + file_path.to_string() + }; + let query_string = if let Some(query_string_start) = query_string_start { + file_path[query_string_start..].to_string() + } else { + "".to_string() + }; + + format!( + "{}{}", + url_path + .split('/') + .map(|p| { urlencoding::encode(p) }) + .join("/"), + // element.outerHTML will escape '&' so need to add a placeholder here + query_string.replace("&", "$$RSPACK_URL_AMP$$") + ) +} diff --git a/crates/rspack_plugin_html/src/visitors/asset.rs b/crates/rspack_plugin_html/src/visitors/asset.rs index d517f0ac5a8..db313114f9d 100644 --- a/crates/rspack_plugin_html/src/visitors/asset.rs +++ b/crates/rspack_plugin_html/src/visitors/asset.rs @@ -8,7 +8,7 @@ use swc_core::{common::DUMMY_SP, ecma::atoms::Atom}; use swc_html::ast::{Attribute, Child, Element, Namespace, Text}; use swc_html::visit::{VisitMut, VisitMutWith}; -use super::utils::create_element; +use super::utils::{append_hash, create_element}; use crate::config::{HtmlInject, HtmlRspackPluginOptions, HtmlScriptLoading}; // the tag @@ -185,6 +185,12 @@ impl VisitMut for AssetWriter<'_, '_> { favicon_link_path = reg.replace_all(favicon_link_path.as_str(), "/").to_string(); } + if self.config.hash { + if let Some(hash) = self.compilation.get_hash() { + favicon_link_path = append_hash(&favicon_link_path, hash); + } + } + n.children.push(Child::Element(Element { tag_name: Atom::from("link"), children: vec![], diff --git a/crates/rspack_plugin_html/src/visitors/mod.rs b/crates/rspack_plugin_html/src/visitors/mod.rs index 2313b1338e9..18daee55235 100644 --- a/crates/rspack_plugin_html/src/visitors/mod.rs +++ b/crates/rspack_plugin_html/src/visitors/mod.rs @@ -1,2 +1,2 @@ pub mod asset; -mod utils; +pub mod utils; diff --git a/crates/rspack_plugin_html/src/visitors/utils.rs b/crates/rspack_plugin_html/src/visitors/utils.rs index 93274d7ee6f..810fcfca8d7 100644 --- a/crates/rspack_plugin_html/src/visitors/utils.rs +++ b/crates/rspack_plugin_html/src/visitors/utils.rs @@ -11,7 +11,7 @@ pub fn create_attribute(name: &str, value: &Option) -> Attribute { name: name.into(), raw_name: None, value: value.as_ref().map(|str| Atom::from(str.as_str())), - raw_value: None, + raw_value: value.as_ref().map(|str| Atom::from(str.as_str())), } } @@ -33,3 +33,16 @@ pub fn create_element(tag: &HTMLPluginTag) -> Element { span: DUMMY_SP, } } + +pub fn append_hash(url: &str, hash: &str) -> String { + format!( + "{}{}{}", + url, + if url.contains("?") { + "$$RSPACK_URL_AMP$$" + } else { + "?" + }, + hash + ) +} diff --git a/packages/rspack/etc/api.md b/packages/rspack/etc/api.md index a6895a2cdbc..463a9f827f5 100644 --- a/packages/rspack/etc/api.md +++ b/packages/rspack/etc/api.md @@ -4587,13 +4587,14 @@ export const HtmlRspackPlugin: { new (c?: { filename?: string | undefined; publicPath?: string | undefined; + hash?: boolean | undefined; chunks?: string[] | undefined; template?: string | undefined; templateContent?: string | undefined; templateParameters?: Record | undefined; inject?: boolean | "head" | "body" | undefined; scriptLoading?: "module" | "blocking" | "defer" | undefined; - excludedChunks?: string[] | undefined; + excludeChunks?: string[] | undefined; sri?: "sha256" | "sha384" | "sha512" | undefined; minify?: boolean | undefined; title?: string | undefined; @@ -4604,13 +4605,14 @@ export const HtmlRspackPlugin: { _args: [c?: { filename?: string | undefined; publicPath?: string | undefined; + hash?: boolean | undefined; chunks?: string[] | undefined; template?: string | undefined; templateContent?: string | undefined; templateParameters?: Record | undefined; inject?: boolean | "head" | "body" | undefined; scriptLoading?: "module" | "blocking" | "defer" | undefined; - excludedChunks?: string[] | undefined; + excludeChunks?: string[] | undefined; sri?: "sha256" | "sha384" | "sha512" | undefined; minify?: boolean | undefined; title?: string | undefined; @@ -4636,22 +4638,24 @@ const htmlRspackPluginOptions: z.ZodObject<{ publicPath: z.ZodOptional; scriptLoading: z.ZodOptional>; chunks: z.ZodOptional>; - excludedChunks: z.ZodOptional>; + excludeChunks: z.ZodOptional>; sri: z.ZodOptional>; minify: z.ZodOptional; title: z.ZodOptional; favicon: z.ZodOptional; meta: z.ZodOptional]>>>; + hash: z.ZodOptional; }, "strict", z.ZodTypeAny, { filename?: string | undefined; publicPath?: string | undefined; + hash?: boolean | undefined; chunks?: string[] | undefined; template?: string | undefined; templateContent?: string | undefined; templateParameters?: Record | undefined; inject?: boolean | "head" | "body" | undefined; scriptLoading?: "module" | "blocking" | "defer" | undefined; - excludedChunks?: string[] | undefined; + excludeChunks?: string[] | undefined; sri?: "sha256" | "sha384" | "sha512" | undefined; minify?: boolean | undefined; title?: string | undefined; @@ -4660,13 +4664,14 @@ const htmlRspackPluginOptions: z.ZodObject<{ }, { filename?: string | undefined; publicPath?: string | undefined; + hash?: boolean | undefined; chunks?: string[] | undefined; template?: string | undefined; templateContent?: string | undefined; templateParameters?: Record | undefined; inject?: boolean | "head" | "body" | undefined; scriptLoading?: "module" | "blocking" | "defer" | undefined; - excludedChunks?: string[] | undefined; + excludeChunks?: string[] | undefined; sri?: "sha256" | "sha384" | "sha512" | undefined; minify?: boolean | undefined; title?: string | undefined; diff --git a/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts b/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts index f0b478208fc..f47f748aa84 100644 --- a/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts +++ b/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts @@ -16,12 +16,13 @@ const htmlRspackPluginOptions = z.strictObject({ publicPath: z.string().optional(), scriptLoading: z.enum(["blocking", "defer", "module"]).optional(), chunks: z.string().array().optional(), - excludedChunks: z.string().array().optional(), + excludeChunks: z.string().array().optional(), sri: z.enum(["sha256", "sha384", "sha512"]).optional(), minify: z.boolean().optional(), title: z.string().optional(), favicon: z.string().optional(), - meta: z.record(z.string().or(z.record(z.string()))).optional() + meta: z.record(z.string().or(z.record(z.string()))).optional(), + hash: z.boolean().optional() }); export type HtmlRspackPluginOptions = z.infer; export const HtmlRspackPlugin = create( diff --git a/tests/plugin-test/html-plugin/basic.test.js b/tests/plugin-test/html-plugin/basic.test.js index df350d369a3..a78523ed26f 100644 --- a/tests/plugin-test/html-plugin/basic.test.js +++ b/tests/plugin-test/html-plugin/basic.test.js @@ -139,49 +139,47 @@ describe("HtmlWebpackPlugin", () => { ); }); - // TODO: url encodes the file name - // it("properly encodes file names in emitted URIs", (done) => { - // testHtmlPlugin( - // { - // mode: "production", - // entry: path.join(__dirname, "fixtures/index.js"), - // output: { - // path: OUTPUT_DIR, - // filename: "foo/very fancy+name.js", - // }, - // plugins: [new HtmlWebpackPlugin()], - // }, - // [ - // /