From f0966e8022f19f2f88fa8de390562a191a0b4314 Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Wed, 9 Feb 2022 15:10:06 +0100 Subject: [PATCH 1/8] Capture console calls --- packages/engine/src/CellEvaluator.ts | 12 +- packages/engine/src/Engine.test.ts | 214 +++++++++++++----- packages/engine/src/Engine.ts | 47 +++- packages/engine/src/HookExecution.ts | 41 +++- .../src/__snapshots__/Engine.test.ts.snap | 34 ++- packages/engine/src/executor.ts | 2 +- 6 files changed, 268 insertions(+), 82 deletions(-) diff --git a/packages/engine/src/CellEvaluator.ts b/packages/engine/src/CellEvaluator.ts index bbd101599..cb4803b2e 100644 --- a/packages/engine/src/CellEvaluator.ts +++ b/packages/engine/src/CellEvaluator.ts @@ -1,4 +1,5 @@ import { TypeCellContext } from "./context"; +import { ConsolePayload } from "./Engine"; import { ModuleExecution, runModule } from "./executor"; import { HookExecution } from "./HookExecution"; import { createExecutionScope, getModulesFromTypeCellCode } from "./modules"; @@ -10,7 +11,8 @@ export function createCellEvaluator( typecellContext: TypeCellContext, resolveImport: (module: string) => Promise, setAndWatchOutput = true, - onOutputChanged: (output: any) => void, + onOutputEvent: (output: any) => void, + onConsoleEvent: (console: ConsolePayload) => void, beforeExecuting: () => void ) { function onExecuted(exports: any) { @@ -42,15 +44,15 @@ export function createCellEvaluator( }); } } - onOutputChanged(newExports); + onOutputEvent(newExports); } function onError(error: any) { // log.warn("cellEvaluator onError", cell.path, error); - onOutputChanged(error); + onOutputEvent(error); } - const hookExecution = new HookExecution(); + const hookExecution = new HookExecution(onConsoleEvent); const executionScope = createExecutionScope( typecellContext, hookExecution.context @@ -84,7 +86,7 @@ export function createCellEvaluator( } catch (e) { console.error(e); // log.warn("cellEvaluator error evaluating", cell.path, e); - onOutputChanged(e); + onOutputEvent(e); } } diff --git a/packages/engine/src/Engine.test.ts b/packages/engine/src/Engine.test.ts index f4e5164a4..adb5d2141 100644 --- a/packages/engine/src/Engine.test.ts +++ b/packages/engine/src/Engine.test.ts @@ -7,92 +7,186 @@ import { toAMDFormat, waitTillEvent, } from "./tests/util/helpers"; - -const getModel1 = () => - buildMockedModel( - "model1", - `let x = 4; +import { CodeModelMock } from "./tests/util/CodeModelMock"; + +describe("engine class execution", function () { + describe("basic model execution", () => { + const getModel1 = () => + buildMockedModel( + "model1", + `let x = 4; let y = 6; let sum = x + y; exports.sum = sum; exports.default = sum;` - ); + ); -const getModel2 = () => - buildMockedModel("model2", `exports.default = $.sum - 5;`); + const getModel2 = () => + buildMockedModel("model2", `exports.default = $.sum - 5;`); -describe("engine class", () => { - it("should execute a single model", async () => { - const engine = new Engine(importResolver); - engine.registerModel(getModel1()); + it("should execute a single model", async () => { + const engine = new Engine(importResolver); + engine.registerModel(getModel1()); - const { model, output } = await event.Event.toPromise(engine.onOutput); + const { model, output } = await event.Event.toPromise(engine.onOutput); - expect(model.path).toBe("model1"); - expect(output.sum).toBe(10); - expect(output.default).toBe(10); - }); + expect(model.path).toBe("model1"); + expect(output.sum).toBe(10); + expect(output.default).toBe(10); + }); - it("should read exported variables from other models", async () => { - const engine = new Engine(importResolver); - engine.registerModel(getModel1()); - await event.Event.toPromise(engine.onOutput); + it("should read exported variables from other models", async () => { + const engine = new Engine(importResolver); + engine.registerModel(getModel1()); + await event.Event.toPromise(engine.onOutput); - engine.registerModel(getModel2()); - const { model, output } = await event.Event.toPromise(engine.onOutput); + engine.registerModel(getModel2()); + const { model, output } = await event.Event.toPromise(engine.onOutput); - expect(model.path).toBe("model2"); - expect(output.default).toBe(5); - }); + expect(model.path).toBe("model2"); + expect(output.default).toBe(5); + }); - it("should re-evaluate code after change", async () => { - const engine = new Engine(importResolver); - const model1 = getModel1(); + it("should re-evaluate code after change", async () => { + const engine = new Engine(importResolver); + const model1 = getModel1(); - engine.registerModel(model1); - await event.Event.toPromise(engine.onOutput); + engine.registerModel(model1); + await event.Event.toPromise(engine.onOutput); - model1.updateCode( - toAMDFormat(`let x = 0; + model1.updateCode( + toAMDFormat(`let x = 0; let y = 6; let sum = x + y; exports.sum = sum; exports.default = sum;`) - ); + ); - const { output } = await event.Event.toPromise(engine.onOutput); + const { output } = await event.Event.toPromise(engine.onOutput); - expect(output.sum).toBe(6); - expect(output.default).toBe(6); - }); + expect(output.sum).toBe(6); + expect(output.default).toBe(6); + }); - it("should re-evaluate other models when global variable changes", async () => { - const engine = new Engine(importResolver); - // TODO: Expected 4 events. Figure out why model 2 re-evaluates. - const eventsPromise = waitTillEvent(engine.onOutput, 5); - const model1 = getModel1(); - const model2 = getModel2(); + it("should re-evaluate other models when global variable changes", async () => { + const engine = new Engine(importResolver); + // TODO: Expected 4 events. Figure out why model 2 re-evaluates. + const eventsPromise = waitTillEvent(engine.onOutput, 5); + const model1 = getModel1(); + const model2 = getModel2(); - engine.registerModel(model1); - engine.registerModel(model2); + engine.registerModel(model1); + engine.registerModel(model2); - model1.updateCode( - toAMDFormat(`let x = 0; + model1.updateCode( + toAMDFormat(`let x = 0; let y = 6; let sum = x + y; exports.sum = sum; exports.default = sum;`) - ); - - const events = await eventsPromise; - const eventsSnapshot = events.map((event) => ({ - path: event.model.path, - output: event.output, - })); - const finalEvent = eventsSnapshot[eventsSnapshot.length - 1]; - - expect(finalEvent.path).toBe("model2"); - expect(finalEvent.output.default).toBe(1); - expect(eventsSnapshot).toMatchSnapshot(); + ); + + const events = await eventsPromise; + const eventsSnapshot = events.map((event) => ({ + path: event.model.path, + output: event.output, + })); + const finalEvent = eventsSnapshot[eventsSnapshot.length - 1]; + + expect(finalEvent.path).toBe("model2"); + expect(finalEvent.output.default).toBe(1); + expect(eventsSnapshot).toMatchSnapshot(); + }); + }); + + describe("console messages", () => { + const getModel1 = () => buildMockedModel("model1", `console.log('hi!');`); + const getModel2 = () => + buildMockedModel( + "model2", + `console.info('info'); console.warn('warn'); console.error('error');` + ); + const getModel3 = () => + buildMockedModel( + "model3", + `console.log('before'); + await new Promise((resolve)=> { + setTimeout(()=> { + resolve(); + }, 1) + }); + console.log('after');` + ); + const getModel4 = () => + new CodeModelMock( + "javascript", + "model4", + `define(["require", "exports", "logdown"], function(require, exports, logdown) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + let logger = logdown("warning message"); + logger.state.isEnabled = true; + logger.log("message 1"); + + setTimeout(() => { + let logger = logdown("warning message"); + logger.state.isEnabled = true; + logger.log("message2"); + }, 1); + });` + ); + + it("should capture console.log message", async () => { + const engine = new Engine(importResolver); + const eventsPromise = waitTillEvent(engine.onConsole, 1); + const model1 = getModel1(); + + engine.registerModel(model1); + + const consoleEvents = await eventsPromise; + + expect(consoleEvents[0].console.level).toBe("info"); + expect(consoleEvents[0].console.message[0]).toBe("hi!"); + }); + + it("should capture console.warn/info/error messages", async () => { + const engine = new Engine(importResolver); + const eventsPromise = waitTillEvent(engine.onConsole, 3); + const model2 = getModel2(); + + engine.registerModel(model2); + + const events = await eventsPromise; + const eventsSnapshot = events.map((event) => { + return { + path: event.model.path, + console: event.console, + }; + }); + expect(eventsSnapshot).toMatchSnapshot(); + }); + + it("should capture console.log messages after async", async () => { + const engine = new Engine(importResolver); + const eventsPromise = waitTillEvent(engine.onConsole, 2); + const model3 = getModel3(); + + engine.registerModel(model3); + + const events = await eventsPromise; + expect(events[0].console.message[0]).toBe("before"); + expect(events[1].console.message[0]).toBe("after"); + }); + + // it("should capture console.log messages from library (sync only)", async () => { + // const engine = new Engine(importResolver); + // const eventsPromise = waitTillEvent(engine.onConsole, 2); + // const model4 = getModel4(); + + // engine.registerModel(model4); + + // const events = await eventsPromise; + // console.log(events); + // }); }); }); diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index 1b92df8bf..708ba92fe 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -8,6 +8,21 @@ export type ResolvedImport = { module: any; } & lifecycle.IDisposable; +export type OutputEvent = { + model: T; + output: any; +}; + +export type ConsolePayload = { + level: "info" | "warn" | "error"; + message: any; +}; + +export type ConsoleEvent = { + model: T; + console: ConsolePayload; +}; + /** * The engine automatically runs models registered to it. * The code of the models is passed a context ($) provided by the engine. @@ -26,17 +41,30 @@ export class Engine extends lifecycle.Disposable { ReturnType >(); - private readonly _onOutput: event.Emitter<{ model: T; output: any }> = - this._register(new event.Emitter<{ model: T; output: any }>()); + private readonly _onOutput: event.Emitter> = this._register( + new event.Emitter>() + ); + + private readonly _onConsole: event.Emitter> = this._register( + new event.Emitter>() + ); /** * Raised whenever a model is (re)evaluated, with the exports by that model * - * @type {event.Event<{ model: T; output: any }>} + * @type {event.Event>} + * @memberof Engine + */ + public readonly onOutput: event.Event> = this._onOutput.event; + + /** + * Raised whenever a model calls console.* functions + * + * @type {event.Event>} * @memberof Engine */ - public readonly onOutput: event.Event<{ model: T; output: any }> = - this._onOutput.event; + public readonly onConsole: event.Event> = + this._onConsole.event; private readonly _onBeforeExecution: event.Emitter<{ model: T }> = this._register(new event.Emitter<{ model: T }>()); @@ -103,7 +131,8 @@ export class Engine extends lifecycle.Disposable { } return ret.module; }, - (model, output) => this._onOutput.fire({ model, output }) + (event) => this._onOutput.fire(event), + (event) => this._onConsole.fire(event) ); // catch errors? }; let prevValue: string | undefined = model.getValue(); @@ -152,7 +181,8 @@ export class Engine extends lifecycle.Disposable { model: T, typecellContext: TypeCellContext, resolveImport: (module: string) => Promise, - onOutput: (model: T, output: any) => void + onOutput: (event: OutputEvent) => void, + onConsole: (event: ConsoleEvent) => void ) { if (!this.evaluatorCache.has(model)) { this.evaluatorCache.set( @@ -161,7 +191,8 @@ export class Engine extends lifecycle.Disposable { typecellContext, resolveImport, true, - (output) => onOutput(model, output), + (output) => onOutput({ model, output }), + (console) => onConsole({ model, console }), () => this._onBeforeExecution.fire({ model }) ) ); diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts index 37c81aa6a..8ed253133 100644 --- a/packages/engine/src/HookExecution.ts +++ b/packages/engine/src/HookExecution.ts @@ -1,3 +1,5 @@ +import { ConsolePayload } from "./Engine"; + const glob = typeof window === "undefined" ? global : window; const overrideFunctions = [ @@ -23,17 +25,41 @@ export type HookContext = { [K in typeof overrideFunctions[number]]: any }; export class HookExecution { public disposers: Array<() => void> = []; public context: HookContext = { - setTimeout: this.applyDisposer(setTimeout, this.disposers, (ret) => { + setTimeout: this.applyDisposer(setTimeout, (ret) => { clearTimeout(ret); }), - setInterval: this.applyDisposer(setInterval, this.disposers, (ret) => { + setInterval: this.applyDisposer(setInterval, (ret) => { clearInterval(ret); }), console: { ...originalReferences.console, log: (...args: any) => { - // TODO: broadcast output to console view originalReferences.console.log(...args); + this.onConsoleEvent({ + level: "info", + message: args, + }); + }, + info: (...args: any) => { + originalReferences.console.info(...args); + this.onConsoleEvent({ + level: "info", + message: args, + }); + }, + warn: (...args: any) => { + originalReferences.console.warn(...args); + this.onConsoleEvent({ + level: "warn", + message: args, + }); + }, + error: (...args: any) => { + originalReferences.console.error(...args); + this.onConsoleEvent({ + level: "error", + message: args, + }); }, }, EventTarget: undefined, @@ -41,7 +67,7 @@ export class HookExecution { global: undefined, }; - constructor() { + constructor(private onConsoleEvent: (console: ConsolePayload) => void) { if (typeof EventTarget !== "undefined") { this.context.EventTarget = { EventTarget, @@ -49,7 +75,6 @@ export class HookExecution { ...EventTarget.prototype, addEventListener: this.applyDisposer( EventTarget.prototype.addEventListener as any, - this.disposers, function (this: any, _ret, args) { this.removeEventListener(args[0], args[1]); } @@ -101,14 +126,16 @@ export class HookExecution { private applyDisposer( original: (...args: T[]) => Y, - disposes: Array<() => void>, disposer: (ret: Y, args: T[]) => void ) { + const self = this; return function newFunction(this: any): Y { const callerArguments = arguments; const ret = original.apply(this, callerArguments as any); // TODO: fix any? const ctx = this; - disposes.push(() => disposer.call(ctx, ret, callerArguments as any)); + self.disposers.push(() => + disposer.call(ctx, ret, callerArguments as any) + ); return ret; }; } diff --git a/packages/engine/src/__snapshots__/Engine.test.ts.snap b/packages/engine/src/__snapshots__/Engine.test.ts.snap index 7deb1487a..a8ade4170 100644 --- a/packages/engine/src/__snapshots__/Engine.test.ts.snap +++ b/packages/engine/src/__snapshots__/Engine.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`engine class should re-evaluate other models when global variable changes 1`] = ` +exports[`engine class execution basic model execution should re-evaluate other models when global variable changes 1`] = ` Array [ Object { "output": Object { @@ -34,3 +34,35 @@ Array [ }, ] `; + +exports[`engine class execution console messages should capture console.warn/info/error messages 1`] = ` +Array [ + Object { + "console": Object { + "level": "info", + "message": Array [ + "info", + ], + }, + "path": "model2", + }, + Object { + "console": Object { + "level": "warn", + "message": Array [ + "warn", + ], + }, + "path": "model2", + }, + Object { + "console": Object { + "level": "error", + "message": Array [ + "error", + ], + }, + "path": "model2", + }, +] +`; diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 4da328b7c..6a29d517e 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -116,7 +116,7 @@ export async function runModule( beforeExecuting(); - disposeEveryRun.push(hookExecution.dispose); + disposeEveryRun.push(hookExecution.dispose.bind(hookExecution)); hookExecution.attachToWindow(); try { From a300e2d5509cdbbf134bb9b62b6f7617f6da4e63 Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Tue, 8 Mar 2022 11:46:50 +0100 Subject: [PATCH 2/8] Fix typo --- packages/engine/src/Engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index 9315dd950..708ba92fe 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -197,7 +197,7 @@ export class Engine extends lifecycle.Disposable { ) ); } - const evaluatxor = this.evaluatorCache.get(model)!; + const evaluator = this.evaluatorCache.get(model)!; if (model.language !== "javascript") { throw new Error("can not evaluate non-javascript code"); } From cc98db03707cd239f33f76a57a0a6f29b419e24c Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Tue, 8 Mar 2022 17:44:17 +0100 Subject: [PATCH 3/8] Add library console test --- package-lock.json | 17 +++++++ .../iframesandbox/FrameConnection.ts | 1 - packages/engine/package.json | 5 +- packages/engine/src/Engine.test.ts | 49 ++++++++++--------- packages/engine/src/Engine.ts | 4 +- packages/engine/src/HookExecution.ts | 6 ++- packages/engine/src/tests/util/helpers.ts | 12 ++++- 7 files changed, 63 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6b8fe6c7..5daf4520b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "frontend-collective-react-dnd-scrollzone": "1.0.2", "glob": "^7.2.0", "lodash": "^4.17.21", + "logdown": "^3.3.1", "lowlight": "^1.20.0", "lz-string": "^1.4.4", "markdown-it": "^12.0.2", @@ -19992,6 +19993,14 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "node_modules/logdown": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/logdown/-/logdown-3.3.1.tgz", + "integrity": "sha512-pjX0vlIJsYQlgVzFba2amXI1wZZnhrEorL68MdLI7B0/sN1TNUozBNFaHfcPHMM3A+INZ0OXFDxtnoaEgOmGjQ==", + "dependencies": { + "chalk": "^2.3.0" + } + }, "node_modules/loglevel": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", @@ -47434,6 +47443,14 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "logdown": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/logdown/-/logdown-3.3.1.tgz", + "integrity": "sha512-pjX0vlIJsYQlgVzFba2amXI1wZZnhrEorL68MdLI7B0/sN1TNUozBNFaHfcPHMM3A+INZ0OXFDxtnoaEgOmGjQ==", + "requires": { + "chalk": "^2.3.0" + } + }, "loglevel": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts index 8f65c5814..ffe9deb23 100644 --- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts +++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts @@ -5,7 +5,6 @@ import { lifecycle } from "vscode-lib"; import { CompiledCodeModel } from "../../../../../models/CompiledCodeModel"; import { getTypeCellResolver } from "../../../resolver/resolver"; import { ModelOutput } from "../../../components/ModelOutput"; - import { ModelReceiver } from "./ModelReceiver"; import type { VisualizersByPath } from "../../../../extensions/visualizer/VisualizerExtension"; import { IframeBridgeMethods } from "./IframeBridgeMethods"; diff --git a/packages/engine/package.json b/packages/engine/package.json index 406efe2c7..897965e05 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -5,16 +5,17 @@ "dependencies": { "es-module-shims": "1.4.3", "lodash": "^4.17.21", + "logdown": "^3.3.1", "mobx": "^6.2.0", "react": "^17.0.2", "vscode-lib": "^0.1.0" }, "devDependencies": { + "@playwright/test": "^1.18.1", "@types/jest": "^26.0.22", "jest": "26.6.0", "rimraf": "^3.0.2", - "typescript": "4.3.2", - "@playwright/test": "^1.18.1" + "typescript": "4.3.2" }, "source": "src/index.ts", "types": "types/index.d.ts", diff --git a/packages/engine/src/Engine.test.ts b/packages/engine/src/Engine.test.ts index adb5d2141..549111033 100644 --- a/packages/engine/src/Engine.test.ts +++ b/packages/engine/src/Engine.test.ts @@ -123,17 +123,17 @@ exports.default = sum;` "model4", `define(["require", "exports", "logdown"], function(require, exports, logdown) { "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - let logger = logdown("warning message"); - logger.state.isEnabled = true; - logger.log("message 1"); - - setTimeout(() => { - let logger = logdown("warning message"); - logger.state.isEnabled = true; - logger.log("message2"); - }, 1); - });` + Object.defineProperty(exports, "__esModule", { value: true }); + + let logger = logdown("logger 1"); + logger.state.isEnabled = true; + logger.log("message 1"); + + setTimeout(() => { + logger.state.isEnabled = true; + logger.log("message 2"); + }, 1); + });` ); it("should capture console.log message", async () => { @@ -145,8 +145,8 @@ exports.default = sum;` const consoleEvents = await eventsPromise; - expect(consoleEvents[0].console.level).toBe("info"); - expect(consoleEvents[0].console.message[0]).toBe("hi!"); + expect(consoleEvents[0].payload.level).toBe("info"); + expect(consoleEvents[0].payload.message[0]).toBe("hi!"); }); it("should capture console.warn/info/error messages", async () => { @@ -160,7 +160,7 @@ exports.default = sum;` const eventsSnapshot = events.map((event) => { return { path: event.model.path, - console: event.console, + console: event.payload, }; }); expect(eventsSnapshot).toMatchSnapshot(); @@ -174,19 +174,20 @@ exports.default = sum;` engine.registerModel(model3); const events = await eventsPromise; - expect(events[0].console.message[0]).toBe("before"); - expect(events[1].console.message[0]).toBe("after"); + expect(events[0].payload.message[0]).toBe("before"); + expect(events[1].payload.message[0]).toBe("after"); }); - // it("should capture console.log messages from library (sync only)", async () => { - // const engine = new Engine(importResolver); - // const eventsPromise = waitTillEvent(engine.onConsole, 2); - // const model4 = getModel4(); + it("should capture console.log messages from library (sync only)", async () => { + const engine = new Engine(importResolver); + const eventsPromise = waitTillEvent(engine.onConsole, 2); + const model4 = getModel4(); - // engine.registerModel(model4); + engine.registerModel(model4); - // const events = await eventsPromise; - // console.log(events); - // }); + const events = await eventsPromise; + expect(events[0].payload.message[1]).toBe("message 1"); + expect(events[1].payload.message[1]).toBe("message 2"); + }); }); }); diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index 708ba92fe..4899b4848 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -20,7 +20,7 @@ export type ConsolePayload = { export type ConsoleEvent = { model: T; - console: ConsolePayload; + payload: ConsolePayload; }; /** @@ -192,7 +192,7 @@ export class Engine extends lifecycle.Disposable { resolveImport, true, (output) => onOutput({ model, output }), - (console) => onConsole({ model, console }), + (console) => onConsole({ model, payload: console }), () => this._onBeforeExecution.fire({ model }) ) ); diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts index eb1c6db6e..d64c92ccd 100644 --- a/packages/engine/src/HookExecution.ts +++ b/packages/engine/src/HookExecution.ts @@ -9,7 +9,7 @@ const overrideFunctions = [ "EventTarget.prototype.addEventListener", ] as const; -const originalReferences: HookContext = { +export const originalReferences: HookContext = { setTimeout: glob.setTimeout, setInterval: glob.setInterval, console: glob.console, @@ -19,6 +19,10 @@ const originalReferences: HookContext = { export type HookContext = { [K in typeof overrideFunctions[number]]: any }; +/** + * Sets object property based on a given path and value. + * E.g. path could be level1.level2.prop + */ function setProperty(base: Object, path: string, value: any) { const layers = path.split("."); if (layers.length > 1) { diff --git a/packages/engine/src/tests/util/helpers.ts b/packages/engine/src/tests/util/helpers.ts index 79a7bf409..e51c5086e 100644 --- a/packages/engine/src/tests/util/helpers.ts +++ b/packages/engine/src/tests/util/helpers.ts @@ -1,6 +1,7 @@ import { CodeModel } from "../../CodeModel"; import { ResolvedImport } from "../../Engine"; import { CodeModelMock } from "./CodeModelMock"; +import * as logdown from "logdown"; export function waitTillEvent( e: (listener: (arg0: T) => void) => void, @@ -20,9 +21,18 @@ export function waitTillEvent( } export async function importResolver( - _module: string, + module: string, _forModel: CodeModel ): Promise { + if (module === "logdown") { + return (async () => { + return { + module: logdown.default, + dispose: () => {}, + }; + })(); + } + const res = async () => { return { module: { From 7a649c8031aad24ddf3211f7d80e6da5b4d4401f Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Wed, 27 Apr 2022 13:07:29 +0200 Subject: [PATCH 4/8] Rename message => arguments --- packages/engine/src/Engine.test.ts | 10 +++++----- packages/engine/src/Engine.ts | 2 +- packages/engine/src/HookExecution.ts | 10 +++++----- .../engine/src/__snapshots__/Engine.test.ts.snap | 12 ++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/engine/src/Engine.test.ts b/packages/engine/src/Engine.test.ts index 549111033..21d31a12c 100644 --- a/packages/engine/src/Engine.test.ts +++ b/packages/engine/src/Engine.test.ts @@ -146,7 +146,7 @@ exports.default = sum;` const consoleEvents = await eventsPromise; expect(consoleEvents[0].payload.level).toBe("info"); - expect(consoleEvents[0].payload.message[0]).toBe("hi!"); + expect(consoleEvents[0].payload.arguments[0]).toBe("hi!"); }); it("should capture console.warn/info/error messages", async () => { @@ -174,8 +174,8 @@ exports.default = sum;` engine.registerModel(model3); const events = await eventsPromise; - expect(events[0].payload.message[0]).toBe("before"); - expect(events[1].payload.message[0]).toBe("after"); + expect(events[0].payload.arguments[0]).toBe("before"); + expect(events[1].payload.arguments[0]).toBe("after"); }); it("should capture console.log messages from library (sync only)", async () => { @@ -186,8 +186,8 @@ exports.default = sum;` engine.registerModel(model4); const events = await eventsPromise; - expect(events[0].payload.message[1]).toBe("message 1"); - expect(events[1].payload.message[1]).toBe("message 2"); + expect(events[0].payload.arguments[1]).toBe("message 1"); + expect(events[1].payload.arguments[1]).toBe("message 2"); }); }); }); diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index 4899b4848..8924e5f04 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -15,7 +15,7 @@ export type OutputEvent = { export type ConsolePayload = { level: "info" | "warn" | "error"; - message: any; + arguments: any[]; }; export type ConsoleEvent = { diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts index 17549c8ca..479dfeeb0 100644 --- a/packages/engine/src/HookExecution.ts +++ b/packages/engine/src/HookExecution.ts @@ -54,28 +54,28 @@ export class HookExecution { originalReferences.console.log(...args); this.onConsoleEvent({ level: "info", - message: args, + arguments: args, }); }, info: (...args: any) => { originalReferences.console.info(...args); this.onConsoleEvent({ level: "info", - message: args, + arguments: args, }); }, warn: (...args: any) => { originalReferences.console.warn(...args); this.onConsoleEvent({ level: "warn", - message: args, + arguments: args, }); }, error: (...args: any) => { originalReferences.console.error(...args); this.onConsoleEvent({ level: "error", - message: args, + arguments: args, }); }, }, @@ -115,7 +115,7 @@ export class HookExecution { } private createHookedFunction( - original: (...args: T[]) => Y, + original: (...args: any[]) => Y, disposer: (ret: Y, args: T[]) => void ) { const self = this; diff --git a/packages/engine/src/__snapshots__/Engine.test.ts.snap b/packages/engine/src/__snapshots__/Engine.test.ts.snap index a8ade4170..edb6f28ce 100644 --- a/packages/engine/src/__snapshots__/Engine.test.ts.snap +++ b/packages/engine/src/__snapshots__/Engine.test.ts.snap @@ -39,28 +39,28 @@ exports[`engine class execution console messages should capture console.warn/inf Array [ Object { "console": Object { - "level": "info", - "message": Array [ + "arguments": Array [ "info", ], + "level": "info", }, "path": "model2", }, Object { "console": Object { - "level": "warn", - "message": Array [ + "arguments": Array [ "warn", ], + "level": "warn", }, "path": "model2", }, Object { "console": Object { - "level": "error", - "message": Array [ + "arguments": Array [ "error", ], + "level": "error", }, "path": "model2", }, From cd2ff1daeef22ab32f643387f3dac5035a4b71e5 Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Wed, 27 Apr 2022 19:49:29 +0200 Subject: [PATCH 5/8] Display console output in cell --- package-lock.json | 82 +++++++++++++++++++ packages/editor/package.json | 3 +- .../runtime/executor/components/Console.tsx | 26 ++++++ .../executor/components/ConsoleOutput.ts | 31 +++++++ .../sandboxed/iframesandbox/Frame.tsx | 20 ++++- .../iframesandbox/FrameConnection.ts | 39 +++++++-- packages/engine/src/HookExecution.ts | 4 - 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 packages/editor/src/runtime/executor/components/Console.tsx create mode 100644 packages/editor/src/runtime/executor/components/ConsoleOutput.ts diff --git a/package-lock.json b/package-lock.json index fca2821b3..e99a0c20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "@typescript/vfs": "^1.3.4", "chai": "^4.3.6", "classnames": "^2.3.1", + "console-feed": "^3.3.0", "cross-env": "^7.0.3", "es-module-shims": "1.4.3", "fake-indexeddb": "^3.1.2", @@ -10975,6 +10976,31 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, + "node_modules/console-feed": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/console-feed/-/console-feed-3.3.0.tgz", + "integrity": "sha512-GS0EtpiLyAZGEYBtTih+uI3s3NDmOsfkgpNGhr7UWeM5BzDT+dKgit2nEMFwDb2w7NaT95774/cwAztA1BxrHQ==", + "dependencies": { + "@emotion/core": "^10.0.10", + "@emotion/styled": "^10.0.12", + "emotion-theming": "^10.0.10", + "linkifyjs": "^2.1.6", + "react-inspector": "^5.1.0" + }, + "peerDependencies": { + "react": "^15.x || ^16.x || ^17.x" + } + }, + "node_modules/console-feed/node_modules/linkifyjs": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz", + "integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==", + "peerDependencies": { + "jquery": ">= 1.11.0", + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -12743,6 +12769,20 @@ "node": ">= 4" } }, + "node_modules/emotion-theming": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.3.0.tgz", + "integrity": "sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/weak-memoize": "0.2.5", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -19295,6 +19335,12 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "peer": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -41437,6 +41483,26 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, + "console-feed": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/console-feed/-/console-feed-3.3.0.tgz", + "integrity": "sha512-GS0EtpiLyAZGEYBtTih+uI3s3NDmOsfkgpNGhr7UWeM5BzDT+dKgit2nEMFwDb2w7NaT95774/cwAztA1BxrHQ==", + "requires": { + "@emotion/core": "^10.0.10", + "@emotion/styled": "^10.0.12", + "emotion-theming": "^10.0.10", + "linkifyjs": "^2.1.6", + "react-inspector": "^5.1.0" + }, + "dependencies": { + "linkifyjs": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.9.tgz", + "integrity": "sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==", + "requires": {} + } + } + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -42877,6 +42943,16 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, + "emotion-theming": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.3.0.tgz", + "integrity": "sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/weak-memoize": "0.2.5", + "hoist-non-react-statics": "^3.3.0" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -47810,6 +47886,12 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "peer": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/packages/editor/package.json b/packages/editor/package.json index c6c233d9a..560be7734 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -99,7 +99,8 @@ "y-protocols": "^1.0.5", "yjs": "^13.5.16", "zxcvbn": "^4.4.2", - "react-router-dom": "^6.2.2" + "react-router-dom": "^6.2.2", + "console-feed": "^3.3.0" }, "scripts": { "copytypes:self": "rimraf public/types && tsc --declaration --stripInternal --emitDeclarationOnly --noEmit false --declarationDir public/types/@typecell-org/editor", diff --git a/packages/editor/src/runtime/executor/components/Console.tsx b/packages/editor/src/runtime/executor/components/Console.tsx new file mode 100644 index 000000000..5a4c41885 --- /dev/null +++ b/packages/editor/src/runtime/executor/components/Console.tsx @@ -0,0 +1,26 @@ +import { ObservableMap } from "mobx"; +import { observer } from "mobx-react-lite"; +import React from "react"; +import { ConsoleOutput } from "./ConsoleOutput"; +import { Console as ConsoleComponent } from "console-feed"; + +type Props = { + modelPath: string; + outputs: ObservableMap; +}; + +const Console: React.FC = observer((props) => { + const consoleOutput = props.outputs.get(props.modelPath); + + let output = (consoleOutput?.events || []).map((event, i) => { + return { + id: i.toString(), + data: event.arguments, + method: event.level, + }; + }); + + return ; +}); + +export default Console; diff --git a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts new file mode 100644 index 000000000..f5a6d02af --- /dev/null +++ b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts @@ -0,0 +1,31 @@ +import { makeObservable, observable, runInAction } from "mobx"; +import { lifecycle } from "vscode-lib"; +import { ConsolePayload } from "../../../../../engine/types/Engine"; + +/** + * Keeps track of console output for a cell. Appends new events to the events array. + */ +export class ConsoleOutput extends lifecycle.Disposable { + private autorunDisposer: (() => void) | undefined; + public events: ConsolePayload[] = []; + + constructor() { + super(); + makeObservable(this, { + events: observable, + }); + } + + async appendEvent(consolePayload: ConsolePayload) { + runInAction(() => { + this.events.push(consolePayload); + }); + } + + public dispose() { + if (this.autorunDisposer) { + this.autorunDisposer(); + } + super.dispose(); + } +} diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx index e525f6f08..ad06b9fd7 100644 --- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx +++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from "react"; import Output from "../../../components/Output"; import { FrameConnection } from "./FrameConnection"; import "./Frame.css"; +import Console from "../../../components/Console"; // The sandbox frame where end-user code gets evaluated. // It is loaded from index.iframe.ts @@ -106,7 +107,10 @@ export const Frame = observer((props: {}) => { style={getOutputOuterStyle(positions.x, positions.y)} onMouseMove={onMouseMoveOutput}>
- + +
+
+
); @@ -122,11 +126,21 @@ const getOutputOuterStyle = (x: number, y: number) => ({ position: "absolute" as "absolute", padding: "10px", width: "100%", + display: "flex", }); const outputInnerStyle = { - maxWidth: "100%", - width: "100%", + maxWidth: "70%", + width: "70%", + overflow: "hidden", +}; + +const consoleStyle = { + width: "30%", + maxHeight: "300px", + overflow: "auto", + display: "flex", + "flex-direction": "column-reverse", }; const containerStyle = { position: "relative" as "relative" }; diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts index ffe9deb23..cc2dfd13f 100644 --- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts +++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/FrameConnection.ts @@ -9,6 +9,7 @@ import { ModelReceiver } from "./ModelReceiver"; import type { VisualizersByPath } from "../../../../extensions/visualizer/VisualizerExtension"; import { IframeBridgeMethods } from "./IframeBridgeMethods"; import { HostBridgeMethods } from "../HostBridgeMethods"; +import { ConsoleOutput } from "../../../components/ConsoleOutput"; let ENGINE_ID = 0; @@ -20,11 +21,24 @@ export class FrameConnection extends lifecycle.Disposable { public readonly id = ENGINE_ID++; /** - * Map of that keeps track of the variables exported by every cell + * Map of that keeps track of the generated output for every cell */ - public readonly outputs = observable.map(undefined, { - deep: false, - }); + public readonly modelOutputs = observable.map( + undefined, + { + deep: false, + } + ); + + /** + * Map of that keeps track of console output for every cell + */ + public readonly consoleOutputs = observable.map( + undefined, + { + deep: false, + } + ); /** * Map of that keeps track of the positions of every cell. @@ -79,15 +93,26 @@ export class FrameConnection extends lifecycle.Disposable { // pass the code to the engine by acting as a ModelProvider this.engine.registerModelProvider(mainModelReceiver); + this._register( + this.engine.onConsole(({ model, payload }) => { + let consoleOutput = this.consoleOutputs.get(model.path); + if (!consoleOutput) { + consoleOutput = this._register(new ConsoleOutput()); + this.consoleOutputs.set(model.path, consoleOutput); + } + consoleOutput.appendEvent(payload); + }) + ); + // Listen to outputs of evaluated cells this._register( this.engine.onOutput(({ model, output }) => { - let modelOutput = this.outputs.get(model.path); + let modelOutput = this.modelOutputs.get(model.path); if (!modelOutput) { modelOutput = this._register( new ModelOutput(this.engine.observableContext.context) ); - this.outputs.set(model.path, modelOutput); + this.modelOutputs.set(model.path, modelOutput); } modelOutput.updateValue(output); }) @@ -230,7 +255,7 @@ export class FrameConnection extends lifecycle.Disposable { // For type visualizers (experimental) updateVisualizers: async (e: VisualizersByPath) => { for (let [path, visualizers] of Object.entries(e)) { - this.outputs.get(path)!.updateVisualizers(visualizers); + this.modelOutputs.get(path)!.updateVisualizers(visualizers); } }, }; diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts index 479dfeeb0..cfa59aeff 100644 --- a/packages/engine/src/HookExecution.ts +++ b/packages/engine/src/HookExecution.ts @@ -51,28 +51,24 @@ export class HookExecution { console: { ...originalReferences.console, log: (...args: any) => { - originalReferences.console.log(...args); this.onConsoleEvent({ level: "info", arguments: args, }); }, info: (...args: any) => { - originalReferences.console.info(...args); this.onConsoleEvent({ level: "info", arguments: args, }); }, warn: (...args: any) => { - originalReferences.console.warn(...args); this.onConsoleEvent({ level: "warn", arguments: args, }); }, error: (...args: any) => { - originalReferences.console.error(...args); this.onConsoleEvent({ level: "error", arguments: args, From 909a44b2ec61083efeecb38dc9ce77ebdb34d657 Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Thu, 28 Apr 2022 18:00:41 +0200 Subject: [PATCH 6/8] Improve styling & Clear console on reload --- .../runtime/executor/components/Console.tsx | 36 ++++++++++++++++++- .../executor/components/ConsoleOutput.ts | 6 +++- .../sandboxed/iframesandbox/Frame.tsx | 16 ++------- packages/engine/src/Engine.ts | 12 +++++-- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/packages/editor/src/runtime/executor/components/Console.tsx b/packages/editor/src/runtime/executor/components/Console.tsx index 5a4c41885..89459e2e4 100644 --- a/packages/editor/src/runtime/executor/components/Console.tsx +++ b/packages/editor/src/runtime/executor/components/Console.tsx @@ -20,7 +20,41 @@ const Console: React.FC = observer((props) => { }; }); - return ; + // Return blank in case there are no console events + if (!output.length) { + return <>; + } + + return ( + <> +
+ +
+
+ + ); }); +const consoleStyle = { + borderLeft: "1px solid #eeeeee", + width: "40%", + maxHeight: "100%", + height: "100%", + overflow: "auto", + display: "flex", + "flex-direction": "column-reverse", + position: "absolute" as "absolute", + bottom: "-1px", + right: "0", + backgroundColor: "white", +}; + export default Console; diff --git a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts index f5a6d02af..ebf60ac29 100644 --- a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts +++ b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts @@ -18,7 +18,11 @@ export class ConsoleOutput extends lifecycle.Disposable { async appendEvent(consolePayload: ConsolePayload) { runInAction(() => { - this.events.push(consolePayload); + if (consolePayload.level === "clear") { + this.events = []; + } else { + this.events.push(consolePayload); + } }); } diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx index ad06b9fd7..d07d95873 100644 --- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx +++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx @@ -109,9 +109,7 @@ export const Frame = observer((props: {}) => {
-
- -
+ ); })} @@ -130,19 +128,11 @@ const getOutputOuterStyle = (x: number, y: number) => ({ }); const outputInnerStyle = { - maxWidth: "70%", - width: "70%", + maxWidth: "100%", + width: "100%", overflow: "hidden", }; -const consoleStyle = { - width: "30%", - maxHeight: "300px", - overflow: "auto", - display: "flex", - "flex-direction": "column-reverse", -}; - const containerStyle = { position: "relative" as "relative" }; export default Frame; diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index 8924e5f04..c9f75b268 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -14,7 +14,7 @@ export type OutputEvent = { }; export type ConsolePayload = { - level: "info" | "warn" | "error"; + level: "info" | "warn" | "error" | "clear"; arguments: any[]; }; @@ -139,8 +139,16 @@ export class Engine extends lifecycle.Disposable { // TODO: maybe only debounce (or increase debounce timeout) if an execution is still pending? const reEvaluate = _.debounce(() => { + // make sure there were actual changes from the previous value if (model.getValue() !== prevValue) { - // make sure there were actual changes from the previous value + // Clear the console upon re-evaluation + this._onConsole.fire({ + model, + payload: { + level: "clear", + arguments: [], + }, + }); prevValue = model.getValue(); evaluate(); From 02be292795d5128844b50de20c2dd6ce96d4364b Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Thu, 28 Apr 2022 20:21:30 +0200 Subject: [PATCH 7/8] Console event limit & fix observable type --- .../runtime/executor/components/Console.tsx | 4 ++-- .../executor/components/ConsoleOutput.ts | 23 +++++++++++++++---- .../sandboxed/iframesandbox/Frame.tsx | 5 ++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/runtime/executor/components/Console.tsx b/packages/editor/src/runtime/executor/components/Console.tsx index 89459e2e4..282a1861c 100644 --- a/packages/editor/src/runtime/executor/components/Console.tsx +++ b/packages/editor/src/runtime/executor/components/Console.tsx @@ -14,7 +14,7 @@ const Console: React.FC = observer((props) => { let output = (consoleOutput?.events || []).map((event, i) => { return { - id: i.toString(), + id: event.id, data: event.arguments, method: event.level, }; @@ -27,6 +27,7 @@ const Console: React.FC = observer((props) => { return ( <> +
= observer((props) => { variant="light" />
-
); }); diff --git a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts index ebf60ac29..aeebaf807 100644 --- a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts +++ b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts @@ -2,26 +2,41 @@ import { makeObservable, observable, runInAction } from "mobx"; import { lifecycle } from "vscode-lib"; import { ConsolePayload } from "../../../../../engine/types/Engine"; +interface ConsoleEvent extends ConsolePayload { + id: string; +} + /** * Keeps track of console output for a cell. Appends new events to the events array. */ export class ConsoleOutput extends lifecycle.Disposable { private autorunDisposer: (() => void) | undefined; - public events: ConsolePayload[] = []; + // Keep track of id's so every new event always has a unique id. + private idIncrement = 1; + public events: ConsoleEvent[] = []; constructor() { super(); makeObservable(this, { - events: observable, + events: observable.shallow, }); } - async appendEvent(consolePayload: ConsolePayload) { + public async appendEvent(consolePayload: ConsolePayload) { runInAction(() => { if (consolePayload.level === "clear") { this.events = []; } else { - this.events.push(consolePayload); + if (this.events.length >= 999) { + // Remove the first event when this arbitrary limit is reached to prevent memory issues. + this.events.shift(); + } + + this.idIncrement++; + this.events.push({ + id: this.idIncrement.toString(), + ...consolePayload, + }); } }); } diff --git a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx index d07d95873..5b18370f7 100644 --- a/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx +++ b/packages/editor/src/runtime/executor/executionHosts/sandboxed/iframesandbox/Frame.tsx @@ -128,9 +128,8 @@ const getOutputOuterStyle = (x: number, y: number) => ({ }); const outputInnerStyle = { - maxWidth: "100%", - width: "100%", - overflow: "hidden", + overflow: "auto", + flex: "1", }; const containerStyle = { position: "relative" as "relative" }; From 862e7ec33c57a500be18bb85b545561554f6a5c1 Mon Sep 17 00:00:00 2001 From: Niklas Kors Date: Thu, 28 Apr 2022 20:39:09 +0200 Subject: [PATCH 8/8] Hook more console methods & rename level => method --- .../runtime/executor/components/Console.tsx | 2 +- .../executor/components/ConsoleOutput.ts | 2 +- packages/engine/src/Engine.test.ts | 2 +- packages/engine/src/Engine.ts | 15 ++++- packages/engine/src/HookExecution.ts | 63 ++++++++++++++----- .../src/__snapshots__/Engine.test.ts.snap | 6 +- 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/packages/editor/src/runtime/executor/components/Console.tsx b/packages/editor/src/runtime/executor/components/Console.tsx index 282a1861c..31690205d 100644 --- a/packages/editor/src/runtime/executor/components/Console.tsx +++ b/packages/editor/src/runtime/executor/components/Console.tsx @@ -16,7 +16,7 @@ const Console: React.FC = observer((props) => { return { id: event.id, data: event.arguments, - method: event.level, + method: event.method, }; }); diff --git a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts index aeebaf807..67223d5b2 100644 --- a/packages/editor/src/runtime/executor/components/ConsoleOutput.ts +++ b/packages/editor/src/runtime/executor/components/ConsoleOutput.ts @@ -24,7 +24,7 @@ export class ConsoleOutput extends lifecycle.Disposable { public async appendEvent(consolePayload: ConsolePayload) { runInAction(() => { - if (consolePayload.level === "clear") { + if (consolePayload.method === "clear") { this.events = []; } else { if (this.events.length >= 999) { diff --git a/packages/engine/src/Engine.test.ts b/packages/engine/src/Engine.test.ts index 21d31a12c..442e7f23e 100644 --- a/packages/engine/src/Engine.test.ts +++ b/packages/engine/src/Engine.test.ts @@ -145,7 +145,7 @@ exports.default = sum;` const consoleEvents = await eventsPromise; - expect(consoleEvents[0].payload.level).toBe("info"); + expect(consoleEvents[0].payload.method).toBe("log"); expect(consoleEvents[0].payload.arguments[0]).toBe("hi!"); }); diff --git a/packages/engine/src/Engine.ts b/packages/engine/src/Engine.ts index c9f75b268..742870c82 100644 --- a/packages/engine/src/Engine.ts +++ b/packages/engine/src/Engine.ts @@ -14,7 +14,18 @@ export type OutputEvent = { }; export type ConsolePayload = { - level: "info" | "warn" | "error" | "clear"; + method: + | "log" + | "debug" + | "info" + | "warn" + | "error" + | "table" + | "clear" + | "time" + | "timeEnd" + | "count" + | "assert"; arguments: any[]; }; @@ -145,7 +156,7 @@ export class Engine extends lifecycle.Disposable { this._onConsole.fire({ model, payload: { - level: "clear", + method: "clear", arguments: [], }, }); diff --git a/packages/engine/src/HookExecution.ts b/packages/engine/src/HookExecution.ts index cfa59aeff..340c512b8 100644 --- a/packages/engine/src/HookExecution.ts +++ b/packages/engine/src/HookExecution.ts @@ -50,30 +50,61 @@ export class HookExecution { }), console: { ...originalReferences.console, - log: (...args: any) => { + log: (...args: any) => this.onConsoleEvent({ - level: "info", + method: "log", arguments: args, - }); - }, - info: (...args: any) => { + }), + debug: (...args: any) => this.onConsoleEvent({ - level: "info", + method: "debug", arguments: args, - }); - }, - warn: (...args: any) => { + }), + info: (...args: any) => this.onConsoleEvent({ - level: "warn", + method: "info", arguments: args, - }); - }, - error: (...args: any) => { + }), + warn: (...args: any) => this.onConsoleEvent({ - level: "error", + method: "warn", arguments: args, - }); - }, + }), + error: (...args: any) => + this.onConsoleEvent({ + method: "error", + arguments: args, + }), + table: (...args: any) => + this.onConsoleEvent({ + method: "table", + arguments: args, + }), + clear: (...args: any) => + this.onConsoleEvent({ + method: "clear", + arguments: args, + }), + time: (...args: any) => + this.onConsoleEvent({ + method: "time", + arguments: args, + }), + timeEnd: (...args: any) => + this.onConsoleEvent({ + method: "timeEnd", + arguments: args, + }), + count: (...args: any) => + this.onConsoleEvent({ + method: "count", + arguments: args, + }), + assert: (...args: any) => + this.onConsoleEvent({ + method: "assert", + arguments: args, + }), }, }; diff --git a/packages/engine/src/__snapshots__/Engine.test.ts.snap b/packages/engine/src/__snapshots__/Engine.test.ts.snap index edb6f28ce..fba8fdf73 100644 --- a/packages/engine/src/__snapshots__/Engine.test.ts.snap +++ b/packages/engine/src/__snapshots__/Engine.test.ts.snap @@ -42,7 +42,7 @@ Array [ "arguments": Array [ "info", ], - "level": "info", + "method": "info", }, "path": "model2", }, @@ -51,7 +51,7 @@ Array [ "arguments": Array [ "warn", ], - "level": "warn", + "method": "warn", }, "path": "model2", }, @@ -60,7 +60,7 @@ Array [ "arguments": Array [ "error", ], - "level": "error", + "method": "error", }, "path": "model2", },