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) => {