lib

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

commit b7015b3ab3ad470e115d88475b9233c95717d7c4
parent fedbccb3bdff2d61d721b9db3119182189ecf658
Author: triesap <tyson@radroots.org>
Date:   Sun, 28 Jun 2026 04:04:05 +0000

packages: generate npm metadata

- add an xtask package metadata generation command
- share README and license validation with the generator
- expose package metadata generation in package scripts
- cover deterministic metadata writes in xtask tests

Diffstat:
Mtools/sdk_xtask_import/src/check.rs | 80+++----------------------------------------------------------------------------
Mtools/sdk_xtask_import/src/fs.rs | 15+++++++++++++++
Mtools/sdk_xtask_import/src/generate.rs | 11++++++++++-
Mtools/sdk_xtask_import/src/main.rs | 17++++++++++++++++-
Atools/sdk_xtask_import/src/package_metadata.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 225 insertions(+), 79 deletions(-)

diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs @@ -8,6 +8,7 @@ use crate::{ FORBIDDEN_PACKAGE_NAMES, WasmPackageSpec, package_specs, validate_package_matrix, wasm_package_specs, }, + package_metadata::{PACKAGE_FILES, check_package_distribution_metadata, package_description}, ts::generated_header, wasm_declarations::declaration_files, }; @@ -17,14 +18,6 @@ const PACKAGE_LICENSE: &str = "MIT OR Apache-2.0"; const PACKAGE_HOMEPAGE: &str = "https://radroots.org"; const PACKAGE_REPOSITORY_URL: &str = "git+https://github.com/radrootslabs/sdk.git"; const PUBLISH_ACCESS: &str = "public"; -const PACKAGE_README_FILE: &str = "README.md"; -const PACKAGE_LICENSE_FILES: [&str; 2] = ["LICENSE-MIT", "LICENSE-APACHE"]; -const PACKAGE_FILES: [&str; 4] = [ - "dist", - PACKAGE_README_FILE, - PACKAGE_LICENSE_FILES[0], - PACKAGE_LICENSE_FILES[1], -]; pub fn check() -> Result<(), String> { validate_package_matrix()?; @@ -251,74 +244,6 @@ fn check_package_json( Ok(json) } -fn check_package_distribution_metadata( - root: &Path, - package_dir: &Path, - package_json_path: &Path, - json: &serde_json::Value, -) -> Result<(), String> { - let package_name = json - .get("name") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| format!("package.json missing name: {}", package_json_path.display()))?; - let description = package_description(json, package_json_path)?; - let readme_path = package_dir.join(PACKAGE_README_FILE); - let expected_readme = package_readme(package_name, description); - check_text_file(&readme_path, &expected_readme, "stale package README")?; - for file_name in PACKAGE_LICENSE_FILES { - let source_path = root.join(file_name); - let package_path = package_dir.join(file_name); - let expected = fs::read(&source_path) - .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; - let actual = fs::read(&package_path) - .map_err(|error| format!("failed to read {}: {error}", package_path.display()))?; - if actual != expected { - return Err(format!( - "stale package license metadata: {}", - package_path.display() - )); - } - } - Ok(()) -} - -fn package_description<'a>( - json: &'a serde_json::Value, - package_json_path: &Path, -) -> Result<&'a str, String> { - let description = json - .get("description") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - format!( - "package.json missing description: {}", - package_json_path.display() - ) - })?; - if description.trim().is_empty() || description.trim() != description { - return Err(format!( - "package.json description must be non-empty and trimmed: {}", - package_json_path.display() - )); - } - Ok(description) -} - -fn package_readme(package_name: &str, description: &str) -> String { - format!( - "# {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" - ) -} - -fn check_text_file(path: &Path, expected: &str, label: &str) -> Result<(), String> { - let actual = fs::read_to_string(path) - .map_err(|error| format!("failed to read {}: {error}", path.display()))?; - if actual != expected { - return Err(format!("{label}: {}", path.display())); - } - Ok(()) -} - pub(crate) fn check_wasm_package_surface(root: &Path, spec: WasmPackageSpec) -> Result<(), String> { let package_dir = root.join(spec.package_dir); let package_json_path = package_dir.join("package.json"); @@ -730,12 +655,13 @@ mod tests { use crate::{ output::package_outputs, package_matrix::{WasmPackageSpec, validate_package_matrix}, + package_metadata::package_readme, }; use super::{ check_binding_crate_sources, check_generated_package_artifact_inventory, check_no_typescript_files, check_package_distribution_metadata, check_package_index, - check_package_json, check_wasm_package_surface, package_readme, + check_package_json, check_wasm_package_surface, }; #[test] diff --git a/tools/sdk_xtask_import/src/fs.rs b/tools/sdk_xtask_import/src/fs.rs @@ -33,6 +33,21 @@ pub fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> { Ok(true) } +pub fn write_bytes_if_changed(path: &Path, contents: &[u8]) -> Result<bool, String> { + if let Ok(existing) = fs::read(path) { + if existing == contents { + return Ok(false); + } + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + fs::write(path, contents) + .map_err(|error| format!("failed to write {}: {error}", path.display()))?; + Ok(true) +} + #[cfg(test)] mod tests { use super::workspace_root; diff --git a/tools/sdk_xtask_import/src/generate.rs b/tools/sdk_xtask_import/src/generate.rs @@ -1,4 +1,7 @@ -use crate::{fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix}; +use crate::{ + fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix, + package_metadata, +}; pub fn generate_ts() -> Result<(), String> { validate_package_matrix()?; @@ -19,3 +22,9 @@ pub fn generate_ts() -> Result<(), String> { } Ok(()) } + +pub fn generate_package_metadata() -> Result<(), String> { + validate_package_matrix()?; + let root = workspace_root()?; + package_metadata::generate_package_metadata(&root) +} diff --git a/tools/sdk_xtask_import/src/main.rs b/tools/sdk_xtask_import/src/main.rs @@ -9,6 +9,7 @@ mod generate; mod manifest; mod output; mod package_matrix; +mod package_metadata; mod ts; mod wasm; mod wasm_declarations; @@ -16,6 +17,7 @@ mod wasm_declarations; enum CommandAction<'a> { GenerateTs, GenerateWasm(&'a [String]), + GeneratePackageMetadata, Coverage(&'a [String]), Check, } @@ -32,6 +34,7 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> { match command_action(&args)? { CommandAction::GenerateTs => generate::generate_ts(), CommandAction::GenerateWasm(rest) => wasm::generate(rest), + CommandAction::GeneratePackageMetadata => generate::generate_package_metadata(), CommandAction::Coverage(rest) => coverage::run(rest), CommandAction::Check => check::check(), } @@ -45,6 +48,9 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { [command, target, rest @ ..] if command == "generate" && target == "wasm" => { Ok(CommandAction::GenerateWasm(rest)) } + [command, target] if command == "generate" && target == "package-metadata" => { + Ok(CommandAction::GeneratePackageMetadata) + } [command, rest @ ..] if command == "coverage" => Ok(CommandAction::Coverage(rest)), [command] if command == "check" => Ok(CommandAction::Check), [] => Err(usage()), @@ -53,7 +59,7 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { } fn usage() -> String { - "usage: cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask check | cargo xtask coverage run" + "usage: cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask coverage run" .to_owned() } @@ -80,6 +86,15 @@ mod tests { } #[test] + fn accepts_generate_package_metadata() { + let args = ["generate".to_owned(), "package-metadata".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GeneratePackageMetadata + )); + } + + #[test] fn accepts_check() { let args = ["check".to_owned()]; assert!(matches!( diff --git a/tools/sdk_xtask_import/src/package_metadata.rs b/tools/sdk_xtask_import/src/package_metadata.rs @@ -0,0 +1,181 @@ +use std::{fs, path::Path}; + +use crate::{ + fs::{write_bytes_if_changed, write_if_changed}, + package_matrix::{package_specs, wasm_package_specs}, +}; + +pub(crate) const PACKAGE_README_FILE: &str = "README.md"; +pub(crate) const PACKAGE_LICENSE_FILES: [&str; 2] = ["LICENSE-MIT", "LICENSE-APACHE"]; +pub(crate) const PACKAGE_FILES: [&str; 4] = [ + "dist", + PACKAGE_README_FILE, + PACKAGE_LICENSE_FILES[0], + PACKAGE_LICENSE_FILES[1], +]; + +pub(crate) fn generate_package_metadata(root: &Path) -> Result<(), String> { + for spec in package_specs() { + write_package_metadata(root, &root.join(spec.package_dir))?; + println!("generated package metadata {}", spec.package_name); + } + for spec in wasm_package_specs() { + write_package_metadata(root, &root.join(spec.package_dir))?; + println!("generated package metadata {}", spec.package_name); + } + Ok(()) +} + +pub(crate) fn check_package_distribution_metadata( + root: &Path, + package_dir: &Path, + package_json_path: &Path, + json: &serde_json::Value, +) -> Result<(), String> { + let package_name = package_name(json, package_json_path)?; + let description = package_description(json, package_json_path)?; + let readme_path = package_dir.join(PACKAGE_README_FILE); + let expected_readme = package_readme(package_name, description); + check_text_file(&readme_path, &expected_readme, "stale package README")?; + for file_name in PACKAGE_LICENSE_FILES { + let source_path = root.join(file_name); + let package_path = package_dir.join(file_name); + let expected = fs::read(&source_path) + .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; + let actual = fs::read(&package_path) + .map_err(|error| format!("failed to read {}: {error}", package_path.display()))?; + if actual != expected { + return Err(format!( + "stale package license metadata: {}", + package_path.display() + )); + } + } + Ok(()) +} + +pub(crate) fn package_description<'a>( + json: &'a serde_json::Value, + package_json_path: &Path, +) -> Result<&'a str, String> { + let description = json + .get("description") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing description: {}", + package_json_path.display() + ) + })?; + if description.trim().is_empty() || description.trim() != description { + return Err(format!( + "package.json description must be non-empty and trimmed: {}", + package_json_path.display() + )); + } + Ok(description) +} + +pub(crate) fn package_readme(package_name: &str, description: &str) -> String { + format!( + "# {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" + ) +} + +fn write_package_metadata(root: &Path, package_dir: &Path) -> Result<bool, String> { + let package_json_path = package_dir.join("package.json"); + let json = read_package_json(&package_json_path)?; + let package_name = package_name(&json, &package_json_path)?; + let description = package_description(&json, &package_json_path)?; + let mut changed = write_if_changed( + &package_dir.join(PACKAGE_README_FILE), + &package_readme(package_name, description), + )?; + for file_name in PACKAGE_LICENSE_FILES { + let source_path = root.join(file_name); + let contents = fs::read(&source_path) + .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; + changed |= write_bytes_if_changed(&package_dir.join(file_name), &contents)?; + } + Ok(changed) +} + +fn read_package_json(path: &Path) -> Result<serde_json::Value, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn package_name<'a>( + json: &'a serde_json::Value, + package_json_path: &Path, +) -> Result<&'a str, String> { + json.get("name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| format!("package.json missing name: {}", package_json_path.display())) +} + +fn check_text_file(path: &Path, expected: &str, label: &str) -> Result<(), String> { + let actual = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected { + return Err(format!("{label}: {}", path.display())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{package_readme, write_package_metadata}; + + #[test] + fn package_metadata_generation_is_deterministic() { + let root = test_root("metadata_generation"); + let package_dir = root.join("packages/example"); + fs::create_dir_all(&package_dir).expect("create package"); + fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license"); + fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license"); + fs::write( + package_dir.join("package.json"), + r#"{ + "name": "@radroots/example", + "description": "Example package" +}"#, + ) + .expect("write package json"); + + assert!(write_package_metadata(&root, &package_dir).expect("first generation")); + assert_eq!( + fs::read_to_string(package_dir.join("README.md")).expect("read README"), + package_readme("@radroots/example", "Example package") + ); + assert_eq!( + fs::read_to_string(package_dir.join("LICENSE-MIT")).expect("read MIT"), + "MIT license\n" + ); + assert!(!write_package_metadata(&root, &package_dir).expect("second generation")); + + let _ = fs::remove_dir_all(root); + } + + fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time after epoch") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "radroots_sdk_xtask_package_metadata_{name}_{}_{}", + std::process::id(), + stamp + )); + let _ = fs::remove_dir_all(&root); + root + } +}