commit ce7d805ae238a314dba05e35b176b3e706985a59
parent 487b85367edab1c8a30bcc19cd4d72e20a01db73
Author: triesap <tyson@radroots.org>
Date: Sun, 28 Jun 2026 06:16:33 +0000
packages: inspect packed manifests
- read package.json from each pnpm-produced tarball
- validate packed package identity exports and metadata
- require internal workspace dependencies to publish as semver ranges
- cover accepted and rejected packed dependency rewrites in xtask tests
Diffstat:
2 files changed, 316 insertions(+), 3 deletions(-)
diff --git a/tools/sdk_xtask_import/Cargo.toml b/tools/sdk_xtask_import/Cargo.toml
@@ -24,7 +24,9 @@ radroots_identity_bindings = { path = "../../crates/identity_bindings" }
radroots_replica_db_schema_bindings = { path = "../../crates/replica_db_schema_bindings" }
radroots_types_bindings = { path = "../../crates/types_bindings" }
radroots_trade_bindings = { path = "../../crates/trade_bindings" }
+flate2 = "1"
serde_json = "1"
serde = { workspace = true, features = ["derive"] }
+tar = "0.4"
tempfile = { workspace = true }
toml = "0.8"
diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs
@@ -1,11 +1,14 @@
use std::{
collections::BTreeSet,
fs,
+ io::Read,
path::{Path, PathBuf},
process::Command,
};
+use flate2::read::GzDecoder;
use serde::Deserialize;
+use tar::Archive;
use crate::{
contracts::validate_sdk_contracts,
@@ -495,6 +498,14 @@ fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
let json = read_package_json_value(&package_json_path)?;
let required_files = required_npm_payload_files(&json, &package_json_path, None)?;
let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?;
+ let packed_json = read_packed_package_json(&packed, spec.package_name)?;
+ check_packed_package_json(
+ &json,
+ &packed_json,
+ &package_json_path,
+ spec.package_name,
+ spec.package_dir,
+ )?;
let payload_files = packed_payload_files(&packed);
validate_npm_pack_payload(spec.package_name, &payload_files, &required_files, None)?;
}
@@ -504,6 +515,14 @@ fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
let json = read_package_json_value(&package_json_path)?;
let required_files = required_npm_payload_files(&json, &package_json_path, Some(*spec))?;
let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?;
+ let packed_json = read_packed_package_json(&packed, spec.package_name)?;
+ check_packed_package_json(
+ &json,
+ &packed_json,
+ &package_json_path,
+ spec.package_name,
+ spec.package_dir,
+ )?;
let payload_files = packed_payload_files(&packed);
validate_npm_pack_payload(
spec.package_name,
@@ -658,6 +677,213 @@ fn packed_payload_files(packed: &PackedPackage) -> BTreeSet<String> {
.collect()
}
+fn read_packed_package_json(
+ packed: &PackedPackage,
+ package_name: &str,
+) -> Result<serde_json::Value, String> {
+ let file = fs::File::open(&packed.tarball_path).map_err(|error| {
+ format!(
+ "failed to open pnpm pack tarball for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ })?;
+ let decoder = GzDecoder::new(file);
+ let mut archive = Archive::new(decoder);
+ let entries = archive.entries().map_err(|error| {
+ format!(
+ "failed to read pnpm pack tarball entries for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ })?;
+ for entry in entries {
+ let mut entry = entry.map_err(|error| {
+ format!(
+ "failed to read pnpm pack tarball entry for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ })?;
+ let path = entry.path().map_err(|error| {
+ format!(
+ "failed to read pnpm pack tarball entry path for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ })?;
+ if path.as_ref() != Path::new("package/package.json") {
+ continue;
+ }
+ let mut raw = String::new();
+ entry.read_to_string(&mut raw).map_err(|error| {
+ format!(
+ "failed to read packed package.json for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ })?;
+ return serde_json::from_str::<serde_json::Value>(&raw).map_err(|error| {
+ format!(
+ "failed to parse packed package.json for {package_name}: {}: {error}",
+ packed.tarball_path.display()
+ )
+ });
+ }
+ Err(format!(
+ "pnpm pack tarball for {package_name} is missing package/package.json: {}",
+ packed.tarball_path.display()
+ ))
+}
+
+fn check_packed_package_json(
+ source_json: &serde_json::Value,
+ packed_json: &serde_json::Value,
+ package_json_path: &Path,
+ expected_name: &str,
+ expected_directory: &str,
+) -> Result<(), String> {
+ let source_description = package_description(source_json, package_json_path)?;
+ let packed_description = package_description(packed_json, package_json_path)?;
+ if packed_description != source_description {
+ return Err(format!(
+ "packed package.json description mismatch in {}: expected {source_description}, found {packed_description}",
+ package_json_path.display()
+ ));
+ }
+ require_string_field(packed_json, package_json_path, "name", expected_name)?;
+ require_string_field(packed_json, package_json_path, "version", PACKAGE_VERSION)?;
+ require_string_field(packed_json, package_json_path, "license", PACKAGE_LICENSE)?;
+ require_string_field(packed_json, package_json_path, "homepage", PACKAGE_HOMEPAGE)?;
+ require_string_field(packed_json, package_json_path, "type", "module")?;
+ require_bool_field(packed_json, package_json_path, "sideEffects", false)?;
+ check_publish_config(packed_json, package_json_path)?;
+ check_repository(packed_json, package_json_path, expected_directory)?;
+ check_package_files(packed_json, package_json_path)?;
+ check_no_pack_lifecycle_scripts(packed_json, package_json_path)?;
+ check_same_packed_field(source_json, packed_json, package_json_path, "main")?;
+ check_same_packed_field(source_json, packed_json, package_json_path, "types")?;
+ check_same_packed_field(source_json, packed_json, package_json_path, "exports")?;
+ check_same_packed_field(source_json, packed_json, package_json_path, "scripts")?;
+ check_packed_dependency_maps(source_json, packed_json, package_json_path)?;
+ let source_surface = package_surface_paths(source_json, package_json_path)?;
+ let packed_surface = package_surface_paths(packed_json, package_json_path)?;
+ if packed_surface != source_surface {
+ return Err(format!(
+ "packed package.json export surface mismatch in {}",
+ package_json_path.display()
+ ));
+ }
+ Ok(())
+}
+
+fn check_same_packed_field(
+ source_json: &serde_json::Value,
+ packed_json: &serde_json::Value,
+ package_json_path: &Path,
+ field: &'static str,
+) -> Result<(), String> {
+ if source_json.get(field) != packed_json.get(field) {
+ return Err(format!(
+ "packed package.json {field} mismatch in {}",
+ package_json_path.display()
+ ));
+ }
+ Ok(())
+}
+
+fn check_packed_dependency_maps(
+ source_json: &serde_json::Value,
+ packed_json: &serde_json::Value,
+ package_json_path: &Path,
+) -> Result<(), String> {
+ for field in ["dependencies", "peerDependencies", "optionalDependencies"] {
+ check_packed_dependency_map(source_json, packed_json, package_json_path, field)?;
+ }
+ Ok(())
+}
+
+fn check_packed_dependency_map(
+ source_json: &serde_json::Value,
+ packed_json: &serde_json::Value,
+ package_json_path: &Path,
+ field: &'static str,
+) -> Result<(), String> {
+ let source_dependencies = optional_dependency_map(source_json, package_json_path, field)?;
+ let packed_dependencies = optional_dependency_map(packed_json, package_json_path, field)?;
+ let source_keys = source_dependencies
+ .iter()
+ .flat_map(|dependencies| dependencies.keys().cloned())
+ .collect::<BTreeSet<_>>();
+ let packed_keys = packed_dependencies
+ .iter()
+ .flat_map(|dependencies| dependencies.keys().cloned())
+ .collect::<BTreeSet<_>>();
+ if packed_keys != source_keys {
+ return Err(format!(
+ "packed package.json {field} keys mismatch in {}: expected {:?}, found {:?}",
+ package_json_path.display(),
+ source_keys,
+ packed_keys
+ ));
+ }
+ let Some(packed_dependencies) = packed_dependencies else {
+ return Ok(());
+ };
+ let source_dependencies = source_dependencies.expect("matching dependency keys require source");
+ for (name, packed_version) in packed_dependencies {
+ let packed_version = packed_version.as_str().ok_or_else(|| {
+ format!(
+ "packed package.json {field} dependency versions must be strings in {}",
+ package_json_path.display()
+ )
+ })?;
+ if packed_version.starts_with("workspace:") {
+ return Err(format!(
+ "packed package.json {field} dependency {name} must not use workspace protocol in {}",
+ package_json_path.display()
+ ));
+ }
+ let source_version = source_dependencies
+ .get(name)
+ .and_then(serde_json::Value::as_str)
+ .ok_or_else(|| {
+ format!(
+ "source package.json {field} dependency {name} must be a string in {}",
+ package_json_path.display()
+ )
+ })?;
+ let expected = expected_packed_dependency_version(name, source_version);
+ if packed_version != expected {
+ return Err(format!(
+ "packed package.json {field} dependency {name} mismatch in {}: expected {expected}, found {packed_version}",
+ package_json_path.display()
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn optional_dependency_map<'a>(
+ json: &'a serde_json::Value,
+ package_json_path: &Path,
+ field: &'static str,
+) -> Result<Option<&'a serde_json::Map<String, serde_json::Value>>, String> {
+ json.get(field)
+ .map(|value| {
+ value.as_object().ok_or_else(|| {
+ format!(
+ "package.json {field} must be an object: {}",
+ package_json_path.display()
+ )
+ })
+ })
+ .transpose()
+}
+
+fn expected_packed_dependency_version(name: &str, source_version: &str) -> String {
+ if name.starts_with("@radroots/") && source_version == "workspace:^" {
+ format!("^{PACKAGE_VERSION}")
+ } else {
+ source_version.to_owned()
+ }
+}
+
fn validate_npm_pack_payload(
package_name: &str,
payload_files: &BTreeSet<String>,
@@ -924,7 +1150,7 @@ mod tests {
use std::{
collections::BTreeSet,
fs,
- path::PathBuf,
+ path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
@@ -937,8 +1163,8 @@ mod tests {
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_package_surface_artifacts, check_wasm_package_surface,
- parse_pnpm_pack_entry, validate_npm_pack_payload,
+ check_package_json, check_package_surface_artifacts, check_packed_package_json,
+ check_wasm_package_surface, parse_pnpm_pack_entry, validate_npm_pack_payload,
};
#[test]
@@ -1223,6 +1449,75 @@ mod tests {
}
#[test]
+ fn packed_manifest_accepts_pnpm_workspace_dependency_rewrite() {
+ let source = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "workspace:^",
+ "@radroots/events-bindings": "workspace:^""#,
+ ));
+ let packed = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "^0.1.0",
+ "@radroots/events-bindings": "^0.1.0""#,
+ ));
+
+ check_packed_package_json(
+ &source,
+ &packed,
+ Path::new("packages/trade-bindings/package.json"),
+ "@radroots/trade-bindings",
+ "packages/trade-bindings",
+ )
+ .expect("packed manifest accepted");
+ }
+
+ #[test]
+ fn packed_manifest_rejects_workspace_dependency_ranges() {
+ let source = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "workspace:^""#,
+ ));
+ let packed = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "workspace:^""#,
+ ));
+
+ let error = check_packed_package_json(
+ &source,
+ &packed,
+ Path::new("packages/trade-bindings/package.json"),
+ "@radroots/trade-bindings",
+ "packages/trade-bindings",
+ )
+ .expect_err("workspace dependency rejected");
+
+ assert!(error.contains("must not use workspace protocol"));
+ }
+
+ #[test]
+ fn packed_manifest_rejects_internal_dependency_range_mismatch() {
+ let source = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "workspace:^""#,
+ ));
+ let packed = package_json_value(&package_json_with_dependencies(
+ "trade-bindings",
+ r#""@radroots/core-bindings": "^0.2.0""#,
+ ));
+
+ let error = check_packed_package_json(
+ &source,
+ &packed,
+ Path::new("packages/trade-bindings/package.json"),
+ "@radroots/trade-bindings",
+ "packages/trade-bindings",
+ )
+ .expect_err("internal dependency mismatch rejected");
+
+ assert!(error.contains("expected ^0.1.0"));
+ }
+
+ #[test]
fn npm_pack_payload_rejects_source_and_provenance_internals() {
let required_files = BTreeSet::from(["package.json".to_owned()]);
for forbidden in [
@@ -1578,4 +1873,20 @@ mod tests {
}}"#
)
}
+
+ fn package_json_with_dependencies(name: &str, dependencies: &str) -> String {
+ let raw = package_json(name);
+ let body = raw.strip_suffix('}').expect("root package JSON object");
+ format!(
+ r#"{body},
+ "dependencies": {{
+ {dependencies}
+ }}
+}}"#
+ )
+ }
+
+ fn package_json_value(raw: &str) -> serde_json::Value {
+ serde_json::from_str(raw).expect("package json parses")
+ }
}