Skip to main content

rspack_plugin_html/
config.rs

1use std::{fmt, path::PathBuf, str::FromStr};
2
3use futures::future::BoxFuture;
4use rspack_core::{Compilation, PublicPath};
5use rspack_error::Result;
6use rspack_util::fx_hash::FxHashMap;
7use serde::Serialize;
8use sugar_path::SugarPath;
9
10#[derive(Serialize, Debug, Clone, Copy, Default)]
11#[serde(rename_all = "snake_case")]
12pub enum HtmlInject {
13  #[default]
14  Head,
15  Body,
16  False,
17}
18
19impl fmt::Display for HtmlInject {
20  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21    f.write_str(match self {
22      HtmlInject::Head => "head",
23      HtmlInject::Body => "body",
24      HtmlInject::False => "false",
25    })
26  }
27}
28
29impl FromStr for HtmlInject {
30  type Err = anyhow::Error;
31
32  fn from_str(s: &str) -> Result<Self, Self::Err> {
33    if s.eq("head") {
34      Ok(HtmlInject::Head)
35    } else if s.eq("body") {
36      Ok(HtmlInject::Body)
37    } else if s.eq("false") {
38      Ok(HtmlInject::False)
39    } else {
40      Err(anyhow::Error::msg(
41        "inject in html config only support 'head', 'body', or 'false'",
42      ))
43    }
44  }
45}
46
47#[derive(Serialize, Debug, Clone, Copy)]
48#[serde(rename_all = "snake_case")]
49pub enum HtmlScriptLoading {
50  Blocking,
51  Defer,
52  Module,
53  SystemjsModule,
54}
55
56impl FromStr for HtmlScriptLoading {
57  type Err = anyhow::Error;
58
59  fn from_str(s: &str) -> Result<Self, Self::Err> {
60    if s.eq("blocking") {
61      Ok(HtmlScriptLoading::Blocking)
62    } else if s.eq("defer") {
63      Ok(HtmlScriptLoading::Defer)
64    } else if s.eq("module") {
65      Ok(HtmlScriptLoading::Module)
66    } else if s.eq("systemjs-module") {
67      Ok(HtmlScriptLoading::SystemjsModule)
68    } else {
69      Err(anyhow::Error::msg(
70        "scriptLoading in html config only support 'blocking', 'defer' or 'module'",
71      ))
72    }
73  }
74}
75
76type TemplateParameterTsfn =
77  Box<dyn for<'a> Fn(String) -> BoxFuture<'static, Result<String>> + Sync + Send>;
78
79pub struct TemplateParameterFn {
80  pub inner: TemplateParameterTsfn,
81}
82
83impl std::fmt::Debug for TemplateParameterFn {
84  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85    f.debug_struct("TemplateParameterFn").finish()
86  }
87}
88
89#[derive(Debug)]
90pub enum TemplateParameters {
91  Map(FxHashMap<String, String>),
92  Function(TemplateParameterFn),
93  Disabled,
94}
95
96#[derive(Serialize, Debug)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct HtmlRspackPluginBaseOptions {
99  pub href: Option<String>,
100  pub target: Option<String>,
101}
102
103type TemplateRenderTsfn =
104  Box<dyn for<'a> Fn(String) -> BoxFuture<'static, Result<String>> + Sync + Send>;
105
106pub struct TemplateRenderFn {
107  pub inner: TemplateRenderTsfn,
108}
109
110impl std::fmt::Debug for TemplateRenderFn {
111  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112    f.debug_struct("TemplateRenderFn").finish()
113  }
114}
115
116#[derive(Serialize, Debug, Clone, Copy, Default)]
117#[serde(rename_all = "snake_case")]
118pub enum HtmlChunkSortMode {
119  #[default]
120  Auto,
121  Manual,
122  // TODO: support function
123}
124
125impl FromStr for HtmlChunkSortMode {
126  type Err = anyhow::Error;
127
128  fn from_str(s: &str) -> Result<Self, Self::Err> {
129    if s.eq("auto") {
130      Ok(HtmlChunkSortMode::Auto)
131    } else if s.eq("manual") {
132      Ok(HtmlChunkSortMode::Manual)
133    } else {
134      Err(anyhow::Error::msg(
135        "chunksSortMode in html config only support 'auto' or 'manual'",
136      ))
137    }
138  }
139}
140
141#[derive(Serialize, Debug)]
142#[serde(rename_all = "camelCase", deny_unknown_fields)]
143pub struct HtmlRspackPluginOptions {
144  /// emitted file name in output path
145  #[serde(default = "default_filename")]
146  pub filename: Vec<String>,
147  /// template html file
148  pub template: Option<String>,
149  #[serde(skip)]
150  pub template_fn: Option<TemplateRenderFn>,
151  pub template_content: Option<String>,
152  #[serde(skip)]
153  pub template_parameters: TemplateParameters,
154  /// `head`, `body`, `false`
155  #[serde(default = "default_inject")]
156  pub inject: HtmlInject,
157  /// path or `auto`
158  pub public_path: Option<String>,
159  /// `blocking`, `defer`, or `module`
160  #[serde(default = "default_script_loading")]
161  pub script_loading: HtmlScriptLoading,
162
163  /// entry_chunk_name (only entry chunks are supported)
164  pub chunks: Option<Vec<String>>,
165  pub exclude_chunks: Option<Vec<String>>,
166  pub chunks_sort_mode: HtmlChunkSortMode,
167
168  #[serde(default)]
169  pub minify: Option<bool>,
170  pub title: Option<String>,
171  pub favicon: Option<String>,
172  pub meta: Option<FxHashMap<String, FxHashMap<String, String>>>,
173  pub hash: Option<bool>,
174  pub base: Option<HtmlRspackPluginBaseOptions>,
175  /// uid is used to identify the plugin instance on javascript side
176  pub uid: Option<u32>,
177}
178
179fn default_filename() -> Vec<String> {
180  vec![String::from("index.html")]
181}
182
183fn default_script_loading() -> HtmlScriptLoading {
184  HtmlScriptLoading::Defer
185}
186
187fn default_inject() -> HtmlInject {
188  HtmlInject::Head
189}
190
191fn default_chunks_sort_mode() -> HtmlChunkSortMode {
192  HtmlChunkSortMode::Auto
193}
194
195impl Default for HtmlRspackPluginOptions {
196  fn default() -> HtmlRspackPluginOptions {
197    HtmlRspackPluginOptions {
198      filename: default_filename(),
199      template: None,
200      template_fn: None,
201      template_content: None,
202      template_parameters: TemplateParameters::Map(Default::default()),
203      inject: default_inject(),
204      public_path: None,
205      script_loading: default_script_loading(),
206      chunks: None,
207      exclude_chunks: None,
208      chunks_sort_mode: default_chunks_sort_mode(),
209      minify: None,
210      title: None,
211      favicon: None,
212      meta: None,
213      hash: None,
214      base: None,
215      uid: None,
216    }
217  }
218}
219
220impl HtmlRspackPluginOptions {
221  pub async fn get_public_path(&self, compilation: &Compilation, filename: &str) -> String {
222    match &self.public_path {
223      Some(p) => PublicPath::ensure_ends_with_slash(p.clone()),
224      None => {
225        compilation
226          .options
227          .output
228          .public_path
229          .render(compilation, filename)
230          .await
231      }
232    }
233  }
234  pub fn get_relative_path(&self, compilation: &Compilation, filename: &str) -> String {
235    let mut file_path = PathBuf::from(filename);
236
237    if file_path.is_absolute() {
238      let context_path = PathBuf::from(compilation.options.context.to_string());
239      file_path = file_path.relative(context_path);
240    }
241
242    file_path.to_string_lossy().to_string()
243  }
244}