Rename the admin example plugin to auth-admin, and let config/menu.ts hide or rename a profile-menu entry
CI / full-gate (push) Successful in 3m32s

This commit is contained in:
2026-11-08 21:25:11 +01:00
parent 34dfbea156
commit 05da1d7897
57 changed files with 317 additions and 270 deletions
+6
View File
@@ -29,6 +29,12 @@ test("findConflicts: duplicate nav id is an error, a shared permission name only
p({ id: "b", nav: [{ id: "dup", label: "B" }] }),
]);
assert.ok(navDup.some((c) => c.kind === "nav-id" && c.level === "error" && c.plugins.includes("a") && c.plugins.includes("b")));
// The override targets profile-menu entries by the same ids, so one namespace covers both.
const profileDup = findConflicts([
p({ id: "a", nav: [{ id: "dup", label: "A" }] }),
p({ id: "b", profileMenu: [{ href: "/b", id: "dup", label: "B", session: true }] }),
]);
assert.ok(profileDup.some((c) => c.kind === "nav-id" && c.level === "error" && c.plugins.includes("a") && c.plugins.includes("b")));
// Sharing a permission across plugins is legitimate → warn, not error.
const permissionDup = findConflicts([
+5 -1
View File
@@ -34,7 +34,11 @@ export function findConflicts(plugins: Plugin[]): PluginConflict[] {
if (owners.length > 1) out.push({ kind: "route", level: "error", message: `${owners.length} routes resolve to "${key}"`, plugins: uniq(owners) });
});
collect(plugins, (plugin, push) => collectNavIds(plugin.nav, push)).forEach((owners, id) => {
const overrideTargets = (plugin: Plugin, push: (id: string) => void): void => {
collectNavIds(plugin.nav, push);
for (const entry of plugin.profileMenu ?? []) if (entry.id != null) push(entry.id);
};
collect(plugins, overrideTargets).forEach((owners, id) => {
if (owners.length > 1) out.push({ kind: "nav-id", level: "error", message: `nav id "${id}" used ${owners.length}×; override targets ids, so they must be unique`, plugins: uniq(owners) });
});
+3 -3
View File
@@ -128,11 +128,11 @@ test("a route + nav node may be marked public, or session, and load fine", async
assert.equal(sess?.nav?.[0]?.session, true);
});
test("`admin` is not reserved — the admin screens ship as a drop-in plugin mounted at /admin", async (t) => {
const dir = scaffold(t, { "admin/plugin.ts": full("admin") });
test("`auth-admin` is not reserved — the admin screens ship as a drop-in plugin mounted at /auth-admin", async (t) => {
const dir = scaffold(t, { "auth-admin/plugin.ts": full("auth-admin") });
const plugins = await discoverPlugins({ dir });
assert.equal(plugins.length, 1);
assert.equal(plugins[0]?.id, "admin");
assert.equal(plugins[0]?.id, "auth-admin");
});
test("a plugin may declare `home` (public /) and `dashboard` (gated /dashboard) handlers", async (t) => {
+3 -3
View File
@@ -108,8 +108,8 @@ test("RESERVED_PLUGIN_IDS covers every built-in top-level mount; `home` (the / f
]);
for (const id of builtins) assert.ok(RESERVED_PLUGIN_IDS.has(id), `built-in mount "${id}" must be a reserved plugin id`);
// "/" is owned by the `home` manifest field (not a /<id> route), so it cannot be shadowed and is
// deliberately not reserved — a plugin folder named "home" is legal. `admin` is likewise free: the
// admin screens ship as a drop-in plugin mounted at /admin, not a built-in route.
// deliberately not reserved — a plugin folder named "home" is legal. `auth-admin` is likewise free:
// the admin screens ship as a drop-in plugin mounted at /auth-admin, not a built-in route.
assert.equal(RESERVED_PLUGIN_IDS.has("home"), false);
assert.equal(RESERVED_PLUGIN_IDS.has("admin"), false);
assert.equal(RESERVED_PLUGIN_IDS.has("auth-admin"), false);
});