#!/usr/bin/env node // Download Cytoscape.js + cose-bilkent layout plugin from CDN. // Skips download if files exist and are < 30 days old. // // Usage: // node scripts/download/download-cytoscape.js import fs from 'fs'; import path from 'path'; import https from 'https'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PUBLIC_JS = path.join(__dirname, '../../site/public/js'); const ASSETS = [ { url: 'https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.29.2/cytoscape.min.js', out: 'cytoscape.min.js', name: 'Cytoscape.js 3.29.2', }, { url: 'https://cdn.jsdelivr.net/npm/cytoscape-cose-bilkent@4.1.0/cytoscape-cose-bilkent.js', out: 'cytoscape-cose-bilkent.js', name: 'cose-bilkent layout 4.1.0', }, ]; const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; function download(url, dest) { return new Promise((resolve, reject) => { https.get(url, (res) => { if (res.statusCode === 301 || res.statusCode === 302) { return download(res.headers.location, dest).then(resolve).catch(reject); } if (res.statusCode !== 200) { return reject(new Error(`HTTP ${res.statusCode}: ${url}`)); } const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, Buffer.concat(chunks)); resolve(); }); }).on('error', reject); }); } async function main() { for (const asset of ASSETS) { const dest = path.join(PUBLIC_JS, asset.out); if (fs.existsSync(dest)) { const age = Date.now() - fs.statSync(dest).mtimeMs; if (age < MAX_AGE_MS) { const kb = (fs.statSync(dest).size / 1024).toFixed(1); console.log(`skip ${asset.name} (${kb} kB, cached)`); continue; } } process.stdout.write(`get ${asset.name} ... `); try { await download(asset.url, dest); const kb = (fs.statSync(dest).size / 1024).toFixed(1); console.log(`ok (${kb} kB)`); } catch (err) { console.error(`\nfail ${asset.name}: ${err.message}`); process.exit(1); } } } main();