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
var content2 = $("content");
@ -24,32 +24,26 @@ var $$ = (tag, innerHTML = "") => {
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
function randomId() {
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
var statusColors = {
@ -166,179 +160,6 @@ function handleOutput(msg) {
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
var apps = [];
function cacheApps(a) {
@ -347,14 +168,6 @@ function cacheApps(a) {
apps.sort();
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
var sessionId = randomId();
@ -370,7 +183,7 @@ function handleSessionStart(msg) {
sessionStore.set("hostname", msg.data.hostname);
updateProjectName(msg.data.project);
updateCwd(msg.data.cwd);
browserCommands.mode?.(msg.data.mode);
mode(msg.data.mode);
}
function handleSessionUpdate(msg) {
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
var FPS = 30;
var HEIGHT2 = 540;
var WIDTH2 = 960;
var HEIGHT = 540;
var WIDTH = 960;
var oldMode = "cinema";
var running = false;
var canvas;
@ -620,14 +455,14 @@ async function handleGameStart(msg) {
const name = msg.data;
let game;
try {
game = await import(`/source/${name}?session=${sessionId}`);
game = await importUrl(`/source/${name}?session=${sessionId}}`);
} catch (err) {
setStatus(msgId, "error");
addOutput(msgId, `Error: ${err.message ? err.message : err}`);
return;
}
if (document.body.dataset.mode === "tall") {
browserCommands.mode?.();
mode();
oldMode = "tall";
}
canvas = createCanvas();
@ -642,8 +477,8 @@ async function handleGameStart(msg) {
function createCanvas() {
const canvas2 = $$("canvas.game.active");
canvas2.id = randomId();
canvas2.height = HEIGHT2;
canvas2.width = WIDTH2;
canvas2.height = HEIGHT;
canvas2.width = WIDTH;
canvas2.tabIndex = 0;
const main = document.querySelector("main");
main?.classList.add("game");
@ -710,12 +545,12 @@ function endGame() {
window.removeEventListener("keyup", handleKeyup);
window.removeEventListener("resize", resizeCanvas);
if (oldMode === "tall")
browserCommands.mode?.();
mode();
const main = document.querySelector("main");
main?.classList.remove("game");
canvas.classList.remove("active");
canvas.style.height = HEIGHT2 / 2 + "px";
canvas.style.width = WIDTH2 / 2 + "px";
canvas.style.height = HEIGHT / 2 + "px";
canvas.style.width = WIDTH / 2 + "px";
const output2 = $$("li.output");
output2.append(canvas);
insert(output2);
@ -811,61 +646,88 @@ function retryConnection() {
setTimeout(startConnection, 2000);
}
// src/js/statusbar.ts
var STATUS_MSG_LENGTH = 3000;
var statusbar = $("statusbar");
var statusbarMsg = $("statusbar-msg");
var timer;
function status(msg) {
showStatusMsg();
statusbarMsg.textContent = msg;
if (timer)
clearTimeout(timer);
timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH);
// src/js/history.ts
var history = ["one", "two", "three"];
var idx = -1;
var savedInput = "";
function initHistory() {
cmdInput.addEventListener("keydown", navigateHistory);
}
function showStatusMsg() {
statusbar.classList.add("showing-msg");
function addToHistory(input) {
if (history.length === 0 || history[0] === input)
return;
history.unshift(input);
resetHistory();
}
function resetHistory() {
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 });
}
function hideStatusMsg() {
statusbar.className = "";
}
// src/js/commands.ts
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()
};
var commands = {};
function cacheCommands(cmds) {
commands.length = 0;
commands.push(...cmds);
commands.push(...Object.keys(browserCommands));
commands.sort();
for (const key in commands)
delete commands[key];
Object.assign(commands, cmds);
}
async function mode(mode2) {
await runCommand(mode2 ? `mode ${mode2}` : "mode", false);
}
// src/js/completion.ts
@ -877,7 +739,7 @@ function handleCompletion(e) {
return;
e.preventDefault();
const input = cmdInput.value;
for (const command of commands) {
for (const command of Object.keys(commands)) {
if (command.startsWith(input)) {
cmdInput.value = command;
return;
@ -1207,70 +1069,153 @@ function handleGamepad() {
requestAnimationFrame(handleGamepad);
}
// src/js/history.ts
var history = ["one", "two", "three"];
var idx = -1;
var savedInput = "";
function initHistory() {
cmdInput.addEventListener("keydown", navigateHistory);
// src/js/browser.ts
var HEIGHT2 = 540;
var WIDTH2 = 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 addToHistory(input) {
if (history.length === 0 || history[0] === input)
return;
history.unshift(input);
resetHistory();
}
function resetHistory() {
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) {
if (!input.trim())
return;
if (input.includes(";")) {
input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
return;
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(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 closeBrowser() {
window.removeEventListener("keydown", handleBrowserKeydown);
window.removeEventListener("message", handleAppMessage);
controls.removeEventListener("click", handleClick);
iframe.removeEventListener("load", handlePageLoad);
const id = randomId();
addToHistory(input);
addInput(id, input);
const [cmd = "", ...args] = input.split(" ");
if (browserCommands[cmd]) {
const result = await browserCommands[cmd](...args);
if (result)
addOutput(id, result);
setStatus(id, "ok");
} else {
send({ id, type: "input", data: input });
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/hyperlink.ts
@ -1348,10 +1293,34 @@ function clearInput() {
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
var vramCounter = $("vram-size");
var startVramCounter = () => {
const timer2 = setInterval(() => {
const timer = setInterval(() => {
const count = parseInt(vramCounter.textContent) + 1;
let val = count + "KB";
if (count < 10)
@ -1359,7 +1328,7 @@ var startVramCounter = () => {
vramCounter.textContent = val;
if (count >= 64) {
vramCounter.textContent += " OK";
clearInterval(timer2);
clearInterval(timer);
}
}, 15);
};

View File

@ -12,6 +12,12 @@ export const Layout: FC = async ({ children, title }) => (
<link href="/css/main.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 src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script>
</head>

View File

@ -40,9 +40,27 @@ export function unique<T>(array: T[]): T[] {
return [...new Set(array)]
}
// import a typescript module. caches in production, doesn't in dev.
export async function importUrl(url: string) {
url += url.includes("?") ? "&" : "?"
url += "t=" + Date.now()
url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now())
console.log("-> 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
}