Demonstrate TypeScript object passing through the complete Emacs-ng ↔ Deno interface stack.
// hello-world.ts
interface Greeting {
message: string;
timestamp: number;
metadata: {
language: string;
version: string;
};
}
const hello: Greeting = {
message: "Hello from TypeScript!",
timestamp: Date.now(),
metadata: {
language: "TypeScript",
version: "5.0"
}
};
hello; // Return to Lisp(eval-ts "const hello = {
message: 'Hello from TypeScript!',
timestamp: Date.now(),
metadata: {language: 'TypeScript', version: '5.0'}
}; hello")
;; Expected return value:
;; ((message . "Hello from TypeScript!")
;; (timestamp . 1733358000000)
;; (metadata . ((language . "TypeScript")
;; (version . "5.0"))))Current Blocker: emacs-sys build requires src/globals.h generated by make globals.h
Resolution Options:
- Fix lib/ build errors preventing globals.h generation
- Use pre-generated bindings from a working build
- Commit generated bindings to repository for CI/development
Command to generate:
cd /home/runner/work/mega/mega
./autogen.sh
./configure --with-gnutls=ifavailable --without-x --without-dbus
make lib
cd src && make globals.hFile: crates/js/src/javascript.rs
Key Changes Needed:
// OLD (commented out):
// use deno::program_state::ProgramState;
// let program_state = ProgramState::build(flags).await?;
// let worker = create_main_worker(&program_state, main_module, permissions);
// NEW (for Deno 0.371):
use deno_runtime::worker::{MainWorker, WorkerOptions};
use deno_runtime::permissions::PermissionsContainer;
use deno_core::ModuleSpecifier;
use std::rc::Rc;
// Create worker options
let module_loader = Rc::new(deno_runtime::module_loader::FsModuleLoader);
let options = WorkerOptions {
module_loader,
..Default::default()
};
// Bootstrap worker
let main_module = ModuleSpecifier::parse("file://github.com/main.js")?;
let mut worker = MainWorker::bootstrap_from_options(
main_module.clone(),
options,
);Function to Update: eval_ts()
#[lisp_fn]
pub fn eval_ts(code: LispStringRef) -> LispObject {
let runtime = unsafe { MAIN_JS_RUNTIME.as_mut() };
// Get or create worker
let worker = runtime.deno_worker.get_or_insert_with(|| {
initialize_deno_worker()
});
// Execute TypeScript code
let code_str = code.to_utf8();
let result = runtime.tokio_runtime
.as_ref()
.unwrap()
.block_on(async {
// Use execute_script for inline code
worker.execute_script(
"<eval>",
deno_core::FastString::from(code_str.as_str())
)
});
match result {
Ok(value) => {
// Convert v8 Value to LispObject
js_value_to_lisp(value, worker)
}
Err(e) => {
// Return error to Lisp
error!("TypeScript eval error: {}", e);
LispObject::from(Qnil)
}
}
}Helper: Convert JS Object to Lisp
fn js_value_to_lisp(value: v8::Global<v8::Value>, worker: &mut MainWorker) -> LispObject {
let scope = &mut worker.js_runtime.handle_scope();
let local = v8::Local::new(scope, value);
if local.is_object() {
let obj = local.to_object(scope).unwrap();
let obj_str = v8::json::stringify(scope, obj).unwrap();
let json_str = obj_str.to_rust_string_lossy(scope);
// Parse JSON and convert to Lisp alist
match serde_json::from_str::<serde_json::Value>(&json_str) {
Ok(json_val) => json_to_lisp(&json_val),
Err(_) => LispObject::from(Qnil)
}
} else if local.is_string() {
let s = local.to_string(scope).unwrap();
let rust_str = s.to_rust_string_lossy(scope);
LispObject::from(rust_str.as_str())
} else if local.is_number() {
let num = local.number_value(scope).unwrap();
LispObject::from(num as i64)
} else {
LispObject::from(Qnil)
}
}
fn json_to_lisp(json: &serde_json::Value) -> LispObject {
match json {
serde_json::Value::Object(map) => {
// Create alist: ((key1 . val1) (key2 . val2) ...)
let mut list = LispObject::from(Qnil);
for (key, val) in map.iter().rev() {
let key_obj = LispObject::from(key.as_str());
let val_obj = json_to_lisp(val);
let pair = LispCons::new(key_obj, val_obj);
list = LispCons::new(pair.into(), list).into();
}
list
}
serde_json::Value::Array(arr) => {
let mut list = LispObject::from(Qnil);
for item in arr.iter().rev() {
let item_obj = json_to_lisp(item);
list = LispCons::new(item_obj, list).into();
}
list
}
serde_json::Value::String(s) => LispObject::from(s.as_str()),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
LispObject::from(i)
} else if let Some(f) = n.as_f64() {
LispObject::from(f)
} else {
LispObject::from(Qnil)
}
}
serde_json::Value::Bool(b) => {
if *b { LispObject::from(Qt) } else { LispObject::from(Qnil) }
}
serde_json::Value::Null => LispObject::from(Qnil),
}
}Once build system works:
# Build with JS feature
cd /home/runner/work/mega/mega
cargo build --features javascript
# Or build entire project
make
# Test from Emacs
./src/emacs -Q --eval "(progn
(require 'js)
(js-initialize)
(message \"%s\" (eval-ts \"const hello = {
message: 'Hello from TypeScript!',
timestamp: Date.now(),
metadata: {language: 'TypeScript', version: '5.0'}
}; hello\"))
(kill-emacs))"The smoke test passes if:
- ✅ TypeScript code compiles without errors
- ✅ Object is created in JS/TS
- ✅ Object is serialized through v8 → Rust
- ✅ Object is converted to Lisp alist structure
- ✅ Lisp receives structured data (not just string)
Success output:
((message . "Hello from TypeScript!") (timestamp . 1733358293847) (metadata (language . "TypeScript") (version . "5.0")))
- ✅ Dependencies: deno_core 0.371, deno_runtime 0.229 ready
- ✅ Rust toolchain: nightly (1.93.0-nightly)
- ✅ Code patterns documented above
- ❌ Build system: Blocked on globals.h generation
- ❌ Implementation: Waiting for working build
- Resolve build blocker - Generate globals.h or use pre-built bindings
- Apply code changes - Update javascript.rs per patterns above
- Build and test - Compile with
--features javascript - Validate smoke test - Run TypeScript hello world
- Document results - Confirm object passing works end-to-end