diff --git a/bun.lock b/bun.lock index 0395627..d51ee3d 100644 --- a/bun.lock +++ b/bun.lock @@ -4,13 +4,11 @@ "workspaces": { "": { "name": "punktfunk-plugin-rom-manager", - "dependencies": { - "@punktfunk/host": "^0.1.0", - "effect": "^4.0.0-beta.98", - }, "devDependencies": { "@biomejs/biome": "^2.5.2", + "@punktfunk/host": "^0.1.0", "@types/bun": "^1.3.0", + "effect": "^4.0.0-beta.98", "typescript": "^5.9.3", }, }, diff --git a/package.json b/package.json index a8bce0c..bf8289e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "punktfunk-plugin-rom-manager", - "version": "0.1.0", + "version": "0.1.1", "private": false, "type": "module", "description": "punktfunk plugin: scans ROM directories, maps them to emulators, and reconciles them into the host game library as a provider — with a console-hosted web UI.", @@ -20,7 +20,6 @@ ], "main": "./dist/index.js", "module": "./dist/index.js", - "types": "./dist/index.d.ts", "bin": { "punktfunk-plugin-rom-manager": "./dist/cli.js" }, @@ -33,7 +32,7 @@ "scripts": { "typecheck": "tsc --noEmit", "test": "bun test", - "build": "tsc -p tsconfig.build.json", + "build": "rm -rf dist && bun build src/index.ts src/cli.ts --target=bun --outdir dist --external undici", "build:ui": "cd ui && bun install --silent && bun run build", "build:all": "bun run build && bun run build:ui", "sync": "bun src/cli.ts sync", @@ -41,13 +40,12 @@ "detect": "bun src/cli.ts detect", "prepublishOnly": "bun run build:all" }, - "dependencies": { - "@punktfunk/host": "^0.1.0", - "effect": "^4.0.0-beta.98" - }, + "dependencies": {}, "devDependencies": { "@biomejs/biome": "^2.5.2", + "@punktfunk/host": "^0.1.0", "@types/bun": "^1.3.0", + "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" } } diff --git a/src/paths.ts b/src/paths.ts index c267723..7e3d52e 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -1,8 +1,10 @@ // Where the plugin's own files live. The host config dir resolution mirrors the SDK's `configDir` // (`@punktfunk/host` does not re-export it) so we always land in the same place the host and runner // use; the plugin owns a `rom-manager/` subtree under it (design §9), created 0700. +import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; /** The host's config dir — the same resolution the host and SDK use. */ export const hostConfigDir = (): string => { @@ -26,3 +28,28 @@ export const configPath = (): string => path.join(pluginDir(), "config.json"); /** `/rom-manager/cache.json` — disposable derived state (art verdicts, detection, fingerprint). */ export const cachePath = (): string => path.join(pluginDir(), "cache.json"); + +/** + * Locate the built SPA directory (`dist/ui`) at runtime. The package ships as ONE self-contained bundle + * (`dist/index.js`) so it installs from the Gitea registry with no external deps — which means + * `import.meta.url` can be `dist/index.js` (bundled) or `dist/server/index.js` (unbundled dev). We walk + * up from the calling module looking for a `ui/index.html`, so both layouts resolve. `fromUrl` is the + * caller's `import.meta.url`. + */ +export const resolveUiDir = (fromUrl: string): string => { + let dir = path.dirname(fileURLToPath(fromUrl)); + for (let i = 0; i < 6; i++) { + for (const cand of [path.join(dir, "ui"), path.join(dir, "dist", "ui")]) { + try { + if (fs.existsSync(path.join(cand, "index.html"))) return cand; + } catch { + // keep walking + } + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + // Fallback to the bundled layout (dist/index.js → dist/ui) even if unbuilt. + return path.join(path.dirname(fileURLToPath(fromUrl)), "ui"); +}; diff --git a/src/server/index.ts b/src/server/index.ts index e80c16e..0514934 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,12 +11,10 @@ import { } from "@punktfunk/host"; import { PLUGIN_NAME, UI_ICON, UI_TITLE } from "../const.js"; import type { Engine } from "../engine/index.js"; +import { resolveUiDir } from "../paths.js"; import { makeRouter } from "./router.js"; -/** - * Serve the UI through the console. `../../dist/ui` resolves to `/dist/ui` whether this module - * runs from source (`src/server/index.ts`) or built (`dist/server/index.js`). - */ +/** Serve the UI through the console. The SPA dir resolves whether we run bundled or from source. */ export const serveConsoleUi = ( pf: Punktfunk, engine: Engine, @@ -27,6 +25,6 @@ export const serveConsoleUi = ( title: UI_TITLE, icon: UI_ICON, version: opts?.version, - staticDir: new URL("../../dist/ui", import.meta.url), + staticDir: resolveUiDir(import.meta.url), fetch: makeRouter(engine), }); diff --git a/src/server/standalone.ts b/src/server/standalone.ts index 4f941c0..e10eaa6 100644 --- a/src/server/standalone.ts +++ b/src/server/standalone.ts @@ -4,10 +4,10 @@ // non-loopback bind REQUIRES a password (a UI that can rewrite launch templates is host-user code). import * as nodePath from "node:path"; -import { fileURLToPath } from "node:url"; import type { Config } from "../config.js"; import type { Engine } from "../engine/index.js"; import { log } from "../log.js"; +import { resolveUiDir } from "../paths.js"; import { verifyPassword } from "./password.js"; import { makeRouter } from "./router.js"; @@ -70,7 +70,7 @@ export const serveStandaloneUi = ( const authRequired = Boolean(passwordHash); // A per-boot session token — any client presenting it in the cookie is authed until the plugin restarts. const sessionToken = crypto.randomUUID().replace(/-/g, ""); - const root = fileURLToPath(new URL("../../dist/ui", import.meta.url)); + const root = resolveUiDir(import.meta.url); const router = makeRouter(engine); const authed = (req: Request) =>