From 3dc6465710705a6226018cefaf0a12d0f3b96cd3 Mon Sep 17 00:00:00 2001 From: Elias Oelschner <62939318+levno-710@users.noreply.github.com> Date: Fri, 8 May 2026 12:39:20 +0200 Subject: [PATCH 1/9] fix wasmoon load failure in vite dev mode --- package-lock.json | 31 +++++++++++++++++++++ package.json | 3 ++ pnpm-lock.yaml | 13 ++++++++- web/src/vite-env.d.ts | 1 + web/src/worker/prometheus.worker.ts | 30 ++++++++++++++++---- web/src/worker/prometheusRunner.ts | 42 ++++++++++++++++++++++++++-- web/vite.config.ts | 43 +++++++++++++++++------------ 7 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 package-lock.json create mode 100644 web/src/vite-env.d.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e886551 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "prometheus-workspace", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prometheus-workspace", + "dependencies": { + "pnpm": "^11.0.8" + } + }, + "node_modules/pnpm": { + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.0.8.tgz", + "integrity": "sha512-TECX4d0tQjcsTn+lp5H/KPx1pITHrBkuZLHfD97xdZS6mC+bT+2a37PHV4RvVlt5mydj+zcz0d4by4LPRmhJEg==", + "license": "MIT", + "bin": { + "pn": "bin/pnpm.mjs", + "pnpm": "bin/pnpm.mjs", + "pnpx": "bin/pnpx.mjs", + "pnx": "bin/pnpx.mjs" + }, + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + } + } +} diff --git a/package.json b/package.json index deaec42..a7f9d37 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,8 @@ "web:build": "pnpm --filter web build", "web:preview": "pnpm --filter web preview", "web:test": "pnpm --filter web test" + }, + "dependencies": { + "pnpm": "^11.0.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d863a..f2caef2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + dependencies: + pnpm: + specifier: ^11.0.8 + version: 11.0.8 web: dependencies: @@ -1840,6 +1844,11 @@ packages: engines: {node: '>=18'} hasBin: true + pnpm@11.0.8: + resolution: {integrity: sha512-TECX4d0tQjcsTn+lp5H/KPx1pITHrBkuZLHfD97xdZS6mC+bT+2a37PHV4RvVlt5mydj+zcz0d4by4LPRmhJEg==} + engines: {node: '>=22.13'} + hasBin: true + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} @@ -3832,6 +3841,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pnpm@11.0.8: {} + postcss@8.5.14: dependencies: nanoid: 3.3.12 diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/src/worker/prometheus.worker.ts b/web/src/worker/prometheus.worker.ts index 546a575..25c1934 100644 --- a/web/src/worker/prometheus.worker.ts +++ b/web/src/worker/prometheus.worker.ts @@ -1,13 +1,31 @@ import type { WorkerRequest, WorkerResponse } from "@/lib/prometheusTypes" -import { runPrometheus } from "./prometheusRunner" +type RunPrometheus = typeof import("./prometheusRunner")["runPrometheus"] + +let runPrometheus: RunPrometheus | null = null + +async function getRunPrometheus(): Promise { + if (runPrometheus) { + return runPrometheus + } + + const module = await import("./prometheusRunner") + runPrometheus = module.runPrometheus + return runPrometheus +} self.onmessage = async (event: MessageEvent) => { const { id, options } = event.data - const result = await runPrometheus(options).catch((error) => ({ - ok: false as const, - error: error instanceof Error ? error.message : String(error), - logs: [], - })) + const result = await getRunPrometheus() + .then((execute) => execute(options)) + .catch((error) => ({ + ok: false as const, + error: + error instanceof Error + ? `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ""}` + : String(error), + logs: [], + })) + const response: WorkerResponse = { id, result } self.postMessage(response) } diff --git a/web/src/worker/prometheusRunner.ts b/web/src/worker/prometheusRunner.ts index 6ced7be..abd7928 100644 --- a/web/src/worker/prometheusRunner.ts +++ b/web/src/worker/prometheusRunner.ts @@ -1,9 +1,42 @@ -import { LuaFactory } from "wasmoon" +import glueWasmUrl from "wasmoon/dist/glue.wasm?url" import luaSources from "virtual:prometheus-lua" import type { PrometheusLog, PrometheusOptions, PrometheusResult } from "@/lib/prometheusTypes" import { toLuaLongString } from "./luaString" +type LuaFactoryConstructor = new ( + customWasmUri?: string, + environmentVariables?: Record, +) => { + createEngine(options?: { openStandardLibs?: boolean }): Promise<{ + doString(luaCode: string): Promise + global: { close(): void } + }> +} + +let luaFactoryCtorPromise: Promise | null = null + +async function getLuaFactoryConstructor(): Promise { + if (luaFactoryCtorPromise) { + return luaFactoryCtorPromise + } + + luaFactoryCtorPromise = import("wasmoon/dist/index.js").then((mod) => { + const globalCandidate = (globalThis as { wasmoon?: { LuaFactory?: unknown } }).wasmoon?.LuaFactory + const moduleCandidate = (mod as { LuaFactory?: unknown }).LuaFactory + const defaultCandidate = (mod as { default?: { LuaFactory?: unknown } }).default?.LuaFactory + const candidate = (globalCandidate ?? moduleCandidate ?? defaultCandidate) as LuaFactoryConstructor | undefined + + if (typeof candidate !== "function") { + throw new Error("Unable to resolve LuaFactory export from wasmoon.") + } + + return candidate + }) + + return luaFactoryCtorPromise +} + const bootstrapLua = Object.entries(luaSources) .map(([name, source]) => { const chunkName = `@/src/${name.split(".").join("/")}.lua` @@ -91,10 +124,13 @@ function normalizeLogs(logs: unknown): PrometheusLog[] { export async function runPrometheus(options: PrometheusOptions): Promise { const logs: PrometheusLog[] = [] - let lua: Awaited> | null = null + let lua: Awaited["createEngine"]>> | null = null try { - lua = await new LuaFactory().createEngine({ openStandardLibs: true }) + // Force a local Vite-managed Wasm URL so dev/preview behave the same and + // we don't depend on wasmoon's default CDN URL resolution in workers. + const LuaFactory = await getLuaFactoryConstructor() + lua = await new LuaFactory(glueWasmUrl).createEngine({ openStandardLibs: true }) const result = (await lua.doString(buildRunLua(options))) as { ok?: unknown diff --git a/web/vite.config.ts b/web/vite.config.ts index fa08831..e0d48d7 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,23 +4,30 @@ import { defineConfig } from "vitest/config" import { prometheusLuaPlugin } from "./src/vite/prometheusLuaPlugin" -export default defineConfig({ - base: "/Prometheus/", - plugins: [react(), tailwindcss(), prometheusLuaPlugin()], - worker: { - plugins: () => [prometheusLuaPlugin()], - }, - resolve: { - alias: { - "@": new URL("./src", import.meta.url).pathname, +export default defineConfig(({ command }) => { + const isDevServer = command === "serve" + + return { + // Use repo base path for production/preview, but root path for local dev. + // This keeps runtime asset URLs (including Wasm files loaded by dependencies) + // valid in both environments. + base: isDevServer ? "/" : "/Prometheus/", + plugins: [react(), tailwindcss(), prometheusLuaPlugin()], + worker: { + plugins: () => [prometheusLuaPlugin()], + }, + resolve: { + alias: { + "@": new URL("./src", import.meta.url).pathname, + }, + }, + optimizeDeps: { + exclude: ["wasmoon"], + }, + test: { + environment: "node", + setupFiles: "./src/test/setup.ts", + exclude: ["src/e2e/**", "node_modules/**", "dist/**"], }, - }, - optimizeDeps: { - exclude: ["wasmoon"], - }, - test: { - environment: "node", - setupFiles: "./src/test/setup.ts", - exclude: ["src/e2e/**", "node_modules/**", "dist/**"], - }, + } }) From b51f8333fd0dba91b2a24f96ab6b331d4d97f0c6 Mon Sep 17 00:00:00 2001 From: Elias Oelschner <62939318+levno-710@users.noreply.github.com> Date: Fri, 8 May 2026 12:56:53 +0200 Subject: [PATCH 2/9] add run script functionality --- web/src/App.tsx | 223 ++++++++++++++++++++-------- web/src/components/CodeEditor.tsx | 25 +++- web/src/e2e/app.spec.ts | 14 ++ web/src/lib/prometheusTypes.ts | 33 ++-- web/src/worker/prometheus.worker.ts | 34 +++-- web/src/worker/prometheusRunner.ts | 92 ++++++++++++ 6 files changed, 337 insertions(+), 84 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 90bcc4c..051ced6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,4 @@ -import { Check, Copy, Download, FileCode2, Github, Loader2, Play, RotateCcw } from "lucide-react" +import { Check, Copy, Download, FileCode2, Github, Loader2, Play, RotateCcw, Square } from "lucide-react" import { useEffect, useRef, useState } from "react" import { toast } from "sonner" @@ -28,6 +28,8 @@ print(message) ` const WORKER_TIMEOUT_MS = 90_000 +type ActiveJob = "idle" | "obfuscate" | "run-input" | "run-output" + function createSeed() { return Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] % 2147483646) + 1 } @@ -50,7 +52,7 @@ function formatWorkerError(event: ErrorEvent): string { const detail = event.error instanceof Error ? `${event.error.name}: ${event.error.message}${event.error.stack ? `\n${event.error.stack}` : ""}` - : event.message || "Worker crashed while processing the obfuscation request." + : event.message || "Worker crashed while processing the request." return `${detail}${location}` } @@ -62,7 +64,7 @@ export default function App() { const [prettyPrint, setPrettyPrint] = useState(false) const [seed, setSeed] = useState(createSeed) const [logs, setLogs] = useState([]) - const [isRunning, setIsRunning] = useState(false) + const [activeJob, setActiveJob] = useState("idle") const [copied, setCopied] = useState(false) const workerRef = useRef(null) const requestIdRef = useRef(0) @@ -74,7 +76,7 @@ export default function App() { const detail = formatWorkerError(errorEvent) console.error("Prometheus worker error event:", event) console.error("Prometheus worker detail:", detail) - setIsRunning(false) + setActiveJob("idle") setLogs((current) => [...current, { level: "error", message: detail }]) toast.error("Worker error") workerRef.current?.terminate() @@ -83,7 +85,7 @@ export default function App() { worker.addEventListener("messageerror", (event) => { console.error("Prometheus worker message error:", event) - setIsRunning(false) + setActiveJob("idle") setLogs((current) => [...current, { level: "error", message: "Worker message decode failed." }]) toast.error("Worker message error") workerRef.current?.terminate() @@ -117,14 +119,32 @@ export default function App() { } } - useEffect(() => { - const workerUrl = new URL("./worker/prometheus.worker.ts", import.meta.url).toString() - workerUrlRef.current = workerUrl + function createWorker() { const worker = new Worker(new URL("./worker/prometheus.worker.ts", import.meta.url), { type: "module", }) workerRef.current = worker setupWorker(worker) + return worker + } + + function stopCurrentJob() { + if (activeJob === "idle") { + return + } + + workerRef.current?.terminate() + workerRef.current = null + createWorker() + setActiveJob("idle") + setLogs((current) => [...current, { level: "warn", message: "Execution stopped by user." }]) + toast("Execution stopped") + } + + useEffect(() => { + const workerUrl = new URL("./worker/prometheus.worker.ts", import.meta.url).toString() + workerUrlRef.current = workerUrl + createWorker() return () => { workerRef.current?.terminate() @@ -133,62 +153,53 @@ export default function App() { }, []) const canExport = output.trim().length > 0 + const isBusy = activeJob !== "idle" + const isObfuscating = activeJob === "obfuscate" + const isRunningInput = activeJob === "run-input" + const isRunningOutput = activeJob === "run-output" - async function obfuscate() { + async function sendWorkerRequest(request: WorkerRequest): Promise { let worker = workerRef.current const workerUrl = workerUrlRef.current || new URL("./worker/prometheus.worker.ts", import.meta.url).toString() const preflight = await canLoadWorker(workerUrl) if (!preflight.ok) { - setIsRunning(false) - setLogs([{ level: "error", message: preflight.message ?? "Worker preflight failed." }]) - toast.error("Worker load failed") - return + setActiveJob("idle") + return { + ok: false, + error: preflight.message ?? "Worker preflight failed.", + logs: [], + } } if (!worker) { - worker = new Worker(new URL("./worker/prometheus.worker.ts", import.meta.url), { - type: "module", - }) - setupWorker(worker) - workerRef.current = worker + worker = createWorker() } - if (!worker || isRunning) { - return - } - - setIsRunning(true) - setLogs([]) - const id = ++requestIdRef.current - const request: WorkerRequest = { - id, - options: { - source, - filename: "browser-input.lua", - preset, - luaVersion, - prettyPrint, - seed, - }, - } - - const result = await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => { - worker.removeEventListener("message", listener) + worker?.removeEventListener("message", listener) reject(new Error("Worker timed out before returning a result.")) }, WORKER_TIMEOUT_MS) const listener = (event: MessageEvent) => { - if (event.data.id !== id) { + const response = event.data + if (response.id !== request.id) { + return + } + if (response.type === "log") { + setLogs((current) => [...current, response.log]) + return + } + if (response.type !== "result") { return } window.clearTimeout(timeout) - worker.removeEventListener("message", listener) - resolve(event.data.result) + worker?.removeEventListener("message", listener) + resolve(response.result) } - worker.addEventListener("message", listener) - worker.postMessage(request) + worker?.addEventListener("message", listener) + worker?.postMessage(request) }).catch((error): PrometheusResult => { return { ok: false, @@ -196,18 +207,85 @@ export default function App() { logs: [], } }) + } - setIsRunning(false) + async function obfuscate() { + if (isBusy) { + return + } + + setActiveJob("obfuscate") + setLogs([]) + const id = ++requestIdRef.current + const request: WorkerRequest = { + id, + action: "obfuscate", + options: { + source, + filename: "browser-input.lua", + preset, + luaVersion, + prettyPrint, + seed, + }, + } + + const result = await sendWorkerRequest(request) + setActiveJob("idle") setLogs(result.logs) + if (result.ok) { setOutput(result.output) setSeed(createSeed()) toast.success("Obfuscation complete") - } else { - setOutput("") - setLogs([...result.logs, { level: "error", message: result.error }]) - toast.error("Obfuscation failed") + return } + + setOutput("") + setLogs([...result.logs, { level: "error", message: result.error }]) + toast.error("Obfuscation failed") + } + + async function runScript(kind: "input" | "output") { + if (isBusy) { + return + } + + const script = kind === "input" ? source : output + if (!script.trim()) { + setLogs([{ level: "warn", message: `No ${kind} script to run.` }]) + return + } + + setActiveJob(kind === "input" ? "run-input" : "run-output") + setLogs([]) + const id = ++requestIdRef.current + const request: WorkerRequest = { + id, + action: "runScript", + source: script, + filename: kind === "input" ? "browser-input.lua" : "browser-output.lua", + } + + const result = await sendWorkerRequest(request) + setActiveJob("idle") + + if (result.ok) { + setLogs((current) => { + if (current.length > 0) { + return current + } + if (result.logs.length > 0) { + return result.logs + } + return [{ level: "info", message: "Script finished without output." }] + }) + toast.success("Script execution complete") + return + } + + setLogs([...result.logs, { level: "error", message: result.error }]) + toast.error("Script execution failed") } async function copyOutput() { @@ -221,7 +299,7 @@ export default function App() { return ( -
+
@@ -246,9 +324,9 @@ export default function App() { GitHub -
@@ -259,7 +337,7 @@ export default function App() {
setLuaVersion(value as LuaVersion)}> - + @@ -287,7 +365,7 @@ export default function App() {
- + @@ -300,11 +378,12 @@ export default function App() { type="number" min={1} value={seed} + disabled={isBusy} onChange={(event) => setSeed(Math.max(1, Number(event.target.value) || 1))} /> - @@ -333,10 +412,32 @@ export default function App() {
-
- - -