diff --git a/README.md b/README.md index 1f20f27..9f1188d 100644 --- a/README.md +++ b/README.md @@ -759,8 +759,7 @@ The menu is **driven entirely by config** and assembled from two sources: in or bind-mounting your own dir onto `/app/config` (a commented example sits in `compose.override.yml`). The file imports its typed builder from **`#menu-config`** (the subpath import mapped to `src/ui/menu-config.ts`), so it resolves wherever it's mounted - (keep the mounted `config/` a plain dir — no `package.json` of its own — or `#menu-config` - resolves against that instead and boot fails loud): + (keep the mounted `config/` a plain dir — no `package.json` of its own): ```ts import { defineMenu } from "#menu-config"; export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } }); diff --git a/src/ui/menu-config.test.ts b/src/ui/menu-config.test.ts index a5657bb..d53f19f 100644 --- a/src/ui/menu-config.test.ts +++ b/src/ui/menu-config.test.ts @@ -1,16 +1,20 @@ import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test, type TestContext } from "node:test"; import { DEFAULT_MENU, loadMenuConfig } from "./menu-config.ts"; // Write a throwaway menu.ts (a plain object — defineMenu is identity) and clean it up after. -function scaffold(t: TestContext, source: string): string { +function scaffold(t: TestContext, source: string, strays: string[] = []): string { const dir = mkdtempSync(join(tmpdir(), "pp-menu-")); t.after(() => rmSync(dir, { force: true, recursive: true })); const file = join(dir, "menu.ts"); writeFileSync(file, source); + for (const stray of strays) { + if (stray.endsWith(".json")) writeFileSync(join(dir, stray), "{}"); + else mkdirSync(join(dir, stray), { recursive: true }); + } return file; } @@ -37,3 +41,14 @@ test("loadMenuConfig fails loud on a malformed config", async (t) => { await assert.rejects(loadMenuConfig({ file: scaffold(t, `export default { branding: { theme: "neon" } };`) }), /theme/); await assert.rejects(loadMenuConfig({ file: scaffold(t, `export default { override: { hide: "teams" } };`) }), /hide.*array/s); }); + +test("loadMenuConfig refuses a stray package.json or node_modules beside the config", async (t) => { + const valid = `export default { branding: { name: "Acme Ops" } };`; + + for (const stray of ["node_modules", "package.json"]) { + await assert.rejects( + loadMenuConfig({ file: scaffold(t, valid, [stray]) }), + new RegExp(`config/${stray.replace(".", "\\.")} must not exist.*delete`, "s"), + ); + } +}); diff --git a/src/ui/menu-config.ts b/src/ui/menu-config.ts index 7c24373..d897c67 100644 --- a/src/ui/menu-config.ts +++ b/src/ui/menu-config.ts @@ -50,6 +50,14 @@ export async function loadMenuConfig(options: LoadMenuOptions = {}): Promise