@appnest/readme
Version:
Automatically generate a beautiful best-practice README file based on the contents of your repository
1,503 lines (1,314 loc) • 37.4 kB
JavaScript
import { green, red, yellow } from 'colors';
import { readFileSync, outputFile, existsSync } from 'fs-extra';
import { resolve } from 'path';
import checkLinks from 'check-links';
import glob from 'glob';
import { analyzeText, transformAnalyzerResult } from 'web-component-analyzer';
import { version as version$1, command, help, on, parse } from 'commander';
/**
* Generate npm badges.
* @param npmId
*/
function npmBadges({
npmId
}) {
return [{
"alt": "Downloads per month",
"url": `https://npmcharts.com/compare/${npmId}?minimal=true`,
"img": `https://img.shields.io/npm/dm/${npmId}.svg`
}, {
"alt": "NPM Version",
"url": `https://www.npmjs.com/package/${npmId}`,
"img": `https://img.shields.io/npm/v/${npmId}.svg`
}];
}
/**
* Generate github badges.
* @param githubId
*/
function githubBadges({
githubId
}) {
return [{
"alt": "Dependencies",
"url": `https://david-dm.org/${githubId}`,
"img": `https://img.shields.io/david/${githubId}.svg`
}, {
"alt": "Contributors",
"url": `https://github.com/${githubId}/graphs/contributors`,
"img": `https://img.shields.io/github/contributors/${githubId}.svg`
}];
}
/**
* Generates the webcomponents badges.
* @param webcomponentsId
*/
function webcomponentsBadges({
webcomponentsId
}) {
return [{
"alt": "Published on webcomponents.org",
"url": `https://www.webcomponents.org/element/${webcomponentsId}`,
"img": `https://img.shields.io/badge/webcomponents.org-published-blue.svg`
}];
}
const URL_PATTERN = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i;
/**
* Returns whether the URL is valid.
* @param url
*/
function isValidURL(url) {
return URL_PATTERN.test(url);
}
/**
* Determines whether an object has the specified key.
* @param obj
* @param key
*/
function hasKey(obj, key) {
return getValue(obj, key) != null;
}
/**
* Returns the license url.
* @param license
*/
function getLicenseUrl(license) {
return `https://opensource.org/licenses/${license}`;
}
/**
* Returns a key from from an object for a key path.
* @param obj
* @param keyPath
*/
function getValue(obj, keyPath) {
let keys = keyPath.split(".");
while (keys.length > 0 && obj != null) {
keyPath = keys.shift();
obj = obj[keyPath];
}
return obj;
}
/**
* Sets a value for a key path (".")
* @param obj
* @param keyPath
* @param value
*/
function setValue(obj, keyPath, value) {
let keys = keyPath.split(".");
while (keys.length > 0) {
// Set value for the last key
if (keys.length === 1) {
obj[keys.shift()] = value;
return;
}
const key = keys.shift();
if (obj[key] != null) {
obj = obj[key];
} else {
obj = obj[key] = {};
}
}
}
/**
* Validates the package.
* @param obj
* @param fileName
*/
function validateObject({
obj,
requiredFields
}) {
for (const key of requiredFields) {
if (!hasKey(obj, key)) {
return false;
}
}
return true;
}
/**
* Returns whether the func is a function.
* @param func
*/
function isFunction(func) {
return typeof func === "function";
}
/**
* Returns whether the obj is an object.
* @param obj
*/
function isObject(obj) {
if (obj == null) {
return false;
}
return typeof obj === "object" && !Array.isArray(obj);
}
/**
* Extracts values from an object.
* @param map
* @param obj
*/
function extractValues({
map,
obj
}) {
const newObj = {};
for (const [k, v] of Object.entries(map)) {
newObj[k] = getValue(obj, v);
}
return newObj;
}
/**
* Returns available badges.
* @param pkg
*/
function getBadges({
config
}) {
const badges = [];
const npmId = getValue(config, "ids.npm");
const githubId = getValue(config, "ids.github");
const webcomponentsId = getValue(config, "ids.webcomponents"); // Add NPM badges
if (npmId != null) {
badges.push(...npmBadges({
npmId
}));
} // Add Github badges
if (githubId != null) {
badges.push(...githubBadges({
githubId
}));
} // Add webcomponents badges
if (webcomponentsId != null) {
badges.push(...webcomponentsBadges({
webcomponentsId
}));
} // Add user badges
badges.push(...(config.badges || []));
return badges;
}
/**
* Reads a file.
* @param name
*/
function readFile(name) {
// Checks whether the file exists
if (!fileExists(name)) {
return null;
}
return readFileSync(resolve(process.cwd(), name)).toString("utf8");
}
/**
* Reads the contents of a json file.
* @param name
*/
function readJSONFile(name) {
const file = readFile(name);
return file != null ? JSON.parse(file) : file;
}
/**
* Escapes a regex.
* @param text
*/
function escapeRegex(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
/**
* Returns a placeholder regex.
* @param text
*/
function placeholderRegexCallback(text) {
return ({
config
}) => {
const {
placeholder
} = config;
return new RegExp(`${escapeRegex(placeholder[0])}\\s*(${text})\\s*${escapeRegex(placeholder[1])}`, "gm");
};
}
/**
* Writes a file to a path.
* @param target
* @param content
*/
async function writeFile({
target,
content
}) {
try {
await outputFile(target, content);
} catch (err) {
console.error(err);
}
}
/**
* Returns the title for a level.
* @param title
* @param level
* @param config
*/
function getTitle({
title,
level,
config
}) {
const prefix = config.headingPrefix[level] || "";
return `${prefix}${title}`;
}
/**
* Cleans the title from weird symbols.
* @param title
*/
function getCleanTitle(title) {
return title.replace(/[^a-zA-Z0-9-_ ]/g, "");
}
/**
* Returns the title link.
* @param title
* @param index
*/
function getTitleLink(title, index = 0) {
return `#${getCleanTitle(title).replace(/ /g, "-").toLowerCase()}${index > 0 ? `-${index}` : ""}`;
}
/**
* Determines whether the file at the path exists.
* @param absolutePath
*/
function fileExists(absolutePath) {
if (absolutePath == null || absolutePath == "") return false;
return existsSync(absolutePath);
}
/**
* Splits an array into smaller arrays.
* @param arr
* @param count
*/
function splitArrayIntoArrays(arr, count) {
arr = [...arr];
const arrs = [];
while (arr.length) {
arrs.push(arr.splice(0, count));
}
return arrs;
}
/**
* Replaces content in string between two indicies.
* @param string
* @param start
* @param end
* @param content
*/
function replaceInString(string, content, {
start,
end
}) {
return string.substring(0, start) + content + string.substring(end);
}
/**
* Loads the package file.
* @param pkgPath
*/
function loadPackage(pkgPath) {
return readJSONFile(pkgPath) || null;
}
/**
* Loads the config file.
* @param configPath
*/
function loadConfig(configPath) {
return readJSONFile(configPath) || null;
}
/**
* Returns links from a text.
* @param text
*/
function getLinks(text) {
return Array.from(text.match(/(http|www)[A-Za-z\d-\._~:\/?#\[\]@!\$&\+;=]+/gm) || []);
}
/**
* Checks all links in the text for aliveness.
* @param text
*/
async function checkLinksAliveness(text) {
const links = getLinks(text);
console.log(green(`[readme] - Found "${links.length}" link${links.length === 1 ? "" : `s`}. Checking all of them now!`)); // Check all links
const results = await checkLinks(links); // Go through the results and notify the user about broken links
for (const [link, {
status,
statusCode
}] of Object.entries(results)) {
if (status === "dead") {
console.log(red(`[readme] - The link "${link}" is dead. Responded with status code "${statusCode}".`));
}
}
}
/**
* Runs the check links command.
* @param options
*/
async function checkLinksCommand(options) {
const path = getValue(options, "input"); // Ensure that a path exists
if (path == null) {
console.log(red(`[readme] - Could not resolve '${path}'.`));
return;
}
const content = readFile(path); // Ensure that the file could be read
if (content == null) {
console.log(red(`[readme] - Could not read the file at path '${path}'.`));
return;
}
await checkLinksAliveness(content);
}
var LineColor;
(function (LineColor) {
LineColor["AQUA"] = "aqua";
LineColor["CLOUDY"] = "cloudy";
LineColor["COLORED"] = "colored";
LineColor["CUT"] = "cut";
LineColor["DARK"] = "dark";
LineColor["FIRE"] = "fire";
LineColor["GRASS"] = "grass";
LineColor["RAINBOW"] = "rainbow";
LineColor["SOLAR"] = "solar";
LineColor["VINTAGE"] = "vintage";
LineColor["WATER"] = "water";
LineColor["NONE"] = "none";
})(LineColor || (LineColor = {}));
const fs = require("fs");
/**
* Creates the template for the logo.
* @param logo
*/
function logoTemplate({
logo
}) {
const {
src,
width = "auto",
height = "auto",
alt = "Logo"
} = logo;
return `<p align="center">
<img src="${src}" alt="${alt}" width="${width}" height="${height}" />
</p>`;
}
/**
* Creates the template for the title.
* @param name
*/
function mainTitleTemplate({
name
}) {
return `<h1 align="center">${name}</h1>`;
}
/**
* Creates a line template.
* @param config
*/
function lineTemplate({
config
}) {
let url = "";
const {
line
} = config; // If the line should not be there we just return an empty string.
if (line === LineColor.NONE) {
return ``;
} // Construct the URL.
if (isValidURL(line)) {
url = line;
} else {
url = `https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/${line}.png`;
}
return ``;
}
/**
* Creates a template for the title.
* @param title
* @param level
* @param config
*/
function titleTemplate({
title,
level,
config
}) {
const beforeTitleContent = level <= 2 ? `${config.lineBreak}[${lineTemplate({
config
})}](${getTitleLink(title)})${config.lineBreak}${config.lineBreak}` : "";
return `${beforeTitleContent}${Array(level).fill("#").join("")} ${getTitle({
title,
level,
config
})}`;
}
/**
* Creates a template for the badges.
* @param badges
* @param config
*/
function badgesTemplate({
badges,
config
}) {
return `<p align="center">
${badges.map(badge => `<a href="${badge.url}"><img alt="${badge.alt}" src="${badge.img}" height="20"/></a>`).join(config.lineBreak)}
</p>
`;
}
/**
* Creates a template for the license.
* @param license
* @returns {string}
*/
function licenseTemplate({
license
}) {
return `## License
Licensed under [${license}](${getLicenseUrl(license)}).`;
}
/**
* Creates a template for the demo link.
* @param url
*/
function demoTemplate({
url
}) {
return `Go here to see a demo <a href="${url}">${url}</a>.`;
}
/**
* Creates a description template.
* @param description
* @param text
* @param demo
*/
function descriptionTemplate({
description,
text,
demo
}) {
return `<p align="center">
<b>${description}</b></br>
<sub>${text != null ? text : ""}${demo != null ? ` ${demoTemplate({
url: demo
})}` : ""}<sub>
</p>
<br />
`;
}
/**
* Creates a bullets template.
* @param bullets
* @param pkg
*/
function bulletsTemplate({
bullets,
config
}) {
return bullets.map(bullet => `* ${bullet}`).join(config.lineBreak);
}
/**
* Creates a table template.
* @param rows
* @param config
* @param centered
*/
function tableTemplate({
rows,
config,
centered
}) {
/**
* Fills the width of the cell.
* @param text
* @param width
* @param paddingStart
*/
function fillWidth(text, width, paddingStart) {
return " ".repeat(paddingStart) + text + " ".repeat(Math.max(1, width - text.length - paddingStart));
}
/**
* Escape a text so it can be used in a markdown table
* @param text
*/
function markdownEscapeTableCell(text) {
return text.replace(/\n/g, "<br />").replace(/\|/g, "\\|");
} // Filter away the rows that have no content
rows = rows.filter(row => row.map(r => r.trim()).join("").length > 0); // Count the amount of columns
const columnCount = Math.max(...rows.map(r => r.length)); // Escape all cells in the markdown output
rows = rows.map(r => r.map(markdownEscapeTableCell));
const MIN_WIDTH = 3;
const MAX_WIDTH = 50;
const PADDING = 1;
const tableColPrefix = centered ? ":" : "";
const columnWidths = Array(columnCount).fill(0).map((c, i) => Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, ...rows.map(r => (r[i] || "").length)) + PADDING * 2));
return `
|${rows[0].map((r, i) => fillWidth(r, columnWidths[i], PADDING)).join("|")}|
|${columnWidths.map(c => `${tableColPrefix}${"-".repeat(c)}${tableColPrefix}`).join("|")}|
${rows.slice(1).map(r => `|${r.map((r, i) => fillWidth(r, columnWidths[i], PADDING)).join("|")}|`).join(config.lineBreak)}
`;
}
/**
* Creates the table of contents.
* @param titles
* @param pkg
*/
function tocTemplate({
titles,
config
}) {
// Create a clean titles array.
// We are going to use it to figure out the index of each title (there might be more titles with the same name).
const tempCleanTitles = titles.map(title => getCleanTitle(title)); // Create a map, mapping each clean title to the amount of times it occurs in the titles array
const countForTitle = tempCleanTitles.reduce((acc, title) => {
acc[title] = (acc[title] || 0) + 1;
return acc;
}, {}); // Map the titles to relevant info.
const titlesInfo = titles.map(title => {
const cleanTitle = getCleanTitle(title);
const titlesWithSameName = tempCleanTitles.filter(t => t === cleanTitle); // Remove title from the temp array and compute the index
tempCleanTitles.splice(tempCleanTitles.indexOf(cleanTitle), 1); // Compute the index (the first will be 0 and so on)
const index = (countForTitle[cleanTitle] || 1) - titlesWithSameName.length; // Compute the level of the title
const level = (title.match(/#/g) || []).length; // Remove the "# " from the titles so eg. "## Hello" becomes "Hello"
title = title.replace(/^#*\s?/, ""); // Compute the title link
const titleLink = getTitleLink(title, index);
return {
title,
cleanTitle,
index,
titleLink,
level
};
}); // Find the lowest level of heading (# is lower than ##)
const lowestLevel = titlesInfo.reduce((acc, {
title,
level
}) => Math.min(acc, level), Infinity); // Format the table of contents title because it is applied after the title template
return `${titleTemplate({
title: "Table of Contents",
level: 2,
config: config
})}
${titlesInfo.map(({
level,
titleLink,
title
}) => {
// Subtract the lowest level from the level to ensure that the lowest level will have 0 tabs in front
// We can't make any assumptions about what level of headings the readme uses.
const tabs = Array(level - lowestLevel).fill(config.tab).join("");
return `${tabs}* [${title}](${titleLink})`;
}).join(config.lineBreak)}`;
}
/**
* Creates the authors template.
* @param contributors
* @param config
*/
function contributorsTemplate({
contributors,
config
}) {
const {
contributorsPerRow
} = config;
const imageSize = 100; // Split the contributors into multiple arrays (one for each row)
const rows = splitArrayIntoArrays(contributors, contributorsPerRow);
return `## Contributors
${rows.map(row => {
// Compile the rows
const imgs = row.map(({
img,
url,
name
}) => img != null ? `[<img alt="${name}" src="${img}" width="${imageSize}">](${url})` : " ");
const names = row.map(({
url,
email,
name
}) => `[${name}](${url})`);
const emails = row.map(({
url,
email
}) => email != null ? `[${email}](mailto:${email})` : ""); // Find the maximum amount of info lines for the row!
const maxInfoLinesCount = row.reduce((acc, {
info
}) => info != null ? Math.max(acc, info.length) : acc, 0); // For each line we go through the row and find the correct info
const infos = Array(maxInfoLinesCount).fill(0).map((_, i) => {
return row.map(({
info
}) => info != null && i < info.length ? info[i] : "");
});
const content = [imgs, names, emails, ...infos];
return tableTemplate({
rows: content,
config,
centered: true
});
}).join(config.lineBreak)}`;
}
/**
* Generates documentation for a glob.
* @param glob
* @param config
*/
async function documentationTemplate({
glob: globString,
config
}) {
// Resolve all paths
const paths = await new Promise((res, rej) => {
glob(globString, (err, paths) => {
err != null ? rej(err) : res(paths);
});
}); // Read all files
const files = paths.map(path => ({
fileName: path,
text: fs.readFileSync(path, {
encoding: "utf8"
})
})); // Analyze the text
const {
results,
program
} = analyzeText(files); // Turn the result into markdown
const format = "markdown";
return transformAnalyzerResult(format, results, program, config.documentationConfig);
}
/**
* Creates a simple template.
* @param name
* @param template
* @param params
*/
function simpleTemplateGenerator({
name,
template,
params
}) {
return {
name,
regex: placeholderRegexCallback(`template:${name}`),
template: () => template,
params
};
}
/**
* Loads markdown.
*/
const generateLoad = {
name: "load",
regex: placeholderRegexCallback("load:(.+?\.md)"),
template: ({
content,
generateReadme,
configPath,
config
}) => {
// Recursively generate the readme for all the files that are being loaded, but only add the load generator
// since all of the generators should only run once.
return generateReadme({
config,
blueprint: content,
configPath,
generators: [generateLoad]
});
},
params: ({
config,
match,
generateReadme,
configPath
}) => {
const absolutePath = resolve(match[2]); // Check if file exists
if (!fileExists(absolutePath)) {
return {
error: `the file "${absolutePath}" doesn't exist.`
};
} // Read the file
const content = readFile(absolutePath) || "";
return {
content,
generateReadme,
configPath,
config
};
}
};
/**
* Generates a logo.
*/
const generateLogo = {
name: "logo",
regex: placeholderRegexCallback("template:logo"),
template: logoTemplate,
params: {
logo: "logo",
src: "logo.src"
}
};
/**
* Generates a title.
*/
const generateMainTitle = {
name: "main-title",
regex: placeholderRegexCallback("template:title"),
template: mainTitleTemplate,
params: {
name: "pkg.name"
}
};
/**
* Generates badges.
*/
const generateBadges = {
name: "badges",
regex: placeholderRegexCallback("template:badges"),
template: badgesTemplate,
params: ({
config
}) => {
const badges = getBadges({
config
});
if (badges.length === 0) {
return {
error: "it could not generate any badges"
};
}
return {
badges,
config
};
}
};
/**
* Generates a description.
*/
const generateDescription = {
name: "description",
regex: placeholderRegexCallback("template:description"),
template: descriptionTemplate,
params: {
description: "pkg.description",
optional: {
demo: "demo",
text: "text"
}
}
};
/**
* Generates a line.
*/
const generateLine = {
name: "line",
regex: placeholderRegexCallback("template:line"),
template: lineTemplate
};
/**
* Generates contributors.
*/
const generateContributors = {
name: "contributors",
regex: placeholderRegexCallback("template:contributors"),
template: contributorsTemplate,
params: {
contributors: "pkg.contributors"
}
};
/**
* Generates license.
*/
const generateLicense = {
name: "license",
regex: placeholderRegexCallback("template:license"),
template: licenseTemplate,
params: {
license: "pkg.license"
}
};
/**
* Generates the titles.
*/
const generateTitle = {
name: "title",
regex: () => /^([#]{1,2}) (.*)$/gm,
template: titleTemplate,
params: ({
config,
match
}) => {
const hashes = match[1];
const title = match[2];
return {
title,
level: hashes.length,
config
};
}
};
/**
* Generates the interpolation.
*/
const generateInterpolate = {
name: "interpolate",
regex: placeholderRegexCallback(`[^\\s:]*`),
template: ({
config,
text
}) => {
let value = getValue(config, text);
if (value == null) return text; // Transform objects into array so they can be transformed into lists
if (isObject(value)) {
value = Object.entries(value).map(([k, v]) => `**${k}**: ${v}`);
} // Transform arrays
if (Array.isArray(value)) {
// Turn 2D arrays into tables
if (value.length > 0 && Array.isArray(value[0])) {
value = tableTemplate({
rows: value,
config: config
});
} // Turn 1D arrays into bullets
else {
value = bulletsTemplate({
bullets: value,
config: config
});
}
}
return value || text;
},
params: ({
config,
match
}) => {
const text = match[1];
return {
config,
text: text.trim()
};
}
};
/**
* Generates the toc.
*/
const generateToc = {
name: "toc",
regex: placeholderRegexCallback("template:toc"),
template: tocTemplate,
params: ({
config,
blueprint
}) => {
const titles = blueprint.match(/^[#]{1,6} .*$/gm);
if (titles == null) {
return {
error: "it could not find any titles"
};
}
return {
titles,
config
};
}
};
/**
* Generates documentation.
*/
const generateDocumentation = {
name: "documentation",
regex: placeholderRegexCallback("doc:(.+?)"),
template: documentationTemplate,
params: ({
match,
config
}) => {
const glob = match[2];
if (glob.length === 0) {
return {
error: "it could not find the glob"
};
}
return {
glob,
config
};
}
};
const defaultGenerators = [// Pre process
generateLoad, // Process
generateLogo, generateMainTitle, generateBadges, generateDescription, generateLine, generateContributors, generateLicense, generateDocumentation, // Post process
generateTitle, generateInterpolate, generateToc];
const defaultDocumentationConfig = {
visibility: "public",
markdown: {
titleLevel: 2
}
};
/**
* Default name of the blueprint configuration.
*/
const defaultConfigName = `blueprint.json`;
/**
* Default configuration.
*/
const defaultConfig = {
lineBreak: "\r\n",
tab: "\t",
input: "blueprint.md",
package: "package.json",
output: "README.md",
checkLinks: false,
placeholder: ["{{", "}}"],
dry: false,
silent: false,
help: false,
line: LineColor.COLORED,
templates: [],
contributorsPerRow: 6,
headingPrefix: {
1: "➤ ",
2: "➤ "
},
pkg: {},
documentationConfig: defaultDocumentationConfig
};
/**
* Converts a value to a boolean.
* @param v
*/
function booleanTransformer(v) {
return v !== "false";
}
/**
* Transforms the value based on the type.
* @param type
* @param value
*/
function transformValue({
type,
value
}) {
if (value == null) {
return null;
}
if (type === Boolean) {
return booleanTransformer(value);
}
return value;
}
/**
* Constructs a config using the extend path if one is defined.
* @param config
*/
function extendConfigWithExtendConfig({
config
}) {
// Recursively load the extend path.
const extend = config.extend;
if (extend != null) {
const extendConfig = loadConfig(extend); // Make sure the config exists
if (extendConfig == null) {
throw new Error(`Could not load extend config at path "${extend}". Make sure the file exists.`);
} // Merge the extend with the config. The config object takes precedence.
config = { ...extendConfigWithExtendConfig({
config: extendConfig
}),
...config
};
}
return config;
}
/**
* Constructs a configuration object with defaults.
* @param pkg
* @param options
* @param config
*/
function extendConfigWithDefaults({
options,
config
}) {
config = { ...config
};
for (let [key, value] of Object.entries(defaultConfig)) {
value = getValue(options, key) || getValue(config, key) || value;
setValue(config, key, value);
}
return config;
}
/**
* Generates a readme.
* @param pkg
* @param blueprint
* @param configPath
* @param generators
*/
async function generateReadme({
config,
blueprint,
configPath,
generators
}) {
const {
silent
} = config; // Go through all of the generators and replace with the template
let defaultArgs = {
config,
configPath,
generateReadme
};
for (const generator of generators) {
const regex = generator.regex({ ...defaultArgs,
blueprint
});
let match = null;
do {
match = regex.exec(blueprint);
if (match != null) {
let markdown = match[0];
let errorReason;
let params = null; // If the params are required we extract them from the package.
if (generator.params != null) {
if (isFunction(generator.params)) {
// Extract the params using the function
params = generator.params({ ...defaultArgs,
blueprint,
match
}); // Validate the params
if (params == null || params.error) {
errorReason = (params || {}).error || `the params couldn't not be generated`;
}
} else {
// Get the required and optional parameters
const optionalParams = generator.params["optional"] || [];
const requiredParams = { ...generator.params
};
delete requiredParams["optional"]; // Validate the params
if (!validateObject({
obj: config,
requiredFields: Object.values(requiredParams)
})) {
errorReason = `"${configPath}" is missing one or more of the keys "${Object.values(requiredParams).join(", ")}"`;
} else {
params = extractValues({
map: { ...optionalParams,
...requiredParams
},
obj: config
});
}
}
} // Use the template if no errors occurred
if (errorReason == null) {
markdown = await generator.template({ ...defaultArgs,
blueprint,
...params
});
} else {
if (!silent) {
console.log(yellow(`[readme] - The readme generator "${generator.name}" matched "${match[0]}" but was skipped because ${errorReason}.`));
}
} // Replace the match with the new markdown
const start = match.index;
const end = start + match[0].length;
blueprint = replaceInString(blueprint, markdown, {
start,
end
}); // Change the regex pointer so we dont parse the newly added content again
regex.lastIndex = start + markdown.length;
}
} while (match != null);
}
return blueprint;
}
/**
* Generates the readme.
*/
async function generate({
config,
configPath,
generators
}) {
const {
dry,
silent,
templates,
output
} = config; // Grab blueprint
let blueprint = "";
if (Array.isArray(config.input)) {
blueprint = config.input.join(config.lineBreak);
} else {
const blueprintPath = resolve(config.input);
if (!fileExists(blueprintPath)) {
console.log(red(`[readme] - Could not find the blueprint file "${blueprintPath}". Make sure to provide a valid path as either the user arguments --readme.input or in the "input" field in the "${configPath}" file.`));
return;
}
blueprint = readFile(blueprintPath) || "";
} // Grab templates
if (templates != null) {
const simpleTemplateGenerators = templates.map(simpleTemplateGenerator); // Append the simple generators after the loading generator
generators.splice(1, 0, ...simpleTemplateGenerators);
} // Generate the readme
let readme = await generateReadme({
config,
blueprint,
configPath,
generators
}); // Add warning
const warning = `<!-- ⚠️ This README has been generated from the file(s) "${Array.isArray(config.input) ? config.input.join(", ") : config.input}" ⚠️-->`;
readme = `${warning}${readme}`; // Check broken links
if (config.checkLinks) {
await checkLinksAliveness(readme);
} // Write the file
if (!dry) {
try {
await writeFile({
target: output,
content: readme
}); // Print the success messsage if not silent
if (!silent) {
console.log(green(`[readme] - A readme file was successfully generated at "${output}".`));
}
} catch (err) {
console.log(red(`[readme] - Could not generate readme at "${output}"`), err);
}
} else {
console.log(green(`[readme] - Created the following readme but did not write it to any files".`), green(readme));
}
}
/**
* Runs the readme command.
* @param options
*/
async function generateCommand(options) {
const configPath = resolve(options["config"] || options["c"] || defaultConfigName);
let config = loadConfig(configPath) || defaultConfig;
config = extendConfigWithExtendConfig({
config
});
config = extendConfigWithDefaults({
config,
options
}); // Extend the config with the package object
config.pkg = { ...(loadPackage(config.package) || {}),
...config.pkg
};
await generate({
config,
configPath,
generators: defaultGenerators
});
}
var name = "@appnest/readme";
var version = "1.2.7";
var description = "Automatically generate a beautiful best-practice README file based on the contents of your repository";
var license = "MIT";
var module = "index.esm.js";
var main = "index.cjs.js";
var types = "index.d.ts";
var author = "Appnest";
var scripts = {
prebuild: "node prebuild.js",
"b:lib": "rollup -c && rollup -c=rollup-cli.config.js",
"b:lib:prebuild": "npm run prebuild && npm run b:lib",
readme: "npm run b:lib && npm run generate:readme",
"generate:readme": "node dist/cli.cjs.js generate",
publish: "cd dist && npm publish --access=public && cd ..",
"git:add:commit:push": "git add . && git commit --no-edit --amend --no-verify && git push",
"bump:patch": "npm version patch && npm run git:add:commit:push",
"bump:minor": "npm version minor && npm run git:add:commit:push",
"bump:major": "npm version major && npm run git:add:commit:push",
"publish:patch": "npm run bump:patch && npm run b:lib:prebuild && npm run publish",
"publish:minor": "npm run bump:minor && npm run b:lib:prebuild && npm run publish",
"publish:major": "npm run bump:major && npm run b:lib:prebuild && npm run publish",
"push:readme": "npm run readme && git add . && git commit -m 'New readme' && git push",
ncu: "ncu -u -a && npm update && npm install"
};
var bugs = {
url: "https://github.com/andreasbm/readme/issues"
};
var homepage = "https://github.com/andreasbm/readme#readme";
var repository = {
type: "git",
url: "git+https://github.com/andreasbm/readme.git"
};
var keywords = ["opensource", "project", "readme", "template", "boilerplate", "nodejs", "maintaining", "generator"];
var contributors = [{
name: "Andreas Mehlsen",
url: "https://twitter.com/andreasmehlsen",
img: "https://avatars1.githubusercontent.com/u/6267397?s=460&v=4",
info: ["🔥"]
}, {
name: "You?",
img: "https://joeschmoe.io/api/v1/random",
url: "https://github.com/andreasbm/readme/blob/master/CONTRIBUTING.md"
}];
var bin = {
readme: "cli.cjs.js"
};
var dependencies = {
"@types/glob": "^7.1.1",
"check-links": "^1.1.8",
colors: "^1.4.0",
commander: "^5.0.0",
"fs-extra": "^9.0.0",
glob: "^7.1.6",
path: "^0.12.7",
"web-component-analyzer": "1.0.3"
};
var devDependencies = {
"@types/fs-extra": "^8.1.0",
"@wessberg/rollup-plugin-ts": "^1.2.21",
rimraf: "^3.0.2",
rollup: "^2.1.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0"
};
var pkg = {
name: name,
version: version,
description: description,
license: license,
module: module,
main: main,
types: types,
author: author,
scripts: scripts,
bugs: bugs,
homepage: homepage,
repository: repository,
keywords: keywords,
contributors: contributors,
bin: bin,
dependencies: dependencies,
devDependencies: devDependencies
};
/**
* Runs the cli.
* @param argv
*/
async function run(argv) {
version$1(pkg.version);
command(`check-links`).description(`Checks all links for aliveness.`).option(`--i, --input <string>`, `Path of the file that needs to be checked. Defaults to '${defaultConfig.output}'`, defaultConfig.output).action(cmd => {
checkLinksCommand(cmd.opts()).then();
});
command(`generate`).description(`Generates a README file.`).option(`-c --config <path>`, `Path of the configuration file. Defaults to '${defaultConfigName}'.`).option(`-p --package <path>`, `Path of the package file. Defaults to '${defaultConfig.package}'.`).option(`-o --output <path>`, `Path of the generated README file. Defaults to '${defaultConfig.output}'.`).option(`-i --input <path>`, `Path of the blueprint. Defaults to '${defaultConfig.input}'.`).option(`-e --extend <path>`, `Path to another configuration object that should be extended.`).option(`-d --dry`, `Whether the command should run as dry. If dry, the output file is not generated but outputted to the console instead.`).option(`--badges <list>`, `Badges. Used for the 'badges' template.`).option(`--text <string>`, `Text describing your project. Used for the 'description' template.`).option(`--demo <string>`, `Demo url for your project. Used for the 'description' template.`).option(`--lineBreak <string>`, `The linebreak used in the generation of the README file. Defaults to '\\r\\n'`).option(`--tab <string>`, `The tab used in the generation of the README file. Defaults to '\\t'`).option(`--placeholder <list>`, `The placeholder syntax used when looking for templates in the blueprint. Defaults to '\["\{\{", "\}\}"\]`).option(`--line <string>`, `The line style of the titles. Can also be an URL. Defaults to 'colored'.`).option(`--templates <list>`, `User created templates.`).option(`--silent`, `Whether the console output from the command should be silent.`).option(`--headingPrefix <object>`, `The prefix of the header tags. Defaults to '\{1: "➤ ", 2: "➤ "\}'`).option(`--logo <object>`, `The logo information. Used for the 'logo' template.`).option(`--contributorsPerRow <integer>`, `The amount of contributors pr row when using the 'contributors' template. Defaults to '${defaultConfig.contributorsPerRow}'`).option(`--documentationConfig <object>`, `Configuration object for automatic documentation template.`).option(`--checkLinks`, `Checks all links for aliveness after the README file has been generated.`).option(`--pkg.name <string>`, `Contributors of the project. Used for the 'contributors' template.`).option(`--pkg.contributors <list>`, `Contributors of the project. Used for the 'contributors' template.`).option(`--pkg.license <license>`, `License kind. Used for the 'license' template.`).action(cmd => {
generateCommand(cmd.opts()).then();
}); // Do some error handling
const userArgs = argv.slice(2);
if (userArgs.length === 0) {
help();
} // Handle unknown commands
on("command:*", () => {
console.error(`Invalid command: ${userArgs.join(" ")}\nSee --help for a list of available commands.`);
process.exit(1);
}); // Parse the input
parse(argv);
}
export { LineColor, URL_PATTERN, badgesTemplate, bulletsTemplate, checkLinksAliveness, checkLinksCommand, contributorsTemplate, defaultConfig, defaultConfigName, defaultDocumentationConfig, defaultGenerators, demoTemplate, descriptionTemplate, documentationTemplate, escapeRegex, extendConfigWithDefaults, extendConfigWithExtendConfig, extractValues, fileExists, generate, generateBadges, generateCommand, generateContributors, generateDescription, generateDocumentation, generateInterpolate, generateLicense, generateLine, generateLoad, generateLogo, generateMainTitle, generateReadme, generateTitle, generateToc, getBadges, getCleanTitle, getLicenseUrl, getLinks, getTitle, getTitleLink, getValue, githubBadges, hasKey, isFunction, isObject, isValidURL, licenseTemplate, lineTemplate, loadConfig, loadPackage, logoTemplate, mainTitleTemplate, npmBadges, placeholderRegexCallback, readFile, readJSONFile, replaceInString, run, setValue, simpleTemplateGenerator, splitArrayIntoArrays, tableTemplate, titleTemplate, tocTemplate, validateObject, webcomponentsBadges, writeFile };
//# sourceMappingURL=index.esm.js.map