From 04c1bdf4a36f4e705f04914b3a8dddf2122c2ee2 Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Sat, 20 Jun 2026 13:28:52 +0000 Subject: [PATCH 1/2] feat: give processor engines http-nu's commands via a store base engine --- Cargo.lock | 5 ++--- Cargo.toml | 5 ++++- src/engine.rs | 20 +++++++++++++++++--- src/store.rs | 11 +++++++++++ src/test_engine.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 130f687d..371335f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1166,9 +1166,8 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "cross-stream" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8138385fb398870cda06244639caa3bf928a1452b8ee5a37c46f3359df87f63" +version = "0.13.4-dev" +source = "git+https://github.com/cablehead/xs?branch=feat%2Fengine-base#5bc1bcd0d1d0bcc1cc46233246afa80b8ea2cd12" dependencies = [ "base64 0.22.1", "bon", diff --git a/Cargo.toml b/Cargo.toml index e04d7517..756bcc34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,11 @@ syntect-assets = "0.23.6" pulldown-cmark = "0.12.2" notify = "8" +# TEMPORARY: track the xs feat/engine-base branch for `Store::with_base_engine` +# (xs PR #144). Return to a published version once that lands and is released. [dependencies.cross-stream] -version = "0.13.3" +git = "https://github.com/cablehead/xs" +branch = "feat/engine-base" optional = true [features] diff --git a/src/engine.rs b/src/engine.rs index c5ffcccd..17ba8652 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -374,9 +374,7 @@ impl Engine { }; let res = xs::nu::add_core_commands(&mut xe, store) .and_then(|()| xs::nu::add_read_commands(&mut xe, store, xs::nu::ReadMode::Stream)) - .and_then(|()| { - xs::nu::add_write_commands(&mut xe, store, xs::nu::AppendMode::Direct) - }); + .and_then(|()| xs::nu::add_write_commands(&mut xe, store, xs::nu::AppendMode::Direct)); self.state = xe.state; res.map_err(|e| Error::from(e.to_string())) } @@ -389,6 +387,22 @@ impl Engine { Box::new(MjCompileCommand::with_store(store.clone())), ]) } + + /// Add http-nu's `.mj` (store-backed), `.md`, and highlight commands to this + /// engine, to use as an xs processor base via `Store::with_base_engine` so + /// actors, services, and actions can use them. See xs ADR 0007. + #[cfg(feature = "cross-stream")] + pub fn add_processor_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> { + self.add_commands(vec![ + Box::new(MjCommand::with_store(store.clone())), + Box::new(MjCompileCommand::with_store(store.clone())), + Box::new(MjRenderCommand::new()), + Box::new(MdCommand::new()), + Box::new(HighlightCommand::new()), + Box::new(HighlightThemeCommand::new()), + Box::new(HighlightLangCommand::new()), + ]) + } } /// Creates an engine from a script by cloning a base engine and parsing the closure. diff --git a/src/store.rs b/src/store.rs index cb0a8a44..7cc11b0e 100644 --- a/src/store.rs +++ b/src/store.rs @@ -32,6 +32,17 @@ impl Store { ) -> Result { let inner = xs::store::Store::new(path.clone())?; + // Hand the processors a base engine with http-nu's `.mj`, `.md`, and + // highlight commands so actors, services, and actions can use them, not + // just HTTP handlers. prepared_base clones this base per spawn. See xs + // ADR 0007. Set before any clone so all share it. + let inner = { + let mut base = crate::Engine::new().expect("Failed to build processor base engine"); + base.add_processor_commands(&inner) + .expect("Failed to register processor base commands"); + inner.with_base_engine(base.state) + }; + // API server let store_for_api = inner.clone(); tokio::spawn(async move { diff --git a/src/test_engine.rs b/src/test_engine.rs index 3ea714ff..3bb2ad57 100644 --- a/src/test_engine.rs +++ b/src/test_engine.rs @@ -8,6 +8,33 @@ fn eval_engine() -> Engine { engine } +#[cfg(feature = "cross-stream")] +#[test] +fn test_processor_base_command_surface() { + use nu_protocol::engine::StateWorkingSet; + + let tmp = tempfile::TempDir::new().unwrap(); + let store = xs::store::Store::new(tmp.path().to_path_buf()).unwrap(); + let mut engine = Engine::new().unwrap(); + engine.add_processor_commands(&store).unwrap(); + + // The context-free commands a processor needs are present... + let ws = StateWorkingSet::new(&engine.state); + assert!( + ws.find_decl(b".mj").is_some(), + ".mj must be on the processor base" + ); + assert!( + ws.find_decl(b".md").is_some(), + ".md must be on the processor base" + ); + // ...and request-scoped commands are not. + assert!( + ws.find_decl(b".static").is_none(), + ".static is request-scoped and must not be on the processor base" + ); +} + #[test] fn test_engine_eval() { let mut engine = Engine::new().unwrap(); From b460336743dc39fd723597fc4cf7f1d116fc6f98 Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Sat, 20 Jun 2026 13:28:52 +0000 Subject: [PATCH 2/2] test: processor base carries .mj/.md but not request-scoped commands --- src/test_engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test_engine.rs b/src/test_engine.rs index 3bb2ad57..48f52700 100644 --- a/src/test_engine.rs +++ b/src/test_engine.rs @@ -18,7 +18,7 @@ fn test_processor_base_command_surface() { let mut engine = Engine::new().unwrap(); engine.add_processor_commands(&store).unwrap(); - // The context-free commands a processor needs are present... + // .mj and .md are present... let ws = StateWorkingSet::new(&engine.state); assert!( ws.find_decl(b".mj").is_some(), @@ -28,10 +28,10 @@ fn test_processor_base_command_surface() { ws.find_decl(b".md").is_some(), ".md must be on the processor base" ); - // ...and request-scoped commands are not. + // ...and .static, an HTTP-handler command, is not. assert!( ws.find_decl(b".static").is_none(), - ".static is request-scoped and must not be on the processor base" + ".static must not be on the processor base" ); }