Compare commits
8 Commits
415cbd0ccd
...
b6c0dde776
| Author | SHA1 | Date | |
|---|---|---|---|
| b6c0dde776 | |||
| b3848c35ee | |||
| 178711cb19 | |||
| ea5b33c5bd | |||
| 81b8a40070 | |||
| 4ada43729a | |||
| 34379e40d4 | |||
| ebfddbb278 |
4
.githooks/pre-commit
Executable file
4
.githooks/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
bun run check || exit 1
|
||||||
|
bun run build || exit 1
|
||||||
23
README.md
23
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
3. From the root of this repo, run:
|
3. From the root of this repo, run:
|
||||||
|
|
||||||
bun remote:install`
|
bun remote:install
|
||||||
|
|
||||||
4. When it's done (it'll reboot) visit:
|
4. When it's done (it'll reboot) visit:
|
||||||
|
|
||||||
|
|
@ -24,6 +24,7 @@ And to make sure DNS is working:
|
||||||
|
|
||||||
Running the server will create `~/nose` for you to play with.
|
Running the server will create `~/nose` for you to play with.
|
||||||
|
|
||||||
|
git config core.hooksPath .githooks
|
||||||
bun install
|
bun install
|
||||||
bun dev
|
bun dev
|
||||||
open localhost:3000
|
open localhost:3000
|
||||||
|
|
@ -84,6 +85,7 @@ https://wakamaifondue.com/
|
||||||
- [x] public tunnel lives through reboots
|
- [x] public tunnel lives through reboots
|
||||||
- [o] tunnel to the terminal -- Not doing this for now!
|
- [o] tunnel to the terminal -- Not doing this for now!
|
||||||
- [x] web browser
|
- [x] web browser
|
||||||
|
- [x] browser commands
|
||||||
- [x] remember your "mode"
|
- [x] remember your "mode"
|
||||||
- [x] status bar on terminal UX
|
- [x] status bar on terminal UX
|
||||||
- [x] quickly open the current webapp
|
- [x] quickly open the current webapp
|
||||||
|
|
@ -98,3 +100,22 @@ https://wakamaifondue.com/
|
||||||
- [ ] sounds
|
- [ ] sounds
|
||||||
- [ ] maps
|
- [ ] maps
|
||||||
- [ ] etc...?
|
- [ ] etc...?
|
||||||
|
|
||||||
|
## Future Goals (need to be split into Phases)
|
||||||
|
|
||||||
|
- [ ] `nose` can find your NOSEputer via mDNS/Bonjour
|
||||||
|
- [ ] Shrimp!
|
||||||
|
- [ ] CodeMirror Editor
|
||||||
|
- [ ] cron tasks
|
||||||
|
- [ ] prompt()
|
||||||
|
- [ ] confirm()
|
||||||
|
- [ ] faster boot (turn off stuff like CUPS)
|
||||||
|
- [ ] SSD
|
||||||
|
- [ ] NOSE App Store -- "There's an app for that" (browse + install carts)
|
||||||
|
- [ ] System dashboard (CPU, memory)
|
||||||
|
- [ ] Write Your Own Daemons (MUD? FTPd?)
|
||||||
|
- [ ] Daemon Dashboard
|
||||||
|
- [ ] cloud backup for projects
|
||||||
|
- [ ] "sleep" / "wake" (disable monitor)
|
||||||
|
- [ ] themes
|
||||||
|
- [ ] pixel art boot animation
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
/// <reference lib="dom" />
|
// Print all the commands, or a subset.
|
||||||
// Print all the commands.
|
|
||||||
|
|
||||||
import { commands } from "@/js/commands"
|
import { commands } from "@/commands"
|
||||||
|
|
||||||
export default function (prefix?: string) {
|
export default async function (prefix?: string) {
|
||||||
let cmds = prefix ? matchingCommands(prefix) : commands
|
let cmds = prefix ? await matchingCommands(prefix) : Object.keys(await commands())
|
||||||
|
|
||||||
return { html: "<div>" + Object.keys(cmds).map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
|
return { html: "<div>" + cmds.map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function matchingCommands(cmd: string): string {
|
async function matchingCommands(cmd: string): Promise<string[]> {
|
||||||
let matched: string[] = []
|
let matched: string[] = []
|
||||||
console.log(Object.keys(commands))
|
|
||||||
for (const command of Object.keys(commands)) {
|
for (const command of Object.keys(await commands()))
|
||||||
console.log("cmd->", command)
|
|
||||||
if (command.startsWith(cmd)) matched.push(command)
|
if (command.startsWith(cmd)) matched.push(command)
|
||||||
}
|
|
||||||
|
|
||||||
|
return matched
|
||||||
return matched.join(" ")
|
|
||||||
}
|
}
|
||||||
30
bin/help.ts
30
bin/help.ts
|
|
@ -2,17 +2,25 @@
|
||||||
//
|
//
|
||||||
// (Hopefully.)
|
// (Hopefully.)
|
||||||
|
|
||||||
import { commandPath, commands } from "@/commands"
|
import { commandPath } from "@/commands"
|
||||||
|
import type { CommandOutput } from "@/shared/types"
|
||||||
|
import { moduleExports, type ExportInfo } from "@/sniffer"
|
||||||
|
import commands from "./commands"
|
||||||
|
|
||||||
export default async function (cmd: string): Promise<string> {
|
export default async function (cmd: string): Promise<CommandOutput> {
|
||||||
if (!cmd) return "usage: help <command>"
|
if (!cmd) return "usage: help <command>"
|
||||||
|
|
||||||
const path = commandPath(cmd)
|
const path = commandPath(cmd)
|
||||||
if (!path) { return matchingCommands(cmd) }
|
if (!path) return await commands(cmd)
|
||||||
|
|
||||||
|
const signatures = await moduleExports(path)
|
||||||
|
const signature = signatures.default
|
||||||
|
|
||||||
const code = (await Bun.file(path).text()).split("\n")
|
const code = (await Bun.file(path).text()).split("\n")
|
||||||
let docs = []
|
let docs = []
|
||||||
|
|
||||||
|
docs.push(usage(cmd, signature), "")
|
||||||
|
|
||||||
for (const line of code) {
|
for (const line of code) {
|
||||||
if (line.startsWith("///")) {
|
if (line.startsWith("///")) {
|
||||||
docs.push("Runs in the browser.\n")
|
docs.push("Runs in the browser.\n")
|
||||||
|
|
@ -27,11 +35,17 @@ export default async function (cmd: string): Promise<string> {
|
||||||
return docs.join("\n")
|
return docs.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function matchingCommands(cmd: string): Promise<string> {
|
function usage(cmd: string, signature?: ExportInfo) {
|
||||||
let matched: string[] = []
|
let out: string[] = [`usage: ${cmd}`]
|
||||||
for (const command of Object.keys(await commands())) {
|
|
||||||
if (command.startsWith(cmd)) matched.push(command)
|
if (signature?.kind === "function" && signature.signatures.length) {
|
||||||
|
const params = signature.signatures[0]!.params
|
||||||
|
for (const param of params) {
|
||||||
|
let desc = `${param.name}=`
|
||||||
|
desc += param.default ? `${param.default}` : `<${param.type}>`
|
||||||
|
out.push(desc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matched.join(" ")
|
return out.join(" ")
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Print information about the current session.
|
||||||
|
|
||||||
import { sessionGet } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
import { highlightToHTML } from "../lib/highlight"
|
import { highlightToHTML } from "../lib/highlight"
|
||||||
|
|
||||||
|
|
|
||||||
621
public/bundle.js
621
public/bundle.js
|
|
@ -1,5 +1,5 @@
|
||||||
////
|
////
|
||||||
// version: 3e67d66
|
// version: 178711c
|
||||||
|
|
||||||
// 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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,23 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
SHA=$(git rev-parse --short HEAD)
|
SHA=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
# Build to a temporary file
|
||||||
bun build ./src/js/main.js \
|
bun build ./src/js/main.js \
|
||||||
--outfile ./public/bundle.js \
|
--outfile ./public/bundle.tmp.js \
|
||||||
--target browser
|
--target browser
|
||||||
printf "////\n// version: %s\n\n" "$SHA" | cat - ./public/bundle.js > ./public/bundle.tmp.js
|
|
||||||
mv ./public/bundle.tmp.js ./public/bundle.js
|
# If bundle.js doesn't exist yet, fake it it
|
||||||
|
if [ ! -f ./public/bundle.js ]; then
|
||||||
|
touch ./public/bundle.js
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip version comments from both files and compare
|
||||||
|
tail -n +4 ./public/bundle.js > ./public/bundle.old.content.tmp
|
||||||
|
tail -n +1 ./public/bundle.tmp.js > ./public/bundle.new.content.tmp
|
||||||
|
|
||||||
|
if ! cmp -s ./public/bundle.old.content.tmp ./public/bundle.new.content.tmp; then
|
||||||
|
printf "////\n// version: %s\n\n" "$SHA" | cat - ./public/bundle.tmp.js > ./public/bundle.js
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm ./public/bundle.tmp.js ./public/bundle.old.content.tmp ./public/bundle.new.content.tmp
|
||||||
|
|
@ -82,7 +82,7 @@ export async function commandSource(name: string): Promise<string> {
|
||||||
export async function loadCommandModule(cmd: string) {
|
export async function loadCommandModule(cmd: string) {
|
||||||
const path = commandPath(cmd)
|
const path = commandPath(cmd)
|
||||||
if (!path) return
|
if (!path) return
|
||||||
return await importUrl(path + "?t+" + Date.now())
|
return await importUrl(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
let noseDirWatcher
|
let noseDirWatcher
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { send } from "./websocket"
|
||||||
import { setState } from "./state"
|
import { setState } from "./state"
|
||||||
|
|
||||||
export async function dispatchMessage(ws: any, msg: Message) {
|
export async function dispatchMessage(ws: any, msg: Message) {
|
||||||
console.log("<- receive", msg)
|
// console.log("<- receive", msg)
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "input":
|
case "input":
|
||||||
await inputMessage(ws, msg); break
|
await inputMessage(ws, msg); break
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export async function handleGameStart(msg: GameStartMessage) {
|
||||||
|
|
||||||
let game
|
let game
|
||||||
try {
|
try {
|
||||||
game = await importUrl(`/source/${name}?session=${sessionId}&t=${Date.now()}`)
|
game = await importUrl(`/source/${name}?session=${sessionId}}`)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setStatus(msgId, "error")
|
setStatus(msgId, "error")
|
||||||
addOutput(msgId, `Error: ${err.message ? err.message : err}`)
|
addOutput(msgId, `Error: ${err.message ? err.message : err}`)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export async function runCommand(input: string, showInput = true) {
|
||||||
const [cmd = "", ...args] = input.split(" ")
|
const [cmd = "", ...args] = input.split(" ")
|
||||||
|
|
||||||
if (commands[cmd]?.type === "browser") {
|
if (commands[cmd]?.type === "browser") {
|
||||||
const mod = await importUrl(`/source/${cmd}?t=${Date.now()}`)
|
const mod = await importUrl(`/source/${cmd}`)
|
||||||
if (!mod.default) {
|
if (!mod.default) {
|
||||||
addOutput(id, `no default export in ${cmd}`)
|
addOutput(id, `no default export in ${cmd}`)
|
||||||
setStatus(id, "error")
|
setStatus(id, "error")
|
||||||
|
|
|
||||||
|
|
@ -40,7 +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 += "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
|
||||||
|
}
|
||||||
|
|
@ -30,8 +30,7 @@ export type ExportInfo =
|
||||||
|
|
||||||
let prevProgram: ts.Program | undefined
|
let prevProgram: ts.Program | undefined
|
||||||
|
|
||||||
|
export async function moduleExports(file: string): Promise<Record<string, ExportInfo>> {
|
||||||
export async function allExports(file: string): Promise<ExportInfo[]> {
|
|
||||||
const program = ts.createProgram([file], {
|
const program = ts.createProgram([file], {
|
||||||
target: ts.ScriptTarget.ESNext,
|
target: ts.ScriptTarget.ESNext,
|
||||||
module: ts.ModuleKind.ESNext,
|
module: ts.ModuleKind.ESNext,
|
||||||
|
|
@ -48,7 +47,7 @@ export async function allExports(file: string): Promise<ExportInfo[]> {
|
||||||
if (!sf) throw new SniffError(`File not found: ${file}`)
|
if (!sf) throw new SniffError(`File not found: ${file}`)
|
||||||
|
|
||||||
const moduleSym = (sf as any).symbol as ts.Symbol | undefined
|
const moduleSym = (sf as any).symbol as ts.Symbol | undefined
|
||||||
if (!moduleSym) return []
|
if (!moduleSym) return {}
|
||||||
|
|
||||||
const exportSymbols = checker.getExportsOfModule(moduleSym)
|
const exportSymbols = checker.getExportsOfModule(moduleSym)
|
||||||
const result: ExportInfo[] = []
|
const result: ExportInfo[] = []
|
||||||
|
|
@ -92,7 +91,9 @@ export async function allExports(file: string): Promise<ExportInfo[]> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return Object.fromEntries(
|
||||||
|
result.map(item => [item.name, item])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
|
|
@ -101,5 +102,5 @@ if (import.meta.main) {
|
||||||
console.error("usage: sniff <path>")
|
console.error("usage: sniff <path>")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
console.log(await allExports(path))
|
console.log(await moduleExports(path))
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ async function findApp(name: string): Promise<App | undefined> {
|
||||||
async function loadApp(path: string): Promise<App | undefined> {
|
async function loadApp(path: string): Promise<App | undefined> {
|
||||||
if (!await Bun.file(path).exists()) return
|
if (!await Bun.file(path).exists()) return
|
||||||
|
|
||||||
const mod = await importUrl(path + `?t=${Date.now()}`)
|
const mod = await importUrl(path)
|
||||||
if (mod?.default)
|
if (mod?.default)
|
||||||
return mod.default as App
|
return mod.default as App
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import type { Message } from "./shared/types"
|
||||||
const wsConnections: any[] = []
|
const wsConnections: any[] = []
|
||||||
|
|
||||||
export function send(ws: any, msg: Message) {
|
export function send(ws: any, msg: Message) {
|
||||||
console.log("-> send", msg)
|
// console.log("-> send", msg)
|
||||||
ws.send(JSON.stringify(msg))
|
ws.send(JSON.stringify(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user