Skip to content
Merged
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
20 changes: 20 additions & 0 deletions src/lib/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import loadPolyFill from "utils/polyfill";
import ConsoleExecutor, {
applyConsoleViewport,
executeConsoleCommand,
executeConsoleScript,
resolveConsoleExecutionContext,
} from "./consoleRuntime";

Expand All @@ -25,6 +26,9 @@ import ConsoleExecutor, {
const themeEndpoint = document.querySelector(
'meta[name="acode-console-theme-endpoint"]',
)?.content;
const startupScriptUrl = document.querySelector(
'meta[name="acode-console-executing-script"]',
)?.content;
const originalConsole = console;
const $input = tag("textarea", {
id: "__c-input",
Expand Down Expand Up @@ -184,6 +188,7 @@ import ConsoleExecutor, {

if (isStandaloneConsole) {
showConsole();
void runStartupScript();
return;
}

Expand All @@ -205,6 +210,21 @@ import ConsoleExecutor, {
themeSyncTimer = null;
}

async function runStartupScript() {
if (!startupScriptUrl) return;

setExecutionState(true);
const result = await executeConsoleScript({
scriptUrl: startupScriptUrl,
workerExecutor: executor,
});
setExecutionState(false);

if (result?.type === "error") {
log("error", getStack(new Error()), result.value);
}
}

async function syncTheme() {
if (themeSyncPending) return;
themeSyncPending = true;
Expand Down
31 changes: 31 additions & 0 deletions src/lib/consoleRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,34 @@ export function executeConsoleCommand({
if (context === "page") return Promise.resolve(pageExecutor(code));
return workerExecutor.execute(code);
}

/**
* Loads a standalone JavaScript file and executes it in the isolated console
* worker. The file is fetched first so it is never inserted as a page script.
*/
export async function executeConsoleScript({
scriptUrl,
workerExecutor,
fetchScript = globalThis.fetch,
}) {
if (!scriptUrl) return null;

try {
if (typeof fetchScript !== "function") {
throw new Error("This WebView cannot load the JavaScript file.");
}

const response = await fetchScript(scriptUrl, { cache: "no-store" });
if (!response.ok) {
const status = response.status ? ` (${response.status})` : "";
throw new Error(`Failed to load JavaScript file${status}.`);
}

return workerExecutor.execute(await response.text());
} catch (error) {
return {
type: "error",
value: error instanceof Error ? error : new Error(String(error)),
};
}
}
9 changes: 6 additions & 3 deletions src/lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ async function run(
let isLoading = false;
let filename, pathName, extension;
let port = appSettings.value.serverPort;
let EXECUTING_SCRIPT = uuid + "_script.js";
let shouldExecuteScript = false;
const MIMETYPE_HTML = mimeType.lookup("html");
const CONSOLE_SCRIPT = uuid + "_console.js";
const CONSOLE_WORKER_SCRIPT = uuid + "_console_worker.js";
// Keep this route independent of the filename because characters such as
// "#" and "?" change how browsers parse a URL.
const EXECUTING_SCRIPT = uuid + "_script.js";
const CONSOLE_THEME_STYLE = uuid + "_console_theme.css";
const MARKDOWN_STYLE = uuid + "_md.css";
const queue = [];
Expand Down Expand Up @@ -143,7 +146,7 @@ async function run(
}

function runConsole() {
if (!isConsole) EXECUTING_SCRIPT = activeFile.filename;
if (!isConsole) shouldExecuteScript = true;
isConsole = true;
target = "inapp";
filename = "console.html";
Expand Down Expand Up @@ -253,7 +256,7 @@ async function run(
CONSOLE_WORKER_SCRIPT,
CONSOLE_THEME_STYLE,
CONSOLE_THEME_STATE,
EXECUTING_SCRIPT,
EXECUTING_SCRIPT: shouldExecuteScript ? EXECUTING_SCRIPT : null,
APP_THEME_TYPE: getConsoleThemeSnapshot().type,
}),
reqId,
Expand Down
8 changes: 5 additions & 3 deletions src/views/console.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
<meta http-equiv='X-UA-Compatible' content='ie=edge' />
<link rel='stylesheet' href='{{CONSOLE_THEME_STYLE}}' />
<meta name='acode-console-theme-endpoint' content='{{CONSOLE_THEME_STATE}}' />
{{#EXECUTING_SCRIPT}}
<meta name='acode-console-executing-script' content='{{EXECUTING_SCRIPT}}' />
{{/EXECUTING_SCRIPT}}
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAGBgAAAEAIACICQAAFgAAACgAAAAYAAAAMAAAAAEAIAAAAAAAAAkAAMMOAADDDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEwAAAGwAAACuAAAA2AAAAO8AAADuAAAA5AAAAK8AAAByAAAAGgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMAAACaAAAA+QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA+QAAAKAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATAAAAOwAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADvAAAAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABfAAAA+wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAD9Ly8v/3Nzc/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/9zc3P/Li4u/wAAAP4AAABaAAAAAAAAAAAAAAAAAAAAFQAAAOYdHR3/9/f3////////////////////////////////////////////////////////////////////////////9/f3/xwcHP8AAADyAAAAGwAAAAAAAAAAAAAAkAAAAP9KSkr//////35+fv8zMzP/MzMz/zMzM/8zMzP/MzMz/zMzM/8zMzP/MzMz/zMzM/8zMzP/MzMz/zMzM/9+fn7//////0pKSv8AAAD/AAAApgAAAAAAAAARAAAA8wAAAP9LS0v//////15eXv8ICAj/IiIi/yIiIv8EBAT/AAAA/wAAAP8GBgb/IiIi/yIiIv8iIiL/ISEh/wAAAP9eXl7//////0tLS/8AAAD/AAAA/QAAABwAAABmAAAA/wAAAP9LS0v//////15eXv8DAwP/pKSk//////+rq6v/BAQE/wAAAP8sLCz/////////////////+fn5/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAHYAAAChAAAA/wAAAP9LS0v//////15eXv8AAAD/BAQE/6mpqf//////qamp/wQEBP8XFxf/iIiI/4iIiP+IiIj/hYWF/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAALcAAADUAAAA/wAAAP9LS0v//////15eXv8AAAD/AAAA/wQEBP+pqan//////5+fn/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAOAAAADiAAAA/wAAAP9LS0v//////15eXv8AAAD/AAAA/wAAAP9dXV3//////9LS0v8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAPsAAADpAAAA/wAAAP9LS0v//////15eXv8AAAD/AAAA/1ZWVv/7+/v/4eHh/yIiIv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAPQAAADOAAAA/wAAAP9LS0v//////15eXv8AAAD/VlZW//v7+//h4eH/IiIi/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAOcAAACkAAAA/wAAAP9LS0v//////15eXv8MDAz/dHR0/3d3d/8gICD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAALIAAABiAAAA/wAAAP9LS0v//////15eXv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9eXl7//////0tLS/8AAAD/AAAA/wAAAHYAAAAOAAAA9QAAAP9LS0v//////+np6f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/p6en//////0tLS/8AAAD/AAAA+gAAAB0AAAAAAAAAkwAAAP9KSkr//////////////////////////////////////////////////////////////////////////////////////0pKSv8AAAD/AAAAoQAAAAAAAAAAAAAADgAAAOYeHh7/9/f3////////////////////////////////////////////////////////////////////////////9/f3/x0dHf8AAADvAAAAHwAAAAAAAAAAAAAAAAAAAEgAAAD6MDAw/3R0dP93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/93d3f/d3d3/3d3d/9zc3P/Ly8v/wAAAP8AAABSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABbAAAA/QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/QAAAGsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQwAAAOUAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADvAAAAVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAACRAAAA9AAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA+gAAAJ8AAAAXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgAAAGkAAAClAAAA2AAAAOsAAADuAAAA1wAAAKwAAABtAAAAFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AH8A+AAfAPAADwDgAAcAwAADAIAAAQCAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAQCAAAEAwAADAOAABwDwAA8A+AAfAP4AfwA=" type="image/x-icon">
<title>Console</title>
<script>
sessionStorage.setItem('__mode', 'console');
window.__consoleWorkerScript = '{{CONSOLE_WORKER_SCRIPT}}';
</script>
<script src='{{CONSOLE_SCRIPT}}' crossorigin='anonymous'></script>
<script src='{{EXECUTING_SCRIPT}}' crossorigin='anonymous'></script>
</head>
<script src='{{CONSOLE_SCRIPT}}' crossorigin='anonymous'></script>
</head>

<body></body>

Expand Down
59 changes: 59 additions & 0 deletions tests/unit/consoleExecutionContext.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
executeConsoleCommand,
executeConsoleScript,
resolveConsoleExecutionContext,
} from "lib/consoleRuntime";

Expand Down Expand Up @@ -49,4 +50,62 @@ describe("console execution context", () => {
expect(calls).toEqual([["page", 'document.querySelector("main")']]);
expect(result.value).toBe("live page");
});

it("loads Run-button scripts and executes them in the worker", async () => {
const calls = [];
const result = await executeConsoleScript({
scriptUrl: "example.js",
fetchScript: async (url, options) => {
calls.push(["fetch", url, options]);
return {
ok: true,
text: async () => "while (true) {}",
};
},
workerExecutor: {
execute(code) {
calls.push(["worker", code]);
return Promise.resolve({ type: "result" });
},
},
});

expect(calls).toEqual([
["fetch", "example.js", { cache: "no-store" }],
["worker", "while (true) {}"],
]);
expect(result).toEqual({ type: "result" });
});

it("does not load a script for an empty menu console", async () => {
let fetched = false;
const result = await executeConsoleScript({
scriptUrl: null,
fetchScript: async () => {
fetched = true;
},
});

expect(fetched).toBe(false);
expect(result).toBeNull();
});

it("returns a console error when a Run-button script cannot be loaded", async () => {
let executed = false;
const result = await executeConsoleScript({
scriptUrl: "missing.js",
fetchScript: async () => ({ ok: false, status: 404 }),
workerExecutor: {
execute() {
executed = true;
},
},
});

expect(executed).toBe(false);
expect(result).toMatchObject({
type: "error",
value: { message: "Failed to load JavaScript file (404)." },
});
});
});