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/);