module caching in prod

This commit is contained in:
Chris Wanstrath 2025-10-07 20:28:10 -07:00
parent ea5b33c5bd
commit 178711cb19
3 changed files with 320 additions and 327 deletions

View File

@ -1,5 +1,5 @@
//// ////
// version: 3e67d66 // version: ea5b33c
// src/js/dom.ts // src/js/dom.ts
var content2 = $("content"); var content2 = $("content");
@ -24,32 +24,26 @@ var $$ = (tag, innerHTML = "") => {
return el; return el;
}; };
// src/js/focus.ts
function initFocus() {
window.addEventListener("click", focusHandler);
focusInput();
}
function focusInput() {
cmdInput.focus();
}
function focusHandler(e) {
const target = e.target;
if (!(target instanceof HTMLElement)) {
focusInput();
return;
}
if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName))
return false;
const selection = window.getSelection() || "";
if (selection.toString() === "")
focusInput();
e.preventDefault();
}
// src/shared/utils.ts // src/shared/utils.ts
function randomId() { function randomId() {
return Math.random().toString(36).slice(7); return Math.random().toString(36).slice(7);
} }
async function importUrl(url) {
url += url.includes("?") ? "&" : "?";
url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now());
console.log("-> import", url);
return import(url);
}
function nodeEnv() {
if (typeof process !== "undefined" && process.env?.NODE_ENV)
return "development";
if (typeof globalThis !== "undefined" && globalThis.NODE_ENV)
return globalThis.NODE_ENV;
return;
}
function gitSHA() {
return globalThis.GIT_SHA;
}
// src/js/scrollback.ts // src/js/scrollback.ts
var statusColors = { var statusColors = {
@ -166,179 +160,6 @@ function handleOutput(msg) {
addOutput(id, result.output); addOutput(id, result.output);
} }
// src/js/browser.ts
var HEIGHT = 540;
var WIDTH = 960;
var controls = $("browser-controls");
var address = $("browser-address");
var iframe;
var realUrl = "";
var showInput = true;
function isBrowsing() {
return document.querySelector("iframe.browser.active") !== null;
}
function openBrowser(url, openedVia = "click") {
showInput = openedVia === "click";
iframe = $$("iframe.browser.active");
iframe.src = url;
iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms");
iframe.height = String(HEIGHT);
iframe.width = String(WIDTH);
iframe.tabIndex = 0;
window.addEventListener("message", handleAppMessage);
window.addEventListener("keydown", handleBrowserKeydown);
controls.addEventListener("click", handleClick);
iframe.addEventListener("load", handlePageLoad);
controls.style.display = "";
const main = document.querySelector("#content");
main?.prepend(iframe);
setAddress(url);
}
function closeBrowser() {
window.removeEventListener("keydown", handleBrowserKeydown);
window.removeEventListener("message", handleAppMessage);
controls.removeEventListener("click", handleClick);
iframe.removeEventListener("load", handlePageLoad);
const id = randomId();
if (showInput)
addInput(id, "browse " + realUrl, "ok");
iframe.style.transformOrigin = "top left";
iframe.style.transform = "scale(0.5)";
iframe.style.pointerEvents = "none";
iframe.tabIndex = -1;
iframe.classList.remove("fullscreen", "active");
scrollback.append(iframe);
controls.style.display = "none";
focusInput();
}
function handleAppMessage(event) {
const origin = event.origin;
if (!origin.includes("localhost") && !origin.match(/\.local$/)) {
return;
}
const { type, data } = event.data;
switch (type) {
case "NAV_READY":
break;
case "URL_CHANGED":
setAddress(data.url);
break;
case "TITLE_CHANGED":
console.log("Page title:", data.title);
break;
case "NAV_BLOCKED":
showNavigationError(data.url, data.reason);
break;
case "KEYDOWN":
const keyEvent = new KeyboardEvent("keydown", {
key: data.key,
ctrlKey: data.ctrlKey,
shiftKey: data.shiftKey,
altKey: data.altKey,
metaKey: data.metaKey,
bubbles: true
});
window.dispatchEvent(keyEvent);
break;
}
}
function handleBrowserKeydown(e) {
if (e.key === "Escape" || e.ctrlKey && e.key === "c") {
e.preventDefault();
closeBrowser();
}
}
function handleClick(e) {
const target = e.target;
if (!(target instanceof HTMLElement))
return;
switch (target.id) {
case "back-button":
navigateBack();
break;
case "forward-button":
navigateForward();
break;
case "stop-button":
stopLoading();
break;
case "reload-button":
reloadBrowser();
break;
case "fullscreen-button":
fullscreenBrowser();
break;
case "close-button":
e.stopImmediatePropagation();
closeBrowser();
break;
default:
return;
}
e.preventDefault();
}
function handlePageLoad() {}
function setAddress(url) {
realUrl = url;
address.textContent = url.replace(/https?:\/\//, "");
}
function navigateBack() {
sendNavCommand("back");
}
function navigateForward() {
sendNavCommand("forward");
}
function reloadBrowser() {
sendNavCommand("reload");
}
function stopLoading() {
sendNavCommand("stop");
}
function fullscreenBrowser() {
controls.style.display = "none";
iframe.classList.add("fullscreen");
document.body.append(iframe);
}
function sendNavCommand(action) {
if (!iframe.contentWindow)
return;
iframe.contentWindow.postMessage({
type: "NAV_COMMAND",
action
}, "*");
}
function showNavigationError(url, reason) {
alert(`NAVIGATION BLOCKED
${url}
${reason}`);
}
// src/js/resize.ts
var content3 = document.getElementById("content");
function initResize() {
window.addEventListener("resize", resize);
resize();
}
function resize() {
if (document.body.dataset.mode === "tall") {
resizeTall();
} else {
resizeCinema();
}
}
function resizeTall() {
const scale = Math.min(1, window.innerWidth / 960);
content3.style.transformOrigin = "top center";
content3.style.transform = `scaleX(${scale})`;
}
function resizeCinema() {
const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540);
content3.style.transformOrigin = "center center";
content3.style.transform = `scale(${scale})`;
}
// src/js/webapp.ts // src/js/webapp.ts
var apps = []; var apps = [];
function cacheApps(a) { function cacheApps(a) {
@ -347,14 +168,6 @@ function cacheApps(a) {
apps.sort(); apps.sort();
window.dispatchEvent(new CustomEvent("apps:change")); window.dispatchEvent(new CustomEvent("apps:change"));
} }
function currentAppUrl() {
const project = sessionStore.get("project") || "root";
if (!apps.includes(project))
return;
const hostname = sessionStore.get("hostname") || "localhost";
const s = hostname.startsWith("localhost") ? "" : "s";
return `http${s}://${project}.${hostname}`;
}
// src/js/session.ts // src/js/session.ts
var sessionId = randomId(); var sessionId = randomId();
@ -370,7 +183,7 @@ function handleSessionStart(msg) {
sessionStore.set("hostname", msg.data.hostname); sessionStore.set("hostname", msg.data.hostname);
updateProjectName(msg.data.project); updateProjectName(msg.data.project);
updateCwd(msg.data.cwd); updateCwd(msg.data.cwd);
browserCommands.mode?.(msg.data.mode); mode(msg.data.mode);
} }
function handleSessionUpdate(msg) { function handleSessionUpdate(msg) {
const data = msg.data; const data = msg.data;
@ -597,10 +410,32 @@ class GameContext {
} }
} }
// src/js/focus.ts
function initFocus() {
window.addEventListener("click", focusHandler);
focusInput();
}
function focusInput() {
cmdInput.focus();
}
function focusHandler(e) {
const target = e.target;
if (!(target instanceof HTMLElement)) {
focusInput();
return;
}
if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName))
return false;
const selection = window.getSelection() || "";
if (selection.toString() === "")
focusInput();
e.preventDefault();
}
// src/js/game.ts // src/js/game.ts
var FPS = 30; var FPS = 30;
var HEIGHT2 = 540; var HEIGHT = 540;
var WIDTH2 = 960; var WIDTH = 960;
var oldMode = "cinema"; var oldMode = "cinema";
var running = false; var running = false;
var canvas; var canvas;
@ -620,14 +455,14 @@ async function handleGameStart(msg) {
const name = msg.data; const name = msg.data;
let game; let game;
try { try {
game = await import(`/source/${name}?session=${sessionId}`); game = await importUrl(`/source/${name}?session=${sessionId}}`);
} catch (err) { } catch (err) {
setStatus(msgId, "error"); setStatus(msgId, "error");
addOutput(msgId, `Error: ${err.message ? err.message : err}`); addOutput(msgId, `Error: ${err.message ? err.message : err}`);
return; return;
} }
if (document.body.dataset.mode === "tall") { if (document.body.dataset.mode === "tall") {
browserCommands.mode?.(); mode();
oldMode = "tall"; oldMode = "tall";
} }
canvas = createCanvas(); canvas = createCanvas();
@ -642,8 +477,8 @@ async function handleGameStart(msg) {
function createCanvas() { function createCanvas() {
const canvas2 = $$("canvas.game.active"); const canvas2 = $$("canvas.game.active");
canvas2.id = randomId(); canvas2.id = randomId();
canvas2.height = HEIGHT2; canvas2.height = HEIGHT;
canvas2.width = WIDTH2; canvas2.width = WIDTH;
canvas2.tabIndex = 0; canvas2.tabIndex = 0;
const main = document.querySelector("main"); const main = document.querySelector("main");
main?.classList.add("game"); main?.classList.add("game");
@ -710,12 +545,12 @@ function endGame() {
window.removeEventListener("keyup", handleKeyup); window.removeEventListener("keyup", handleKeyup);
window.removeEventListener("resize", resizeCanvas); window.removeEventListener("resize", resizeCanvas);
if (oldMode === "tall") if (oldMode === "tall")
browserCommands.mode?.(); mode();
const main = document.querySelector("main"); const main = document.querySelector("main");
main?.classList.remove("game"); main?.classList.remove("game");
canvas.classList.remove("active"); canvas.classList.remove("active");
canvas.style.height = HEIGHT2 / 2 + "px"; canvas.style.height = HEIGHT / 2 + "px";
canvas.style.width = WIDTH2 / 2 + "px"; canvas.style.width = WIDTH / 2 + "px";
const output2 = $$("li.output"); const output2 = $$("li.output");
output2.append(canvas); output2.append(canvas);
insert(output2); insert(output2);
@ -811,61 +646,88 @@ function retryConnection() {
setTimeout(startConnection, 2000); setTimeout(startConnection, 2000);
} }
// src/js/statusbar.ts // src/js/history.ts
var STATUS_MSG_LENGTH = 3000; var history = ["one", "two", "three"];
var statusbar = $("statusbar"); var idx = -1;
var statusbarMsg = $("statusbar-msg"); var savedInput = "";
var timer; function initHistory() {
function status(msg) { cmdInput.addEventListener("keydown", navigateHistory);
showStatusMsg();
statusbarMsg.textContent = msg;
if (timer)
clearTimeout(timer);
timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH);
} }
function showStatusMsg() { function addToHistory(input) {
statusbar.classList.add("showing-msg"); if (history.length === 0 || history[0] === input)
return;
history.unshift(input);
resetHistory();
} }
function hideStatusMsg() { function resetHistory() {
statusbar.className = ""; idx = -1;
savedInput = "";
}
function navigateHistory(e) {
if (cmdLine.dataset.extended)
return;
if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") {
e.preventDefault();
if (idx >= history.length - 1)
return;
if (idx === -1)
savedInput = cmdInput.value;
cmdInput.value = history[++idx] || "";
if (idx >= history.length)
idx = history.length - 1;
} else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") {
e.preventDefault();
if (idx <= 0) {
cmdInput.value = savedInput;
idx = -1;
return;
}
cmdInput.value = history[--idx] || "";
if (idx < -1)
idx = -1;
} else if (idx !== -1) {
resetHistory();
}
}
// src/js/shell.ts
async function runCommand(input, showInput = true) {
if (!input.trim())
return;
if (input.includes(";")) {
input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
return;
}
const id = randomId();
addToHistory(input);
if (showInput)
addInput(id, input);
const [cmd = "", ...args] = input.split(" ");
if (commands[cmd]?.type === "browser") {
const mod = await importUrl(`/source/${cmd}`);
if (!mod.default) {
addOutput(id, `no default export in ${cmd}`);
setStatus(id, "error");
return;
}
const result = mod.default(...args);
if (result)
addOutput(id, result);
setStatus(id, "ok");
} else {
send({ id, type: "input", data: input });
}
} }
// src/js/commands.ts // src/js/commands.ts
var commands = []; var commands = {};
var browserCommands = {
browse: (url) => {
const currentUrl = url ?? currentAppUrl();
if (currentUrl) {
openBrowser(currentUrl, "command");
} else {
setTimeout(() => setStatus(latestId(), "error"), 0);
return "usage: browse <url>";
}
},
"browser-session": () => sessionId,
clear: () => scrollback.innerHTML = "",
commands: () => {
return { html: "<div>" + commands.map((cmd) => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" };
},
fullscreen: () => document.body.requestFullscreen(),
mode: (mode) => {
if (!mode) {
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall";
send({ type: "session:update", data: { "ui:mode": mode } });
}
content2.style.display = "";
document.body.dataset.mode = mode;
resize();
focusInput();
},
status: (msg) => status(msg),
reload: () => window.location.reload()
};
function cacheCommands(cmds) { function cacheCommands(cmds) {
commands.length = 0; for (const key in commands)
commands.push(...cmds); delete commands[key];
commands.push(...Object.keys(browserCommands)); Object.assign(commands, cmds);
commands.sort(); }
async function mode(mode2) {
await runCommand(mode2 ? `mode ${mode2}` : "mode", false);
} }
// src/js/completion.ts // src/js/completion.ts
@ -877,7 +739,7 @@ function handleCompletion(e) {
return; return;
e.preventDefault(); e.preventDefault();
const input = cmdInput.value; const input = cmdInput.value;
for (const command of commands) { for (const command of Object.keys(commands)) {
if (command.startsWith(input)) { if (command.startsWith(input)) {
cmdInput.value = command; cmdInput.value = command;
return; return;
@ -1207,70 +1069,153 @@ function handleGamepad() {
requestAnimationFrame(handleGamepad); requestAnimationFrame(handleGamepad);
} }
// src/js/history.ts // src/js/browser.ts
var history = ["one", "two", "three"]; var HEIGHT2 = 540;
var idx = -1; var WIDTH2 = 960;
var savedInput = ""; var controls = $("browser-controls");
function initHistory() { var address = $("browser-address");
cmdInput.addEventListener("keydown", navigateHistory); var iframe;
var realUrl = "";
var showInput = true;
function isBrowsing() {
return document.querySelector("iframe.browser.active") !== null;
} }
function addToHistory(input) { function openBrowser(url, openedVia = "click") {
if (history.length === 0 || history[0] === input) showInput = openedVia === "click";
return; iframe = $$("iframe.browser.active");
history.unshift(input); iframe.src = url;
resetHistory(); iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms");
iframe.height = String(HEIGHT2);
iframe.width = String(WIDTH2);
iframe.tabIndex = 0;
window.addEventListener("message", handleAppMessage);
window.addEventListener("keydown", handleBrowserKeydown);
controls.addEventListener("click", handleClick);
iframe.addEventListener("load", handlePageLoad);
controls.style.display = "";
const main = document.querySelector("#content");
main?.prepend(iframe);
setAddress(url);
} }
function resetHistory() { function closeBrowser() {
idx = -1; window.removeEventListener("keydown", handleBrowserKeydown);
savedInput = ""; window.removeEventListener("message", handleAppMessage);
} controls.removeEventListener("click", handleClick);
function navigateHistory(e) { iframe.removeEventListener("load", handlePageLoad);
if (cmdLine.dataset.extended)
return;
if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") {
e.preventDefault();
if (idx >= history.length - 1)
return;
if (idx === -1)
savedInput = cmdInput.value;
cmdInput.value = history[++idx] || "";
if (idx >= history.length)
idx = history.length - 1;
} else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") {
e.preventDefault();
if (idx <= 0) {
cmdInput.value = savedInput;
idx = -1;
return;
}
cmdInput.value = history[--idx] || "";
if (idx < -1)
idx = -1;
} else if (idx !== -1) {
resetHistory();
}
}
// src/js/shell.ts
async function runCommand(input) {
if (!input.trim())
return;
if (input.includes(";")) {
input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
return;
}
const id = randomId(); const id = randomId();
addToHistory(input); if (showInput)
addInput(id, input); addInput(id, "browse " + realUrl, "ok");
const [cmd = "", ...args] = input.split(" "); iframe.style.transformOrigin = "top left";
if (browserCommands[cmd]) { iframe.style.transform = "scale(0.5)";
const result = await browserCommands[cmd](...args); iframe.style.pointerEvents = "none";
if (result) iframe.tabIndex = -1;
addOutput(id, result); iframe.classList.remove("fullscreen", "active");
setStatus(id, "ok"); scrollback.append(iframe);
} else { controls.style.display = "none";
send({ id, type: "input", data: input }); focusInput();
}
function handleAppMessage(event) {
const origin = event.origin;
if (!origin.includes("localhost") && !origin.match(/\.local$/)) {
return;
} }
const { type, data } = event.data;
switch (type) {
case "NAV_READY":
break;
case "URL_CHANGED":
setAddress(data.url);
break;
case "TITLE_CHANGED":
console.log("Page title:", data.title);
break;
case "NAV_BLOCKED":
showNavigationError(data.url, data.reason);
break;
case "KEYDOWN":
const keyEvent = new KeyboardEvent("keydown", {
key: data.key,
ctrlKey: data.ctrlKey,
shiftKey: data.shiftKey,
altKey: data.altKey,
metaKey: data.metaKey,
bubbles: true
});
window.dispatchEvent(keyEvent);
break;
}
}
function handleBrowserKeydown(e) {
if (e.key === "Escape" || e.ctrlKey && e.key === "c") {
e.preventDefault();
closeBrowser();
}
}
function handleClick(e) {
const target = e.target;
if (!(target instanceof HTMLElement))
return;
switch (target.id) {
case "back-button":
navigateBack();
break;
case "forward-button":
navigateForward();
break;
case "stop-button":
stopLoading();
break;
case "reload-button":
reloadBrowser();
break;
case "fullscreen-button":
fullscreenBrowser();
break;
case "close-button":
e.stopImmediatePropagation();
closeBrowser();
break;
default:
return;
}
e.preventDefault();
}
function handlePageLoad() {}
function setAddress(url) {
realUrl = url;
address.textContent = url.replace(/https?:\/\//, "");
}
function navigateBack() {
sendNavCommand("back");
}
function navigateForward() {
sendNavCommand("forward");
}
function reloadBrowser() {
sendNavCommand("reload");
}
function stopLoading() {
sendNavCommand("stop");
}
function fullscreenBrowser() {
controls.style.display = "none";
iframe.classList.add("fullscreen");
document.body.append(iframe);
}
function sendNavCommand(action) {
if (!iframe.contentWindow)
return;
iframe.contentWindow.postMessage({
type: "NAV_COMMAND",
action
}, "*");
}
function showNavigationError(url, reason) {
alert(`NAVIGATION BLOCKED
${url}
${reason}`);
} }
// src/js/hyperlink.ts // src/js/hyperlink.ts
@ -1348,10 +1293,34 @@ function clearInput() {
delete cmdLine.dataset.extended; delete cmdLine.dataset.extended;
} }
// src/js/resize.ts
var content3 = document.getElementById("content");
function initResize() {
window.addEventListener("resize", resize);
resize();
}
function resize() {
if (document.body.dataset.mode === "tall") {
resizeTall();
} else {
resizeCinema();
}
}
function resizeTall() {
const scale = Math.min(1, window.innerWidth / 960);
content3.style.transformOrigin = "top center";
content3.style.transform = `scaleX(${scale})`;
}
function resizeCinema() {
const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540);
content3.style.transformOrigin = "center center";
content3.style.transform = `scale(${scale})`;
}
// src/js/vram.ts // src/js/vram.ts
var vramCounter = $("vram-size"); var vramCounter = $("vram-size");
var startVramCounter = () => { var startVramCounter = () => {
const timer2 = setInterval(() => { const timer = setInterval(() => {
const count = parseInt(vramCounter.textContent) + 1; const count = parseInt(vramCounter.textContent) + 1;
let val = count + "KB"; let val = count + "KB";
if (count < 10) if (count < 10)
@ -1359,7 +1328,7 @@ var startVramCounter = () => {
vramCounter.textContent = val; vramCounter.textContent = val;
if (count >= 64) { if (count >= 64) {
vramCounter.textContent += " OK"; vramCounter.textContent += " OK";
clearInterval(timer2); clearInterval(timer);
} }
}, 15); }, 15);
}; };

View File

@ -12,6 +12,12 @@ export const Layout: FC = async ({ children, title }) => (
<link href="/css/main.css" rel="stylesheet" /> <link href="/css/main.css" rel="stylesheet" />
<link href="/css/game.css" rel="stylesheet" /> <link href="/css/game.css" rel="stylesheet" />
<script dangerouslySetInnerHTML={{
__html: `
window.NODE_ENV = ${process.env.NODE_ENV ? "'" + process.env.NODE_ENV + "'" : 'undefined'};
window.GIT_SHA = "${GIT_SHA}";
`}} />
<script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} /> <script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} />
<script src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script> <script src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script>
</head> </head>

View File

@ -40,9 +40,27 @@ export function unique<T>(array: T[]): T[] {
return [...new Set(array)] return [...new Set(array)]
} }
// import a typescript module. caches in production, doesn't in dev.
export async function importUrl(url: string) { export async function importUrl(url: string) {
url += url.includes("?") ? "&" : "?" url += url.includes("?") ? "&" : "?"
url += "t=" + Date.now() url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now())
console.log("-> import", url) console.log("-> import", url)
return import(url) return import(url)
} }
// "production" or nuttin
function nodeEnv(): string | undefined {
if (typeof process !== 'undefined' && process.env?.NODE_ENV)
return process.env.NODE_ENV
if (typeof globalThis !== 'undefined' && (globalThis as any).NODE_ENV)
return (globalThis as any).NODE_ENV
return undefined
}
// should always be set
function gitSHA(): string {
return (globalThis as any).GIT_SHA
}