npm-to-cdn
Version:
A CLI tool to get CDN links for any npm package.
285 lines (284 loc) • 9.12 kB
JavaScript
;
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();