vite_rust/
vite.rs

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    /// Creates a new Vite instance.
23    ///
24    /// # Arguments
25    /// * `config`  - a [`ViteConfig<'_>`] instance.
26    ///
27    /// # Errors
28    /// Returns `Err` if the given manifest path is not valid.
29    ///
30    /// # Example
31    /// ```rust
32    /// use vite_rust::{Vite, ViteConfig};
33    ///
34    /// #[tokio::main]
35    /// async fn main() {
36    ///     let mut vite_config = ViteConfig::default()
37    ///         .set_manifest_path("tests/test-manifest.json")
38    ///         .set_entrypoints(vec!["views/foo.js"])
39    ///         .set_force_mode(vite_rust::ViteMode::Manifest);
40    ///     
41    ///     let vite = Vite::new(vite_config).await.unwrap();
42    ///
43    ///     let expected =
44    ///         r#"<link rel="stylesheet" href="/assets/foo-5UjPuW-k.css" />
45    ///         <link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css" />
46    ///         <script type="module" src="/assets/foo-BRBmoGS9.js"></script>
47    ///         <link rel="modulepreload" href="/assets/shared-B7PI925R.js" />"#;
48    ///
49    ///     let expected = expected.replace("\t", "     ")
50    ///         .lines()
51    ///         .map(str::trim)
52    ///         .collect::<Vec::<&str>>()
53    ///         .join("\n");
54    ///     
55    ///     assert_eq!(vite.get_tags().unwrap(), expected);
56    /// }
57    /// ```
58    ///
59    /// [`ViteConfig`]: crate::config::ViteConfig
60    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    /// Generates assets HTML tags from `manifest.json` file.
123    ///
124    /// # Errors
125    /// Returns a `ViteError` if there is no Manifest.
126    ///
127    /// # Panics
128    /// Might panic if the target file doesn't exist.
129    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    /// Generates scripts and stylesheet link HTML tags referencing
142    /// the entrypoints directly from the Vite dev-server.
143    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    /// Generates HTML tags considering the current [`ViteMode`]:
158    /// -   If `Development` mode, calls `Vite::get_development_scripts()` and `Vite::get_hmr_script()`
159    ///     and return a concatenation of their returns;
160    /// -   If `Manifest` mode, calls `Vite::get_tags()` and return the assets HTML tags.
161    ///
162    /// # Errors
163    /// Returns a `ViteError` instance if mode is `Manifest` and there is no Manifest.
164    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    /// Returns a script tag referencing the Hot Module Reload client script from the Vite dev-server.
176    ///
177    /// If [`ViteMode`] is set to `Manifest`, only an empty string is returned.
178    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    /// Returns the bundled file by the given original file's path. If it is not present in the
191    /// manifest file, an empty string is returned.
192    ///
193    /// # Arguments
194    /// - `path`    - the root-relative path to an asset file. E.g. "src/assets/react.svg".
195    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    /// Returns the [react fast refresh script] relative to the current Vite dev-server URL.
211    ///
212    /// [react fast refresh script]: https://vite.dev/guide/backend-integration
213    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    /// Returns the current `manifest.json` file hash. Might be used for
227    /// assets versioning.
228    ///
229    /// The resultant string is a hex-encoded MD5 hash.
230    #[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    /// Returns the Vite instance's dev-server URL.
239    pub fn get_dev_server_url(&self) -> &str {
240        self.dev_server_host
241    }
242
243    /// Returns the current Vite instance's mode.
244    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}