diff options
-rw-r--r-- | cli/tests/integration_tests.rs | 5 | ||||
-rw-r--r-- | cli/tests/unit/console_test.ts | 38 | ||||
-rw-r--r-- | core/bindings.rs | 52 | ||||
-rw-r--r-- | extensions/console/02_console.js | 27 | ||||
-rw-r--r-- | runtime/js/99_main.js | 16 |
5 files changed, 119 insertions, 19 deletions
diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 5c4e43d66..d93c9ae1c 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -4556,6 +4556,7 @@ console.log("finish"); child.wait().unwrap(); } + #[derive(Debug)] enum TestStep { StdOut(&'static str), StdErr(&'static str), @@ -4806,6 +4807,9 @@ console.log("finish"); // Expect the number {i} on stdout. let s = i.to_string(); assert_eq!(stdout_lines.next().unwrap(), s); + // Expect console.log + let s = r#"{"method":"Runtime.consoleAPICalled","#; + assert!(socket_rx.next().await.unwrap().starts_with(s)); // Expect hitting the `debugger` statement. let s = r#"{"method":"Debugger.paused","#; assert!(socket_rx.next().await.unwrap().starts_with(s)); @@ -4918,6 +4922,7 @@ console.log("finish"); WsSend( r#"{"id":6,"method":"Runtime.evaluate","params":{"expression":"console.error('done');","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":1,"returnByValue":true,"generatePreview":true,"userGesture":true,"awaitPromise":false,"replMode":true}}"#, ), + WsRecv(r#"{"method":"Runtime.consoleAPICalled"#), WsRecv(r#"{"id":6,"result":{"result":{"type":"undefined"}}}"#), StdErr("done"), ]; diff --git a/cli/tests/unit/console_test.ts b/cli/tests/unit/console_test.ts index edb1b245f..776e3ce2d 100644 --- a/cli/tests/unit/console_test.ts +++ b/cli/tests/unit/console_test.ts @@ -315,25 +315,25 @@ unitTest(function consoleTestStringifyCircular(): void { assertEquals( stringify(console), `console { - log: [Function: log], - debug: [Function: debug], - info: [Function: info], - dir: [Function: dir], - dirxml: [Function: dir], - warn: [Function: warn], - error: [Function: error], - assert: [Function: assert], - count: [Function: count], - countReset: [Function: countReset], - table: [Function: table], - time: [Function: time], - timeLog: [Function: timeLog], - timeEnd: [Function: timeEnd], - group: [Function: group], - groupCollapsed: [Function: group], - groupEnd: [Function: groupEnd], - clear: [Function: clear], - trace: [Function: trace], + log: [Function: bound ], + debug: [Function: bound ], + info: [Function: bound ], + dir: [Function: bound ], + dirxml: [Function: bound ], + warn: [Function: bound ], + error: [Function: bound ], + assert: [Function: bound ], + count: [Function: bound ], + countReset: [Function: bound ], + table: [Function: bound ], + time: [Function: bound ], + timeLog: [Function: bound ], + timeEnd: [Function: bound ], + group: [Function: bound ], + groupCollapsed: [Function: bound ], + groupEnd: [Function: bound ], + clear: [Function: bound ], + trace: [Function: bound ], indentLevel: 0, [Symbol(isConsoleInstance)]: true }`, diff --git a/core/bindings.rs b/core/bindings.rs index c96a8559c..fd683b3ba 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -59,6 +59,9 @@ lazy_static::lazy_static! { v8::ExternalReference { function: memory_usage.map_fn_to(), }, + v8::ExternalReference { + function: call_console.map_fn_to(), + }, ]); } @@ -134,6 +137,7 @@ pub fn initialize_context<'s>( set_func(scope, core_val, "getPromiseDetails", get_promise_details); set_func(scope, core_val, "getProxyDetails", get_proxy_details); set_func(scope, core_val, "memoryUsage", memory_usage); + set_func(scope, core_val, "callConsole", call_console); set_func(scope, core_val, "createHostObject", create_host_object); // Direct bindings on `window`. @@ -460,6 +464,54 @@ fn eval_context( rv.set(to_v8(tc_scope, output).unwrap()); } +/// This binding should be used if there's a custom console implementation +/// available. Using it will make sure that proper stack frames are displayed +/// in the inspector console. +/// +/// Each method on console object should be bound to this function, eg: +/// ```ignore +/// function wrapConsole(consoleFromDeno, consoleFromV8) { +/// const callConsole = core.callConsole; +/// +/// for (const key of Object.keys(consoleFromV8)) { +/// if (consoleFromDeno.hasOwnProperty(key)) { +/// consoleFromDeno[key] = callConsole.bind( +/// consoleFromDeno, +/// consoleFromV8[key], +/// consoleFromDeno[key], +/// ); +/// } +/// } +/// } +/// ``` +/// +/// Inspired by: +/// https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/src/inspector_js_api.cc#L194-L222 +fn call_console( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + assert!(args.length() >= 2); + + assert!(args.get(0).is_function()); + assert!(args.get(1).is_function()); + + let mut call_args = vec![]; + for i in 2..args.length() { + call_args.push(args.get(i)); + } + + let receiver = args.this(); + let inspector_console_method = + v8::Local::<v8::Function>::try_from(args.get(0)).unwrap(); + let deno_console_method = + v8::Local::<v8::Function>::try_from(args.get(1)).unwrap(); + + inspector_console_method.call(scope, receiver.into(), &call_args); + deno_console_method.call(scope, receiver.into(), &call_args); +} + fn encode( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, diff --git a/extensions/console/02_console.js b/extensions/console/02_console.js index ddfcbf47a..2d293a1eb 100644 --- a/extensions/console/02_console.js +++ b/extensions/console/02_console.js @@ -1774,6 +1774,32 @@ }); } + // A helper function that will bind our own console implementation + // with default implementation of Console from V8. This will cause + // console messages to be piped to inspector console. + // + // We are using `Deno.core.callConsole` binding to preserve proper stack + // frames in inspector console. This has to be done because V8 considers + // the last JS stack frame as gospel for the inspector. In our case we + // specifically want the latest user stack frame to be the one that matters + // though. + // + // Inspired by: + // https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/internal/util/inspector.js#L39-L61 + function wrapConsole(consoleFromDeno, consoleFromV8) { + const callConsole = core.callConsole; + + for (const key of Object.keys(consoleFromV8)) { + if (consoleFromDeno.hasOwnProperty(key)) { + consoleFromDeno[key] = callConsole.bind( + consoleFromDeno, + consoleFromV8[key], + consoleFromDeno[key], + ); + } + } + } + // Expose these fields to internalObject for tests. window.__bootstrap.internals = { ...window.__bootstrap.internals ?? {}, @@ -1790,5 +1816,6 @@ Console, customInspect, inspect, + wrapConsole, }; })(this); diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index db334caea..91d485069 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -459,6 +459,10 @@ delete Object.prototype.__proto__; if (hasBootstrapped) { throw new Error("Worker runtime already bootstrapped"); } + + const consoleFromV8 = window.console; + const wrapConsole = window.__bootstrap.console.wrapConsole; + // Remove bootstrapping data from the global scope delete globalThis.__bootstrap; delete globalThis.bootstrap; @@ -467,6 +471,10 @@ delete Object.prototype.__proto__; Object.defineProperties(globalThis, windowOrWorkerGlobalScope); Object.defineProperties(globalThis, mainRuntimeGlobalProperties); Object.setPrototypeOf(globalThis, Window.prototype); + + const consoleFromDeno = globalThis.console; + wrapConsole(consoleFromDeno, consoleFromV8); + eventTarget.setEventTargetData(globalThis); defineEventHandler(window, "load", null); @@ -539,6 +547,10 @@ delete Object.prototype.__proto__; if (hasBootstrapped) { throw new Error("Worker runtime already bootstrapped"); } + + const consoleFromV8 = window.console; + const wrapConsole = window.__bootstrap.console.wrapConsole; + // Remove bootstrapping data from the global scope delete globalThis.__bootstrap; delete globalThis.bootstrap; @@ -548,6 +560,10 @@ delete Object.prototype.__proto__; Object.defineProperties(globalThis, workerRuntimeGlobalProperties); Object.defineProperties(globalThis, { name: util.readOnly(name) }); Object.setPrototypeOf(globalThis, DedicatedWorkerGlobalScope.prototype); + + const consoleFromDeno = globalThis.console; + wrapConsole(consoleFromDeno, consoleFromV8); + eventTarget.setEventTargetData(globalThis); runtimeStart( |