Skip to content

Commit

Permalink
Merge pull request #121 from extism/fix-console
Browse files Browse the repository at this point in the history
fix: implement console in TS
  • Loading branch information
mhmd-azeez authored Jan 21, 2025
2 parents f11b2d0 + 1eae507 commit 27a1399
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 52 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ endif
echo "Got: $$error_msg"; \
exit 1; \
fi
@extism call examples/console.wasm greet --wasi --input="Benjamin" --log-level=debug
@extism call examples/base64.wasm greet --wasi --input="Benjamin" --log-level=debug

compile-examples: cli
Expand All @@ -82,6 +83,7 @@ compile-examples: cli
./target/release/extism-js examples/host_funcs/script.js -i examples/host_funcs/script.d.ts -o examples/host_funcs.wasm
./target/release/extism-js examples/exports/script.js -i examples/exports/script.d.ts -o examples/exports.wasm
./target/release/extism-js examples/exception/script.js -i examples/exception/script.d.ts -o examples/exception.wasm
./target/release/extism-js examples/console/script.js -i examples/console/script.d.ts -o examples/console.wasm
./target/release/extism-js examples/base64/script.js -i examples/base64/script.d.ts -o examples/base64.wasm

kitchen:
Expand Down
89 changes: 37 additions & 52 deletions crates/core/src/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ pub fn inject_globals(context: &JSContext) -> anyhow::Result<()> {
context.with(|this| {
let module = build_module_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;

let console =
build_console_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
let console_write = build_console_writer(this.clone())?;
let var = build_var_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
let http = build_http_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
let cfg = build_config_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
Expand All @@ -26,7 +25,7 @@ pub fn inject_globals(context: &JSContext) -> anyhow::Result<()> {
let mem = build_memory(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
let host = build_host_object(this.clone()).map_err(|e| to_js_error(this.clone(), e))?;
let global = this.globals();
global.set("console", console)?;
global.set("__consoleWrite", console_write)?;
global.set("module", module)?;
global.set("Host", host)?;
global.set("Var", var)?;
Expand Down Expand Up @@ -74,19 +73,6 @@ extern "C" {
) -> u64;
}

fn get_args_as_str(args: &Rest<Value>) -> anyhow::Result<String> {
args.iter()
.map(|arg| {
arg.clone()
.into_string()
.ok_or(rquickjs::Error::Unknown)
.and_then(|s| s.to_string())
})
.collect::<Result<Vec<String>, _>>()
.map(|vec| vec.join(" "))
.context("Failed to convert args to string")
}

fn to_js_error(cx: Ctx, e: anyhow::Error) -> rquickjs::Error {
match e.downcast::<rquickjs::Error>() {
Ok(e) => e,
Expand All @@ -97,44 +83,43 @@ fn to_js_error(cx: Ctx, e: anyhow::Error) -> rquickjs::Error {
}
}

fn build_console_object(this: Ctx) -> anyhow::Result<Object> {
let console = Object::new(this.clone())?;
let console_info_callback = Function::new(
fn build_console_writer<'js>(this: Ctx<'js>) -> Result<Function<'js>, rquickjs::Error> {
Function::new(
this.clone(),
MutFn::new(move |cx, args| {
let statement = get_args_as_str(&args).map_err(|e| to_js_error(cx, e))?;
info!("{}", statement);
Ok::<_, rquickjs::Error>(())
}),
)?;
console.set("log", console_info_callback.clone())?;
console.set("info", console_info_callback)?;

console.set(
"error",
Function::new(
this.clone(),
MutFn::new(move |cx, args| {
let statement = get_args_as_str(&args).map_err(|e| to_js_error(cx, e))?;
warn!("{}", statement);
Ok::<_, rquickjs::Error>(())
}),
),
)?;

console.set(
"debug",
Function::new(
this.clone(),
MutFn::new(move |cx, args| {
let statement = get_args_as_str(&args).map_err(|e| to_js_error(cx, e))?;
debug!("{}", &statement);
Ok::<_, rquickjs::Error>(())
}),
),
)?;
MutFn::new(move |cx: Ctx<'js>, args: Rest<Value<'js>>| {
if args.len() != 2 {
return Err(to_js_error(
cx.clone(),
anyhow!("Expected level and message arguments"),
));
}

let level = args[0]
.as_string()
.and_then(|s| s.to_string().ok())
.ok_or_else(|| {
to_js_error(cx.clone(), anyhow!("Level must be a string"))
})?;

let message = args[1]
.as_string()
.and_then(|s| s.to_string().ok())
.ok_or_else(|| {
to_js_error(cx.clone(), anyhow!("Message must be a string"))
})?;

Ok(console)
match level.as_str() {
"info" | "log" => info!("{}", message),
"warn" => warn!("{}", message),
"error" => error!("{}", message),
"debug" => debug!("{}", message),
"trace" => trace!("{}", message),
_ => warn!("{}", message) // Default to warn for unknown levels, this should never happen
}

Ok(())
}),
)
}

fn build_module_object(this: Ctx) -> anyhow::Result<Object> {
Expand Down
74 changes: 74 additions & 0 deletions crates/core/src/prelude/src/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

declare global {
interface Console {
debug(...data: any[]): void;
error(...data: any[]): void;
info(...data: any[]): void;
log(...data: any[]): void;
warn(...data: any[]): void;
}

/**
* @internal
*/
var __consoleWrite: (level: string, message: string) => void;
}

function stringifyArg(arg: any): string {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';

if (typeof arg === 'symbol') return arg.toString();
if (typeof arg === 'bigint') return `${arg}n`;
if (typeof arg === 'function') return `[Function ${arg.name ? `${arg.name}` : '(anonymous)'}]`;

if (typeof arg === 'object') {
if (arg instanceof Error) {
return arg.stack || `${arg.name}: ${arg.message}`;
}
if (arg instanceof Set) {
return `Set(${arg.size}) { ${Array.from(arg).map(String).join(', ')} }`;
}
if (arg instanceof Map) {
return `Map(${arg.size}) { ${Array.from(arg).map(([k, v]) => `${k} => ${v}`).join(', ')} }`;
}
if (Array.isArray(arg)) {
const items = [];
for (let i = 0; i < arg.length; i++) {
items.push(i in arg ? stringifyArg(arg[i]) : '<empty>');
}
return `[ ${items.join(', ')} ]`;
}

// For regular objects, use JSON.stringify first for clean output
try {
return JSON.stringify(arg);
} catch {
// For objects that can't be JSON stringified (circular refs etc)
// fall back to Object.prototype.toString behavior
return Object.prototype.toString.call(arg);
}
}

return String(arg);
}

function createLogFunction(level: string) {
return function (...args: any[]) {
const message = args.map(stringifyArg).join(' ');
__consoleWrite(level, message);
};
}

const console = {
trace: createLogFunction('trace'),
debug: createLogFunction('debug'),
log: createLogFunction('info'),
info: createLogFunction('info'),
warn: createLogFunction('warn'),
error: createLogFunction('error'),
};

globalThis.console = console;

export { };
1 change: 1 addition & 0 deletions crates/core/src/prelude/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ import "./http";
import "./memory";
import "./memory-handle";
import "./var";
import "./console";
6 changes: 6 additions & 0 deletions examples/console/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": [],
"types": ["../../crates/core/src/prelude"]
}
}
3 changes: 3 additions & 0 deletions examples/console/script.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "main" {
export function greet(): I32;
}
49 changes: 49 additions & 0 deletions examples/console/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* An example of a plugin that uses Console extensively, CJS flavored plug-in:
*/

function greet() {
const n = 1;
tryPrint('n + 1', n + 1);
tryPrint('multiple string args', 'one', 'two', 'three');
tryPrint('single n', n);
tryPrint('three ns', n, n, n);
tryPrint('n with label', 'n', n);
tryPrint('boolean', true);
tryPrint('null', null);
tryPrint('undefined', undefined);
tryPrint('empty object', {});
tryPrint('empty array', []);
tryPrint('object with key', { key: 'value' });
console.warn('This is a warning', 123);
console.error('This is an error', 456);
console.info('This is an info', 789);
console.debug('This is a debug', 101112);
console.trace('This is a trace', 131415);

console.log('This is an object', { key: 'value' });
console.log('This is an array', [1, 2, 3]);
console.log('This is a string', 'Hello, World!');
console.log('This is a number', 123);
console.log('This is a boolean', true);
console.log('This is a null', null);
console.log('This is an undefined', undefined);
console.log('This is a function', function() {});
console.log('This is a symbol', Symbol('test'));
console.log('This is a date', new Date());
console.log('This is an error', new Error('Hi there!'));
console.log('This is a map', new Map([[1, 'one'], [2, 'two']] ));
}

function tryPrint(text, ...args) {
try {
console.log(...args);
console.log(`${text} - ✅`);
} catch (e) {
console.log(`${text} - ❌ - ${e.message}`);
}
console.log('------')
}


module.exports = { greet };

0 comments on commit 27a1399

Please sign in to comment.