lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

package_metadata.rs (6282B)


      1 use std::{fs, path::Path};
      2 
      3 use crate::{
      4     build_control::Mode,
      5     sdk_generation::{
      6         fs::{write_bytes_if_changed, write_or_check},
      7         package_matrix::{package_specs, wasm_package_specs},
      8     },
      9 };
     10 
     11 pub(crate) const PACKAGE_README_FILE: &str = "README.md";
     12 pub(crate) const PACKAGE_LICENSE_FILES: [&str; 2] = ["LICENSE-MIT", "LICENSE-APACHE"];
     13 pub(crate) fn generate_package_metadata(
     14     source_root: &Path,
     15     consumer_root: &Path,
     16     mode: Mode,
     17 ) -> Result<(), String> {
     18     for spec in package_specs() {
     19         write_package_metadata(source_root, &consumer_root.join(spec.package_dir), mode)?;
     20         println!("generated package metadata {}", spec.package_name);
     21     }
     22     for spec in wasm_package_specs() {
     23         write_package_metadata(source_root, &consumer_root.join(spec.package_dir), mode)?;
     24         println!("generated package metadata {}", spec.package_name);
     25     }
     26     Ok(())
     27 }
     28 
     29 pub(crate) fn package_description<'a>(
     30     json: &'a serde_json::Value,
     31     package_json_path: &Path,
     32 ) -> Result<&'a str, String> {
     33     let description = json
     34         .get("description")
     35         .and_then(serde_json::Value::as_str)
     36         .ok_or_else(|| {
     37             format!(
     38                 "package.json missing description: {}",
     39                 package_json_path.display()
     40             )
     41         })?;
     42     if description.trim().is_empty() || description.trim() != description {
     43         return Err(format!(
     44             "package.json description must be non-empty and trimmed: {}",
     45             package_json_path.display()
     46         ));
     47     }
     48     Ok(description)
     49 }
     50 
     51 pub(crate) fn package_readme(package_name: &str, description: &str) -> String {
     52     format!(
     53         "# {package_name}\n\n{description}\n\nThis package publishes generated ESM JavaScript, TypeScript declarations, and any runtime artifacts from the Radroots SDK build pipeline. Runtime files are distributed from `dist/`; source and provenance metadata are kept outside the npm package payload.\n\n## License\n\nLicensed under either MIT or Apache-2.0, at your option. See `LICENSE-MIT` and `LICENSE-APACHE`.\n"
     54     )
     55 }
     56 
     57 fn write_package_metadata(
     58     source_root: &Path,
     59     package_dir: &Path,
     60     mode: Mode,
     61 ) -> Result<bool, String> {
     62     let package_json_path = package_dir.join("package.json");
     63     let json = read_package_json(&package_json_path)?;
     64     let package_name = package_name(&json, &package_json_path)?;
     65     let description = package_description(&json, &package_json_path)?;
     66     let readme_path = package_dir.join(PACKAGE_README_FILE);
     67     let readme = package_readme(package_name, description);
     68     let previous_readme = fs::read_to_string(&readme_path).ok();
     69     write_or_check(&readme_path, &readme, mode)?;
     70     let mut changed = previous_readme.as_deref() != Some(readme.as_str());
     71     for file_name in PACKAGE_LICENSE_FILES {
     72         let source_path = source_root.join(file_name);
     73         let contents = fs::read(&source_path)
     74             .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?;
     75         let destination = package_dir.join(file_name);
     76         match mode {
     77             Mode::Write => changed |= write_bytes_if_changed(&destination, &contents)?,
     78             Mode::Check => {
     79                 let current = fs::read(&destination).map_err(|error| {
     80                     format!("failed to read {}: {error}", destination.display())
     81                 })?;
     82                 if current != contents {
     83                     return Err(format!(
     84                         "stale package license metadata: {}",
     85                         destination.display()
     86                     ));
     87                 }
     88             }
     89         }
     90     }
     91     Ok(changed)
     92 }
     93 
     94 fn read_package_json(path: &Path) -> Result<serde_json::Value, String> {
     95     let raw = fs::read_to_string(path)
     96         .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
     97     serde_json::from_str::<serde_json::Value>(&raw)
     98         .map_err(|error| format!("failed to parse {}: {error}", path.display()))
     99 }
    100 
    101 fn package_name<'a>(
    102     json: &'a serde_json::Value,
    103     package_json_path: &Path,
    104 ) -> Result<&'a str, String> {
    105     json.get("name")
    106         .and_then(serde_json::Value::as_str)
    107         .ok_or_else(|| format!("package.json missing name: {}", package_json_path.display()))
    108 }
    109 
    110 #[cfg(test)]
    111 mod tests {
    112     use std::{
    113         fs,
    114         path::PathBuf,
    115         time::{SystemTime, UNIX_EPOCH},
    116     };
    117 
    118     use crate::build_control::Mode;
    119 
    120     use super::{package_readme, write_package_metadata};
    121 
    122     #[test]
    123     fn package_metadata_generation_is_deterministic() {
    124         let root = test_root("metadata_generation");
    125         let package_dir = root.join("packages/example");
    126         fs::create_dir_all(&package_dir).expect("create package");
    127         fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license");
    128         fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license");
    129         fs::write(
    130             package_dir.join("package.json"),
    131             r#"{
    132   "name": "@radroots/example",
    133   "description": "Example package"
    134 }"#,
    135         )
    136         .expect("write package json");
    137 
    138         assert!(
    139             write_package_metadata(&root, &package_dir, Mode::Write).expect("first generation")
    140         );
    141         assert_eq!(
    142             fs::read_to_string(package_dir.join("README.md")).expect("read README"),
    143             package_readme("@radroots/example", "Example package")
    144         );
    145         assert_eq!(
    146             fs::read_to_string(package_dir.join("LICENSE-MIT")).expect("read MIT"),
    147             "MIT license\n"
    148         );
    149         assert!(
    150             !write_package_metadata(&root, &package_dir, Mode::Write).expect("second generation")
    151         );
    152         write_package_metadata(&root, &package_dir, Mode::Check).expect("fresh metadata");
    153 
    154         let _ = fs::remove_dir_all(root);
    155     }
    156 
    157     fn test_root(name: &str) -> PathBuf {
    158         let stamp = SystemTime::now()
    159             .duration_since(UNIX_EPOCH)
    160             .expect("system time after epoch")
    161             .as_nanos();
    162         let root = std::env::temp_dir().join(format!(
    163             "radroots_sdk_xtask_package_metadata_{name}_{}_{}",
    164             std::process::id(),
    165             stamp
    166         ));
    167         let _ = fs::remove_dir_all(&root);
    168         root
    169     }
    170 }