npm-fetch-changelog
Version:
fetch the changelog for an npm package from GitHub
183 lines • 5.89 kB
JavaScript
import npmRegistryFetch from 'npm-registry-fetch';
import { Base64 } from 'js-base64';
import parseChangelog from "./parseChangelog.mjs";
import semver from 'semver';
import getConfig from "./getConfig.mjs";
import { Octokit } from 'octokit';
import memoize from "./util/memoize.mjs";
import once from "./util/once.mjs";
import { debug } from "./util/debug.mjs";
import chalk from 'chalk';
const getOctokit = once(async () => {
const {
githubToken
} = await getConfig();
return new Octokit({
...(githubToken ? {
auth: `token ${githubToken}`
} : {})
});
});
async function doOctokit(resource, method, arg) {
const octokit = await getOctokit();
debug(`calling... octokit.rest.${resource}.${method}(${JSON.stringify(arg)})`);
try {
const result = await octokit.rest[resource][method](arg);
debug(`call success octokit.rest.${resource}.${method}(${JSON.stringify(arg)})`);
return result;
} catch (error) {
debug(`cail failed octokit.rest.${resource}.${method}(${JSON.stringify(arg)})`, error);
throw error;
}
}
export const getChangelogFromFile = memoize(async (owner, repo) => {
let changelog;
let lastError;
for (const file of ['CHANGELOG.md', 'changelog.md']) {
try {
const {
data: _data
} = await doOctokit('repos', 'getContent', {
owner,
repo,
path: file
});
const data = Array.isArray(_data) ? _data[0] : _data;
if (data.type !== 'file') {
throw new Error(`content isn't a file`);
}
changelog = data.content ? Base64.decode(data.content) : undefined;
break;
} catch (error) {
lastError = error;
continue;
}
}
if (!changelog) {
throw lastError || new Error(`failed to find changelog file`);
}
return await parseChangelog(changelog);
}, (owner, repo) => `${owner}/${repo}`);
function parseRepositoryUrl(url) {
const match = /github\.com[:/]([^\\]+)\/([^\\]+)/i.exec(url);
if (!match) throw new Error(`repository.url not supported: ${url}`);
const [, owner, repo] = match;
return {
owner,
repo: repo.replace(/\.git$/, '')
};
}
export function includeFilter(include) {
if (!include) return () => true;
if (typeof include === 'function') return include;
const {
range,
prerelease,
minor,
patch = minor
} = include;
return version => {
if (range && !semver.satisfies(version, range)) return false;
if (!prerelease && semver.prerelease(version)) return false;
if (minor === false && semver.minor(version)) return false;
if (patch === false && semver.patch(version)) return false;
return true;
};
}
export function describeFilter({
pkg,
include,
highlight
}) {
if (typeof include === 'function') {
return `versions matching custom function`;
}
const {
range,
prerelease,
minor,
patch
} = include || {};
const types = [...(prerelease ? ['prerelease'] : []), 'major', ...(minor === false ? [] : ['minor']), ...(patch === false ? [] : ['patch'])];
return [...(range ? [] : [highlight ? chalk.greenBright('all') : 'all']), types.slice(0, Math.max(1, types.length - 1)).join(', '), ...(types.length > 1 ? [`and ${types[types.length - 1]}`] : []), 'versions', ...(pkg ? [`of ${highlight ? chalk.bold(pkg) : pkg}`] : []), ...(range ? [/^[<>]/.test(range) ? `${highlight ? chalk.greenBright(range) : range}` : `satisfying ${highlight ? chalk.greenBright(range) : range}`] : [])].join(' ');
}
export async function fetchChangelog(pkg, {
include
} = {}) {
const {
npmToken
} = await getConfig();
debug(`fetchChangelog(${JSON.stringify(pkg)}, ${JSON.stringify({
include
})})`);
const npmInfo = await npmRegistryFetch.json(pkg, {
forceAuth: {
token: npmToken
}
});
debug(`npmInfo.versions:\n ${Object.keys(npmInfo.versions).join('\n ')}`);
const versions = Object.keys(npmInfo.versions).filter(includeFilter(include));
debug(`filtered versions:\n ${versions.join('\n ')}`);
const releases = await Promise.all(versions.map(async version => {
const release = {
version,
header: `# ${version}`,
date: new Date(npmInfo.time[version])
};
const {
url
} = npmInfo.versions[version].repository || npmInfo.repository || {};
if (!url) {
release.error = new Error('failed to get repository url from npm');
}
try {
const {
owner,
repo
} = parseRepositoryUrl(url);
try {
const body = (await doOctokit('repos', 'getReleaseByTag', {
owner,
repo,
tag: `v${version}`
}).catch(() => {
return doOctokit('repos', 'getReleaseByTag', {
owner,
repo,
tag: version
});
})).data.body ?? undefined;
release.body = body;
if (body) {
const parsed = parseChangelog(body);
for (const v in parsed) {
if (v === version) {
const {
header,
body
} = parsed[v];
if (header) release.header = header;
if (body) release.body = body;
break;
}
}
}
} catch (error) {
const changelog = await getChangelogFromFile(owner, repo);
const {
header,
body
} = changelog[version] || {};
if (header) release.header = header;
release.body = body;
}
if (!release.body) {
release.error = new Error(`failed to find GitHub release or changelog entry for version ${version}`);
}
} catch (error) {
release.error = error instanceof Error ? error : new Error(String(error));
}
return release;
}));
return Object.fromEntries(releases.sort((a, b) => semver.compare(a.version, b.version)).map(release => [release.version, release]));
}