Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: implement console in TS #121

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

compile-examples: cli
cd examples/react && npm install && npm run build && cd ../..
Expand All @@ -81,6 +82,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

kitchen:
cd examples/kitchen-sink && npm install && npm run build && cd ../..
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>(())
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"))
})?;

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(())
}),
)?;
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>(())
}),
),
)?;

Ok(console)
)
}

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 };
Loading