commit 6e6966629e309273891a98b2f6a134bf03a424c0
parent ce7d805ae238a314dba05e35b176b3e706985a59
Author: triesap <tyson@radroots.org>
Date: Sun, 28 Jun 2026 06:19:36 +0000
packages: require full dist payloads
- derive expected binding dist files from generated TypeScript sources
- enforce exact dist inventory for packed binding and WASM packages
- reject malformed package payload paths before validation
- cover generated dist inventory and path validation in xtask tests
Diffstat:
1 file changed, 182 insertions(+), 18 deletions(-)
diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs
@@ -496,7 +496,9 @@ fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
let package_dir = root.join(spec.package_dir);
let package_json_path = package_dir.join("package.json");
let json = read_package_json_value(&package_json_path)?;
- let required_files = required_npm_payload_files(&json, &package_json_path, None)?;
+ let expected_dist_files =
+ expected_packed_dist_files(&package_dir, &json, &package_json_path, None)?;
+ let required_files = required_npm_payload_files(&expected_dist_files);
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(
@@ -506,14 +508,17 @@ fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
spec.package_name,
spec.package_dir,
)?;
- let payload_files = packed_payload_files(&packed);
+ let payload_files = packed_payload_files(&packed)?;
validate_npm_pack_payload(spec.package_name, &payload_files, &required_files, None)?;
+ validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?;
}
for spec in wasm_package_specs() {
let package_dir = root.join(spec.package_dir);
let package_json_path = package_dir.join("package.json");
let json = read_package_json_value(&package_json_path)?;
- let required_files = required_npm_payload_files(&json, &package_json_path, Some(*spec))?;
+ let expected_dist_files =
+ expected_packed_dist_files(&package_dir, &json, &package_json_path, Some(*spec))?;
+ let required_files = required_npm_payload_files(&expected_dist_files);
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(
@@ -523,13 +528,14 @@ fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
spec.package_name,
spec.package_dir,
)?;
- let payload_files = packed_payload_files(&packed);
+ let payload_files = packed_payload_files(&packed)?;
validate_npm_pack_payload(
spec.package_name,
&payload_files,
&required_files,
Some(&format!("dist/{}_bg.wasm", spec.out_name)),
)?;
+ validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?;
}
Ok(())
}
@@ -571,26 +577,58 @@ fn read_package_json_value(path: &Path) -> Result<serde_json::Value, String> {
.map_err(|error| format!("failed to parse {}: {error}", path.display()))
}
-fn required_npm_payload_files(
- json: &serde_json::Value,
- package_json_path: &Path,
- wasm_spec: Option<WasmPackageSpec>,
-) -> Result<BTreeSet<String>, String> {
- let mut required = BTreeSet::from([
+fn required_npm_payload_files(expected_dist_files: &BTreeSet<String>) -> BTreeSet<String> {
+ let mut required = package_metadata_payload_files();
+ required.extend(expected_dist_files.iter().cloned());
+ required
+}
+
+fn package_metadata_payload_files() -> BTreeSet<String> {
+ BTreeSet::from([
"package.json".to_owned(),
"README.md".to_owned(),
"LICENSE-MIT".to_owned(),
"LICENSE-APACHE".to_owned(),
- ]);
+ ])
+}
+
+fn expected_packed_dist_files(
+ package_dir: &Path,
+ json: &serde_json::Value,
+ package_json_path: &Path,
+ wasm_spec: Option<WasmPackageSpec>,
+) -> Result<BTreeSet<String>, String> {
+ let mut expected = BTreeSet::new();
for path in package_surface_paths(json, package_json_path)? {
- required.insert(normalized_package_path(&path));
+ expected.insert(normalized_package_path(&path)?);
}
if let Some(spec) = wasm_spec {
for path in wasm_runtime_files(spec) {
- required.insert(path);
+ expected.insert(path);
}
+ } else {
+ expected.extend(expected_binding_generated_dist_files(package_dir)?);
+ }
+ Ok(expected)
+}
+
+fn expected_binding_generated_dist_files(package_dir: &Path) -> Result<BTreeSet<String>, String> {
+ let generated_dir = package_dir.join("src/generated");
+ let mut expected = BTreeSet::new();
+ for source in generated_package_files(package_dir, &generated_dir)? {
+ let stem = source
+ .strip_prefix("src/generated/")
+ .and_then(|path| path.strip_suffix(".ts"))
+ .ok_or_else(|| {
+ format!(
+ "generated package source must be a TypeScript file under src/generated: {}",
+ package_dir.join(&source).display()
+ )
+ })?;
+ expected.insert(format!("dist/generated/{stem}.js"));
+ expected.insert(format!("dist/generated/{stem}.d.ts"));
}
- Ok(required)
+ Ok(expected)
}
fn pnpm_pack_package(
@@ -668,7 +706,7 @@ fn packed_package_from_pnpm_entry(
})
}
-fn packed_payload_files(packed: &PackedPackage) -> BTreeSet<String> {
+fn packed_payload_files(packed: &PackedPackage) -> Result<BTreeSet<String>, String> {
debug_assert!(packed.tarball_path.is_file());
packed
.files
@@ -921,8 +959,45 @@ fn validate_npm_pack_payload(
Ok(())
}
-fn normalized_package_path(path: &str) -> String {
- path.trim_start_matches("./").to_owned()
+fn validate_packed_dist_inventory(
+ package_name: &str,
+ payload_files: &BTreeSet<String>,
+ expected_dist_files: &BTreeSet<String>,
+) -> Result<(), String> {
+ let actual_dist_files = payload_files
+ .iter()
+ .filter(|path| path.starts_with("dist/"))
+ .cloned()
+ .collect::<BTreeSet<_>>();
+ if &actual_dist_files != expected_dist_files {
+ let missing = expected_dist_files
+ .difference(&actual_dist_files)
+ .cloned()
+ .collect::<Vec<_>>();
+ let extra = actual_dist_files
+ .difference(expected_dist_files)
+ .cloned()
+ .collect::<Vec<_>>();
+ return Err(format!(
+ "npm pack payload for {package_name} has dist inventory mismatch: missing {:?}, extra {:?}",
+ missing, extra
+ ));
+ }
+ Ok(())
+}
+
+fn normalized_package_path(path: &str) -> Result<String, String> {
+ let normalized = path.trim_start_matches("./");
+ if normalized.is_empty()
+ || normalized.starts_with('/')
+ || normalized.contains('\\')
+ || normalized
+ .split('/')
+ .any(|segment| segment.is_empty() || segment == "." || segment == "..")
+ {
+ return Err(format!("invalid npm pack payload path: {path}"));
+ }
+ Ok(normalized.to_owned())
}
fn forbidden_npm_payload_path(path: &str) -> Option<&'static str> {
@@ -1164,7 +1239,8 @@ mod tests {
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_packed_package_json,
- check_wasm_package_surface, parse_pnpm_pack_entry, validate_npm_pack_payload,
+ check_wasm_package_surface, expected_packed_dist_files, normalized_package_path,
+ parse_pnpm_pack_entry, validate_npm_pack_payload, validate_packed_dist_inventory,
};
#[test]
@@ -1396,6 +1472,45 @@ mod tests {
}
#[test]
+ fn binding_dist_inventory_includes_generated_js_and_declarations() {
+ let root = test_root("binding_dist_inventory");
+ let package_dir = root.join("packages").join("example");
+ fs::create_dir_all(package_dir.join("src/generated")).expect("create generated dir");
+ fs::write(
+ package_dir.join("src/generated").join("types.ts"),
+ "export type Example = string;\n",
+ )
+ .expect("write generated types");
+ fs::write(
+ package_dir.join("src/generated").join("constants.ts"),
+ "export const EXAMPLE = \"example\";\n",
+ )
+ .expect("write generated constants");
+ let json = package_json_value(&package_json("example"));
+
+ let expected = expected_packed_dist_files(
+ &package_dir,
+ &json,
+ Path::new("packages/example/package.json"),
+ None,
+ )
+ .expect("expected dist files");
+
+ assert_eq!(
+ expected,
+ BTreeSet::from([
+ "dist/generated/constants.d.ts".to_owned(),
+ "dist/generated/constants.js".to_owned(),
+ "dist/generated/types.d.ts".to_owned(),
+ "dist/generated/types.js".to_owned(),
+ "dist/index.d.ts".to_owned(),
+ "dist/index.js".to_owned(),
+ ])
+ );
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
fn npm_pack_payload_requires_metadata_and_export_files() {
let payload_files = BTreeSet::from([
"package.json".to_owned(),
@@ -1421,6 +1536,55 @@ mod tests {
}
#[test]
+ fn packed_dist_inventory_rejects_missing_generated_files() {
+ let expected_dist_files = BTreeSet::from([
+ "dist/generated/types.d.ts".to_owned(),
+ "dist/generated/types.js".to_owned(),
+ "dist/index.d.ts".to_owned(),
+ "dist/index.js".to_owned(),
+ ]);
+ let payload_files = BTreeSet::from([
+ "package.json".to_owned(),
+ "README.md".to_owned(),
+ "LICENSE-MIT".to_owned(),
+ "LICENSE-APACHE".to_owned(),
+ "dist/generated/types.js".to_owned(),
+ "dist/index.d.ts".to_owned(),
+ "dist/index.js".to_owned(),
+ ]);
+
+ let error = validate_packed_dist_inventory(
+ "@radroots/example",
+ &payload_files,
+ &expected_dist_files,
+ )
+ .expect_err("missing generated declaration rejected");
+
+ assert!(error.contains("dist inventory mismatch"));
+ assert!(error.contains("dist/generated/types.d.ts"));
+ }
+
+ #[test]
+ fn packed_payload_path_normalization_rejects_invalid_paths() {
+ assert_eq!(
+ normalized_package_path("./dist/index.js").expect("valid path"),
+ "dist/index.js"
+ );
+ for invalid in [
+ "",
+ "./",
+ "/dist/index.js",
+ "dist\\index.js",
+ "dist/../index.js",
+ "dist//index.js",
+ "dist/./index.js",
+ ] {
+ let error = normalized_package_path(invalid).expect_err("invalid path rejected");
+ assert!(error.contains("invalid npm pack payload path"));
+ }
+ }
+
+ #[test]
fn pnpm_pack_json_parser_accepts_single_package_object() {
let entry = parse_pnpm_pack_entry(
"@radroots/example",