Add the dialog role and the interaction affordances the free-interaction layer depends on: Setup.interactions, Package.dialogs, and the Interaction, DialogAction, and Dialog types. Wire the dialog role through the zod schemas, package collection (with duplicate checking), and vite serialization, and update the tabletop serializedToPackage and test fixtures to carry dialogs.
110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
/**
|
|
* Vite plugin: resolve `virtual:bgm/packages` and `virtual:bgm/package/<id>`
|
|
* imports to JSON.
|
|
*
|
|
* The loader reads a games root (markdown code blocks + real
|
|
* yaml/json/toml/csv files), collects packages, and this plugin serves them
|
|
* as modules:
|
|
*
|
|
* - `virtual:bgm/packages` — the default export is an array of every
|
|
* discovered package.
|
|
* - `virtual:bgm/package/<id>` — the default export is that single package's
|
|
* assembled JSON.
|
|
*
|
|
* The `virtual:` prefix marks these as plugin-provided modules, so they can't
|
|
* be mistaken for real installed packages. Editing a game definition
|
|
* hot-reloads the app via `addWatchFile`.
|
|
*/
|
|
import * as path from "node:path";
|
|
import { normalizePath, type ModuleNode, type Plugin } from "vite";
|
|
import { collectPackages, loadDefs } from "./collect.js";
|
|
import { readDefFiles } from "./parse.js";
|
|
import type { Package, SerializedPackage } from "./types.js";
|
|
|
|
const VIRTUAL_PREFIX = "\0bgm:";
|
|
/** Public specifier for the module that lists every package. */
|
|
const PACKAGES = "virtual:bgm/packages";
|
|
/** Public specifier prefix for a single package module. */
|
|
const PACKAGE = "virtual:bgm/package/";
|
|
|
|
export interface BgmOptions {
|
|
/** Absolute path to the games root (e.g. `<repo>/games`). */
|
|
root: string;
|
|
}
|
|
|
|
export function bgm(options: BgmOptions): Plugin {
|
|
// Vite normalizes `ctx.file` to POSIX separators before HMR hooks run, but
|
|
// `fileURLToPath` retains backslashes on Windows. Normalize `root` to the
|
|
// same form so `ctx.file.startsWith(root)` matches regardless of platform.
|
|
const root = normalizePath(options.root);
|
|
|
|
const collect = (): Package[] => {
|
|
const defMap = loadDefs("", root);
|
|
return collectPackages(defMap, root);
|
|
};
|
|
|
|
return {
|
|
name: "bgm",
|
|
buildStart() {
|
|
// Watch every real definition source under the games root so edits
|
|
// trigger a re-collect. `defMap.files` only holds the virtual code-block
|
|
// files plus real non-markdown files; the markdown files themselves are
|
|
// consumed for their code blocks and never appear there, so watch the
|
|
// real files on disk too (markdown and anything else the loader reads).
|
|
const defMap = loadDefs("", root);
|
|
for (const name of defMap.files.keys()) {
|
|
this.addWatchFile(path.join(root, name));
|
|
}
|
|
for (const file of readDefFiles(root, "")) {
|
|
this.addWatchFile(file.source);
|
|
}
|
|
},
|
|
resolveId(id) {
|
|
if (id === PACKAGES) return VIRTUAL_PREFIX + PACKAGES;
|
|
if (id.startsWith(PACKAGE)) return VIRTUAL_PREFIX + id;
|
|
},
|
|
handleHotUpdate(ctx) {
|
|
// Re-collect and invalidate the virtual modules when a game definition
|
|
// changes, so edits hot-reload instead of requiring a manual refresh.
|
|
if (!ctx.file.startsWith(root)) return;
|
|
const invalidated: ModuleNode[] = [];
|
|
const mod = ctx.server.moduleGraph.getModuleById(
|
|
VIRTUAL_PREFIX + PACKAGES,
|
|
);
|
|
if (mod) {
|
|
ctx.server.moduleGraph.invalidateModule(mod);
|
|
invalidated.push(mod);
|
|
}
|
|
return invalidated;
|
|
},
|
|
load(id) {
|
|
if (!id.startsWith(VIRTUAL_PREFIX)) return;
|
|
const virtual = id.slice(VIRTUAL_PREFIX.length);
|
|
if (virtual === PACKAGES) {
|
|
const packages = collect().map(toJson);
|
|
return `export default ${JSON.stringify(packages)}`;
|
|
}
|
|
const name = virtual.slice(PACKAGE.length);
|
|
const pkg = collect().find((p) => p.meta.id === name);
|
|
if (!pkg) {
|
|
throw new Error(`bgm package "${name}" not found`);
|
|
}
|
|
return `export default ${JSON.stringify(toJson(pkg))}`;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Serialize a `Package` for JSON emission. The parts/surfaces/setups are
|
|
* `Map`s, which `JSON.stringify` would otherwise turn into `{}`.
|
|
*/
|
|
function toJson(pkg: Package): SerializedPackage {
|
|
return {
|
|
meta: pkg.meta,
|
|
parts: Object.fromEntries(pkg.parts),
|
|
surfaces: Object.fromEntries(pkg.surfaces),
|
|
setups: Object.fromEntries(pkg.setups),
|
|
dialogs: Object.fromEntries(pkg.dialogs),
|
|
};
|
|
}
|