Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: improve HtmlRspackPlugin #7577

Merged
merged 3 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading