1use std::env;
2
3use crate::asset::Asset;
4use crate::config::{ViteConfig, ViteMode};
5use crate::error::{ViteError, ViteErrorKind};
6use crate::manifest::Manifest;
7use crate::CLIENT_SCRIPT_PATH;
8
9pub(crate) type Entrypoints = Vec<Box<str>>;
10
11#[derive(Debug)]
12pub struct Vite {
13 pub(crate) manifest: Option<Manifest>,
14 pub(crate) entrypoints: Entrypoints,
15 pub(crate) mode: ViteMode,
16 pub(crate) dev_server_host: &'static str,
17 pub(crate) prefix: Option<&'static str>,
18 pub(crate) app_url: &'static str,
19}
20
21impl Vite {
22 pub async fn new(config: ViteConfig<'_>) -> Result<Vite, ViteError> {
61 let dev_host = Box::leak(
62 config
63 .server_host
64 .unwrap_or("http://localhost:5173")
65 .to_string()
66 .into_boxed_str(),
67 );
68
69 let mode = match config.force_mode {
70 Some(mode) => mode,
71 None => {
72 ViteMode::discover(
73 config.use_heart_beat_check,
74 config.enable_dev_server,
75 dev_host,
76 config.heart_beat_retries_limit.unwrap(),
77 )
78 .await
79 }
80 };
81
82 let manifest = if mode.eq(&ViteMode::Manifest) || config.entrypoints.is_none() {
83 if let Some(manifest_path) = config.manifest_path {
84 Some(Manifest::new(manifest_path)?)
85 } else {
86 panic!(
87 "Tried to start Vite in Manifest mode, but no manifest.json file has been set."
88 );
89 }
90 } else {
91 None
92 };
93
94 let entrypoints: Entrypoints = match config.entrypoints {
95 Some(entrypoints) => entrypoints.into_iter().map(|entry| entry.into()).collect(),
96 None => match &manifest {
97 Some(manifest) => manifest
98 .get_manifest_entries()
99 .into_iter()
100 .map(|entry| entry.into())
101 .collect(),
102 None => {
103 panic!("Tried to start Vite without entrypoints set nor manifest.json file");
104 }
105 },
106 };
107
108 let prefix = resolve_prefix(config.prefix);
109
110 let app_url = resolve_app_url(config.app_url);
111
112 Ok(Vite {
113 entrypoints,
114 manifest,
115 mode,
116 dev_server_host: dev_host,
117 prefix,
118 app_url,
119 })
120 }
121
122 pub fn get_tags(&self) -> Result<String, ViteError> {
130 match &self.manifest {
131 Some(manifest) => {
132 Ok(manifest.generate_html_tags(&self.entrypoints, self.prefix, self.app_url))
133 }
134 None => Err(ViteError::new(
135 "Tried to get html tags from manifest, but there is no manifest file.",
136 ViteErrorKind::Manifest,
137 )),
138 }
139 }
140
141 pub fn get_development_scripts(&self) -> Result<String, ViteError> {
144 let mut tags = vec![];
145
146 for entry in self.entrypoints.iter() {
147 if entry.ends_with(".css") {
148 tags.push(Asset::StyleSheet(self.get_asset_url(entry)?).into_html());
149 } else {
150 tags.push(Asset::EntryPoint(self.get_asset_url(entry)?).into_html());
151 }
152 }
153
154 Ok(tags.join("\n"))
155 }
156
157 pub fn get_resolved_vite_scripts(&self) -> Result<String, ViteError> {
165 match self.mode {
166 ViteMode::Development => Ok(format!(
167 "{}\n{}",
168 self.get_development_scripts()?,
169 self.get_hmr_script()
170 )),
171 ViteMode::Manifest => self.get_tags(),
172 }
173 }
174
175 pub fn get_hmr_script(&self) -> String {
179 match self.mode {
180 ViteMode::Development => {
181 format!(
182 r#"<script type="module" src="{}/{}"></script>"#,
183 &self.dev_server_host, CLIENT_SCRIPT_PATH
184 )
185 }
186 ViteMode::Manifest => "".to_string(),
187 }
188 }
189
190 pub fn get_asset_url(&self, path: &str) -> Result<String, ViteError> {
196 let path = path.strip_prefix("/").unwrap_or(path).replace("'", "");
197
198 match &self.mode {
199 ViteMode::Development => Ok(format!("{}/{}", self.dev_server_host, path)),
200 ViteMode::Manifest => match &self.manifest {
201 Some(manifest) => Ok(manifest.get_asset_url(&path, self.prefix, self.app_url)),
202 None => Err(ViteError::new(
203 "Tried to get asset's URL from manifest, but there is no manifest file.",
204 ViteErrorKind::Manifest,
205 )),
206 },
207 }
208 }
209
210 pub fn get_react_script(&self) -> String {
214 format!(
215 r#"<script type="module">
216 import RefreshRuntime from '{}/@react-refresh'
217 RefreshRuntime.injectIntoGlobalHook(window)
218 window.$RefreshReg$ = () => {{}}
219 window.$RefreshSig$ = () => (type) => type
220 window.__vite_plugin_react_preamble_installed__ = true
221 </script>"#,
222 &self.dev_server_host
223 )
224 }
225
226 #[inline]
231 pub fn get_hash(&self) -> Option<&str> {
232 match &self.manifest {
233 Some(manifest) => Some(manifest.get_hash()),
234 None => None,
235 }
236 }
237
238 pub fn get_dev_server_url(&self) -> &str {
240 self.dev_server_host
241 }
242
243 pub fn mode(&self) -> &ViteMode {
245 &self.mode
246 }
247}
248
249pub(crate) fn resolve_prefix(prefix: Option<&str>) -> Option<&'static str> {
250 if let Some(prefix) = prefix {
251 if prefix.is_empty() || prefix.eq("/") {
252 return None;
253 }
254
255 let prefix = prefix.strip_prefix("/").unwrap_or(prefix);
256 let prefix = prefix.strip_suffix("/").unwrap_or(prefix);
257
258 return Some(Box::leak(prefix.to_string().into_boxed_str()));
259 }
260
261 None
262}
263
264pub(crate) fn resolve_app_url(app_url: Option<&str>) -> &'static str {
265 if let Some(app_url) = app_url {
266 let app_url = app_url.strip_suffix("/").unwrap_or(app_url);
267
268 return Box::leak(app_url.to_string().into_boxed_str());
269 }
270
271 let app_url = if let Ok(app_url) = env::var("APP_URL") {
272 app_url.strip_suffix("/").unwrap_or(&app_url).to_string()
273 } else {
274 String::new()
275 };
276
277 Box::leak(app_url.into_boxed_str())
278}
279
280#[cfg(test)]
281mod test {
282 use std::env;
283
284 use crate::vite::{resolve_app_url, resolve_prefix};
285
286 #[test]
287 fn test_resolve_prefix() {
288 const EXPECTED_RESULT: &str = "bundle";
289
290 assert_eq!(EXPECTED_RESULT, resolve_prefix(Some("bundle")).unwrap());
291 assert_eq!(EXPECTED_RESULT, resolve_prefix(Some("/bundle")).unwrap());
292 assert_eq!(EXPECTED_RESULT, resolve_prefix(Some("bundle/")).unwrap());
293 assert_eq!(EXPECTED_RESULT, resolve_prefix(Some("/bundle/")).unwrap());
294 }
295 #[test]
296 fn test_resolve_app_url() {
297 const EXPECTED_RESULT: &str = "http://foo.baz";
298
299 assert_eq!(EXPECTED_RESULT, resolve_app_url(Some("http://foo.baz/")));
300 assert_eq!(EXPECTED_RESULT, resolve_app_url(Some("http://foo.baz")));
301
302 assert_eq!(resolve_app_url(None), "");
303
304 env::set_var("APP_URL", "http://foo.baz");
305
306 assert_eq!(resolve_app_url(None), EXPECTED_RESULT);
307
308 env::remove_var("APP_URL");
309 }
310}