Refuse a stray package.json in config/, and let Renovate reach the example plugins #77
@@ -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
|
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
|
`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
|
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`
|
(keep the mounted `config/` a plain dir — no `package.json` of its own):
|
||||||
resolves against that instead and boot fails loud):
|
|
||||||
```ts
|
```ts
|
||||||
import { defineMenu } from "#menu-config";
|
import { defineMenu } from "#menu-config";
|
||||||
export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } });
|
export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } });
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import assert from "node:assert/strict";
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { test, type TestContext } from "node:test";
|
import { test, type TestContext } from "node:test";
|
||||||
import { DEFAULT_MENU, loadMenuConfig } from "./menu-config.ts";
|
import { DEFAULT_MENU, loadMenuConfig } from "./menu-config.ts";
|
||||||
|
|
||||||
// Write a throwaway menu.ts (a plain object — defineMenu is identity) and clean it up after.
|
// 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-"));
|
const dir = mkdtempSync(join(tmpdir(), "pp-menu-"));
|
||||||
t.after(() => rmSync(dir, { force: true, recursive: true }));
|
t.after(() => rmSync(dir, { force: true, recursive: true }));
|
||||||
const file = join(dir, "menu.ts");
|
const file = join(dir, "menu.ts");
|
||||||
writeFileSync(file, source);
|
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;
|
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 { branding: { theme: "neon" } };`) }), /theme/);
|
||||||
await assert.rejects(loadMenuConfig({ file: scaffold(t, `export default { override: { hide: "teams" } };`) }), /hide.*array/s);
|
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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ export async function loadMenuConfig(options: LoadMenuOptions = {}): Promise<Men
|
|||||||
const file = options.file ?? MENU_CONFIG_FILE;
|
const file = options.file ?? MENU_CONFIG_FILE;
|
||||||
if (!existsSync(file)) return DEFAULT_MENU; // clean clone: no central override
|
if (!existsSync(file)) return DEFAULT_MENU; // clean clone: no central override
|
||||||
|
|
||||||
|
// Guarded before the import: Node's own ERR_PACKAGE_IMPORT_NOT_DEFINED names neither cause nor remedy.
|
||||||
|
const dir = dirname(file);
|
||||||
|
for (const stray of ["node_modules", "package.json"]) {
|
||||||
|
if (existsSync(join(dir, stray))) {
|
||||||
|
throw new Error(`config/${stray} must not exist — it makes config/ its own package scope, so the #menu-config import in config/menu.ts no longer resolves; delete config/{node_modules,package.json,package-lock.json} and keep config/ a plain dir`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mod: { default?: unknown };
|
let mod: { default?: unknown };
|
||||||
try {
|
try {
|
||||||
mod = await import(pathToFileURL(file).href);
|
mod = await import(pathToFileURL(file).href);
|
||||||
|
|||||||
Reference in New Issue
Block a user