vite-static-html 0.4.0

HTML generation support for `vite-static`
Documentation
#![warn(clippy::pedantic)]

use vite_static_shared::Manifest;

// This type is used in documentation.
#[allow(unused_imports)]
use vite_static_shared::ManifestChunk;

#[derive(Clone, Debug, Default)]
struct HtmlLinks {
    stylesheets: Vec<&'static str>,
    scripts: Vec<&'static str>,
    preloads: Vec<&'static str>,
}

// TODO: add importmap.json support

/// HTML integration.
///
/// Builds links, preloads for specified chunks.
///
/// ```rust
/// // Create `HtmlIntegration` struct:
/// let html = HtmlIntegration::new(MyViteStatic.boxed())
///     // import all required chunks:
///     .import("src/entry1.js")
///     .import("src/entry2.js")
///
///     // and then, you can build `String` of HTML (lines joined by '\n')
///     .build();
///     // or you can build array of lines
///     // .build_lines();
/// ```
pub struct HtmlIntegration {
    manifest: Box<dyn Manifest<'static>>,
    links: HtmlLinks,
}

impl HtmlIntegration {
    /// Create new [`HtmlIntegration`] builder.
    ///
    /// Takes boxed [`Manifest`] and returns [`HtmlIntegration`].
    #[must_use]
    pub fn new(manifest: Box<dyn Manifest<'static>>) -> Self {
        Self {
            manifest,
            links: HtmlLinks::default(),
        }
    }

    /// Internal function to recursively import chunks.
    fn import_as(&mut self, chunk: &str, is_dependency: bool) {
        let chunk = self
            .manifest
            .chunk_by_key(chunk)
            .unwrap_or_else(|| panic!(r#"failed to find chunk "{chunk}""#));

        for css in chunk.css {
            if !self.links.stylesheets.contains(css) {
                self.links.stylesheets.push(css);
            }
        }

        for import in chunk.imports {
            let import_file = self
                .manifest
                .resolve_output(import)
                .unwrap_or_else(|| panic!(r#"failed to find imported chunk "{import}""#));

            if !self.links.preloads.contains(&import_file) {
                self.import_as(import, true);
            }
        }

        if is_dependency {
            self.links.preloads.push(chunk.file);
        } else {
            self.links.scripts.push(chunk.file);
        }
    }

    /// Import [`ManifestChunk`] by key (input filename).
    ///
    /// ```rust
    /// .import("src/this-chunk.js")
    /// .import("src/components/MyCoolComponent.tsx")
    /// .import("src/or/some/styles.scss")
    /// ```
    #[must_use]
    pub fn import(mut self, chunk: &str) -> Self {
        self.import_as(chunk, false);
        self
    }

    /// Builds HTML and returns array of lines.
    #[must_use]
    pub fn build_lines(self) -> Vec<String> {
        let links = self.links;
        let mut html = Vec::new();

        for style in links.stylesheets {
            html.push(format!(r#"<link rel="stylesheet" href="/{style}" />"#));
        }
        for script in links.scripts {
            html.push(format!(
                r#"<script type="module" src="/{script}"></script>"#
            ));
        }
        for preload in links.preloads {
            html.push(format!(r#"<link rel="modulepreload" href="/{preload}" />"#));
        }

        html
    }

    /// Builds HTML and returns [`String`].
    #[must_use]
    pub fn build(self) -> String {
        self.build_lines().join("\n")
    }
}

impl Clone for HtmlIntegration {
    fn clone(&self) -> Self {
        Self {
            manifest: self.manifest.boxed(),
            links: self.links.clone(),
        }
    }
}