Skip to content

Commit

Permalink
feat!: improve HtmlRspackPlugin (#7577)
Browse files Browse the repository at this point in the history
  • Loading branch information
LingyuCoder authored Aug 15, 2024
1 parent 827758e commit 250304a
Show file tree
Hide file tree
Showing 39 changed files with 868 additions and 822 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,11 @@ export interface RawGeneratorOptions {
cssModule?: RawCssModuleGeneratorOptions
}

export interface RawHtmlRspackPluginBaseOptions {
href?: string
target?: "_self" | "_blank" | "_parent" | "_top"
}

export interface RawHtmlRspackPluginOptions {
/** emitted file name in output path */
filename?: string
Expand All @@ -1240,16 +1245,18 @@ export interface RawHtmlRspackPluginOptions {
inject: "head" | "body" | "false"
/** path or `auto` */
publicPath?: string
/** `blocking`, `defer`, or `module` */
scriptLoading: "blocking" | "defer" | "module"
/** `blocking`, `defer`, `module` or `systemjs-module` */
scriptLoading: "blocking" | "defer" | "module" | "systemjs-module"
/** entry_chunk_name (only entry chunks are supported) */
chunks?: Array<string>
excludedChunks?: Array<string>
excludeChunks?: Array<string>
sri?: "sha256" | "sha384" | "sha512"
minify?: boolean
title?: string
favicon?: string
meta?: Record<string, Record<string, string>>
hash?: boolean
base?: RawHtmlRspackPluginBaseOptions
}

export interface RawHttpExternalsRspackPluginOptions {
Expand Down
32 changes: 27 additions & 5 deletions crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::str::FromStr;

use napi_derive::napi;
use rspack_plugin_html::config::HtmlInject;
use rspack_plugin_html::config::HtmlRspackPluginBaseOptions;
use rspack_plugin_html::config::HtmlRspackPluginOptions;
use rspack_plugin_html::config::HtmlScriptLoading;
use rspack_plugin_html::sri::HtmlSriHashFunction;
Expand All @@ -27,19 +28,21 @@ pub struct RawHtmlRspackPluginOptions {
pub inject: RawHtmlInject,
/// path or `auto`
pub public_path: Option<String>,
/// `blocking`, `defer`, or `module`
#[napi(ts_type = "\"blocking\" | \"defer\" | \"module\"")]
/// `blocking`, `defer`, `module` or `systemjs-module`
#[napi(ts_type = "\"blocking\" | \"defer\" | \"module\" | \"systemjs-module\"")]
pub script_loading: RawHtmlScriptLoading,

/// entry_chunk_name (only entry chunks are supported)
pub chunks: Option<Vec<String>>,
pub excluded_chunks: Option<Vec<String>>,
pub exclude_chunks: Option<Vec<String>>,
#[napi(ts_type = "\"sha256\" | \"sha384\" | \"sha512\"")]
pub sri: Option<RawHtmlSriHashFunction>,
pub minify: Option<bool>,
pub title: Option<String>,
pub favicon: Option<String>,
pub meta: Option<HashMap<String, HashMap<String, String>>>,
pub hash: Option<bool>,
pub base: Option<RawHtmlRspackPluginBaseOptions>,
}

impl From<RawHtmlRspackPluginOptions> for HtmlRspackPluginOptions {
Expand All @@ -62,12 +65,31 @@ impl From<RawHtmlRspackPluginOptions> 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(),
minify: value.minify,
title: value.title,
favicon: value.favicon,
meta: value.meta,
hash: value.hash,
base: value.base.map(|v| v.into()),
}
}
}

#[derive(Debug)]
#[napi(object)]
pub struct RawHtmlRspackPluginBaseOptions {
pub href: Option<String>,
#[napi(ts_type = "\"_self\" | \"_blank\" | \"_parent\" | \"_top\"")]
pub target: Option<String>,
}

impl From<RawHtmlRspackPluginBaseOptions> for HtmlRspackPluginBaseOptions {
fn from(value: RawHtmlRspackPluginBaseOptions) -> Self {
HtmlRspackPluginBaseOptions {
href: value.href,
target: value.target,
}
}
}
1 change: 1 addition & 0 deletions crates/rspack_plugin_html/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
23 changes: 19 additions & 4 deletions crates/rspack_plugin_html/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub enum HtmlScriptLoading {
Blocking,
Defer,
Module,
SystemjsModule,
}

impl FromStr for HtmlScriptLoading {
Expand All @@ -54,6 +55,8 @@ impl FromStr for HtmlScriptLoading {
Ok(HtmlScriptLoading::Defer)
} else if s.eq("module") {
Ok(HtmlScriptLoading::Module)
} else if s.eq("systemjs-module") {
Ok(HtmlScriptLoading::SystemjsModule)
} else {
Err(anyhow::Error::msg(
"scriptLoading in html config only support 'blocking', 'defer' or 'module'",
Expand All @@ -62,6 +65,14 @@ impl FromStr for HtmlScriptLoading {
}
}

#[cfg_attr(feature = "testing", derive(JsonSchema))]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HtmlRspackPluginBaseOptions {
pub href: Option<String>,
pub target: Option<String>,
}

#[cfg_attr(feature = "testing", derive(JsonSchema))]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand All @@ -84,16 +95,18 @@ pub struct HtmlRspackPluginOptions {

/// entry_chunk_name (only entry chunks are supported)
pub chunks: Option<Vec<String>>,
pub excluded_chunks: Option<Vec<String>>,
pub exclude_chunks: Option<Vec<String>>,

/// hash func that used in subsource integrity
/// sha384, sha256 or sha512
pub sri: Option<HtmlSriHashFunction>,
#[serde(default)]
pub minify: bool,
pub minify: Option<bool>,
pub title: Option<String>,
pub favicon: Option<String>,
pub meta: Option<HashMap<String, HashMap<String, String>>>,
pub hash: Option<bool>,
pub base: Option<HtmlRspackPluginBaseOptions>,
}

fn default_filename() -> String {
Expand All @@ -119,12 +132,14 @@ impl Default for HtmlRspackPluginOptions {
public_path: None,
script_loading: default_script_loading(),
chunks: None,
excluded_chunks: None,
exclude_chunks: None,
sri: None,
minify: false,
minify: None,
title: None,
favicon: None,
meta: None,
hash: None,
base: None,
}
}
}
Expand Down
16 changes: 11 additions & 5 deletions crates/rspack_plugin_html/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use rspack_core::ErrorSpan;
use rspack_core::{Compilation, ErrorSpan};
use rspack_error::{error, DiagnosticKind, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray};
use swc_core::common::{sync::Lrc, FileName, FilePathMapping, SourceFile, SourceMap, GLOBALS};
use swc_html::{
Expand All @@ -27,7 +27,7 @@ impl<'a> HtmlCompiler<'a> {

pub fn parse_file(&self, path: &str, source: String) -> Result<TWithDiagnosticArray<Document>> {
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let fm = cm.new_source_file(Arc::new(FileName::Custom(path.to_string())), source);
let fm = cm.new_source_file(Arc::new(FileName::Custom(path.to_string())), source.clone());

let mut errors = vec![];
let document = parse_file_as_document(fm.as_ref(), ParserConfig::default(), &mut errors);
Expand All @@ -40,13 +40,19 @@ impl<'a> HtmlCompiler<'a> {
.map_err(|e| html_parse_error_to_traceable_error(e, &fm))
}

pub fn codegen(&self, ast: &mut Document) -> Result<String> {
pub fn codegen(&self, ast: &mut Document, compilation: &Compilation) -> Result<String> {
let writer_config = BasicHtmlWriterConfig::default();
let minify = self.config.minify.unwrap_or(matches!(
compilation.options.mode,
rspack_core::Mode::Production
));
let codegen_config = CodegenConfig {
minify: self.config.minify,
minify,
quotes: Some(true),
tag_omission: Some(false),
..Default::default()
};
if self.config.minify {
if minify {
// Minify can't leak to user land because it doesn't implement `ToNapiValue` Trait
GLOBALS.set(&Default::default(), || {
minify_document(ast, &MinifyOptions::default());
Expand Down
95 changes: 80 additions & 15 deletions crates/rspack_plugin_html/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, generate_posix_path},
},
};

#[plugin]
Expand Down Expand Up @@ -75,7 +79,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
};

// process with template parameters
let template_result = if let Some(template_parameters) = &self.config.template_parameters {
let mut template_result = if let Some(template_parameters) = &self.config.template_parameters {
let mut dj = Dojang::new();
dj.add(url.clone(), content)
.expect("failed to add template");
Expand All @@ -85,6 +89,11 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
content
};

let has_doctype = template_result.contains("!DOCTYPE") || template_result.contains("!doctype");
if !has_doctype {
template_result = format!("<!DOCTYPE html>{template_result}");
}

let ast_with_diagnostic = parser.parse_file(&url, template_result)?;

let (mut current_ast, diagnostic) = ast_with_diagnostic.split_into_parts();
Expand All @@ -100,8 +109,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
})
Expand All @@ -121,17 +130,28 @@ 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.unwrap_or_default() {
if let Some(hash) = compilation.get_hash() {
asset_uri = append_hash(&asset_uri, hash);
}
}
let mut tag: Option<HTMLPluginTag> = None;
if extension.eq_ignore_ascii_case("css") {
tag = Some(HTMLPluginTag::create_style(&asset_uri, HtmlInject::Head));
tag = Some(HTMLPluginTag::create_style(
&generate_posix_path(&asset_uri),
HtmlInject::Head,
));
} else if extension.eq_ignore_ascii_case("js") || extension.eq_ignore_ascii_case("mjs") {
tag = Some(HTMLPluginTag::create_script(
&asset_uri,
&generate_posix_path(&asset_uri),
config.inject,
&config.script_loading,
))
Expand All @@ -157,18 +177,39 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
}

let tags = tags.into_iter().map(|(tag, _)| tag).collect::<Vec<_>>();
let mut visitor = AssetWriter::new(config, &tags, compilation);
current_ast.visit_mut_with(&mut visitor);

let source = parser.codegen(&mut current_ast)?;
let hash = hash_for_source(&source);
let html_file_name = FilenameTemplate::from(config.filename.clone());
// Use the same filename as template
let output_path = compilation
.options
.output
.path
.join(normalized_template_name);

let html_file_name = FilenameTemplate::from(
config
.filename
.replace("[templatehash]", "[contenthash]")
.clone(),
);
// use to calculate relative favicon path when no publicPath
let fake_html_file_name = compilation
.get_path(
&html_file_name,
PathData::default().filename(&output_path.to_string_lossy()),
)
.always_ok();

let mut visitor = AssetWriter::new(config, &tags, compilation, &fake_html_file_name);
current_ast.visit_mut_with(&mut visitor);

let mut source = parser
.codegen(&mut current_ast, compilation)?
.replace("$$RSPACK_URL_AMP$$", "&");

if !has_doctype {
source = source.replace("<!DOCTYPE html>", "");
}
let hash = hash_for_source(&source);

let (output_path, asset_info) = compilation
.get_path_with_info(
&html_file_name,
Expand Down Expand Up @@ -243,3 +284,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]
} else {
file_path
};
let query_string = if let Some(query_string_start) = query_string_start {
&file_path[query_string_start..]
} else {
""
};

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$$")
)
}
Loading

2 comments on commit 250304a

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Benchmark detail: Open

Name Base (2024-08-15 5271a8b) Current Change
10000_development-mode + exec 2.31 s ± 16 ms 2.37 s ± 29 ms +2.43 %
10000_development-mode_hmr + exec 697 ms ± 6.7 ms 715 ms ± 20 ms +2.58 %
10000_production-mode + exec 2.85 s ± 37 ms 2.9 s ± 18 ms +1.69 %
arco-pro_development-mode + exec 1.88 s ± 77 ms 1.89 s ± 84 ms +0.96 %
arco-pro_development-mode_hmr + exec 433 ms ± 2.7 ms 434 ms ± 2.3 ms +0.28 %
arco-pro_production-mode + exec 3.43 s ± 72 ms 3.48 s ± 64 ms +1.57 %
arco-pro_production-mode_generate-package-json-webpack-plugin + exec 3.49 s ± 75 ms 3.5 s ± 65 ms +0.26 %
threejs_development-mode_10x + exec 1.68 s ± 15 ms 1.69 s ± 20 ms +0.77 %
threejs_development-mode_10x_hmr + exec 797 ms ± 1.8 ms 797 ms ± 10 ms -0.07 %
threejs_production-mode_10x + exec 5.48 s ± 28 ms 5.53 s ± 44 ms +1.02 %

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Ran ecosystem CI: Open

suite result
modernjs ✅ success
_selftest ✅ success
nx ❌ failure
rspress ✅ success
rsbuild ✅ success
examples ✅ success

Please sign in to comment.