diff --git a/sandpack-client/src/inject-scripts/consoleHook.test.ts b/sandpack-client/src/inject-scripts/consoleHook.test.ts new file mode 100644 index 000000000..0683b8442 --- /dev/null +++ b/sandpack-client/src/inject-scripts/consoleHook.test.ts @@ -0,0 +1,87 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import Methods from "console-feed/lib/definitions/Methods"; + +const flush = (): Promise => + new Promise((resolve) => { + setTimeout(resolve, 0); + }); + +describe("consoleHook", () => { + const originalScope = (globalThis as any).scope; + const originalMethods = new Map(); + + beforeEach(() => { + jest.resetModules(); + originalMethods.clear(); + + for (const method of Methods) { + originalMethods.set(method, (window.console as any)[method]); + } + + (globalThis as any).scope = { channelId: "test-channel" }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + + for (const [method, originalMethod] of originalMethods.entries()) { + if (typeof originalMethod === "undefined") { + delete (window.console as any)[method]; + } else { + (window.console as any)[method] = originalMethod; + } + } + + if (typeof originalScope === "undefined") { + delete (globalThis as any).scope; + } else { + (globalThis as any).scope = originalScope; + } + }); + + it("captures array values before later mutations", async () => { + const postMessageSpy = jest + .spyOn(window.parent, "postMessage") + .mockImplementation(() => undefined); + jest.spyOn(window.console, "log").mockImplementation(() => undefined); + + require("./consoleHook"); + + const numbers = [1, 2, 3]; + window.console.log(numbers); + numbers.push(4); + + await flush(); + + expect(postMessageSpy).toHaveBeenCalledTimes(1); + const [message, targetOrigin] = postMessageSpy.mock.calls[0]; + + expect(targetOrigin).toBe("*"); + expect((message as any).channelId).toBe("test-channel"); + expect((message as any).log.method).toBe("log"); + expect((message as any).log.data[0]).toEqual([1, 2, 3]); + }); + + it("keeps primitive payloads unchanged", async () => { + const postMessageSpy = jest + .spyOn(window.parent, "postMessage") + .mockImplementation(() => undefined); + jest.spyOn(window.console, "info").mockImplementation(() => undefined); + + require("./consoleHook"); + + window.console.info("Theme updated"); + + await flush(); + + expect(postMessageSpy).toHaveBeenCalledTimes(1); + const [message] = postMessageSpy.mock.calls[0]; + + expect((message as any).log.method).toBe("info"); + expect((message as any).log.data).toEqual(["Theme updated"]); + }); +}); diff --git a/sandpack-client/src/inject-scripts/consoleHook.ts b/sandpack-client/src/inject-scripts/consoleHook.ts index 2fc38d769..fc3c2e3e9 100644 --- a/sandpack-client/src/inject-scripts/consoleHook.ts +++ b/sandpack-client/src/inject-scripts/consoleHook.ts @@ -1,20 +1,51 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/ban-ts-comment,no-console,@typescript-eslint/explicit-function-return-type, no-restricted-globals */ -import Hook from "console-feed/lib/Hook"; +import type { Methods as ConsoleMethod } from "console-feed/lib/definitions/Methods"; +import Methods from "console-feed/lib/definitions/Methods"; +import Parse from "console-feed/lib/Hook/parse"; import { Encode } from "console-feed/lib/Transform"; declare global { const scope: { channelId: string }; } -Hook(window.console, (log) => { - const encodeMessage = Encode(log) as any; - parent.postMessage( - { - type: "console", - codesandbox: true, - log: Array.isArray(encodeMessage) ? encodeMessage[0] : encodeMessage, - channelId: scope.channelId, - }, - "*" - ); -}); +const targetConsole = window.console as Console & { + feed?: { pointers: Record }; + [key: string]: unknown; +}; + +targetConsole.feed = { pointers: {} }; + +for (const method of Methods as ConsoleMethod[]) { + const nativeMethod = targetConsole[method]; + + if (typeof nativeMethod !== "function") { + continue; + } + + targetConsole[method] = function wrappedConsoleMethod( + this: Console, + ...args: unknown[] + ): void { + (nativeMethod as (...nativeArgs: unknown[]) => unknown).apply(this, args); + + const parsed = Parse(method, args as any[]); + if (!parsed) { + return; + } + + const encodedMessage = Encode(parsed) as any; + parent.postMessage( + { + type: "console", + codesandbox: true, + log: Array.isArray(encodedMessage) + ? encodedMessage[0] + : encodedMessage, + channelId: scope.channelId, + }, + "*" + ); + }; + + targetConsole.feed.pointers[method] = nativeMethod; +}