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 ? "" : ``}`; + 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", - // ); - // }, - // }), - // ], - // }, - // ['', - // ], - // 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 }) => ` + // + // ${htmlWebpackPlugin.tags.headTags} + // ${htmlWebpackPlugin.tags.bodyTags} + // + // `, + templateContent: ({ htmlRspackPlugin }) => ` + + ${htmlRspackPlugin.tags.headTags} + ${htmlRspackPlugin.tags.bodyTags} + + `, + }), + ], + }, + [ + '', + '', + ], + null, + done, + ); + }); it("should add the javascript assets to the head for inject:true with scriptLoading:defer", (done) => { testHtmlPlugin( @@ -3646,44 +3649,50 @@ describe("HtmlWebpackPlugin", () => { ); }); - // TODO: support templateContent function - // 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({ - // inject: false, - // templateContent: ({ htmlWebpackPlugin }) => ` - // - // ${htmlWebpackPlugin.tags.headTags} - // ${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({ + inject: false, + // DIFF: + // templateContent: ({ htmlWebpackPlugin }) => ` + // + // ${htmlWebpackPlugin.tags.headTags} + // ${htmlWebpackPlugin.tags.bodyTags} + // + // `, + templateContent: ({ htmlRspackPlugin }) => ` + + ${htmlRspackPlugin.tags.headTags} + ${htmlRspackPlugin.tags.bodyTags} + + `, + }), + ], + }, + [ + '', + ], + null, + done, + ); + }); it("should allow to use experiments:{outputModule:true}", (done) => { testHtmlPlugin( @@ -3704,7 +3713,7 @@ describe("HtmlWebpackPlugin", () => { ); }); - // TODO: support loader in template + // TODO: template with loader // it("generates relative path for asset/resource", (done) => { // testHtmlPlugin( // { @@ -3732,7 +3741,7 @@ describe("HtmlWebpackPlugin", () => { // ); // }); - // TODO: support loader in template + // TODO: template with loader // it("uses the absolute path for asset/resource", (done) => { // testHtmlPlugin( // { diff --git a/tests/plugin-test/html-plugin/fixtures/interpolation.html b/tests/plugin-test/html-plugin/fixtures/interpolation.html index 5647aec292b..75465bd57dc 100644 --- a/tests/plugin-test/html-plugin/fixtures/interpolation.html +++ b/tests/plugin-test/html-plugin/fixtures/interpolation.html @@ -2,7 +2,8 @@ - {%= htmlWebpackPlugin.options.title %} + + {%= htmlRspackPlugin.options.title %}

Some unique text

diff --git a/tests/plugin-test/html-plugin/fixtures/templateParam.cjs b/tests/plugin-test/html-plugin/fixtures/templateParam.cjs index 32135ecb545..400c0da4839 100644 --- a/tests/plugin-test/html-plugin/fixtures/templateParam.cjs +++ b/tests/plugin-test/html-plugin/fixtures/templateParam.cjs @@ -17,5 +17,8 @@ module.exports = function (templateParams) { throw new Error('Error'); } - return 'templateParams keys: "' + Object.keys(templateParams).join(',') + '"'; + /// DIFF: return 'templateParams keys: "' + Object.keys(templateParams).join(',') + '"'; + const keys = Object.keys(templateParams); + keys.sort(); + return 'templateParams keys: "' + keys.join(',') + '"'; }; diff --git a/tests/plugin-test/html-plugin/fixtures/templateParam.js b/tests/plugin-test/html-plugin/fixtures/templateParam.js index 32135ecb545..400c0da4839 100644 --- a/tests/plugin-test/html-plugin/fixtures/templateParam.js +++ b/tests/plugin-test/html-plugin/fixtures/templateParam.js @@ -17,5 +17,8 @@ module.exports = function (templateParams) { throw new Error('Error'); } - return 'templateParams keys: "' + Object.keys(templateParams).join(',') + '"'; + /// DIFF: return 'templateParams keys: "' + Object.keys(templateParams).join(',') + '"'; + const keys = Object.keys(templateParams); + keys.sort(); + return 'templateParams keys: "' + keys.join(',') + '"'; };