Skip to content

Commit

Permalink
feat(html): improve error handling (#7600)
Browse files Browse the repository at this point in the history
  • Loading branch information
LingyuCoder authored Aug 16, 2024
1 parent 4505da7 commit a3897c7
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 116 deletions.
120 changes: 87 additions & 33 deletions crates/rspack_plugin_html/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +31,10 @@ use crate::{
},
};

static MATCH_DOJANG_FRAGMENT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"<%[-=]?\s*([\w.]+)\s*%>"#).expect("Failed to initialize `MATCH_DOJANG_FRAGMENT`")
});

#[plugin]
#[derive(Debug)]
pub struct HtmlRspackPlugin {
Expand All @@ -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 {
(
Expand All @@ -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(),
Expand All @@ -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!("<!DOCTYPE html>{template_result}");
Expand Down Expand Up @@ -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::<Path>::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#"
<pre>
Error: {msg}
</pre>
"#
))
.join("\n")
)
} else {
parser
.codegen(&mut current_ast, compilation)?
.replace("$$RSPACK_URL_AMP$$", "&")
};

if !has_doctype {
source = source.replace("<!DOCTYPE html>", "");
Expand All @@ -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::<Path>::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(())
}

Expand Down
166 changes: 83 additions & 83 deletions tests/plugin-test/html-plugin/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(
Expand All @@ -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) => {
Expand Down

1 comment on commit a3897c7

@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 ❌ failure
_selftest βœ… success
nx ❌ failure
rspress βœ… success
rslib βœ… success
rsbuild ❌ failure
examples βœ… success

Please sign in to comment.