Skip to main content

euv_cli/server/
fn.rs

1use super::*;
2
3/// Sets the global application state.
4///
5/// # Arguments
6///
7/// - `Arc<AppState>` - The shared application state to store globally.
8///
9/// # Returns
10///
11/// - `Result<(), EuvError>` - Indicates success or failure of the initialization.
12pub(crate) fn set_global_state(state: Arc<AppState>) -> Result<(), EuvError> {
13    APP_STATE.set(state).map_err(|_: Arc<AppState>| {
14        EuvError::Message(ERROR_GLOBAL_STATE_ALREADY_INITIALIZED.to_string())
15    })
16}
17
18/// Retrieves the global application state.
19///
20/// # Returns
21///
22/// - `Option<Arc<AppState>>` - The global state if initialized.
23pub(crate) fn get_global_state() -> Option<Arc<AppState>> {
24    APP_STATE.get().cloned()
25}
26
27/// Resolves the bootstrap script body to inline into the generated HTML.
28///
29/// When JS bridge inlining is enabled and the `pkg/<name>.js` file is present
30/// after a successful `wasm-pack` build, reads the bridge, strips its
31/// top-level `import` statements, inlines the 2 snippet helper modules, and
32/// wraps everything in a synchronous IIFE that fetches the wasm and calls
33/// `main()`. The result is zero extra HTTP requests for the JS bridge file
34/// and no ES module graph parsing on the critical path.
35///
36/// When inlining is disabled (env var or missing `pkg/<name>.js` file),
37/// returns the classic `<script type="module">` import fallback so the
38/// page still boots.
39///
40/// # Arguments
41///
42/// - `&HtmlConfig` - The HTML configuration holding the serving root and import path.
43///
44/// # Returns
45///
46/// - `String` - The bootstrap script body to inline into the generated HTML.
47async fn resolve_inline_js(config: &HtmlConfig) -> String {
48    if inline_bridge_disabled() {
49        return build_module_fallback_bridge(config.get_import_path());
50    }
51    let pkg_dir: PathBuf = config.get_serving_root().join(PKG_DIR_NAME);
52    let js_name: String = config
53        .get_import_path()
54        .rsplit('/')
55        .next()
56        .unwrap_or("")
57        .to_string();
58    if js_name.is_empty() {
59        return build_module_fallback_bridge(config.get_import_path());
60    }
61    let js_path: PathBuf = pkg_dir.join(&js_name);
62    if !js_path.exists() {
63        return build_module_fallback_bridge(config.get_import_path());
64    }
65    let wasm_url: String = if let Some(stem) = js_name.strip_suffix(".js") {
66        format!("pkg/{stem}_bg.wasm")
67    } else {
68        config
69            .get_import_path()
70            .replace(JS_EXTENSION, WASM_FILE_SUFFIX)
71    };
72    match build_inline_bridge(&pkg_dir, &js_name, &wasm_url).await {
73        Ok(snippet) => snippet,
74        Err(error) => {
75            log::warn!("Falling back to module import bridge: {error}");
76            build_module_fallback_bridge(config.get_import_path())
77        }
78    }
79}
80
81/// Generates `index.html` based on the build profile.
82///
83/// Uses `INDEX_HTML_RELEASE` when `is_release` is `true` (no live-reload script),
84/// otherwise uses `INDEX_HTML_DEV` (includes live-reload instrumentation).
85///
86/// The wasm-bindgen JS bridge is **inlined** into the HTML at build time
87/// (Rust reads `pkg/<name>.js`, strips its top-level `import` statements,
88/// inlines the 2 snippet helpers, and wraps the body in a synchronous IIFE)
89/// so the browser incurs zero extra HTTP requests for the JS bridge file
90/// and skips ES module graph parsing. Set the `EUV_NO_INLINE_BRIDGE` env
91/// var to opt out.
92///
93/// Then writes the template with the import path placeholder replaced to disk.
94///
95/// # Arguments
96///
97/// - `&HtmlConfig` - The HTML generation configuration.
98///
99/// # Returns
100///
101/// - `Result<String, EuvError>` - The generated HTML content written to disk.
102pub(crate) async fn generate_html(config: &HtmlConfig) -> Result<String, EuvError> {
103    let template_content: String = if let Some(custom_path) = config.try_get_custom_index_html() {
104        let bytes: Vec<u8> =
105            read(custom_path)
106                .await
107                .map_err(|error: io::Error| EuvError::IoPath {
108                    message: ERROR_READ_CUSTOM_INDEX_HTML.to_string(),
109                    path: custom_path.to_path_buf(),
110                    error,
111                })?;
112        String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
113            message: ERROR_CUSTOM_INDEX_HTML_NOT_UTF8.to_string(),
114            error,
115        })?
116    } else if config.get_is_release() {
117        INDEX_HTML_RELEASE.to_string()
118    } else {
119        INDEX_HTML_DEV.to_string()
120    };
121    let inline_js: String = resolve_inline_js(config).await;
122    let base_href_tag: String = if config.get_is_dev_server() {
123        format!(
124            "<base href=\"/{}/\" />",
125            config
126                .get_serving_root()
127                .file_name()
128                .and_then(|name: &ffi::OsStr| name.to_str())
129                .unwrap_or("")
130        )
131    } else {
132        String::new()
133    };
134    let html: String = template_content
135        .replace(BASE_HREF_PLACEHOLDER, &base_href_tag)
136        .replace(IMPORT_PATH_PLACEHOLDER, config.get_import_path())
137        .replace(RELOAD_ROUTE_PLACEHOLDER, RELOAD_ROUTE)
138        .replace(INLINE_JS_PLACEHOLDER, &inline_js);
139    let index_path: PathBuf = config.get_serving_root().join(INDEX_HTML_FILE_NAME);
140    create_dir_all(config.get_serving_root())
141        .await
142        .map_err(|error: io::Error| EuvError::Io {
143            message: ERROR_CREATE_STATIC_DIRECTORY.to_string(),
144            error,
145        })?;
146    write(&index_path, &html)
147        .await
148        .map_err(|error: io::Error| EuvError::Io {
149            message: ERROR_WRITE_INDEX_HTML.to_string(),
150            error,
151        })?;
152    Ok(html)
153}
154
155/// Resolves the effective www directory, handling wasm-pack nested output.
156///
157/// # Arguments
158///
159/// - `&Path` - The candidate www directory path.
160///
161/// # Returns
162///
163/// - `PathBuf` - The resolved www directory containing `index.html`.
164pub async fn resolve_www_dir(www_dir: &Path) -> PathBuf {
165    if metadata(www_dir.join(INDEX_HTML_FILE_NAME)).await.is_ok() {
166        return www_dir.to_path_buf();
167    }
168    let parent_name: Option<&str> = www_dir
169        .file_name()
170        .and_then(|file_name_os_str: &ffi::OsStr| file_name_os_str.to_str());
171    if let Some(name) = parent_name {
172        let nested: PathBuf = www_dir.join(name);
173        if metadata(nested.join(INDEX_HTML_FILE_NAME)).await.is_ok() {
174            return nested;
175        }
176    }
177    www_dir.to_path_buf()
178}
179
180/// Resolves the pkg directory for serving WASM artifacts.
181///
182/// Delegates to `resolve_out_dir` which respects `--out-dir`
183/// from wasm-pack args or defaults to `{www_dir}/pkg`.
184///
185/// # Arguments
186///
187/// - `&ModeArgs` - The CLI arguments for resolving out_dir.
188///
189/// # Returns
190///
191/// - `PathBuf` - The resolved pkg directory containing WASM build artifacts.
192pub fn resolve_pkg_dir(args: &ModeArgs) -> PathBuf {
193    resolve_out_dir(args)
194}
195
196/// Resolves a file path within a base directory with path-traversal protection.
197///
198/// Canonicalizes both the file path and the base directory, then verifies
199/// that the canonical file path starts with the canonical base directory.
200///
201/// # Arguments
202///
203/// - `&Path` - The base directory to resolve within.
204/// - `&str` - The relative path to the file.
205///
206/// # Returns
207///
208/// - `Option<PathBuf>` - The resolved file path if valid, `None` otherwise.
209pub async fn resolve_file_in_base(base: &Path, path: &str) -> Option<PathBuf> {
210    let file_path: PathBuf = base.join(path);
211    let canonical_path: PathBuf = canonicalize(&file_path).await.ok()?;
212    let base_canonical: PathBuf = canonicalize(base).await.ok()?;
213    canonical_path
214        .starts_with(&base_canonical)
215        .then_some(file_path)
216}