feat: fetch-novel builds an EPUB from any plugin; Kavita TOC fix
Publish Plugins / Publish Plugins (push) Has been cancelled

- scripts/load-plugins.js: shared plugin loader for the novel CLI scripts
- scripts/fetch-novel.js: download a novel via its plugin into a clean EPUB 3
- find-novel prints the plugin id/path for each match
- kavita 1.0.1: innermost TOC entry names the chapter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
masi
2026-09-25 13:40:23 -03:00
parent fbb91a9c84
commit fba9a35075
4 changed files with 307 additions and 106 deletions
+68
View File
@@ -0,0 +1,68 @@
// Load compiled plugin instances outside the app, for the novel CLI scripts.
import * as esbuild from 'esbuild';
import { execFileSync } from 'child_process';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import path, { dirname } from 'path';
import fs from 'fs';
import os from 'os';
const require = createRequire(import.meta.url);
export const ROOT = path.resolve(dirname(fileURLToPath(import.meta.url)), '..');
// The shared fetch helper logs every request with its headers (credentials
// included). Callers print their output explicitly with `print`.
export const print = (...args) => process.stdout.write(args.join(' ') + '\n');
console.log = () => undefined;
/** Bundle and instantiate every plugin in the given language folders. */
export async function loadPlugins(langs) {
// Multisrc plugins (Madara, NovelFire, ...) are generated, not committed.
execFileSync('node', ['plugins/multisrc/generate-multisrc-plugins.js'], {
cwd: ROOT,
stdio: 'ignore',
});
const entryPoints = langs.flatMap(lang => {
const dir = path.join(ROOT, 'plugins', lang);
if (!fs.existsSync(dir))
throw new Error(`No plugin folder: plugins/${lang}`);
return fs
.readdirSync(dir)
.filter(f => f.endsWith('.ts'))
.map(f => path.join(dir, f));
});
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'novel-plugins-'));
try {
const result = await esbuild.build({
entryPoints,
outdir,
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outExtension: { '.js': '.cjs' },
logLevel: 'silent',
alias: {
'@libs': path.join(ROOT, 'src/libs'),
'@': path.join(ROOT, 'src'),
},
});
if (result.errors.length) throw new Error('Plugin bundling failed');
const plugins = [];
for (const file of fs.readdirSync(outdir, { recursive: true })) {
if (!file.endsWith('.cjs')) continue;
try {
const mod = require(path.join(outdir, file));
plugins.push(mod.default ?? mod);
} catch {
// A plugin that throws on load is skipped, like a broken source in the app.
}
}
return plugins;
} finally {
fs.rmSync(outdir, { recursive: true, force: true });
}
}