Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
87 changes: 87 additions & 0 deletions sandpack-client/src/inject-scripts/consoleHook.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> =>
new Promise((resolve) => {
setTimeout(resolve, 0);
});

describe("consoleHook", () => {
const originalScope = (globalThis as any).scope;
const originalMethods = new Map<string, unknown>();

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"]);
});
});
57 changes: 44 additions & 13 deletions sandpack-client/src/inject-scripts/consoleHook.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> };
[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;
}