diff --git a/packages/bgm/src/__fixtures__/vite-build/src/main.ts b/packages/bgm/src/__fixtures__/vite-build/src/main.ts index 3eab879..64e1b29 100644 --- a/packages/bgm/src/__fixtures__/vite-build/src/main.ts +++ b/packages/bgm/src/__fixtures__/vite-build/src/main.ts @@ -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. export const parts = [...harbor.parts.keys()]; export const surfaces = [...harbor.surfaces.keys()]; export const setups = [...harbor.setups.keys()]; 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); diff --git a/packages/bgm/src/vite.test.ts b/packages/bgm/src/vite.test.ts index fc253c7..a53aed3 100644 --- a/packages/bgm/src/vite.test.ts +++ b/packages/bgm/src/vite.test.ts @@ -25,6 +25,8 @@ describe('bgm vite plugin (integration)', () => { outDir, write: true, emptyOutDir: true, + // Keep identifiers readable so the test can assert on them. + minify: false, }, plugins: [bgm({ root: gamesRoot })], }); @@ -38,6 +40,10 @@ describe('bgm vite plugin (integration)', () => { expect(code).toContain('board#harbor'); expect(code).toContain('game#main'); expect(code).toContain('Harbor'); + + // The `bgm` module lists every discovered package. + expect(code).toContain('allIds'); + expect(code).toContain('"harbor"'); } finally { await fs.promises.rm(outDir, { recursive: true, force: true }); } diff --git a/packages/bgm/src/vite.ts b/packages/bgm/src/vite.ts index b57e234..07065d9 100644 --- a/packages/bgm/src/vite.ts +++ b/packages/bgm/src/vite.ts @@ -1,10 +1,19 @@ /** - * Vite plugin: resolve `bgm/` imports to the package's JSON. + * Vite plugin: resolve `virtual:bgm/packages` and `virtual:bgm/package/` + * imports to JSON. * * The loader reads a games root (markdown code blocks + real - * yaml/json/toml/csv files), collects packages, and this plugin serves each - * package as a module whose default export is the assembled JSON. Editing a - * game definition hot-reloads the app via `addWatchFile`. + * 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/` — 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 type { Plugin } from 'vite'; @@ -12,6 +21,10 @@ import { collectPackages, loadDefs } from './collect.js'; import type { Package } 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. `/games`). */ @@ -36,11 +49,17 @@ export function bgm(options: BgmOptions): Plugin { } }, 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) { 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); if (!pkg) { throw new Error(`bgm package "${name}" not found`); diff --git a/packages/bgm/src/vite.unit.test.ts b/packages/bgm/src/vite.unit.test.ts index 3c6573a..646c02e 100644 --- a/packages/bgm/src/vite.unit.test.ts +++ b/packages/bgm/src/vite.unit.test.ts @@ -24,16 +24,28 @@ function load(plugin: ReturnType, id: string): unknown { } 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 }); - expect(resolveId(plugin, 'bgm/harbor')).toBe('\0bgm:bgm/harbor'); - expect(resolveId(plugin, 'bgm/nope')).toBe('\0bgm:bgm/nope'); + expect(resolveId(plugin, 'virtual:bgm/packages')).toBe('\0bgm:virtual:bgm/packages'); + 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(); }); + 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', () => { 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(String(code).startsWith('export default ')).toBe(true); const pkg = JSON.parse(String(code).slice('export default '.length)); @@ -44,6 +56,6 @@ describe('bgm vite plugin', () => { it('errors on an unknown package', () => { 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/); }); });