refactor(bgm): use virtual: module specifiers

Rename the plugin's virtual modules to virtual:bgm/packages and
virtual:bgm/package/<id> so they read as plugin-provided modules rather
than real packages. Update the unit and build integration tests.
This commit is contained in:
2026-08-09 19:16:17 +08:00
parent ba16f17d8a
commit 845d510948
4 changed files with 52 additions and 13 deletions
@@ -1,9 +1,11 @@
import harbor from 'bgm/harbor'; import harbor from 'virtual:bgm/package/harbor';
import packages from 'virtual:bgm/packages';
// Re-export the package data so the test can assert the bundled output. // Re-export the package data so the test can assert the bundled output.
export const parts = [...harbor.parts.keys()]; export const parts = [...harbor.parts.keys()];
export const surfaces = [...harbor.surfaces.keys()]; export const surfaces = [...harbor.surfaces.keys()];
export const setups = [...harbor.setups.keys()]; export const setups = [...harbor.setups.keys()];
export const title = harbor.meta.title; export const title = harbor.meta.title;
export const allIds = packages.map((p) => p.meta.id);
console.log(title, parts, surfaces, setups); console.log(title, parts, surfaces, setups, allIds);
+6
View File
@@ -25,6 +25,8 @@ describe('bgm vite plugin (integration)', () => {
outDir, outDir,
write: true, write: true,
emptyOutDir: true, emptyOutDir: true,
// Keep identifiers readable so the test can assert on them.
minify: false,
}, },
plugins: [bgm({ root: gamesRoot })], plugins: [bgm({ root: gamesRoot })],
}); });
@@ -38,6 +40,10 @@ describe('bgm vite plugin (integration)', () => {
expect(code).toContain('board#harbor'); expect(code).toContain('board#harbor');
expect(code).toContain('game#main'); expect(code).toContain('game#main');
expect(code).toContain('Harbor'); expect(code).toContain('Harbor');
// The `bgm` module lists every discovered package.
expect(code).toContain('allIds');
expect(code).toContain('"harbor"');
} finally { } finally {
await fs.promises.rm(outDir, { recursive: true, force: true }); await fs.promises.rm(outDir, { recursive: true, force: true });
} }
+25 -6
View File
@@ -1,10 +1,19 @@
/** /**
* Vite plugin: resolve `bgm/<package>` imports to the package's JSON. * Vite plugin: resolve `virtual:bgm/packages` and `virtual:bgm/package/<id>`
* imports to JSON.
* *
* The loader reads a games root (markdown code blocks + real * The loader reads a games root (markdown code blocks + real
* yaml/json/toml/csv files), collects packages, and this plugin serves each * yaml/json/toml/csv files), collects packages, and this plugin serves them
* package as a module whose default export is the assembled JSON. Editing a * as modules:
* game definition hot-reloads the app via `addWatchFile`. *
* - `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 * as path from 'node:path';
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
@@ -12,6 +21,10 @@ import { collectPackages, loadDefs } from './collect.js';
import type { Package } from './types.js'; import type { Package } from './types.js';
const VIRTUAL_PREFIX = '\0bgm:'; 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 { export interface BgmOptions {
/** Absolute path to the games root (e.g. `<repo>/games`). */ /** Absolute path to the games root (e.g. `<repo>/games`). */
@@ -36,11 +49,17 @@ export function bgm(options: BgmOptions): Plugin {
} }
}, },
resolveId(id) { resolveId(id) {
if (id.startsWith('bgm/')) return VIRTUAL_PREFIX + id; if (id === PACKAGES) return VIRTUAL_PREFIX + PACKAGES;
if (id.startsWith(PACKAGE)) return VIRTUAL_PREFIX + id;
}, },
load(id) { load(id) {
if (!id.startsWith(VIRTUAL_PREFIX)) return; if (!id.startsWith(VIRTUAL_PREFIX)) return;
const name = id.slice(VIRTUAL_PREFIX.length + 'bgm/'.length); 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); const pkg = collect().find((p) => p.meta.id === name);
if (!pkg) { if (!pkg) {
throw new Error(`bgm package "${name}" not found`); throw new Error(`bgm package "${name}" not found`);
+17 -5
View File
@@ -24,16 +24,28 @@ function load(plugin: ReturnType<typeof bgm>, id: string): unknown {
} }
describe('bgm vite plugin', () => { describe('bgm vite plugin', () => {
it('resolves bgm/ imports to the virtual module', () => { it('resolves bgm imports to the virtual module', () => {
const plugin = bgm({ root: gamesRoot }); const plugin = bgm({ root: gamesRoot });
expect(resolveId(plugin, 'bgm/harbor')).toBe('\0bgm:bgm/harbor'); expect(resolveId(plugin, 'virtual:bgm/packages')).toBe('\0bgm:virtual:bgm/packages');
expect(resolveId(plugin, 'bgm/nope')).toBe('\0bgm:bgm/nope'); expect(resolveId(plugin, 'virtual:bgm/package/harbor')).toBe('\0bgm:virtual:bgm/package/harbor');
expect(resolveId(plugin, 'virtual:bgm/package/nope')).toBe('\0bgm:virtual:bgm/package/nope');
expect(resolveId(plugin, 'other')).toBeUndefined(); expect(resolveId(plugin, 'other')).toBeUndefined();
}); });
it('loads every package as a JSON module', () => {
const plugin = bgm({ root: gamesRoot });
const code = load(plugin, '\0bgm:virtual:bgm/packages');
expect(code).toBeDefined();
expect(String(code).startsWith('export default ')).toBe(true);
const packages = JSON.parse(String(code).slice('export default '.length));
expect(packages).toHaveLength(1);
expect(packages[0].meta.id).toBe('harbor');
expect(packages[0].parts).toHaveProperty('token#wood');
});
it('loads a package as a JSON module', () => { it('loads a package as a JSON module', () => {
const plugin = bgm({ root: gamesRoot }); const plugin = bgm({ root: gamesRoot });
const code = load(plugin, '\0bgm:bgm/harbor'); const code = load(plugin, '\0bgm:virtual:bgm/package/harbor');
expect(code).toBeDefined(); expect(code).toBeDefined();
expect(String(code).startsWith('export default ')).toBe(true); expect(String(code).startsWith('export default ')).toBe(true);
const pkg = JSON.parse(String(code).slice('export default '.length)); const pkg = JSON.parse(String(code).slice('export default '.length));
@@ -44,6 +56,6 @@ describe('bgm vite plugin', () => {
it('errors on an unknown package', () => { it('errors on an unknown package', () => {
const plugin = bgm({ root: gamesRoot }); const plugin = bgm({ root: gamesRoot });
expect(() => load(plugin, '\0bgm:bgm/unknown')).toThrow(/not found/); expect(() => load(plugin, '\0bgm:virtual:bgm/package/unknown')).toThrow(/not found/);
}); });
}); });