From 132ec5805b7ebf9c9442273d12867e170ee51d87 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 21 Sep 2026 17:07:54 -0400 Subject: [PATCH 1/4] Find bundled plugin assets through links metadata, not manifest paths --- build.rs | 15 ++++++++++----- plugins/context/Cargo.toml | 1 + plugins/context/build.rs | 11 +++++++++++ plugins/deleted-bodies/Cargo.toml | 1 + plugins/deleted-bodies/build.rs | 11 +++++++++++ plugins/group/Cargo.toml | 1 + plugins/group/build.rs | 11 +++++++++++ plugins/hide-files/Cargo.toml | 1 + plugins/hide-files/build.rs | 11 +++++++++++ plugins/removed-runs/Cargo.toml | 1 + plugins/removed-runs/build.rs | 11 +++++++++++ plugins/summarize/Cargo.toml | 1 + plugins/summarize/build.rs | 11 +++++++++++ plugins/test-bodies/Cargo.toml | 1 + plugins/test-bodies/build.rs | 11 +++++++++++ 15 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 plugins/context/build.rs create mode 100644 plugins/deleted-bodies/build.rs create mode 100644 plugins/group/build.rs create mode 100644 plugins/hide-files/build.rs create mode 100644 plugins/removed-runs/build.rs create mode 100644 plugins/summarize/build.rs create mode 100644 plugins/test-bodies/build.rs diff --git a/build.rs b/build.rs index fdd737f60..d520b65a7 100644 --- a/build.rs +++ b/build.rs @@ -125,11 +125,17 @@ fn native_plugins() { "shared/queries", &mut files, ); - for (dependency, spec) in manifest["dependencies"].as_table().unwrap() { - let Some(path) = spec.get("path").and_then(toml::Value::as_str) else { + for dependency in manifest["dependencies"].as_table().unwrap().keys() { + // Plugin crates publish their asset directory through links metadata. + if !dependency.starts_with("diffr-plugin-") || dependency == "diffr-plugin-sdk" { continue; - }; - let file = root.join(path).join("Cargo.toml"); + } + let variable = format!("DEP_{}_ASSETS", dependency.to_uppercase().replace('-', "_")); + let folder = PathBuf::from( + std::env::var_os(&variable) + .unwrap_or_else(|| panic!("{dependency} did not publish {variable}")), + ); + let file = folder.join("Cargo.toml"); println!("cargo:rerun-if-changed={}", file.display()); let package: toml::Value = std::fs::read_to_string(file).unwrap().parse().unwrap(); let Some(plugin) = package @@ -143,7 +149,6 @@ fn native_plugins() { .get("native") .and_then(toml::Value::as_bool) .unwrap_or(false); - let folder = root.join(path); let plugin_manifest = folder.join("plugin.toml"); let description: toml::Value = std::fs::read_to_string(&plugin_manifest) .expect("a plugin has plugin.toml") diff --git a/plugins/context/Cargo.toml b/plugins/context/Cargo.toml index 8926c1a06..8fbf4e9e5 100644 --- a/plugins/context/Cargo.toml +++ b/plugins/context/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-context" publish = false [dependencies] diff --git a/plugins/context/build.rs b/plugins/context/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/context/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/deleted-bodies/Cargo.toml b/plugins/deleted-bodies/Cargo.toml index 606690399..57f9bea93 100644 --- a/plugins/deleted-bodies/Cargo.toml +++ b/plugins/deleted-bodies/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-deleted-bodies" publish = false [dependencies] diff --git a/plugins/deleted-bodies/build.rs b/plugins/deleted-bodies/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/deleted-bodies/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/group/Cargo.toml b/plugins/group/Cargo.toml index 6573ed15e..e8739f043 100644 --- a/plugins/group/Cargo.toml +++ b/plugins/group/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-group" publish = false [dependencies] diff --git a/plugins/group/build.rs b/plugins/group/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/group/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/hide-files/Cargo.toml b/plugins/hide-files/Cargo.toml index 40769e826..a95e41726 100644 --- a/plugins/hide-files/Cargo.toml +++ b/plugins/hide-files/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-hide-files" publish = false [dependencies] diff --git a/plugins/hide-files/build.rs b/plugins/hide-files/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/hide-files/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/removed-runs/Cargo.toml b/plugins/removed-runs/Cargo.toml index b1f06bdb0..08a8f1fbe 100644 --- a/plugins/removed-runs/Cargo.toml +++ b/plugins/removed-runs/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-removed-runs" publish = false [dependencies] diff --git a/plugins/removed-runs/build.rs b/plugins/removed-runs/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/removed-runs/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/summarize/Cargo.toml b/plugins/summarize/Cargo.toml index 845148d86..5ecee53d8 100644 --- a/plugins/summarize/Cargo.toml +++ b/plugins/summarize/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-summarize" publish = false [dependencies] diff --git a/plugins/summarize/build.rs b/plugins/summarize/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/summarize/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} diff --git a/plugins/test-bodies/Cargo.toml b/plugins/test-bodies/Cargo.toml index b8721eb5a..b0fc5e80e 100644 --- a/plugins/test-bodies/Cargo.toml +++ b/plugins/test-bodies/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.85.0" license = "MIT" +links = "diffr-plugin-test-bodies" publish = false [dependencies] diff --git a/plugins/test-bodies/build.rs b/plugins/test-bodies/build.rs new file mode 100644 index 000000000..b88e3e575 --- /dev/null +++ b/plugins/test-bodies/build.rs @@ -0,0 +1,11 @@ +//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and +//! `plugin.wasm` live, whether the crate comes from the workspace or from +//! the registry. The CLI's build script reads it as +//! `DEP__ASSETS`. +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!( + "cargo::metadata=assets={}", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); +} From 11d168fb50808a89a1aec7ccbbbb066fcab83e72 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 21 Sep 2026 17:08:46 -0400 Subject: [PATCH 2/4] Make diffr-cli, the plugin SDK and the plugins publishable at 0.1.1 --- CHANGELOG.md | 6 + Cargo.lock | 140 +++++++------- Cargo.toml | 28 ++- crates/diffr-plugin-sdk/Cargo.toml | 3 +- crates/diffr-plugin-sdk/src/lib.rs | 2 +- crates/diffr-plugin-sdk/wit/plugin.wit | 253 +++++++++++++++++++++++++ plugins/context/Cargo.toml | 5 +- plugins/deleted-bodies/Cargo.toml | 5 +- plugins/group/Cargo.toml | 5 +- plugins/hide-files/Cargo.toml | 5 +- plugins/removed-runs/Cargo.toml | 5 +- plugins/summarize/Cargo.toml | 5 +- plugins/test-bodies/Cargo.toml | 5 +- tests/plugin_contract.rs | 8 + 14 files changed, 371 insertions(+), 104 deletions(-) create mode 100644 crates/diffr-plugin-sdk/wit/plugin.wit create mode 100644 tests/plugin_contract.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ad58640f5..f404e29bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ diffr's history before this file begins is difftastic's; see https://github.com/Wilfred/difftastic/blob/master/CHANGELOG.md for releases up to 0.71. +## 0.1.1 + +- Published to crates.io as `diffr-cli` (binary `diffr`) and + `diffr-plugin-sdk`, with the bundled plugins as `diffr-plugin-*`. +- `cargo binstall diffr-cli` installs the prebuilt `diffr` from a release. + ## 0.1.0 First public release of diffr as its own project. diff --git a/Cargo.lock b/Cargo.lock index e724bddab..8519f6da8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -676,76 +676,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "diffr-plugin-context" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "diffr-plugin-deleted-bodies" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "diffr-plugin-group" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "diffr-plugin-hide-files" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "diffr-plugin-removed-runs" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "diffr-plugin-sdk" -version = "0.1.0" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "wit-bindgen 0.60.0", -] - -[[package]] -name = "diffr-plugin-summarize" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", - "serde_json", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "diffr-plugin-test-bodies" -version = "0.1.0" -dependencies = [ - "diffr-plugin-sdk", - "serde", -] - -[[package]] -name = "difftastic" -version = "0.1.0" +name = "diffr-cli" +version = "0.1.1" dependencies = [ "anyhow", "assert_cmd", @@ -864,6 +796,74 @@ dependencies = [ "wasmtime-wasi-http", ] +[[package]] +name = "diffr-plugin-context" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + +[[package]] +name = "diffr-plugin-deleted-bodies" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + +[[package]] +name = "diffr-plugin-group" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + +[[package]] +name = "diffr-plugin-hide-files" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + +[[package]] +name = "diffr-plugin-removed-runs" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + +[[package]] +name = "diffr-plugin-sdk" +version = "0.1.1" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "wit-bindgen 0.60.0", +] + +[[package]] +name = "diffr-plugin-summarize" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", + "serde_json", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "diffr-plugin-test-bodies" +version = "0.1.1" +dependencies = [ + "diffr-plugin-sdk", + "serde", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/Cargo.toml b/Cargo.toml index 98e31213b..197d12270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "difftastic" +name = "diffr-cli" description = "Structural diffs with a streaming API and interactive terminal frontend." repository = "https://github.com/devdotfast/diffr" homepage = "https://github.com/devdotfast/diffr" license = "MIT" -version = "0.1.0" +version = "0.1.1" authors = ["dev.fast"] keywords = ["diff", "syntax"] categories = ["development-tools", "command-line-utilities", "parser-implementations"] @@ -23,6 +23,7 @@ rust-version = "1.85.0" include = [ "/build.rs", "/src/", + "/wit/", "/plugins/", "/vendored_parsers/highlights/*.scm", "/vendored_parsers/*-src/**/*.c", @@ -145,14 +146,14 @@ wasmtime-wasi = { version = "34.0.2", default-features = false } bytes = "1.12" # The plugin contract, and the bundled plugins: each a crate the native # registry calls, which also builds as a WASM component. -diffr-plugin-sdk = { path = "crates/diffr-plugin-sdk" } -diffr-plugin-context = { path = "plugins/context" } -diffr-plugin-deleted-bodies = { path = "plugins/deleted-bodies" } -diffr-plugin-group = { path = "plugins/group" } -diffr-plugin-hide-files = { path = "plugins/hide-files" } -diffr-plugin-removed-runs = { path = "plugins/removed-runs" } -diffr-plugin-summarize = { path = "plugins/summarize" } -diffr-plugin-test-bodies = { path = "plugins/test-bodies" } +diffr-plugin-sdk = { path = "crates/diffr-plugin-sdk", version = "0.1.1" } +diffr-plugin-context = { path = "plugins/context", version = "0.1.1" } +diffr-plugin-deleted-bodies = { path = "plugins/deleted-bodies", version = "0.1.1" } +diffr-plugin-group = { path = "plugins/group", version = "0.1.1" } +diffr-plugin-hide-files = { path = "plugins/hide-files", version = "0.1.1" } +diffr-plugin-removed-runs = { path = "plugins/removed-runs", version = "0.1.1" } +diffr-plugin-summarize = { path = "plugins/summarize", version = "0.1.1" } +diffr-plugin-test-bodies = { path = "plugins/test-bodies", version = "0.1.1" } wasmtime-wasi-http = { version = "34.0.2", default-features = false } @@ -210,6 +211,13 @@ lto = "thin" name = "diffr" path = "src/main.rs" +[package.metadata.binstall] +# Release archives are diffr--.tar.gz with diffr at the root; +# tags carry no "v" prefix. +pkg-url = "{ repo }/releases/download/{ version }/diffr-{ version }-{ target }{ archive-suffix }" +pkg-fmt = "tgz" +bin-dir = "{ bin }{ binary-ext }" + [package.metadata.deb] depends = "$auto" section = "utility" diff --git a/crates/diffr-plugin-sdk/Cargo.toml b/crates/diffr-plugin-sdk/Cargo.toml index ae33cc9e7..58715ab4b 100644 --- a/crates/diffr-plugin-sdk/Cargo.toml +++ b/crates/diffr-plugin-sdk/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "diffr-plugin-sdk" description = "The diffr plugin contract in Rust: one trait for native and WASM plugins." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" -publish = false [dependencies] anyhow = "1.0" diff --git a/crates/diffr-plugin-sdk/src/lib.rs b/crates/diffr-plugin-sdk/src/lib.rs index 271ce2f99..f4dedbee1 100644 --- a/crates/diffr-plugin-sdk/src/lib.rs +++ b/crates/diffr-plugin-sdk/src/lib.rs @@ -89,7 +89,7 @@ pub trait Plugin: Sized { #[doc(hidden)] pub mod bindings { wit_bindgen::generate!({ - path: "../../wit", + path: "wit", world: "plugin", pub_export_macro: true, default_bindings_module: "diffr_plugin_sdk::bindings", diff --git a/crates/diffr-plugin-sdk/wit/plugin.wit b/crates/diffr-plugin-sdk/wit/plugin.wit new file mode 100644 index 000000000..c6843a6c3 --- /dev/null +++ b/crates/diffr-plugin-sdk/wit/plugin.wit @@ -0,0 +1,253 @@ +/// diffr's plugin contract. Every plugin implements it, whether diffr runs it +/// natively (a bundled plugin compiled in) or as a WASM component: diffr +/// builds the same records for both and carries out the same moves. +/// +/// A plugin is created once, then shapes each file at two points of its +/// life. `new` runs when diffr builds its pipeline, before any file is +/// listed, and makes the plugin from its options, failing when it cannot run +/// with them. `classify` runs before the stream starts, once per changed +/// file, and returns tags to add to the file's manifest entry: the tags +/// decide how the file is diffed (a `generated` file gets a line diff) and +/// what later plugins see. `mutate` runs after the file is diffed and its +/// region trees are built, and returns moves that decide how the file starts +/// out on screen. `queries` returns named tree-sitter source text once during +/// setup; diffr assembles and validates it before any file is processed. +/// +/// diffr makes one instance of each enabled plugin per run and calls it for +/// every file, so what a plugin keeps from `new` lasts the whole run. +package diffr:plugin@0.2.0; + +interface types { + /// Deferred presentation content for an existing region. This cannot + /// change topology, alignment, fold identity, or collapsed state. + record annotation { + region-id: u32, + label: string, + } + + /// A named query source. diffr compiles all enabled sources per language. + /// Names identify sources for imports and diagnostics; shared names must + /// have identical text. Relative imports resolve against this name. + record query-source { + language: string, + name: string, + text: string, + } + + /// The before (`lhs`) or after (`rhs`) side of a comparison. + enum side { + lhs, + rhs, + } + + /// Git's delta status. + enum file-status { + added, + deleted, + modified, + renamed, + copied, + type-changed, + } + + /// One side of a git delta, as the manifest names it. + record file-ref { + path: string, + /// The blob. Empty when the comparison is not of a repository: two + /// paths compared directly have no blobs. + oid: string, + /// Git's octal mode text, e.g. `100644`. + mode: string, + } + + /// The sides a changed file has. A deletion is `left-only`, an addition + /// `right-only`. + variant file-sides { + both(tuple), + left-only(file-ref), + right-only(file-ref), + } + + /// One changed file, as its manifest entry describes it. `file` says + /// which sides the file has and names each; `status` says how the two + /// relate, which having both sides does not answer: a path that changed + /// is `renamed`, an object kind that changed `type-changed`. + record file-entry { + file: file-sides, + status: file-status, + /// Sorted and deduplicated. During `classify`, the tags so far: the + /// bundled rules, git attributes, and the plugins before this one. + tags: list, + } + + /// 0-based line, and 0-based byte column in the UTF-8 text. + record position { + line: u32, + column: u32, + } + + /// Half-open. + record range { + start: position, + end: position, + } + + /// Bytes painted as changed on one line. + record span { + line: u32, + start-column: u32, + end-column: u32, + } + + /// How a region starts out: collapsed or open, and the label shown while + /// it is collapsed (empty for none). + record visibility { + collapsed: bool, + label: string, + } + + record leaf { + /// Row alignment: the leaf on the other side with the same value + /// lines up with this one. + alignment-id: u32, + changed: list, + } + + /// A leaf tiles the file; a fold's range is the hull of its children. + variant kind { + leaf(leaf), + fold, + } + + /// One region of a side's tree. + record region { + /// Unique across both sides of the file, never 0. + id: u32, + /// The fold that holds this region, or 0 (the file) for a top-level + /// region. + parent: u32, + /// Regions sharing it open and close together, on either side. + fold-state-id: u32, + range: range, + /// The tags the fold queries set, `:`. + tags: list, + visibility: visibility, + kind: kind, + } + + /// One side of a diffed file. `regions` lists the tree in preorder: a + /// fold comes before its children, and siblings keep their order. A + /// binary file's sides have empty text and no regions. + record source { + text: string, + regions: list, + } + + /// The sides a diffed file has, the same sides as its entry's `file`. + variant source-sides { + both(tuple), + left-only(source), + right-only(source), + } + + record cut { + /// A leaf's id. + region: u32, + /// A line offset relative to the leaf's first line, strictly inside + /// it. + at: u32, + } + + /// What a plugin asks for. A region is named by its id, or the file by + /// 0 where a move allows it. Moves are carried out in order, and the next + /// plugin sees the result; a move that cannot be carried out aborts the + /// run with `mutation_failed`. + /// + /// Fresh ids. When a plugin's moves begin, the next fresh id is one above + /// the largest region id in the file (both sides), and the next fresh + /// alignment id one above the largest leaf alignment id. Moves take fresh + /// ids in order, lhs side before rhs side: + /// + /// - a `cut` takes the next alignment id, which the second pieces share, + /// then gives the second piece on each side that holds the leaf (or its + /// paired leaf) the next id, lhs first. Both second pieces take the lhs + /// piece's id (or the only piece's) as their fold state id. The first + /// piece keeps the leaf's ids. + /// - a `join-folds` gives the new fold on each side that holds the listed + /// regions the next id, lhs first; both share the lhs fold's id as + /// their fold state id. + /// + /// No other move takes an id. So a plugin that cuts a leaf with id `l`, + /// paired on the rhs, when the largest id is `n`, names the lhs second + /// piece `n + 1` and the rhs one `n + 2`. + variant move { + /// Split a leaf, and the leaf paired with it, at a line offset. + cut(cut), + /// Wrap two or more consecutive siblings on each side that holds + /// them in a new open, unlabelled fold without tags. + join-folds(list), + /// Every region in the listed regions' fold states takes the first + /// region's fold state id and collapsed state. + link-fold-state(list), + /// Collapse or open every region sharing the region's fold state, or + /// hide or show the file (0). + set-collapsed(tuple), + /// Set or clear the label of one region, or of the file (0). + set-label(tuple>), + /// Replace one region's tags. The file's tags cannot be set here. + set-tags(tuple>), + } +} + +/// What diffr gives every plugin. A component also gets WASI, with full +/// access: the repository's working directory is preopened read-write as +/// `.`, the environment is inherited, and the network is open. What it +/// writes to stdout or stderr reaches diffr's stderr, a line at a time and +/// with the plugin's name in front, since diffr's stdout is the stream. +interface host { + /// Run `git` with these arguments in the repository's working + /// directory: its stdout when it exits successfully, its stderr + /// otherwise. + git: func(args: list) -> result; +} + +/// What every plugin exports: the plugin itself, as a resource. +interface guest { + use types.{file-entry, source-sides, move, query-source, annotation}; + + resource plugin { + /// Make the plugin from `options`, its bundled or external config + /// entry as a JSON object, defaults filled in and validated against + /// the options schema in `plugin.toml`. It runs + /// once, when diffr builds its pipeline. An error is a setup error: + /// diffr stops before the stream starts and names the plugin. (A + /// static function rather than a constructor, since a constructor + /// cannot fail.) + new: static func(options: string) -> result; + + /// Query sources, collected once during setup. An error is a setup + /// error. Plugins without queries return an empty list. + queries: func() -> result, string>; + + /// Tags to add to the file's manifest entry, before any diff runs. + /// Tags are lowercase letters, digits, `-` and `_`. A plugin that + /// does not classify returns an empty list. An error stops diffr + /// before the stream starts. + classify: func(file: file-entry) -> result, string>; + + /// The moves that shape how the diffed file starts out. `sides` are + /// the sides the file has, the same sides as its entry's `file`. An + /// error aborts the run with `mutation_failed`. + mutate: func(file: file-entry, sides: source-sides) -> result, string>; + + /// Optional slow enrichment after every plugin has shaped the file. + /// Errors leave the initial diff usable. Return no annotations when + /// this plugin has no deferred work. + enrich: func(file: file-entry, sides: source-sides) -> result, string>; + } +} + +world plugin { + import host; + export guest; +} diff --git a/plugins/context/Cargo.toml b/plugins/context/Cargo.toml index 8fbf4e9e5..1c9750514 100644 --- a/plugins/context/Cargo.toml +++ b/plugins/context/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-context" description = "The context plugin: collapse unchanged stretches far from any change." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-context" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/plugins/deleted-bodies/Cargo.toml b/plugins/deleted-bodies/Cargo.toml index 57f9bea93..585db2732 100644 --- a/plugins/deleted-bodies/Cargo.toml +++ b/plugins/deleted-bodies/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-deleted-bodies" description = "The deleted-bodies plugin: collapse deleted function bodies." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-deleted-bodies" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/plugins/group/Cargo.toml b/plugins/group/Cargo.toml index e8739f043..9c103dd10 100644 --- a/plugins/group/Cargo.toml +++ b/plugins/group/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-group" description = "The group plugin: wrap runs of collapsed regions in one fold." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-group" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/plugins/hide-files/Cargo.toml b/plugins/hide-files/Cargo.toml index a95e41726..3802a3c1d 100644 --- a/plugins/hide-files/Cargo.toml +++ b/plugins/hide-files/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-hide-files" description = "The hide-files plugin: hide whole files by their tags or status." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-hide-files" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/plugins/removed-runs/Cargo.toml b/plugins/removed-runs/Cargo.toml index 08a8f1fbe..a2e9a6cdb 100644 --- a/plugins/removed-runs/Cargo.toml +++ b/plugins/removed-runs/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-removed-runs" description = "The removed-runs plugin: collapse the middle of long removed stretches." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-removed-runs" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/plugins/summarize/Cargo.toml b/plugins/summarize/Cargo.toml index 5ecee53d8..0315212b3 100644 --- a/plugins/summarize/Cargo.toml +++ b/plugins/summarize/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-summarize" description = "The summarize plugin: pseudocode summaries for large new function bodies." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-summarize" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" wasi = "=0.14.2" diff --git a/plugins/test-bodies/Cargo.toml b/plugins/test-bodies/Cargo.toml index b0fc5e80e..f0a6c4d7d 100644 --- a/plugins/test-bodies/Cargo.toml +++ b/plugins/test-bodies/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "diffr-plugin-test-bodies" description = "The test-bodies plugin: collapse test bodies and test modules." -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85.0" license = "MIT" links = "diffr-plugin-test-bodies" -publish = false [dependencies] -diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk" } +diffr-plugin-sdk = { path = "../../crates/diffr-plugin-sdk", version = "0.1.1" } serde = { version = "1.0", features = ["derive"] } [package.metadata.diffr] diff --git a/tests/plugin_contract.rs b/tests/plugin_contract.rs new file mode 100644 index 000000000..302dc3f32 --- /dev/null +++ b/tests/plugin_contract.rs @@ -0,0 +1,8 @@ +// Both crates ship the contract so they can build independently from crates.io. +#[test] +fn packaged_plugin_contract_matches_host() { + assert_eq!( + include_str!("../wit/plugin.wit"), + include_str!("../crates/diffr-plugin-sdk/wit/plugin.wit"), + ); +} From 40db6d5e7ffbd2150768ac87c3fa429d18dfc6f2 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 21 Sep 2026 17:08:46 -0400 Subject: [PATCH 3/4] Document the crates.io install --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77a180029..407f3d14e 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,16 @@ for every upstream and its license. ## Installation +The CLI, from crates.io (prebuilt via [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), or compiled): + ```sh -cargo xtask install +cargo binstall diffr-cli +cargo install diffr-cli --locked ``` -Run this from the checkout with Rust and [Bun](https://bun.sh) on `PATH`. +The terminal UI is not yet published. From a checkout, with Rust and +[Bun](https://bun.sh) installed, `cargo xtask install` builds and installs +both the CLI and the UI. ## Configuration From 1bf4fbf9780390851d546a698d6b7c394f8826b8 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 21 Sep 2026 17:15:38 -0400 Subject: [PATCH 4/4] Trim distribution comments --- Cargo.toml | 3 +-- build.rs | 2 +- plugins/context/build.rs | 4 ---- plugins/deleted-bodies/build.rs | 4 ---- plugins/group/build.rs | 4 ---- plugins/hide-files/build.rs | 4 ---- plugins/removed-runs/build.rs | 4 ---- plugins/summarize/build.rs | 4 ---- plugins/test-bodies/build.rs | 4 ---- tests/plugin_contract.rs | 2 +- 10 files changed, 3 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 197d12270..9a180a8f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,8 +212,7 @@ name = "diffr" path = "src/main.rs" [package.metadata.binstall] -# Release archives are diffr--.tar.gz with diffr at the root; -# tags carry no "v" prefix. +# Tags have no "v" prefix; binaries sit at the archive root. pkg-url = "{ repo }/releases/download/{ version }/diffr-{ version }-{ target }{ archive-suffix }" pkg-fmt = "tgz" bin-dir = "{ bin }{ binary-ext }" diff --git a/build.rs b/build.rs index d520b65a7..9682ce1c9 100644 --- a/build.rs +++ b/build.rs @@ -126,7 +126,7 @@ fn native_plugins() { &mut files, ); for dependency in manifest["dependencies"].as_table().unwrap().keys() { - // Plugin crates publish their asset directory through links metadata. + // Resolve assets through Cargo links metadata. if !dependency.starts_with("diffr-plugin-") || dependency == "diffr-plugin-sdk" { continue; } diff --git a/plugins/context/build.rs b/plugins/context/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/context/build.rs +++ b/plugins/context/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/deleted-bodies/build.rs b/plugins/deleted-bodies/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/deleted-bodies/build.rs +++ b/plugins/deleted-bodies/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/group/build.rs b/plugins/group/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/group/build.rs +++ b/plugins/group/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/hide-files/build.rs b/plugins/hide-files/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/hide-files/build.rs +++ b/plugins/hide-files/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/removed-runs/build.rs b/plugins/removed-runs/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/removed-runs/build.rs +++ b/plugins/removed-runs/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/summarize/build.rs b/plugins/summarize/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/summarize/build.rs +++ b/plugins/summarize/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/plugins/test-bodies/build.rs b/plugins/test-bodies/build.rs index b88e3e575..e91be7633 100644 --- a/plugins/test-bodies/build.rs +++ b/plugins/test-bodies/build.rs @@ -1,7 +1,3 @@ -//! Tells the diffr CLI where this plugin's `plugin.toml`, `queries/` and -//! `plugin.wasm` live, whether the crate comes from the workspace or from -//! the registry. The CLI's build script reads it as -//! `DEP__ASSETS`. fn main() { println!("cargo::rerun-if-changed=build.rs"); println!( diff --git a/tests/plugin_contract.rs b/tests/plugin_contract.rs index 302dc3f32..46d2e1499 100644 --- a/tests/plugin_contract.rs +++ b/tests/plugin_contract.rs @@ -1,4 +1,4 @@ -// Both crates ship the contract so they can build independently from crates.io. +// Keep the independently packaged contracts identical. #[test] fn packaged_plugin_contract_matches_host() { assert_eq!(