fix(bgm): resolve include patterns relative to package dir

The default include matched every yaml across the games root, so a
package could absorb a sibling game's defs (e.g. two game#main setups).
Resolve patterns against the package declaration's directory instead;
a leading slash marks a pattern as root-relative.
This commit is contained in:
2026-08-10 08:52:51 +08:00
parent 77ffd8ff00
commit 2001616d9e
3 changed files with 38 additions and 3 deletions
+7
View File
@@ -104,6 +104,13 @@ package. **Defaults to `./**/*.yaml`**, so all yaml in the same and sub
folders is discovered with no configuration. This also matches the package folders is discovered with no configuration. This also matches the package
declaration itself, which is fine — it's the package, not a part. declaration itself, which is fine — it's the package, not a part.
Patterns are resolved **relative to the package declaration's own directory**,
not the games root. So a package declared in `carcassonne/carcassonne.md`
with the default `./**/*.yaml` only picks up yaml under `carcassonne/` — it
never absorbs defs from a sibling game. To reach outside its folder, a
package can use a `../`-relative pattern or an absolute-from-root pattern
(e.g. `**/shared/*.yaml`).
--- ---
## 3. Roles ## 3. Roles
+18
View File
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import { loadDefs, collectPackages } from './collect.js'; import { loadDefs, collectPackages } from './collect.js';
const fixtureRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__', 'harbor'); const fixtureRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__', 'harbor');
const multiRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__', 'vite-build', 'games');
describe('collectPackages', () => { describe('collectPackages', () => {
it('collects the harbor package from markdown code blocks', () => { it('collects the harbor package from markdown code blocks', () => {
@@ -60,6 +61,23 @@ describe('collectPackages', () => {
}); });
}); });
it('scopes include patterns to the package declaration directory', () => {
// Two packages share a games root. Each uses the default `./**/*.yaml`
// include, which must resolve relative to its own folder so neither
// absorbs the other's defs (both define a `game#main` setup).
const defMap = loadDefs('', multiRoot);
const packages = collectPackages(defMap, multiRoot);
expect(packages).toHaveLength(2);
const azul = packages.find((p) => p.meta.id === 'azul')!;
const harbor = packages.find((p) => p.meta.id === 'harbor')!;
expect([...azul.parts.keys()]).toEqual(['tile#blue']);
expect([...azul.setups.keys()]).toEqual(['game#main']);
expect([...harbor.parts.keys()]).toEqual(['token#wood']);
expect([...harbor.setups.keys()]).toEqual(['game#main']);
});
it('throws on a duplicate type#id', () => { it('throws on a duplicate type#id', () => {
const defMap = loadDefs('', fixtureRoot); const defMap = loadDefs('', fixtureRoot);
// Inject a duplicate part into the map under a new file name. // Inject a duplicate part into the map under a new file name.
+13 -3
View File
@@ -101,7 +101,8 @@ export function collectPackages(defMap: DefMap, rootDir: string): Package[] {
const role = def.value['role'] as Role; const role = def.value['role'] as Role;
if (role === 'package') { if (role === 'package') {
const pkg = asPackage(def, file); const pkg = asPackage(def, file);
accs.push(new PackageAcc(pkg, defMap, rootDir)); const baseDir = path.posix.dirname(file).replace(/^\/+/, '');
accs.push(new PackageAcc(pkg, defMap, rootDir, baseDir));
} }
} }
} }
@@ -125,6 +126,8 @@ class PackageAcc {
readonly pkg: PackageDef, readonly pkg: PackageDef,
private readonly defs: DefMap, private readonly defs: DefMap,
private readonly rootDir: string, private readonly rootDir: string,
/** Path-style directory of the package declaration, e.g. `carcassonne`. */
private readonly baseDir: string,
) {} ) {}
collect() { collect() {
@@ -152,10 +155,17 @@ class PackageAcc {
private expandIncludes(patterns: string[]): string[] { private expandIncludes(patterns: string[]): string[] {
// Match include patterns against the parsed definitions' names, which // Match include patterns against the parsed definitions' names, which
// cover both real files and markdown code blocks. Patterns are relative // cover both real files and markdown code blocks. Patterns are relative
// to the games root (e.g. `./**/*.yaml`). // to the package declaration's own directory (e.g. `./**/*.yaml` means
// this package's folder and below), so a package never absorbs defs from
// a sibling game. A leading `/` marks a pattern as root-relative.
// Def names carry a leading `/` (from the empty games root), so resolved
// patterns are prefixed to match.
const names = new Set<string>(); const names = new Set<string>();
for (const pattern of patterns) { for (const pattern of patterns) {
const matcher = picomatch(pattern, { dot: true }); const resolved = pattern.startsWith('/')
? pattern
: `/${path.posix.join(this.baseDir, pattern)}`;
const matcher = picomatch(resolved, { dot: true });
for (const name of this.defs.defs.keys()) { for (const name of this.defs.defs.keys()) {
if (matcher(name)) names.add(name); if (matcher(name)) names.add(name);
} }