Normalize the games root to POSIX separators so the HMR path guard matches on Windows, and watch the real on-disk markdown files (which defMap.files omits in favor of their virtual code blocks) so edits trigger a re-collect instead of a manual restart.
107 lines
3.9 KiB
TypeScript
107 lines
3.9 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),
|
|
};
|
|
}
|