vite-static-derive 0.4.0

Derive macro support for `vite-static`
Documentation
use std::{env, fs::File, io::Read, path::Path};

use vite_static_shared::{ManifestChunk, ViteManifest};

use anyhow::Context;
use base64::prelude::*;

use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use syn::LitByteStr;

fn vec_to_tokens<T: ToTokens>(data: &[T]) -> TokenStream {
    quote! { &[ #(#data),* ] }
}

fn option_to_tokens<T: ToTokens>(data: &Option<T>) -> TokenStream {
    if let Some(data) = data {
        quote! { Some(#data) }
    } else {
        quote! { None }
    }
}

fn construct_chunk<S: ToTokens>(chunk: &ManifestChunk<S>) -> TokenStream {
    let key = &chunk.key;
    let contents = LitByteStr::new(&chunk.contents, Span::call_site());
    let hash = &chunk.hash;
    let mime_type = &chunk.mime_type;

    let src = option_to_tokens(&chunk.src);
    let file = &chunk.file;
    let css = vec_to_tokens(&chunk.css);
    let assets = vec_to_tokens(&chunk.assets);
    let is_entry = &chunk.is_entry;
    let name = option_to_tokens(&chunk.name);
    let is_dynamic_entry = &chunk.is_dynamic_entry;
    let imports = vec_to_tokens(&chunk.imports);
    let dynamic_imports = vec_to_tokens(&chunk.dynamic_imports);

    quote! {
        ::vite_static::StaticManifestChunk {
            key: #key,
            contents: #contents,
            hash: #hash,
            mime_type: #mime_type,

            src: #src,
            file: #file,
            css: #css,
            assets: #assets,
            is_entry: #is_entry,
            name: #name,
            is_dynamic_entry: #is_dynamic_entry,
            imports: #imports,
            dynamic_imports: #dynamic_imports,
        }
    }
}

pub fn get_manifest(dir: &str) -> anyhow::Result<ViteManifest> {
    // TODO: fix microslop windows paths
    let vite_dist = if dir.starts_with('/') {
        Path::new(dir).to_path_buf()
    } else {
        let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
        let current_dir = Path::new(&cargo_manifest_dir);

        current_dir.join(dir)
    };

    let file = File::open(vite_dist.join(".vite").join("manifest.json"))
        .context("failed to open vite manifest")?;

    let mut manifest: ViteManifest =
        serde_json::from_reader(file).context("failed to parse vite manifest")?;

    // for whatever reason, "cssCodeSplit" doesn't add splitted CSS files into vite manifest as chunks/assets,
    // to fix it, we are manually add all referenced CSS files into manifest
    for chunk in manifest.clone().values() {
        for css in &chunk.css {
            manifest.insert(
                css.clone(),
                ManifestChunk {
                    key: css.clone(),
                    file: css.clone(),
                    ..Default::default()
                },
            );
        }
    }

    for (key, chunk) in &mut manifest {
        chunk.key = key.clone();

        let mut file = File::open(vite_dist.join(&chunk.file)).with_context(|| {
            format!(
                r#"failed to open vite chunk at "{path}""#,
                path = chunk.file
            )
        })?;
        file.read_to_end(&mut chunk.contents).with_context(|| {
            format!(
                r#"failed to read vite chunk at "{path}""#,
                path = chunk.file
            )
        })?;

        chunk.hash = {
            let hash = blake3::hash(&chunk.contents);
            BASE64_STANDARD.encode(hash.as_bytes())
        };

        chunk.mime_type = mime_guess::from_path(&chunk.file)
            .first_or_octet_stream()
            .to_string();
    }

    Ok(manifest)
}

pub fn generate_manifest_data(manifest: &ViteManifest) -> (Vec<TokenStream>, Vec<TokenStream>) {
    let mut manifest_items = manifest.iter().collect::<Vec<_>>();

    let key_output_pairs = {
        manifest_items.sort_unstable_by_key(|&(key, _)| key);

        manifest_items
            .iter()
            .map(|(key, chunk)| {
                let output = &chunk.file;
                quote! { (#key, #output) }
            })
            .collect()
    };

    let chunks = {
        manifest_items.sort_unstable_by_key(|&(_, chunk)| &chunk.file);

        manifest_items
            .iter()
            .map(|&(_, chunk)| {
                let output = &chunk.file;
                let chunk = construct_chunk(chunk);
                quote! { (#output, &#chunk) }
            })
            .collect()
    };

    (key_output_pairs, chunks)
}