Skip to content

Commit

Permalink
Add support for integrity field in manifest (#19) (#24)
Browse files Browse the repository at this point in the history
In this PR:
- Introduce support for `integrity` field in `manifest.json` to provide [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) attribute for scripts. 
- Introduce new test for functions in `vite.rs` file.

One of plugins that fills in this field on `manifest.json` is:
- [`vite-plugin-manifest-sri` plugin](https://www.npmjs.com/package/vite-plugin-manifest-sri)

This fixes #19.
  • Loading branch information
chez14 authored Jun 26, 2024
1 parent 8315673 commit ad3a0cf
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

- Now supports precalculated integrity hash in the manifest file (via
`integrity` field).

## [0.4.0] 2024-06-12

### Added
Expand Down
171 changes: 163 additions & 8 deletions src/vite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ window.__vite_plugin_react_preamble_installed__ = true
}

pub struct Production {
main: String,
main: ManifestEntry,
css: Option<String>,
title: &'static str,
lang: &'static str,
Expand All @@ -145,11 +145,18 @@ impl Production {
main: &'static str,
) -> Result<Self, Box<dyn std::error::Error>> {
let bytes = std::fs::read(manifest_path)?;
let manifest: HashMap<String, ManifestEntry> =
serde_json::from_str(&String::from_utf8(bytes.clone())?)?;
let entry = manifest.get(main).ok_or(ViteError::EntryMissing(main))?;

Self::new_from_string(&String::from_utf8(bytes)?, main)
}

fn new_from_string(
manifest_string: &str,
main: &'static str,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut manifest: HashMap<String, ManifestEntry> = serde_json::from_str(&manifest_string)?;
let entry = manifest.remove(main).ok_or(ViteError::EntryMissing(main))?;
let mut hasher = Sha1::new();
hasher.update(&bytes);
hasher.update(manifest_string.as_bytes());
let result = hasher.finalize();
let version = encode(result);
let css = {
Expand All @@ -164,7 +171,7 @@ impl Production {
}
};
Ok(Self {
main: format!("/{}", entry.file),
main: entry,
css,
title: "Vite",
lang: "en",
Expand All @@ -185,13 +192,19 @@ impl Production {
pub fn into_config(self) -> InertiaConfig {
let layout = Box::new(move |props| {
let css = self.css.clone().unwrap_or("".to_string());
let main_path = format!("/{}", self.main.file);
let main_integrity = self.main.integrity.clone();
html! {
html lang=(self.lang) {
head {
title { (self.title) }
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
script type="module" src=(self.main) {}
@if let Some(integrity) = main_integrity {
script type="module" src=(main_path) integrity=(integrity) {}
} else {
script type="module" src=(main_path) {}
}
(PreEscaped(css))
}
body {
Expand Down Expand Up @@ -229,8 +242,150 @@ impl std::error::Error for ViteError {
}
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
struct ManifestEntry {
file: String,
integrity: Option<String>,
css: Option<Vec<String>>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_development_default() {
let development = Development::default();

assert_eq!(development.port, 5173);
assert_eq!(development.main, "src/main.ts");
assert_eq!(development.lang, "en");
assert_eq!(development.title, "Vite");
assert_eq!(development.react, false);
}

#[test]
fn test_development_builder_methods() {
let development = Development::default()
.port(8080)
.main("src/deep/index.ts")
.lang("id")
.title("Untitled Axum Inertia App")
.react();

assert_eq!(development.port, 8080);
assert_eq!(development.main, "src/deep/index.ts");
assert_eq!(development.lang, "id");
assert_eq!(development.title, "Untitled Axum Inertia App");
assert_eq!(development.react, true);
}

#[test]
fn test_development_into_config() {
let main_script = "src/index.ts";
let development = Development::default()
.port(8080)
.main(main_script)
.lang("lang-id")
.title("app-title-here")
.react();

let config = development.into_config();

assert_eq!(config.version(), None);

let config_layout = config.layout();
let binding = config_layout(r#"{"someprops": "somevalues"}"#.to_string());
let rendered_layout = binding.as_str();

assert!(rendered_layout.contains(r#"<html lang="lang-id">"#));
assert!(rendered_layout.contains(r#"<title>app-title-here</title>"#));
assert!(rendered_layout.contains(r#"{&quot;someprops&quot;: &quot;somevalues&quot;}"#));
assert!(rendered_layout.contains(r#"http://localhost:8080/@vite/client"#));
assert!(
rendered_layout.contains(r#"window.__vite_plugin_react_preamble_installed__ = true"#)
);
}

#[test]
fn test_production_new_entry_missing() {
let manifest_content = r#"{"main.js": {}}"#;
let result = Production::new_from_string(manifest_content, "nonexistent.js");

assert!(matches!(result, Err(_)));
}

#[test]
fn test_production_new() {
let manifest_content =
r#"{"main.js": {"file": "main.hash-id-here.js", "css": ["style.css"]}}"#;
let production_res = Production::new_from_string(manifest_content, "main.js");

assert!(production_res.is_ok());

let production = production_res.unwrap();
let content_hash = encode(Sha1::digest(manifest_content.as_bytes()));

assert_eq!(production.main.css, Some(vec!(String::from("style.css"))));
assert_eq!(production.title, "Vite");
assert_eq!(production.main.file, "main.hash-id-here.js");
assert_eq!(production.main.integrity, None);
assert_eq!(production.lang, "en");
assert_eq!(production.version, content_hash);
}

#[test]
fn test_production_builder_methods() {
let manifest_content =
r#"{"main.js": {"file": "main.hash-id-here.js", "css": ["style.css"]}}"#;
let production = Production::new_from_string(manifest_content, "main.js")
.unwrap()
.lang("fr")
.title("Untitled Axum Inertia App");

assert_eq!(production.lang, "fr");
assert_eq!(production.title, "Untitled Axum Inertia App");
}

#[test]
fn test_production_into_config() {
let manifest_content =
r#"{"main.js": {"file": "main.hash-id-here.js", "css": ["style.css"]}}"#;
let production = Production::new_from_string(manifest_content, "main.js")
.unwrap()
.lang("jv")
.title("Untitled Axum Inertia App");

let config = production.into_config();
let config_layout = config.layout();
let binding = config_layout(r#"{"someprops": "somevalues"}"#.to_string());
let rendered_layout = binding.as_str();

assert!(rendered_layout
.contains(r#"<script type="module" src="/main.hash-id-here.js"></script>"#));
assert!(rendered_layout.contains(r#"<link rel="stylesheet" href="/style.css"/>"#));
assert!(rendered_layout.contains(r#"<html lang="jv">"#));
assert!(rendered_layout.contains(r#"<title>Untitled Axum Inertia App</title>"#));
assert!(rendered_layout.contains(r#"{&quot;someprops&quot;: &quot;somevalues&quot;}"#));
}

#[test]
fn test_production_into_config_with_integrity() {
let manifest_content = r#"{"main.js": {"file": "main.hash-id-here.js", "integrity": "sha000-shaHashHere1234", "css": ["style.css"]}}"#;
let production = Production::new_from_string(manifest_content, "main.js")
.unwrap()
.lang("jv")
.title("Untitled Axum Inertia App");

let config = production.into_config();
let config_layout = config.layout();
let binding = config_layout(r#"{"someprops": "somevalues"}"#.to_string());
let rendered_layout = binding.as_str();

assert!(rendered_layout.contains(r#"<script type="module" src="/main.hash-id-here.js" integrity="sha000-shaHashHere1234"></script>"#));
assert!(rendered_layout.contains(r#"<link rel="stylesheet" href="/style.css"/>"#));
assert!(rendered_layout.contains(r#"<html lang="jv">"#));
assert!(rendered_layout.contains(r#"<title>Untitled Axum Inertia App</title>"#));
assert!(rendered_layout.contains(r#"{&quot;someprops&quot;: &quot;somevalues&quot;}"#));
}
}

0 comments on commit ad3a0cf

Please sign in to comment.