From 2464bcdff4c415425d316522be2a8c2819192e9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 09:17:43 +0000 Subject: [PATCH] fix: Accept Undefined Site On Configurable Plugins To Fix Plugins CI Komga 1.0.2 sets site from storage.get("url"), which is undefined until configured, so the daily plugin load check failed. Normalize it to '' for plugins with settings; the runner already reports NEEDS_CONFIG on use. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PMGDVpRXfbP2CPMQj15iow --- .changeset/komga-unset-site.md | 5 ++++ .../plugin-runtime/src/plugins/sandbox.ts | 11 +++++---- packages/plugin-runtime/test/sandbox.test.ts | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .changeset/komga-unset-site.md diff --git a/.changeset/komga-unset-site.md b/.changeset/komga-unset-site.md new file mode 100644 index 0000000..f1fb92f --- /dev/null +++ b/.changeset/komga-unset-site.md @@ -0,0 +1,5 @@ +--- +'@lnreader/plugin-runtime': patch +--- + +Accept self-hosted plugins (such as komga) whose `site` is undefined until their URL setting is configured. diff --git a/packages/plugin-runtime/src/plugins/sandbox.ts b/packages/plugin-runtime/src/plugins/sandbox.ts index 74e0598..e456b1e 100644 --- a/packages/plugin-runtime/src/plugins/sandbox.ts +++ b/packages/plugin-runtime/src/plugins/sandbox.ts @@ -46,11 +46,14 @@ export function assertPluginShape( throw new InvalidPluginError(id, 'module did not export a plugin instance'); } const p = value as Record; + // Self-hosted sources (e.g. komga) read `site` from a user setting, so it + // is empty or undefined until configured. Normalize it to '' so callers + // can treat it as a string; the runner reports NEEDS_CONFIG on use. + if (p.pluginSettings && (p.site === undefined || p.site === null)) { + p.site = ''; + } for (const field of ['id', 'name', 'site', 'version'] as const) { - // Self-hosted sources (e.g. komga) read `site` from a user setting, so it - // may be empty until configured. - const optional = - field === 'site' && typeof p.site === 'string' && !!p.pluginSettings; + const optional = field === 'site' && !!p.pluginSettings; if (typeof p[field] !== 'string' || (!p[field] && !optional)) { throw new InvalidPluginError(id, `missing string field "${field}"`); } diff --git a/packages/plugin-runtime/test/sandbox.test.ts b/packages/plugin-runtime/test/sandbox.test.ts index 523b6be..3575d90 100644 --- a/packages/plugin-runtime/test/sandbox.test.ts +++ b/packages/plugin-runtime/test/sandbox.test.ts @@ -78,6 +78,30 @@ describe('sandbox', () => { ); }); + it('accepts an unset site on plugins with settings', async () => { + const d = await deps(); + const methods = [ + 'popularNovels', + 'parseNovel', + 'parseChapter', + 'searchNovels', + ] + .map(m => `${m}() {}`) + .join(', '); + const code = (site: string) => + `exports.default = { id: 'k', name: 'K', version: '1', site: ${site}, ` + + `pluginSettings: { url: { value: '', label: 'URL' } }, ${methods} }`; + expect(loadPlugin(code('undefined'), 'k', d).site).toBe(''); + expect(loadPlugin(code("''"), 'k', d).site).toBe(''); + const noSettings = code('undefined').replace( + /pluginSettings: .*?\} \},/, + '', + ); + expect(() => loadPlugin(noSettings, 'k', d)).toThrow( + 'missing string field "site"', + ); + }); + it('times out runaway top-level code', async () => { const d = { ...(await deps()), loadTimeoutMs: 50 }; expect(() => loadPlugin('while (true) {}', 'spin', d)).toThrow(/timed out/);