compile-readme.js
#! /usr/bin/env node
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import _ from 'lodash';
import { simple } from 'trucolor';
import { truwrap } from 'truwrap';
import { TemplateTag, replaceSubstitutionTransformer, stripIndent } from 'common-tags';
import { box } from '@thebespokepixel/string';
import meta from '@thebespokepixel/meta';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import updateNotifier from 'update-notifier';
import { createConsole } from 'verbosity';
import { packageConfig } from 'pkg-conf';
import { readPackageUp } from 'read-pkg-up';
import { remark } from 'remark';
import { image, link, root, paragraph, brk, rootWithTitle, text } from 'mdast-builder';
import remarkGap from 'remark-heading-gap';
import remarkSqueeze from 'remark-squeeze-paragraphs';
import remarkGfm from 'remark-gfm';
import urlencode from 'urlencode';
const name = "@thebespokepixel/badges";
const version = "4.0.8";
const description = "documentation/readme badge generation and management";
const main = "index.js";
const types = "index.d.ts";
const type = "module";
const bin = {
"compile-readme": "./compile-readme.js"
};
const directories = {
test: "test"
};
const files = [
"index.js",
"index.d.ts",
"icons"
];
const scripts = {
build: "rollup -c && chmod 755 compile-readme.js && npm run readme",
test: "xo && c8 --reporter=text ava",
"doc-build": "echo 'No Documentation to build'",
readme: "./compile-readme.js -u src/docs/example.md src/docs/readme.md > readme.md",
coverage: "c8 --reporter=lcov ava; open coverage/lcov-report/index.html",
prepublishOnly: "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly"
};
const repository = {
type: "git",
url: "git+https://github.com/thebespokepixel/badges.git"
};
const keywords = [
"readme",
"badges",
"documentation",
"docs"
];
const author = "Mark Griffiths <mark@thebespokepixel.com> (http://thebespokepixel.com/)";
const license = "MIT";
const bugs = {
url: "https://github.com/thebespokepixel/badges/issues"
};
const homepage = "https://github.com/thebespokepixel/badges#readme";
const copyright = {
year: "2021",
owner: "The Bespoke Pixel"
};
const dependencies = {
"@thebespokepixel/meta": "^3.0.4",
"@thebespokepixel/string": "^2.0.1",
"common-tags": "^1.8.0",
lodash: "^4.17.21",
"mdast-builder": "^1.1.1",
"pkg-conf": "^4.0.0",
"read-pkg-up": "^9.0.0",
remark: "^14.0.1",
"remark-gfm": "^3.0.1",
"remark-heading-gap": "^5.0.0",
"remark-squeeze-paragraphs": "^5.0.0",
trucolor: "^4.0.3",
truwrap: "^4.0.3",
"update-notifier": "^5.1.0",
urlencode: "^1.1.0",
verbosity: "^3.0.2",
yargs: "^17.2.1"
};
const devDependencies = {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.6",
"@types/estree": "^0.0.50",
ava: "^4.0.0-rc.1",
c8: "^7.10.0",
rollup: "^2.58.3",
"rollup-plugin-cleanup": "^3.2.1",
xo: "^0.46.4"
};
const xo = {
semicolon: false,
ignores: [
"index.js",
"index.d.ts",
"compile-readme.js",
"docs/**",
"coverage/**"
]
};
const badges = {
github: "thebespokepixel",
npm: "thebespokepixel",
"libraries-io": "TheBespokePixel",
codeclimate: "07f2fcfc32f33b4acc05",
name: "badges",
devBranch: "develop",
providers: {
status: {
text: "production",
color: "green"
},
aux1: {
title: "github",
text: "source",
color: "4E73B6",
link: "https://github.com/thebespokepixel/badges"
}
},
"test-1": [
"status"
],
readme: {
"Publishing Status": [
[
"status",
"npm",
"libraries-io-npm"
],
[
"travis-com",
"rollup"
]
],
"Development Status": [
[
"travis-com-dev",
"libraries-io-github"
],
[
"snyk",
"code-climate",
"code-climate-coverage"
]
],
"Documentation/Help": [
"twitter"
]
},
docs: [
[
"aux1"
],
[
"travis-com"
],
[
"david"
],
[
"code-climate-coverage"
],
[
"inch"
]
]
};
const engines = {
node: ">=14.0"
};
var pkg = {
name: name,
version: version,
description: description,
main: main,
types: types,
type: type,
bin: bin,
directories: directories,
files: files,
scripts: scripts,
repository: repository,
keywords: keywords,
author: author,
license: license,
bugs: bugs,
homepage: homepage,
copyright: copyright,
dependencies: dependencies,
devDependencies: devDependencies,
xo: xo,
badges: badges,
engines: engines
};
function render$a(config) {
const badgeNode = image(
`https://img.shields.io/badge/status-${config.text}-${config.color}`,
config.title,
config.title,
);
if (config.link) {
return link(
config.link,
config.title,
[badgeNode],
)
}
return badgeNode
}
function render$9(config) {
const badgeNode = image(
`https://img.shields.io/badge/${config.title}-${config.text}-${config.color}`,
config.title,
config.title,
);
if (config.link) {
return link(
config.link,
config.title,
[badgeNode],
)
}
return badgeNode
}
function render$8(config) {
const badgeNode = image(
`https://img.shields.io/badge/${config.title}-${config.text}-${config.color}`,
config.title,
config.title,
);
if (config.link) {
return link(
config.link,
config.title,
[badgeNode],
)
}
return badgeNode
}
function ccPath(user) {
return user.codeclimateRepoToken
? `repos/${user.codeclimateRepoToken}`
: `github/${user.github.slug}`
}
function cc(config, user) {
return link(
`https://codeclimate.com/${ccPath(user)}/maintainability`,
config.title,
[
image(
`https://api.codeclimate.com/v1/badges/${user.codeclimateToken}/maintainability`,
config.title,
config.title,
),
],
)
}
function ccCoverage(config, user) {
return link(
`https://codeclimate.com/${ccPath(user)}/test_coverage`,
config.title,
[
image(
`https://api.codeclimate.com/v1/badges/${user.codeclimateToken}/test_coverage`,
config.title,
config.title,
),
],
)
}
function renderIcon(file, type) {
const iconSource = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), file));
const iconBuffer = Buffer.from(iconSource);
return `&logo=${urlencode(`data:${type};base64,${iconBuffer.toString('base64')}`)}`
}
const renderIconSVG = id => renderIcon(`icons/${id}.svg`, 'image/svg+xml');
function libsRelease(config, user) {
return link(
`https://libraries.io/github/${user.github.slug}`,
config.title,
[
image(
`https://img.shields.io/librariesio/release/npm/${
user.fullName
}/latest?${config.icon && renderIconSVG('libraries-io')}`,
config.title,
config.title,
),
],
)
}
function libsRepo(config, user) {
return link(
`https://libraries.io/github/${user.github.slug}`,
config.title,
[
image(
`https://img.shields.io/librariesio/github/${
user.librariesIoName
}?${config.icon && renderIconSVG('libraries-io')}`,
config.title,
config.title,
),
],
)
}
function render$7(config, user) {
return link(
`https://gitter.im/${
user.github.user
}/${
config.room
}?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge`,
config.title,
[
image(
`https://img.shields.io/gitter/room/${
user.github.user
}/${
config.room
}`,
config.title,
config.title,
),
],
)
}
function render$6(config, user) {
return link(
`https://twitter.com/${user.twitter}`,
config.title,
[
image(
`https://img.shields.io/twitter/follow/${user.twitter}?style=social`,
config.title,
config.title,
),
],
)
}
function render$5(config, user) {
return link(
`https://inch-ci.org/github/${user.github.slug}`,
config.title,
[
image(
`https://inch-ci.org/github/${
user.github.slug
}.svg?branch=${
config.branch === 'dev' ? user.devBranch : config.branch
}&style=shields`,
config.title,
config.title,
),
],
)
}
function render$4(config, user) {
return link(
`https://www.npmjs.com/package/${user.fullName}`,
config.title,
[
image(
`https://img.shields.io/npm/v/${user.fullName}?logo=npm`,
config.title,
config.title,
),
],
)
}
function render$3(config) {
return link(
'https://github.com/rollup/rollup/wiki/pkg.module',
config.title,
[
image(
`https://img.shields.io/badge/es6-${
urlencode('type: module ✔')
}-64CA39?${config.icon && renderIconSVG('rollup')}`,
config.title,
config.title,
),
],
)
}
function render$2(config, user) {
return link(
`https://snyk.io/test/github/${user.github.slug}`,
config.title,
[
image(
`https://snyk.io/test/github/${user.github.slug}/badge.svg`,
config.title,
config.title,
),
],
)
}
function travis(config, user) {
return link(
`https://travis-ci.com/${user.github.slug}`,
config.title,
[
image(
`https://img.shields.io/travis/com/${user.github.slug}/${
config.branch === 'dev' ? user.devBranch : config.branch
}?logo=travis`,
config.title,
config.title,
),
],
)
}
function travisPro(config, user) {
return link(
`https://travis-ci.com/${user.github.slug}`,
config.title,
[
image(
`https://api.travis-ci.com/${user.github.slug}.svg?branch=${
config.branch === 'dev' ? user.devBranch : config.branch
}&token=${
user.travisToken
}`,
config.title,
config.title,
),
],
)
}
const services = {
status: render$a,
aux1: render$9,
aux2: render$8,
gitter: render$7,
twitter: render$6,
'code-climate': cc,
'code-climate-coverage': ccCoverage,
'libraries-io-npm': libsRelease,
'libraries-io-github': libsRepo,
inch: render$5,
'inch-dev': render$5,
npm: render$4,
rollup: render$3,
snyk: render$2,
travis,
'travis-dev': travis,
'travis-com': travis,
'travis-com-dev': travis,
'travis-pro': travisPro,
'travis-pro-dev': travisPro
};
function parseQueue(collection, providers, user) {
if (Array.isArray(collection)) {
if (Array.isArray(collection[0])) {
return paragraph(collection.map(content => parseQueue(content, providers, user)))
}
const badges = collection.map(content => parseQueue(content, providers, user));
badges.push(brk);
return paragraph(badges)
}
if (_.isObject(collection)) {
return _.map(collection, (content, title) => {
return rootWithTitle(5, text(title), parseQueue(content, providers, user))
})
}
if (!services[collection]) {
throw new Error(`${collection} not found`)
}
return paragraph([services[collection](providers[collection], user), text(' ')])
}
async function render$1(context, asAST = false) {
const configArray = await Promise.all([
packageConfig('badges'),
readPackageUp()
]);
const config = configArray[0];
const pkg = configArray[1].packageJson;
if (!config.name || !config.github || !config.npm) {
throw new Error('Badges requires at least a package name, github repo and npm user account.')
}
if (!config[context]) {
throw new Error(`${context} is not provided in package.json.`)
}
if (!config.providers) {
throw new Error('At least one badge provider must be specified.')
}
const badgeQueue = {
user: {
name: config.name,
fullName: pkg.name,
librariesIoName: `${config['libraries-io']}/${config.name}`,
scoped: /^@.+?\//.test(pkg.name),
github: {
user: config.github,
slug: `${config.github}/${config.name}`
},
npm: config.npm,
twitter: config.twitter || config.github,
devBranch: 'develop',
codeclimateToken: config.codeclimate,
codeclimateRepoToken: config['codeclimate-repo'],
travisToken: config.travis
},
providers: _.forIn(_.defaultsDeep(config.providers, {
status: {
title: 'Status',
text: 'badge',
color: 'red',
link: false
},
'aux-1': {
title: 'Green',
text: 'badge',
color: 'green',
link: false
},
'aux-2': {
title: 'Blue',
text: 'badge',
color: 'blue',
link: false
},
gitter: {
title: 'Gitter',
room: 'help'
},
twitter: {
title: 'Twitter'
},
'code-climate': {
title: 'Code-Climate'
},
'code-climate-coverage': {
title: 'Code-Climate Coverage'
},
'libraries-io-npm': {
title: 'Libraries.io',
icon: true
},
'libraries-io-github': {
title: 'Libraries.io',
icon: true
},
inch: {
title: 'Inch.io',
branch: 'master'
},
'inch-dev': {
title: 'Inch.io',
branch: 'dev'
},
npm: {
title: 'npm',
icon: true
},
rollup: {
title: 'Rollup',
icon: true
},
snyk: {
title: 'Snyk'
},
travis: {
title: 'Travis',
branch: 'master'
},
'travis-com': {
title: 'Travis',
branch: 'master'
},
'travis-pro': {
title: 'Travis',
branch: 'master'
},
'travis-dev': {
title: 'Travis',
branch: 'dev'
},
'travis-com-dev': {
title: 'Travis',
branch: 'dev'
},
'travis-pro-dev': {
title: 'Travis',
branch: 'dev'
}
}), value => _.defaultsDeep(value, {
icon: false
})),
queue: config[context]
};
const ast = root(parseQueue(badgeQueue.queue, badgeQueue.providers, badgeQueue.user));
if (asAST) {
return ast
}
return remark().use(remarkGfm).use(remarkGap).use(remarkSqueeze).stringify(ast)
}
const console = createConsole({outStream: process.stderr});
const clr = simple({format: 'sgr'});
const metadata = meta(dirname(fileURLToPath(import.meta.url)));
const renderer = truwrap({
right: 4,
outStream: process.stderr,
});
const colorReplacer = new TemplateTag(
replaceSubstitutionTransformer(
/([a-zA-Z]+?)[:/|](.+)/,
(match, colorName, content) => `${clr[colorName]}${content}${clr[colorName].out}`,
),
);
const title = box(colorReplacer`${'title|compile-readme'}${`dim| │ ${metadata.version(3)}`}`, {
borderColor: 'yellow',
margin: {
top: 1,
},
padding: {
bottom: 0,
top: 0,
left: 2,
right: 2,
},
});
const usage = stripIndent(colorReplacer)`
Inject project badges into a tagged markdown-formatted source file.
Usage:
${'command|compile-readme'} ${'option|[options]'} ${'operator|>'} ${'argument|outputFile'}`;
const epilogue = colorReplacer`${'brightGreen|' + metadata.copyright} ${'grey|Released under the MIT License.'}`;
const yargsInstance = yargs(hideBin(process.argv))
.strictOptions()
.help(false)
.version(false)
.options({
h: {
alias: 'help',
describe: 'Display help.',
},
v: {
alias: 'version',
count: true,
describe: 'Print version to stdout. -vv Print name & version.',
},
V: {
alias: 'verbose',
count: true,
describe: 'Be verbose. -VV Be loquacious.',
},
c: {
alias: 'context',
default: 'readme',
describe: 'The named badges context in package.json.',
},
u: {
alias: 'usage',
describe: 'Path to a markdown usage example',
},
color: {
describe: 'Force color output. Disable with --no-color',
},
});
const {argv} = yargsInstance;
if (!(process.env.USER === 'root' && process.env.SUDO_USER !== process.env.USER)) {
updateNotifier({
pkg,
}).notify();
}
if (argv.verbose) {
switch (argv.verbose) {
case 1:
console.verbosity(4);
console.log(`${clr.title}Verbose mode${clr.title.out}:`);
break
case 2:
console.verbosity(5);
console.log(`${clr.title}Extra-Verbose mode${clr.title.out}:`);
console.yargs(argv);
break
default:
console.verbosity(3);
}
}
if (argv._.length === 0) {
argv.help = true;
}
if (argv.version) {
process.stdout.write(metadata.version(argv.version));
process.exit(0);
}
async function render(template) {
const content = {
badges: await render$1(argv.context),
usage: '',
};
if (argv.usage) {
content.usage = readFileSync(resolve(argv.usage));
}
process.stdout.write(template(content).replace(/\\\n/g, ' \n'));
}
if (argv.help) {
(async () => {
const usageContent = await yargsInstance.wrap(renderer.getWidth()).getHelp();
renderer.write(title).break(2);
renderer.write(usage);
renderer.break(2);
renderer.write(usageContent);
renderer.break(2);
renderer.write(epilogue);
renderer.break(2);
})();
} else {
const source = resolve(argv._[0]);
console.debug('Source path:', source);
render(_.template(readFileSync(source)));
}