diff --git a/crates/rspack_plugin_html/src/plugin.rs b/crates/rspack_plugin_html/src/plugin.rs
index ccd74de7412..f70512bdfe3 100644
--- a/crates/rspack_plugin_html/src/plugin.rs
+++ b/crates/rspack_plugin_html/src/plugin.rs
@@ -3,18 +3,20 @@ use std::{
fs,
hash::{Hash, Hasher},
path::{Path, PathBuf},
+ sync::LazyLock,
};
use anyhow::Context;
use dojang::dojang::Dojang;
use itertools::Itertools;
use rayon::prelude::*;
+use regex::Regex;
use rspack_core::{
parse_to_url,
rspack_sources::{RawSource, SourceExt},
Compilation, CompilationAsset, CompilationProcessAssets, FilenameTemplate, PathData, Plugin,
};
-use rspack_error::{AnyhowError, Result};
+use rspack_error::{miette, AnyhowError, Diagnostic, Result};
use rspack_hook::{plugin, plugin_hook};
use rspack_util::infallible::ResultInfallibleExt as _;
use swc_html::visit::VisitMutWith;
@@ -29,6 +31,10 @@ use crate::{
},
};
+static MATCH_DOJANG_FRAGMENT: LazyLock = LazyLock::new(|| {
+ Regex::new(r#"<%[-=]?\s*([\w.]+)\s*%>"#).expect("Failed to initialize `MATCH_DOJANG_FRAGMENT`")
+});
+
#[plugin]
#[derive(Debug)]
pub struct HtmlRspackPlugin {
@@ -45,6 +51,8 @@ impl HtmlRspackPlugin {
async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
let config = &self.config;
+ let mut error_content = vec![];
+
let parser = HtmlCompiler::new(config);
let (content, url, normalized_template_name) = if let Some(content) = &config.template_content {
(
@@ -60,16 +68,28 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
let content = fs::read_to_string(&resolved_template)
.context(format!(
- "failed to read `{}` from `{}`",
- resolved_template.display(),
- &compilation.options.context
+ "HtmlRspackPlugin: could not load file `{}` from `{}`",
+ template, &compilation.options.context
))
- .map_err(AnyhowError::from)?;
+ .map_err(AnyhowError::from);
- let url = resolved_template.to_string_lossy().to_string();
- compilation.file_dependencies.insert(resolved_template);
+ match content {
+ Ok(content) => {
+ let url = resolved_template.to_string_lossy().to_string();
+ compilation.file_dependencies.insert(resolved_template);
- (content, url, template.clone())
+ (content, url, template.clone())
+ }
+ Err(err) => {
+ error_content.push(err.to_string());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::from(err)));
+ (
+ default_template().to_owned(),
+ parse_to_url("default.html").path().to_string(),
+ template.clone(),
+ )
+ }
+ }
} else {
(
default_template().to_owned(),
@@ -89,6 +109,15 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
content
};
+ // dojang will not throw error when replace failed https://github.com/kev0960/dojang/issues/2
+ if let Some(captures) = MATCH_DOJANG_FRAGMENT.captures(&template_result) {
+ if let Some(name) = captures.get(1).map(|m| m.as_str()) {
+ let error_msg = format!("ReferenceError: {name} is not defined");
+ error_content.push(error_msg.clone());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(error_msg)));
+ }
+ }
+
let has_doctype = template_result.contains("!DOCTYPE") || template_result.contains("!doctype");
if !has_doctype {
template_result = format!("{template_result}");
@@ -201,9 +230,56 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
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 let Some(favicon) = &self.config.favicon {
+ let url = parse_to_url(favicon);
+ let favicon_file_path = PathBuf::from(config.get_relative_path(compilation, favicon))
+ .file_name()
+ .expect("Should have favicon file name")
+ .to_string_lossy()
+ .to_string();
+
+ let resolved_favicon = AsRef::::as_ref(&compilation.options.context).join(url.path());
+
+ let content = fs::read(resolved_favicon)
+ .context(format!(
+ "HtmlRspackPlugin: could not load file `{}` from `{}`",
+ favicon, &compilation.options.context
+ ))
+ .map_err(AnyhowError::from);
+
+ match content {
+ Ok(content) => {
+ compilation.emit_asset(
+ favicon_file_path,
+ CompilationAsset::from(RawSource::from(content).boxed()),
+ );
+ }
+ Err(err) => {
+ error_content.push(err.to_string());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::from(err)));
+ }
+ };
+ }
+
+ let mut source = if !error_content.is_empty() {
+ format!(
+ r#"Html Rspack Plugin:\n{}"#,
+ error_content
+ .iter()
+ .map(|msg| format!(
+ r#"
+
+ Error: {msg}
+
+ "#
+ ))
+ .join("\n")
+ )
+ } else {
+ parser
+ .codegen(&mut current_ast, compilation)?
+ .replace("$$RSPACK_URL_AMP$$", "&")
+ };
if !has_doctype {
source = source.replace("", "");
@@ -223,28 +299,6 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
CompilationAsset::new(Some(RawSource::from(source).boxed()), asset_info),
);
- if let Some(favicon) = &self.config.favicon {
- let url = parse_to_url(favicon);
- let favicon_file_path = PathBuf::from(config.get_relative_path(compilation, favicon))
- .file_name()
- .expect("Should have favicon file name")
- .to_string_lossy()
- .to_string();
-
- let resolved_favicon = AsRef::::as_ref(&compilation.options.context).join(url.path());
- let content = fs::read(resolved_favicon)
- .context(format!(
- "failed to read `{}` from `{}`",
- url.path(),
- &compilation.options.context
- ))
- .map_err(AnyhowError::from)?;
- compilation.emit_asset(
- favicon_file_path,
- CompilationAsset::from(RawSource::from(content).boxed()),
- );
- }
-
Ok(())
}
diff --git a/tests/plugin-test/html-plugin/basic.test.js b/tests/plugin-test/html-plugin/basic.test.js
index 3c39ccc2f1a..52277bf76fe 100644
--- a/tests/plugin-test/html-plugin/basic.test.js
+++ b/tests/plugin-test/html-plugin/basic.test.js
@@ -230,34 +230,34 @@ describe("HtmlWebpackPlugin", () => {
// );
// });
- // TODO: optimization.emitOnErrors
- // it("should pass through loader errors", (done) => {
- // testHtmlPlugin(
- // {
- // mode: "production",
- // optimization: {
- // emitOnErrors: true,
- // },
- // entry: {
- // app: path.join(__dirname, "fixtures/index.js"),
- // },
- // output: {
- // path: OUTPUT_DIR,
- // filename: "[name]_bundle.js",
- // },
- // plugins: [
- // new HtmlWebpackPlugin({
- // inject: false,
- // template: path.join(__dirname, "fixtures/invalid.html"),
- // }),
- // ],
- // },
- // ["ReferenceError: foo is not defined"],
- // null,
- // done,
- // true,
- // );
- // });
+ it("should pass through loader errors", (done) => {
+ testHtmlPlugin(
+ {
+ mode: "production",
+ optimization: {
+ emitOnErrors: true,
+ },
+ entry: {
+ app: path.join(__dirname, "fixtures/index.js"),
+ },
+ output: {
+ path: OUTPUT_DIR,
+ filename: "[name]_bundle.js",
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ inject: false,
+ template: path.join(__dirname, "fixtures/invalid.html"),
+ }),
+ ],
+ },
+ // DIFF: ["ReferenceError: foo is not defined"],
+ ["ReferenceError: foo.bar is not defined"],
+ null,
+ done,
+ true,
+ );
+ });
// TODO: template with loaders
// it("uses a custom loader from webpack config", (done) => {
@@ -2859,32 +2859,31 @@ describe("HtmlWebpackPlugin", () => {
// );
// });
- // TODO: support optimization.emitOnErrors
- // it("shows an error if the favicon could not be load", (done) => {
- // testHtmlPlugin(
- // {
- // mode: "production",
- // entry: path.join(__dirname, "fixtures/index.js"),
- // output: {
- // path: OUTPUT_DIR,
- // filename: "index_bundle.js",
- // },
- // optimization: {
- // emitOnErrors: true,
- // },
- // plugins: [
- // new HtmlWebpackPlugin({
- // inject: true,
- // favicon: path.join(__dirname, "fixtures/does_not_exist.ico"),
- // }),
- // ],
- // },
- // ["Error: HtmlWebpackPlugin: could not load file"],
- // null,
- // done,
- // true,
- // );
- // });
+ it("shows an error if the favicon could not be load", (done) => {
+ testHtmlPlugin(
+ {
+ mode: "production",
+ entry: path.join(__dirname, "fixtures/index.js"),
+ output: {
+ path: OUTPUT_DIR,
+ filename: "index_bundle.js",
+ },
+ optimization: {
+ emitOnErrors: true,
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ inject: true,
+ favicon: path.join(__dirname, "fixtures/does_not_exist.ico"),
+ }),
+ ],
+ },
+ ["Error: HtmlRspackPlugin: could not load file"],
+ null,
+ done,
+ true,
+ );
+ });
it("works with webpack BannerPlugin", (done) => {
testHtmlPlugin(
@@ -2906,35 +2905,36 @@ describe("HtmlWebpackPlugin", () => {
);
});
- // TODO: compilation error: Module not found
- // it("shows an error when a template fails to load", (done) => {
- // testHtmlPlugin(
- // {
- // mode: "development",
- // entry: path.join(__dirname, "fixtures/index.js"),
- // output: {
- // path: OUTPUT_DIR,
- // filename: "index_bundle.js",
- // },
- // plugins: [
- // new HtmlWebpackPlugin({
- // template: path.join(
- // __dirname,
- // "fixtures/non-existing-template.html",
- // ),
- // }),
- // ],
- // },
- // [
- // Number(webpackMajorVersion) >= 5
- // ? "Child compilation failed:\n Module not found:"
- // : "Child compilation failed:\n Entry module not found:",
- // ],
- // null,
- // done,
- // true,
- // );
- // });
+ it("shows an error when a template fails to load", (done) => {
+ testHtmlPlugin(
+ {
+ mode: "development",
+ entry: path.join(__dirname, "fixtures/index.js"),
+ output: {
+ path: OUTPUT_DIR,
+ filename: "index_bundle.js",
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: path.join(
+ __dirname,
+ "fixtures/non-existing-template.html",
+ ),
+ }),
+ ],
+ },
+ [
+ // DIFF:
+ // Number(webpackMajorVersion) >= 5
+ // ? "Child compilation failed:\n Module not found:"
+ // : "Child compilation failed:\n Entry module not found:",
+ "Error: HtmlRspackPlugin: could not load file",
+ ],
+ null,
+ done,
+ true,
+ );
+ });
// TODO: support `chunksSortMode`
// it("should sort the chunks in auto mode", (done) => {