diff --git a/Cargo.lock b/Cargo.lock
index 2edc6dfca0c..3a1bf6bb355 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3654,6 +3654,7 @@ name = "rspack_plugin_html"
version = "0.1.0"
dependencies = [
"anyhow",
+ "futures",
"itertools 0.13.0",
"path-clean 1.0.1",
"rayon",
diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts
index 002c566499d..fe53dffb43c 100644
--- a/crates/node_binding/binding.d.ts
+++ b/crates/node_binding/binding.d.ts
@@ -1239,8 +1239,9 @@ export interface RawHtmlRspackPluginOptions {
filename?: string
/** template html file */
template?: string
+ templateFn?: (data: string) => Promise
templateContent?: string
- templateParameters?: Record
+ templateParameters?: boolean | Record | ((params: string) => Promise)
/** "head", "body" or "false" */
inject: "head" | "body" | "false"
/** path or `auto` */
diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs
index 96453596112..9c9ffd68082 100644
--- a/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs
+++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_html.rs
@@ -1,11 +1,16 @@
use std::collections::HashMap;
use std::str::FromStr;
+use napi::bindgen_prelude::Either3;
use napi_derive::napi;
+use rspack_napi::threadsafe_function::ThreadsafeFunction;
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::config::TemplateParameterFn;
+use rspack_plugin_html::config::TemplateParameters;
+use rspack_plugin_html::config::TemplateRenderFn;
use rspack_plugin_html::sri::HtmlSriHashFunction;
pub type RawHtmlScriptLoading = String;
@@ -13,16 +18,24 @@ pub type RawHtmlInject = String;
pub type RawHtmlSriHashFunction = String;
pub type RawHtmlFilename = String;
+type RawTemplateRenderFn = ThreadsafeFunction;
+
+type RawTemplateParameter =
+ Either3, bool, ThreadsafeFunction>;
+
#[derive(Debug)]
-#[napi(object)]
+#[napi(object, object_to_js = false)]
pub struct RawHtmlRspackPluginOptions {
/// emitted file name in output path
#[napi(ts_type = "string")]
pub filename: Option,
/// template html file
pub template: Option,
+ #[napi(ts_type = "(data: string) => Promise")]
+ pub template_fn: Option,
pub template_content: Option,
- pub template_parameters: Option>,
+ #[napi(ts_type = "boolean | Record | ((params: string) => Promise)")]
+ pub template_parameters: Option,
/// "head", "body" or "false"
#[napi(ts_type = "\"head\" | \"body\" | \"false\"")]
pub inject: RawHtmlInject,
@@ -59,8 +72,32 @@ impl From for HtmlRspackPluginOptions {
HtmlRspackPluginOptions {
filename: value.filename.unwrap_or_else(|| String::from("index.html")),
template: value.template,
+ template_fn: value.template_fn.map(|func| TemplateRenderFn {
+ inner: Box::new(move |data| {
+ let f = func.clone();
+ Box::pin(async move { f.call(data).await })
+ }),
+ }),
template_content: value.template_content,
- template_parameters: value.template_parameters,
+ template_parameters: match value.template_parameters {
+ Some(parameters) => match parameters {
+ Either3::A(data) => TemplateParameters::Map(data),
+ Either3::B(enabled) => {
+ if enabled {
+ TemplateParameters::Map(Default::default())
+ } else {
+ TemplateParameters::Disabled
+ }
+ }
+ Either3::C(func) => TemplateParameters::Function(TemplateParameterFn {
+ inner: Box::new(move |data| {
+ let f = func.clone();
+ Box::pin(async move { f.call(data).await })
+ }),
+ }),
+ },
+ None => TemplateParameters::Map(Default::default()),
+ },
inject,
public_path: value.public_path,
script_loading,
diff --git a/crates/rspack_plugin_html/Cargo.toml b/crates/rspack_plugin_html/Cargo.toml
index f6db970ae0b..de37fff47ab 100644
--- a/crates/rspack_plugin_html/Cargo.toml
+++ b/crates/rspack_plugin_html/Cargo.toml
@@ -10,6 +10,7 @@ default = []
[dependencies]
anyhow = { workspace = true }
+futures = { workspace = true }
itertools = { workspace = true }
path-clean = { workspace = true }
rayon = { workspace = true }
diff --git a/crates/rspack_plugin_html/src/config.rs b/crates/rspack_plugin_html/src/config.rs
index 4bebd50c231..daecf783c25 100644
--- a/crates/rspack_plugin_html/src/config.rs
+++ b/crates/rspack_plugin_html/src/config.rs
@@ -1,6 +1,8 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr};
+use futures::future::BoxFuture;
use rspack_core::{Compilation, PublicPath};
+use rspack_error::Result;
use serde::Serialize;
use sugar_path::SugarPath;
@@ -62,6 +64,26 @@ impl FromStr for HtmlScriptLoading {
}
}
+type TemplateParameterTsfn =
+ Box Fn(String) -> BoxFuture<'static, Result> + Sync + Send>;
+
+pub struct TemplateParameterFn {
+ pub inner: TemplateParameterTsfn,
+}
+
+impl std::fmt::Debug for TemplateParameterFn {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TemplateParameterFn").finish()
+ }
+}
+
+#[derive(Debug)]
+pub enum TemplateParameters {
+ Map(HashMap),
+ Function(TemplateParameterFn),
+ Disabled,
+}
+
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HtmlRspackPluginBaseOptions {
@@ -69,6 +91,19 @@ pub struct HtmlRspackPluginBaseOptions {
pub target: Option,
}
+type TemplateRenderTsfn =
+ Box Fn(String) -> BoxFuture<'static, Result> + Sync + Send>;
+
+pub struct TemplateRenderFn {
+ pub inner: TemplateRenderTsfn,
+}
+
+impl std::fmt::Debug for TemplateRenderFn {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TemplateRenderFn").finish()
+ }
+}
+
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HtmlRspackPluginOptions {
@@ -77,8 +112,11 @@ pub struct HtmlRspackPluginOptions {
pub filename: String,
/// template html file
pub template: Option,
+ #[serde(skip)]
+ pub template_fn: Option,
pub template_content: Option,
- pub template_parameters: Option>,
+ #[serde(skip)]
+ pub template_parameters: TemplateParameters,
/// `head`, `body`, `false`
#[serde(default = "default_inject")]
pub inject: HtmlInject,
@@ -121,8 +159,9 @@ impl Default for HtmlRspackPluginOptions {
HtmlRspackPluginOptions {
filename: default_filename(),
template: None,
+ template_fn: None,
template_content: None,
- template_parameters: None,
+ template_parameters: TemplateParameters::Map(Default::default()),
inject: default_inject(),
public_path: None,
script_loading: default_script_loading(),
diff --git a/crates/rspack_plugin_html/src/plugin.rs b/crates/rspack_plugin_html/src/plugin.rs
index ab8097b34dd..76a8397e886 100644
--- a/crates/rspack_plugin_html/src/plugin.rs
+++ b/crates/rspack_plugin_html/src/plugin.rs
@@ -25,7 +25,7 @@ use sugar_path::SugarPath;
use swc_html::visit::VisitMutWith;
use crate::{
- config::{HtmlInject, HtmlRspackPluginOptions, HtmlScriptLoading},
+ config::{HtmlInject, HtmlRspackPluginOptions, HtmlScriptLoading, TemplateParameters},
parser::HtmlCompiler,
sri::{add_sri, create_digest_from_asset},
visitors::{
@@ -35,6 +35,11 @@ use crate::{
},
};
+pub enum Renderer {
+ Template(String),
+ Function,
+}
+
#[plugin]
#[derive(Debug)]
pub struct HtmlRspackPlugin {
@@ -54,9 +59,14 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
let mut error_content = vec![];
let parser = HtmlCompiler::new(config);
+
let (content, url, normalized_template_name) = if let Some(content) = &config.template_content {
(
- content.clone(),
+ if config.template_fn.is_some() {
+ Renderer::Function
+ } else {
+ Renderer::Template(content.clone())
+ },
parse_to_url("template_content.html").path().to_string(),
"template_content.html".to_string(),
)
@@ -70,31 +80,35 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
.join(template.as_str()),
)
.assert_utf8();
+ let url = resolved_template.as_str().to_string();
- let content = fs::read_to_string(&resolved_template)
- .context(format!(
- "HtmlRspackPlugin: could not load file `{}` from `{}`",
- template, &compilation.options.context
- ))
- .map_err(AnyhowError::from);
+ if config.template_fn.is_some() {
+ (Renderer::Function, url, template.clone())
+ } else {
+ let content = fs::read_to_string(&resolved_template)
+ .context(format!(
+ "HtmlRspackPlugin: could not load file `{}` from `{}`",
+ template, &compilation.options.context
+ ))
+ .map_err(AnyhowError::from);
- match content {
- Ok(content) => {
- let url = resolved_template.as_str().to_string();
- compilation
- .file_dependencies
- .insert(resolved_template.into_std_path_buf());
+ match content {
+ Ok(content) => {
+ compilation
+ .file_dependencies
+ .insert(resolved_template.into_std_path_buf());
- (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(),
- )
+ (Renderer::Template(content), url, template.clone())
+ }
+ Err(err) => {
+ error_content.push(err.to_string());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::from(err)));
+ (
+ Renderer::Template(default_template().to_owned()),
+ parse_to_url("default.html").path().to_string(),
+ template.clone(),
+ )
+ }
}
}
} else {
@@ -107,10 +121,14 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
.file_dependencies
.insert(default_src_template.into_std_path_buf());
- (content, url, "src/index.ejs".to_string())
+ (
+ Renderer::Template(content),
+ url,
+ "src/index.ejs".to_string(),
+ )
} else {
(
- default_template().to_owned(),
+ Renderer::Template(default_template().to_owned()),
parse_to_url("default.html").path().to_string(),
"default.html".to_string(),
)
@@ -288,81 +306,143 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
.collect::>(),
);
- let mut render_data = serde_json::json!(&self.config.template_parameters);
-
- let mut body_tags = vec![];
- let mut head_tags = vec![];
- for tag in &tags {
- if tag.tag_name == "script" {
- if matches!(self.config.script_loading, HtmlScriptLoading::Blocking) {
- body_tags.push(tag);
+ let parameters = if matches!(
+ self.config.template_parameters,
+ TemplateParameters::Disabled
+ ) {
+ serde_json::json!({})
+ } else {
+ let mut res = serde_json::json!({});
+ let mut body_tags = vec![];
+ let mut head_tags = vec![];
+ for tag in &tags {
+ if tag.tag_name == "script" {
+ if matches!(self.config.script_loading, HtmlScriptLoading::Blocking) {
+ body_tags.push(tag);
+ } else {
+ head_tags.push(tag);
+ }
} else {
head_tags.push(tag);
}
- } else {
- head_tags.push(tag);
}
- }
-
- merge_json(
- &mut render_data,
- serde_json::json!({
- "htmlRspackPlugin": {
- "tags": {
- "headTags": head_tags,
- "bodyTags": body_tags,
- },
- "files": {
- "favicon": favicon,
- "js": assets.entry("js".into()).or_default(),
- "css": assets.entry("css".into()).or_default(),
- "publicPath": config.get_public_path(compilation, &self.config.filename),
- },
- "options": &self.config
- },
- }),
- );
- // only support "mode" and some fields of "output"
- merge_json(
- &mut render_data,
- serde_json::json!({
- "rspackConfig": {
- "mode": match compilation.options.mode {
- Mode::Development => "development",
- Mode::Production => "production",
- Mode::None => "none",
+ merge_json(
+ &mut res,
+ serde_json::json!({
+ "htmlRspackPlugin": {
+ "tags": {
+ "headTags": head_tags,
+ "bodyTags": body_tags,
+ },
+ "files": {
+ "favicon": favicon,
+ "js": assets.entry("js".into()).or_default(),
+ "css": assets.entry("css".into()).or_default(),
+ "publicPath": config.get_public_path(compilation, &self.config.filename),
+ },
+ "options": &self.config
},
- "output": {
- "publicPath": config.get_public_path(compilation, &self.config.filename),
+ }),
+ );
+
+ // only support "mode" and some fields of "output"
+ merge_json(
+ &mut res,
+ serde_json::json!({
+ "rspackConfig": {
+ "mode": match compilation.options.mode {
+ Mode::Development => "development",
+ Mode::Production => "production",
+ Mode::None => "none",
+ },
+ "output": {
+ "publicPath": config.get_public_path(compilation, &self.config.filename),
"crossOriginLoading": match &compilation.options.output.cross_origin_loading {
CrossOriginLoading::Disable => "false",
CrossOriginLoading::Enable(value) => value,
},
+ }
+ },
+ }),
+ );
+
+ match &self.config.template_parameters {
+ TemplateParameters::Map(data) => {
+ merge_json(&mut res, serde_json::json!(&data));
+ }
+ TemplateParameters::Function(func) => {
+ let func_res = (func.inner)(
+ serde_json::to_string(&res).unwrap_or_else(|_| panic!("invalid json to_string")),
+ )
+ .await;
+ match func_res {
+ Ok(new_data) => match serde_json::from_str(&new_data) {
+ Ok(data) => res = data,
+ Err(err) => {
+ error_content.push(format!(
+ "HtmlRspackPlugin: failed to parse template parameters: {err}",
+ ));
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(err)));
+ }
+ },
+ Err(err) => {
+ error_content.push(format!(
+ "HtmlRspackPlugin: failed to generate template parameters: {err}",
+ ));
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(err)));
+ }
}
- },
- }),
- );
+ }
+ TemplateParameters::Disabled => {}
+ };
+
+ res
+ };
+
+ let mut template_result = match content {
+ Renderer::Template(content) => {
+ // process with template parameters
+ let mut dj = Dojang::new();
+ // align escape | unescape with lodash.template syntax https://lodash.com/docs/4.17.15#template which is html-webpack-plugin's default behavior
+ dj.with_options(DojangOptions {
+ escape: "-".to_string(),
+ unescape: "=".to_string(),
+ });
+
+ dj.add_function_1("toHtml".into(), render_tag)
+ .expect("failed to add template function `renderTag`");
- // process with template parameters
- let mut dj = Dojang::new();
- // align escape | unescape with lodash.template syntax https://lodash.com/docs/4.17.15#template which is html-webpack-plugin's default behavior
- dj.with_options(DojangOptions {
- escape: "-".to_string(),
- unescape: "=".to_string(),
- });
-
- dj.add_function_1("toHtml".into(), render_tag)
- .expect("failed to add template function `renderTag`");
-
- dj.add_with_option(url.clone(), content.clone())
- .expect("failed to add template");
- let mut template_result = match dj.render(&url, render_data) {
- Ok(compiled) => compiled,
- Err(err) => {
- error_content.push(err.clone());
- compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(err)));
- String::default()
+ dj.add_with_option(url.clone(), content.clone())
+ .expect("failed to add template");
+
+ match dj.render(&url, parameters) {
+ Ok(compiled) => compiled,
+ Err(err) => {
+ error_content.push(err.clone());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(err)));
+ String::default()
+ }
+ }
+ }
+ Renderer::Function => {
+ let res = (config
+ .template_fn
+ .as_ref()
+ .unwrap_or_else(|| unreachable!())
+ .inner)(
+ serde_json::to_string(¶meters).unwrap_or_else(|_| panic!("invalid json to_string")),
+ )
+ .await;
+
+ match res {
+ Ok(compiled) => compiled,
+ Err(err) => {
+ error_content.push(err.to_string());
+ compilation.push_diagnostic(Diagnostic::from(miette::Error::msg(err)));
+ String::default()
+ }
+ }
}
};
diff --git a/packages/rspack/etc/api.md b/packages/rspack/etc/api.md
index 9c47e3792c4..85d8d5b946f 100644
--- a/packages/rspack/etc/api.md
+++ b/packages/rspack/etc/api.md
@@ -4610,8 +4610,8 @@ export const HtmlRspackPlugin: {
hash?: boolean | undefined;
chunks?: string[] | undefined;
template?: string | undefined;
- templateContent?: string | undefined;
- templateParameters?: Record | undefined;
+ templateContent?: string | ((args_0: Record, ...args_1: unknown[]) => string | Promise) | undefined;
+ templateParameters?: boolean | Record | ((args_0: Record, ...args_1: unknown[]) => Record | Promise>) | undefined;
inject?: boolean | "head" | "body" | undefined;
base?: string | {
target?: "_self" | "_blank" | "_parent" | "_top" | undefined;
@@ -4632,8 +4632,8 @@ export const HtmlRspackPlugin: {
hash?: boolean | undefined;
chunks?: string[] | undefined;
template?: string | undefined;
- templateContent?: string | undefined;
- templateParameters?: Record | undefined;
+ templateContent?: string | ((args_0: Record, ...args_1: unknown[]) => string | Promise) | undefined;
+ templateParameters?: boolean | Record | ((args_0: Record, ...args_1: unknown[]) => Record | Promise>) | undefined;
inject?: boolean | "head" | "body" | undefined;
base?: string | {
target?: "_self" | "_blank" | "_parent" | "_top" | undefined;
@@ -4648,8 +4648,8 @@ export const HtmlRspackPlugin: {
meta?: Record> | undefined;
} | undefined];
affectedHooks: "done" | "make" | "compile" | "emit" | "afterEmit" | "invalid" | "thisCompilation" | "afterDone" | "compilation" | "normalModuleFactory" | "contextModuleFactory" | "initialize" | "shouldEmit" | "infrastructureLog" | "beforeRun" | "run" | "assetEmitted" | "failed" | "shutdown" | "watchRun" | "watchClose" | "environment" | "afterEnvironment" | "afterPlugins" | "afterResolvers" | "beforeCompile" | "afterCompile" | "finishMake" | "entryOption" | undefined;
- raw(compiler: Compiler_2): BuiltinPlugin;
- apply(compiler: Compiler_2): void;
+ raw(compiler: Compiler): BuiltinPlugin;
+ apply(compiler: Compiler): void;
};
};
@@ -4659,9 +4659,9 @@ export type HtmlRspackPluginOptions = z.infer;
// @public (undocumented)
const htmlRspackPluginOptions: z.ZodObject<{
filename: z.ZodOptional;
- template: z.ZodOptional;
- templateContent: z.ZodOptional;
- templateParameters: z.ZodOptional>;
+ template: z.ZodOptional>;
+ templateContent: z.ZodOptional], z.ZodUnknown>, z.ZodUnion<[z.ZodString, z.ZodPromise]>>]>>;
+ templateParameters: z.ZodOptional, z.ZodBoolean]>, z.ZodFunction], z.ZodUnknown>, z.ZodUnion<[z.ZodRecord, z.ZodPromise>]>>]>>;
inject: z.ZodOptional, z.ZodBoolean]>>;
publicPath: z.ZodOptional;
base: z.ZodOptional | undefined;
+ templateContent?: string | ((args_0: Record, ...args_1: unknown[]) => string | Promise) | undefined;
+ templateParameters?: boolean | Record | ((args_0: Record, ...args_1: unknown[]) => Record | Promise>) | undefined;
inject?: boolean | "head" | "body" | undefined;
base?: string | {
target?: "_self" | "_blank" | "_parent" | "_top" | undefined;
@@ -4709,8 +4709,8 @@ const htmlRspackPluginOptions: z.ZodObject<{
hash?: boolean | undefined;
chunks?: string[] | undefined;
template?: string | undefined;
- templateContent?: string | undefined;
- templateParameters?: Record | undefined;
+ templateContent?: string | ((args_0: Record, ...args_1: unknown[]) => string | Promise) | undefined;
+ templateParameters?: boolean | Record | ((args_0: Record, ...args_1: unknown[]) => Record | Promise>) | undefined;
inject?: boolean | "head" | "body" | undefined;
base?: string | {
target?: "_self" | "_blank" | "_parent" | "_top" | undefined;
diff --git a/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts b/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts
index c0d253ccb75..0d028a77b0c 100644
--- a/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts
+++ b/packages/rspack/src/builtin-plugin/HtmlRspackPlugin.ts
@@ -1,17 +1,54 @@
+import fs from "node:fs";
+import path from "node:path";
import {
BuiltinPluginName,
type RawHtmlRspackPluginOptions
} from "@rspack/binding";
import { z } from "zod";
+import type { Compilation } from "../Compilation";
+import type { Compiler } from "../Compiler";
import { validate } from "../util/validate";
import { create } from "./base";
+type HtmlPluginTag = {
+ tagName: string;
+ attributes: Record;
+ voidTag: boolean;
+ innerHTML?: string;
+ toString?: () => string;
+};
+
+const templateRenderFunction = z
+ .function()
+ .args(z.record(z.string(), z.any()))
+ .returns(z.string().or(z.promise(z.string())));
+
+const templateParamFunction = z
+ .function()
+ .args(z.record(z.string(), z.any()))
+ .returns(
+ z.record(z.string(), z.any()).or(z.promise(z.record(z.string(), z.any())))
+ );
+
const htmlRspackPluginOptions = z.strictObject({
filename: z.string().optional(),
- template: z.string().optional(),
- templateContent: z.string().optional(),
- templateParameters: z.record(z.string()).optional(),
+ template: z
+ .string()
+ .refine(
+ val => !val.includes("!"),
+ () => ({
+ message:
+ "HtmlRspackPlugin does not support template path with loader yet"
+ })
+ )
+ .optional(),
+ templateContent: z.string().or(templateRenderFunction).optional(),
+ templateParameters: z
+ .record(z.string())
+ .or(z.boolean())
+ .or(templateParamFunction)
+ .optional(),
inject: z.enum(["head", "body"]).or(z.boolean()).optional(),
publicPath: z.string().optional(),
base: z
@@ -38,7 +75,10 @@ const htmlRspackPluginOptions = z.strictObject({
export type HtmlRspackPluginOptions = z.infer;
export const HtmlRspackPlugin = create(
BuiltinPluginName.HtmlRspackPlugin,
- (c: HtmlRspackPluginOptions = {}): RawHtmlRspackPluginOptions => {
+ function (
+ this: Compiler,
+ c: HtmlRspackPluginOptions = {}
+ ): RawHtmlRspackPluginOptions {
validate(c, htmlRspackPluginOptions);
const meta: Record> = {};
for (const key in c.meta) {
@@ -66,12 +106,130 @@ export const HtmlRspackPlugin = create(
? "false"
: configInject;
const base = typeof c.base === "string" ? { href: c.base } : c.base;
+
+ let compilation: Compilation | null = null;
+ this.hooks.compilation.tap("HtmlRspackPlugin", c => {
+ compilation = c;
+ });
+
+ function generateRenderData(data: string): Record {
+ const json = JSON.parse(data);
+ if (typeof c.templateParameters !== "function") {
+ json.compilation = compilation;
+ }
+ const renderTag = function (this: HtmlPluginTag) {
+ return htmlTagObjectToString(this);
+ };
+ const renderTagList = function (this: HtmlPluginTag[]) {
+ return this.join("");
+ };
+ if (Array.isArray(json.htmlRspackPlugin?.tags?.headTags)) {
+ for (const tag of json.htmlRspackPlugin.tags.headTags) {
+ tag.toString = renderTag;
+ }
+ json.htmlRspackPlugin.tags.headTags.toString = renderTagList;
+ }
+ if (Array.isArray(json.htmlRspackPlugin?.tags?.bodyTags)) {
+ for (const tag of json.htmlRspackPlugin.tags.bodyTags) {
+ tag.toString = renderTag;
+ }
+ json.htmlRspackPlugin.tags.bodyTags.toString = renderTagList;
+ }
+ return json;
+ }
+
+ let templateContent = c.templateContent;
+ let templateFn = undefined;
+ if (typeof templateContent === "function") {
+ templateFn = async (data: string) => {
+ try {
+ const renderer = c.templateContent as (
+ data: Record
+ ) => Promise | string;
+ if (c.templateParameters === false) {
+ return await renderer({});
+ }
+ return await renderer(generateRenderData(data));
+ } catch (e) {
+ const error = new Error(
+ `HtmlRspackPlugin: render template function failed, ${(e as Error).message}`
+ );
+ error.stack = (e as Error).stack;
+ throw error;
+ }
+ };
+ templateContent = "";
+ } else if (c.template) {
+ const filename = c.template.split("?")[0];
+ if ([".js", ".cjs"].includes(path.extname(filename))) {
+ templateFn = async (data: string) => {
+ const context = this.options.context || process.cwd();
+ const templateFilePath = path.resolve(context, filename);
+ if (!fs.existsSync(templateFilePath)) {
+ throw new Error(
+ `HtmlRspackPlugin: could not load file \`${filename}\` from \`${context}\``
+ );
+ }
+ try {
+ const renderer = require(templateFilePath) as (
+ data: Record
+ ) => Promise | string;
+ if (c.templateParameters === false) {
+ return await renderer({});
+ }
+ return await renderer(generateRenderData(data));
+ } catch (e) {
+ const error = new Error(
+ `HtmlRspackPlugin: render template function failed, ${(e as Error).message}`
+ );
+ error.stack = (e as Error).stack;
+ throw error;
+ }
+ };
+ }
+ }
+
+ const rawTemplateParameters = c.templateParameters;
+ let templateParameters;
+ if (typeof rawTemplateParameters === "function") {
+ templateParameters = async (data: string) => {
+ const newData = await rawTemplateParameters(JSON.parse(data));
+ return JSON.stringify(newData);
+ };
+ } else {
+ templateParameters = rawTemplateParameters;
+ }
+
return {
...c,
meta,
scriptLoading,
inject,
- base
+ base,
+ templateFn,
+ templateContent,
+ templateParameters
};
}
);
+
+function htmlTagObjectToString(tag: {
+ tagName: string;
+ attributes: Record;
+ voidTag: boolean;
+ innerHTML?: string;
+}) {
+ const attributes = Object.keys(tag.attributes || {})
+ .filter(
+ attributeName =>
+ tag.attributes[attributeName] === "" || tag.attributes[attributeName]
+ )
+ .map(attributeName => {
+ if (tag.attributes[attributeName] === "true") {
+ return attributeName;
+ }
+ return `${attributeName}="${tag.attributes[attributeName]}"`;
+ });
+ const res = `<${[tag.tagName].concat(attributes).join(" ")}${tag.voidTag && !tag.innerHTML ? "/" : ""}>${tag.innerHTML || ""}${tag.voidTag && !tag.innerHTML ? "" : `${tag.tagName}>`}`;
+ return res;
+}
diff --git a/tests/plugin-test/copy-plugin/build/main.js b/tests/plugin-test/copy-plugin/build/main.js
index e82b1db0fec..7424d448bb3 100644
--- a/tests/plugin-test/copy-plugin/build/main.js
+++ b/tests/plugin-test/copy-plugin/build/main.js
@@ -1 +1 @@
-(() => { "use strict"; var r = {}, t = {}; function e(i) { var o = t[i]; if (void 0 !== o) return o.exports; var n = t[i] = { exports: {} }; return r[i](n, n.exports, e), n.exports } e.g = function () { if ("object" == typeof globalThis) return globalThis; try { return this || Function("return this")() } catch (r) { if ("object" == typeof window) return window } }(), e.rv = function () { return "1.0.0-beta.5" }, (() => { e.g.importScripts && (r = e.g.location + ""); var r, t = e.g.document; if (!r && t && (t.currentScript && "SCRIPT" === t.currentScript.tagName.toUpperCase() && (r = t.currentScript.src), !r)) { var i = t.getElementsByTagName("script"); if (i.length) { for (var o = i.length - 1; o > -1 && (!r || !/^http(s?):/.test(r));)r = i[o--].src } } if (!r) throw Error("Automatic publicPath is not supported in this browser"); r = r.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/"), e.p = r })(), e.ruid = "bundler=rspack@1.0.0-beta.5", e.p })();
+(()=>{"use strict";var r={},t={};function e(i){var c=t[i];if(void 0!==c)return c.exports;var o=t[i]={exports:{}};return r[i](o,o.exports,e),o.exports}e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(r){if("object"==typeof window)return window}}(),e.rv=function(){return"1.0.0-rc.0"},(()=>{e.g.importScripts&&(r=e.g.location+"");var r,t=e.g.document;if(!r&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(r=t.currentScript.src),!r)){var i=t.getElementsByTagName("script");if(i.length){for(var c=i.length-1;c>-1&&(!r||!/^http(s?):/.test(r));)r=i[c--].src}}if(!r)throw Error("Automatic publicPath is not supported in this browser");r=r.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=r})(),e.ruid="bundler=rspack@1.0.0-rc.0",e.p})();
\ No newline at end of file
diff --git a/tests/plugin-test/html-plugin/basic.test.js b/tests/plugin-test/html-plugin/basic.test.js
index 3abaece43d3..35a9b1ac7a4 100644
--- a/tests/plugin-test/html-plugin/basic.test.js
+++ b/tests/plugin-test/html-plugin/basic.test.js
@@ -258,7 +258,7 @@ describe("HtmlWebpackPlugin", () => {
);
});
- // TODO: template with loaders
+ // TODO: template with loader
// it("uses a custom loader from webpack config", (done) => {
// testHtmlPlugin(
// {
@@ -596,32 +596,31 @@ describe("HtmlWebpackPlugin", () => {
);
});
- // TODO: support templateContent function
- // it("allows you to specify your own HTML template function", (done) => {
- // testHtmlPlugin(
- // {
- // mode: "production",
- // entry: { app: path.join(__dirname, "fixtures/index.js") },
- // output: {
- // path: OUTPUT_DIR,
- // filename: "app_bundle.js",
- // },
- // plugins: [
- // new HtmlWebpackPlugin({
- // templateContent: function () {
- // return fs.readFileSync(
- // path.join(__dirname, "fixtures/plain.html"),
- // "utf8",
- // );
- // },
- // }),
- // ],
- // },
- // ['
${htmlWebpackPlugin.tags.bodyTags}',
- // ],
- // null,
- // done,
- // );
- // });
+ it("should allow to use headTags and bodyTags directly in string literals", (done) => {
+ testHtmlPlugin(
+ {
+ mode: "production",
+ entry: path.join(__dirname, "fixtures/theme.js"),
+ output: {
+ path: OUTPUT_DIR,
+ filename: "index_bundle.js",
+ },
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
+ },
+ ],
+ },
+ plugins: [
+ new MiniCssExtractPlugin({ filename: "styles.css" }),
+ new HtmlWebpackPlugin({
+ scriptLoading: "blocking",
+ inject: false,
+ // DIFF:
+ // templateContent: ({ htmlWebpackPlugin }) => `
+ //
+ //