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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user