Bundle server (and mcp entry) into a single JS file with deps inlined, minify UI into one HTML, and ship only dist/ in the runtime image.
106 lines
3.1 KiB
JavaScript
106 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Production build: one Node bundle (all deps inlined) + minified single-file Web UI.
|
|
*
|
|
* Outputs:
|
|
* dist/server.js — main app (Torznab, qBt, MCP, UI API)
|
|
* dist/mcp.js — optional MCP-only process
|
|
* dist/ui/index.html — Search/Queue UI (CSS+JS inlined)
|
|
*/
|
|
import * as esbuild from 'esbuild';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const dist = path.join(root, 'dist');
|
|
const distUi = path.join(dist, 'ui');
|
|
|
|
fs.rmSync(dist, { recursive: true, force: true });
|
|
fs.mkdirSync(distUi, { recursive: true });
|
|
|
|
const nodeBundle = {
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node20',
|
|
format: 'cjs',
|
|
minify: true,
|
|
legalComments: 'none',
|
|
sourcemap: false,
|
|
// Keep Node builtins external; pull npm deps into the file.
|
|
packages: 'bundle',
|
|
};
|
|
|
|
async function bundleServer() {
|
|
const result = await esbuild.build({
|
|
...nodeBundle,
|
|
entryPoints: [path.join(root, 'src/index.js')],
|
|
outfile: path.join(dist, 'server.js'),
|
|
metafile: true,
|
|
logLevel: 'info',
|
|
});
|
|
const out = result.metafile.outputs[Object.keys(result.metafile.outputs)[0]];
|
|
console.log(`server.js ${(out.bytes / 1024 / 1024).toFixed(2)} MB`);
|
|
}
|
|
|
|
async function bundleMcp() {
|
|
const result = await esbuild.build({
|
|
...nodeBundle,
|
|
entryPoints: [path.join(root, 'src/mcp.js')],
|
|
outfile: path.join(dist, 'mcp.js'),
|
|
banner: { js: '#!/usr/bin/env node\n' },
|
|
metafile: true,
|
|
logLevel: 'info',
|
|
});
|
|
const out = result.metafile.outputs[Object.keys(result.metafile.outputs)[0]];
|
|
console.log(`mcp.js ${(out.bytes / 1024 / 1024).toFixed(2)} MB`);
|
|
try {
|
|
fs.chmodSync(path.join(dist, 'mcp.js'), 0o755);
|
|
} catch {
|
|
/* windows */
|
|
}
|
|
}
|
|
|
|
async function bundleUi() {
|
|
const cssSrc = fs.readFileSync(path.join(root, 'ui/styles.css'), 'utf8');
|
|
const jsSrc = fs.readFileSync(path.join(root, 'ui/scripts.js'), 'utf8');
|
|
const htmlSrc = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
|
|
const css = await esbuild.transform(cssSrc, { loader: 'css', minify: true });
|
|
const js = await esbuild.transform(jsSrc, {
|
|
loader: 'js',
|
|
minify: true,
|
|
target: ['es2020'],
|
|
});
|
|
|
|
// Prefer a single HTML payload (no extra static requests) for the tiny UI.
|
|
let html = htmlSrc
|
|
.replace(/<link rel="stylesheet" href="styles\.css">\s*/i, '')
|
|
.replace(/<script src="scripts\.js"><\/script>\s*/i, '');
|
|
|
|
// Inject minified assets before </head> and </body>
|
|
if (!html.includes('</head>')) {
|
|
throw new Error('ui/index.html missing </head>');
|
|
}
|
|
html = html.replace(
|
|
'</head>',
|
|
`<style>${css.code}</style>\n</head>`,
|
|
);
|
|
if (!html.includes('</body>')) {
|
|
throw new Error('ui/index.html missing </body>');
|
|
}
|
|
html = html.replace(
|
|
'</body>',
|
|
`<script>${js.code}</script>\n</body>`,
|
|
);
|
|
|
|
const outHtml = path.join(distUi, 'index.html');
|
|
fs.writeFileSync(outHtml, html);
|
|
console.log(`ui/index.html ${(Buffer.byteLength(html) / 1024).toFixed(1)} KB (inlined css+js)`);
|
|
}
|
|
|
|
await bundleServer();
|
|
await bundleMcp();
|
|
await bundleUi();
|
|
console.log('build ok → dist/');
|