Follow symlinked plugin folders, and keep a plugin .npmrc out of the image
CI / full-gate (push) Successful in 2m44s

This commit is contained in:
lilleman
2026-08-18 07:49:51 +02:00
parent 77343e859a
commit 3f9787df48
5 changed files with 32 additions and 5 deletions
+18 -1
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test, type TestContext } from "node:test";
@@ -127,6 +127,23 @@ test("a plugin may carry its own package.json, node_modules and dependencies", a
assert.deepEqual(await plugins[0]?.routes?.[0]?.handler(null as never), { html: "20 kr" });
});
test("a plugin folder may be a symlink — a plugin kept in its own repo", async (t) => {
const ownRepo = scaffold(t, { "my-plugin/plugin.ts": full("my-plugin") });
const dir = scaffold(t, {});
symlinkSync(join(ownRepo, "my-plugin"), join(dir, "linked"));
const plugins = await discoverPlugins({ dir });
assert.deepEqual(plugins.map((p) => p.id), ["linked"]); // the link name is the id, not the target's
});
test("a dangling plugin symlink fails loud rather than vanishing", async (t) => {
const dir = scaffold(t, {});
symlinkSync(join(dir, "gone"), join(dir, "broken"));
await assert.rejects(discoverPlugins({ dir }), /broken.*plugin\.ts/s);
});
test("a shared permission name only warns — both plugins still load", async (t) => {
const shared = `export default { apiVersion: "1.0.0", permissions: [{ name: "shared:read" }] };`;
const dir = scaffold(t, { "x/plugin.ts": shared, "y/plugin.ts": shared });
+3 -3
View File
@@ -84,11 +84,11 @@ export async function discoverPlugins(options: DiscoverOptions = {}): Promise<Pl
}
// Subfolders of plugins/, sorted for deterministic load order + stable conflict messages. Hidden
// entries (.git, .DS_Store, …) and non-directories are skipped — only folders are plugins. So is
// node_modules, which npm leaves here when a dependency install is pointed at plugins/ itself.
// entries, plain files and node_modules are skipped; a symlink counts, so a plugin kept in its own
// repo joins the tree with `ln -s`, and a dangling one trips "no plugin.ts found" rather than vanishing.
function pluginFolders(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true })
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
.filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith(".") && e.name !== "node_modules")
.map((e) => e.name)
.sort();
}