UNPKG

npm-to-cdn

Version:

A CLI tool to get CDN links for any npm package.

285 lines (284 loc) 9.12 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const clipboardy_1 = __importDefault(require("clipboardy")); const node_fetch_1 = __importDefault(require("node-fetch")); const package_json_1 = require("../package.json"); const cdnProviders_1 = require("./cdnProviders"); const guessUmdFile_1 = require("./guessUmdFile"); // CLI Banner console.log(`\nnpm-to-cdn v${package_json_1.version}\n`); const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .usage('Usage: n2cdn <package[@version]> [options]') .option('json', { type: 'boolean', description: 'Output as JSON', }) .option('clipboard', { type: 'boolean', description: 'Copy first CDN link to clipboard', }) .option('umd', { type: 'boolean', description: 'Show only unpkg and jsDelivr UMD links', }) .option('min', { type: 'boolean', description: 'Prefer minified builds (default)', }) .option('dev', { type: 'boolean', description: 'Prefer development (non-minified) builds', }) .option('versions', { type: 'boolean', description: 'List all available versions for the package', }) .option('verbose', { type: 'boolean', description: 'Show all attempted URLs and debug info', }) .option('silent', { type: 'boolean', description: 'Suppress banner and only output links', }) .demandCommand(1, 'You must provide at least one package name') .help() .argv; async function getLatestVersion(pkg) { const url = `https://registry.npmjs.org/${pkg}`; try { const res = await (0, node_fetch_1.default)(url); if (!res.ok) throw new Error('Package not found'); const data = await res.json(); return data['dist-tags'].latest; } catch (err) { throw new Error(`Failed to fetch latest version for ${pkg}`); } } async function urlExists(url) { try { const res = await (0, node_fetch_1.default)(url, { method: 'HEAD' }); return res.ok; } catch { return false; } } async function getAllVersions(pkg) { const url = `https://registry.npmjs.org/${pkg}`; try { const res = await (0, node_fetch_1.default)(url); if (!res.ok) throw new Error('Package not found'); const data = await res.json(); return Object.keys(data.versions || {}); } catch (err) { throw new Error(`Failed to fetch versions for ${pkg}`); } } async function getUnpkgFiles(pkg, version) { const url = `https://unpkg.com/${pkg}@${version}/?meta`; try { const res = await (0, node_fetch_1.default)(url); if (!res.ok) return []; const data = await res.json(); const files = []; function walk(node, path) { if (node.type === 'file') files.push(path); if (node.type === 'directory' && node.files) { for (const f of node.files) walk(f, path + '/' + f.name); } } walk(data, ''); return files.map(f => f.replace(/^\//, '')); } catch { return []; } } async function getJsDelivrFiles(pkg, version) { const url = `https://data.jsdelivr.com/v1/package/npm/${pkg}@${version}/flat`; try { const res = await (0, node_fetch_1.default)(url); if (!res.ok) return []; const data = await res.json(); return (data.files || []).map((f) => f.name.replace(/^\//, '')); } catch { return []; } } async function findFirstExisting(files, candidates) { for (const cand of candidates) { if (files.includes(cand)) return cand; } return null; } async function getCdnLinks(pkg, version, opts) { const umdCandidates = (0, guessUmdFile_1.getPossibleUmdFiles)(pkg, version, { min: !opts.dev, dev: !!opts.dev }); // unpkg let unpkgFile = null; let unpkgFiles = []; if (opts.umd || opts.verbose) { unpkgFiles = await getUnpkgFiles(pkg, version); unpkgFile = await findFirstExisting(unpkgFiles, umdCandidates); if (opts.verbose) console.error(`[unpkg] Candidates: ${umdCandidates.join(', ')} | Found: ${unpkgFile}`); } else { unpkgFile = umdCandidates[0]; } // jsDelivr let jsDelivrFile = null; let jsDelivrFiles = []; if (opts.umd || opts.verbose) { jsDelivrFiles = await getJsDelivrFiles(pkg, version); jsDelivrFile = await findFirstExisting(jsDelivrFiles, umdCandidates); if (opts.verbose) console.error(`[jsDelivr] Candidates: ${umdCandidates.join(', ')} | Found: ${jsDelivrFile}`); } else { jsDelivrFile = umdCandidates[0]; } const cdnLinksRaw = { unpkg: unpkgFile ? (0, cdnProviders_1.unpkgUrl)(pkg, version, unpkgFile) : null, jsDelivr: jsDelivrFile ? (0, cdnProviders_1.jsDelivrUrl)(pkg, version, jsDelivrFile) : null, esmSh: (0, cdnProviders_1.esmShUrl)(pkg, version), skypack: (0, cdnProviders_1.skypackUrl)(pkg, version), }; // Check which URLs are valid const cdnLinks = {}; for (const [cdn, url] of Object.entries(cdnLinksRaw)) { if (url && (cdn === 'unpkg' || cdn === 'jsDelivr')) { if (opts.umd || opts.verbose) { // Already checked existence via file listing cdnLinks[cdn] = url; } else if (await urlExists(url)) { cdnLinks[cdn] = url; } } else if (url && (cdn === 'esmSh' || cdn === 'skypack')) { if (await urlExists(url)) { cdnLinks[cdn] = url; } } } return cdnLinks; } async function processPackage(input, opts) { let [pkg, version] = input.split('@'); if (pkg === '' && input.startsWith('@')) { const atIdx = input.indexOf('@', 1); if (atIdx !== -1) { pkg = input.slice(0, atIdx); version = input.slice(atIdx + 1); } else { pkg = input; version = ''; } } if (!pkg) { if (!opts.silent) console.error('Invalid package name.'); return null; } if (!version) { try { version = await getLatestVersion(pkg); } catch (err) { if (!opts.silent) console.error(err.message); return null; } } if (opts.versions) { try { const versions = await getAllVersions(pkg); console.log(`${pkg}: ${versions.join(', ')}`); return null; } catch (err) { if (!opts.silent) console.error(err.message); return null; } } const cdnLinks = await getCdnLinks(pkg, version, opts); if (Object.keys(cdnLinks).length === 0) { if (!opts.silent) console.error(`No valid CDN links found for ${pkg}@${version}.`); return null; } // Filter for --umd flag let outputLinks = cdnLinks; if (opts.umd) { outputLinks = {}; for (const cdn of ['unpkg', 'jsDelivr']) { if (cdnLinks[cdn]) outputLinks[cdn] = cdnLinks[cdn]; } if (Object.keys(outputLinks).length === 0) { if (!opts.silent) console.error(`No valid UMD CDN links found for ${pkg}@${version}.`); return null; } } if (opts.json) { console.log(JSON.stringify({ package: pkg, version, cdns: outputLinks }, null, 2)); } else { if (!opts.silent) console.log(`\n${pkg}@${version}`); const order = opts.umd ? ['unpkg', 'jsDelivr'] : ['unpkg', 'jsDelivr', 'esmSh', 'skypack']; for (const cdn of order) { if (outputLinks[cdn]) { console.log(`${cdn}:\t${outputLinks[cdn]}`); } } } if (opts.clipboard) { try { await clipboardy_1.default.write(Object.values(outputLinks)[0]); if (!opts.silent) console.log('\nCopied first CDN link to clipboard!'); } catch (err) { if (!opts.silent) console.error('Failed to copy to clipboard.'); } } return { pkg, version, cdns: outputLinks }; } async function main() { if (!argv.silent) console.log(`\nnpm-to-cdn v${package_json_1.version}\n`); const pkgs = argv._.map((x) => String(x)); const results = []; for (const pkg of pkgs) { const res = await processPackage(pkg, argv); if (res) results.push(res); } // If batch mode and --json, output array if (argv.json && pkgs.length > 1) { console.log(JSON.stringify(results, null, 2)); } } main();