mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
M7: WASM plugins (#101)
Sandboxed WebAssembly plugins whose API is the command registry: - figcraft-plugin (new crate, L2): the figcraft-plugin.json manifest (menu commands with parameters, read/write access, command allowlist) and the sandbox: wasmi (pure Rust, no JIT, also in the web build) with fuel metering, a memory cap, no WASI and seven host functions (call, selection, reply_len, reply_read, log, notify, set_output). - Engine: plugin.list / plugin.import / plugin.remove / plugin.run, plugins.runLast (Alt+Cmd+P; Ctrl+Alt+Shift+P on Windows/Linux) and plugins.manage. A run is one undo step (Session::grouped, which command.batch now uses too) and is rolled back when it fails. Files, other documents, the undo history, dialogs and plugins are off limits; read-only plugins can't change the document. Installed plugins live in the preferences; development plugins re-read their module every run. - UI: installed plugins in the Plugins menu, a parameters form drawn from the manifest, Manage plugins, Plugins > Development > Import plugin from manifest. - plugins/: a no_std Rust SDK and two example plugins (Tidy names, Swatch grid), built for wasm32 by `cargo xtask plugins` (part of `cargo xtask wasm`). - Docs: docs/plugins.md (manifest, permissions, limits, ABI, SDK), docs/mcp.md, docs/control-protocol.md, README, ROADMAP, parity. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const TABLE: &[(&str, u8)] = &[
|
||||
("layout", 2),
|
||||
("text", 2),
|
||||
("pathops", 2),
|
||||
("plugin", 2),
|
||||
("render", 3),
|
||||
("format", 3),
|
||||
("svg", 4),
|
||||
|
||||
+47
-2
@@ -6,7 +6,10 @@
|
||||
//! - `parity` menu-catalog coverage → docs/parity.md
|
||||
//! - `version` print the version, or `version set X.Y.Z`
|
||||
//! - `icon` regenerate the app icon PNGs/ICO from code (original artwork)
|
||||
//! - `wasm` check the core crates and web app for wasm32
|
||||
//! - `wasm` check the core crates and web app for wasm32, and build the plugin SDK and examples
|
||||
//! - `plugins` format-check, lint, test and build the plugin SDK and example plugins (`plugins/`, a
|
||||
//! workspace of its own) for wasm32: `<target>/plugins/dist/<example>/` holds each manifest and
|
||||
//! module, ready for Plugins ▸ Development ▸ Import plugin from manifest…
|
||||
//! - `bench` release-build timings at 1k/10k/50k layers (`bench 1000,10000` for other sizes) and
|
||||
//! with a synthetic design system used by 2,000 instances
|
||||
|
||||
@@ -39,9 +42,10 @@ fn main() -> ExitCode {
|
||||
"version" => version(args.get(1).map(String::as_str), args.get(2).map(String::as_str)),
|
||||
"icon" => icon::generate(&root()),
|
||||
"wasm" => wasm(),
|
||||
"plugins" => plugins(),
|
||||
"bench" => bench(args.get(1).map_or("1000,10000,50000", String::as_str)),
|
||||
_ => {
|
||||
eprintln!("tasks: ci, layers, assets, parity, version [set X.Y.Z], icon, wasm, bench [SIZES]");
|
||||
eprintln!("tasks: ci, layers, assets, parity, version [set X.Y.Z], icon, wasm, plugins, bench [SIZES]");
|
||||
task == "help"
|
||||
}
|
||||
};
|
||||
@@ -59,6 +63,47 @@ fn ci() -> bool {
|
||||
|
||||
fn wasm() -> bool {
|
||||
run(cargo().args(["check", "--target", "wasm32-unknown-unknown", "-p", "figcraft-engine", "-p", "figcraft-ui-egui", "-p", "figcraft-web"]))
|
||||
&& plugins()
|
||||
}
|
||||
|
||||
/// The example plugins in `plugins/examples` (each folder's name is its package; its module is
|
||||
/// `<name with _>.wasm`).
|
||||
const EXAMPLE_PLUGINS: &[&str] = &["tidy-names", "swatch-grid"];
|
||||
|
||||
/// The plugin workspace: fmt, clippy and build for wasm32, host tests, then each example's manifest
|
||||
/// and module side by side in `<target>/plugins/dist/<example>/`.
|
||||
fn plugins() -> bool {
|
||||
let root = root();
|
||||
let manifest = root.join("plugins").join("Cargo.toml");
|
||||
let target = std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| root.join("target")).join("plugins");
|
||||
let ws = |args: &[&str]| {
|
||||
let mut c = cargo();
|
||||
c.args(args).arg("--manifest-path").arg(&manifest);
|
||||
c
|
||||
};
|
||||
let wasm = ["--target", "wasm32-unknown-unknown", "--locked"];
|
||||
let ok = run(&mut ws(&["fmt", "--all", "--check"]))
|
||||
&& run(ws(&["clippy", "--workspace", "--release"]).args(wasm).arg("--target-dir").arg(&target).args(["--", "-D", "warnings"]))
|
||||
&& run(ws(&["test", "--workspace", "--locked"]).arg("--target-dir").arg(&target))
|
||||
&& run(ws(&["build", "--workspace", "--release"]).args(wasm).arg("--target-dir").arg(&target));
|
||||
if !ok {
|
||||
return false;
|
||||
}
|
||||
for name in EXAMPLE_PLUGINS {
|
||||
let module = format!("{}.wasm", name.replace('-', "_"));
|
||||
let dist = target.join("dist").join(name);
|
||||
let copied = std::fs::create_dir_all(&dist)
|
||||
.and_then(|()| std::fs::copy(root.join("plugins/examples").join(name).join("figcraft-plugin.json"), dist.join("figcraft-plugin.json")))
|
||||
.and_then(|_| std::fs::copy(target.join("wasm32-unknown-unknown/release").join(&module), dist.join(&module)));
|
||||
match copied {
|
||||
Ok(bytes) => eprintln!("plugins: {} ({} KB)", dist.display(), bytes / 1024),
|
||||
Err(e) => {
|
||||
eprintln!("plugins: {name}: {e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Engine/render timings (`figcraft-cli perf`) and UI frame timings (an ignored test driving the
|
||||
|
||||
Reference in New Issue
Block a user