@bfra.me/badge-config
Version:
TypeScript API for generating shields.io badge URLs with preset generators
555 lines (548 loc) • 15.4 kB
JavaScript
// src/types.ts
var BadgeError = class extends Error {
/** An optional error code for specific failure types. */
code;
/**
* Creates a new BadgeError instance.
* @param message - The error message.
* @param code - An optional error code.
*/
constructor(message, code) {
super(message);
this.name = "BadgeError";
this.code = code;
}
};
// src/utils.ts
function encodeText(text) {
if (typeof text !== "string") {
throw new BadgeError("Text must be a string", "INVALID_TEXT_TYPE");
}
let encoded = encodeURIComponent(text);
encoded = encoded.replaceAll("-", "--").replaceAll("_", "__");
return encoded;
}
function validateColor(color) {
if (typeof color !== "string") {
throw new BadgeError("Color must be a string", "INVALID_COLOR_TYPE");
}
if (color.startsWith("#")) {
const hex = color.slice(1);
if (!/^[\da-f]{3}$|^[\da-f]{6}$/i.test(hex)) {
throw new BadgeError(`Invalid hex color: ${color}`, "INVALID_HEX_COLOR");
}
return encodeURIComponent(color);
}
if (color.startsWith("rgb(") && color.endsWith(")")) {
const rgbValues = color.slice(4, -1).split(",");
if (rgbValues.length !== 3) {
throw new BadgeError(`Invalid RGB color: ${color}`, "INVALID_RGB_COLOR");
}
for (const value of rgbValues) {
const num = Number.parseInt(value.trim(), 10);
if (Number.isNaN(num) || num < 0 || num > 255) {
throw new BadgeError(`Invalid RGB color: ${color}`, "INVALID_RGB_COLOR");
}
}
return encodeURIComponent(color);
}
return color;
}
function validateCacheSeconds(cacheSeconds) {
if (!Number.isInteger(cacheSeconds) || cacheSeconds < 0) {
throw new BadgeError("Cache seconds must be a non-negative integer", "INVALID_CACHE_SECONDS");
}
return cacheSeconds;
}
function validateLogoSize(logoSize) {
if (logoSize === "auto") {
return "auto";
}
if (!Number.isInteger(logoSize) || logoSize <= 0) {
throw new BadgeError('Logo size must be a positive integer or "auto"', "INVALID_LOGO_SIZE");
}
return logoSize.toString();
}
function sanitizeInput(input) {
if (typeof input !== "string") {
throw new BadgeError("Input must be a string", "INVALID_INPUT_TYPE");
}
const sanitized = input.replaceAll(/[<>"']/g, "").replaceAll(/javascript:/gi, "").replaceAll(/vbscript:/gi, "").replaceAll(/data:/gi, "").trim();
if (sanitized.length === 0) {
throw new BadgeError("Input cannot be empty after sanitization", "EMPTY_INPUT");
}
return sanitized;
}
// src/create-badge.ts
async function createBadge(options, fetchOptions) {
if (!options.label || !options.message) {
throw new BadgeError("Label and message are required", "MISSING_REQUIRED_FIELDS");
}
const label = encodeText(sanitizeInput(options.label));
const message = encodeText(sanitizeInput(options.message));
const baseUrl = "https://img.shields.io/badge";
const color = options.color !== void 0 && options.color !== null && options.color.trim() !== "" ? validateColor(options.color) : "blue";
let url = `${baseUrl}/${label}-${message}-${color}`;
const searchParams = new URLSearchParams();
if (options.labelColor !== void 0) {
const labelColor = validateColor(options.labelColor);
searchParams.set("labelColor", labelColor);
}
if (options.style !== void 0) {
searchParams.set("style", options.style);
}
if (options.logo !== void 0) {
searchParams.set("logo", options.logo);
}
if (options.logoColor !== void 0) {
const logoColor = validateColor(options.logoColor);
searchParams.set("logoColor", logoColor);
}
if (options.logoSize !== void 0) {
const logoSize = validateLogoSize(options.logoSize);
searchParams.set("logoSize", logoSize);
}
if (options.cacheSeconds !== void 0) {
const cacheSeconds = validateCacheSeconds(options.cacheSeconds);
searchParams.set("cacheSeconds", cacheSeconds.toString());
}
const queryString = searchParams.toString();
if (queryString.length > 0) {
url += `?${queryString}`;
}
const result = { url };
if (fetchOptions?.fetchSvg === true) {
try {
const response = await fetch(url, {
signal: typeof fetchOptions.timeout === "number" ? AbortSignal.timeout(fetchOptions.timeout) : void 0
});
if (!response.ok) {
throw new BadgeError(
`Failed to fetch badge: ${response.status} ${response.statusText}`,
"FETCH_ERROR"
);
}
result.svg = await response.text();
} catch (error) {
if (error instanceof BadgeError) {
throw error;
}
const message2 = error instanceof Error ? error.message : "Unknown error";
throw new BadgeError(`Failed to fetch SVG: ${message2}`, "FETCH_ERROR");
}
}
return result;
}
function createBadgeUrl(options) {
if (!options.label || !options.message) {
throw new BadgeError("Label and message are required", "MISSING_REQUIRED_FIELDS");
}
const label = encodeText(sanitizeInput(options.label));
const message = encodeText(sanitizeInput(options.message));
const baseUrl = "https://img.shields.io/badge";
const color = options.color !== void 0 && options.color !== null && options.color.trim() !== "" ? validateColor(options.color) : "blue";
let url = `${baseUrl}/${label}-${message}-${color}`;
const searchParams = new URLSearchParams();
if (options.labelColor !== void 0) {
const labelColor = validateColor(options.labelColor);
searchParams.set("labelColor", labelColor);
}
if (options.style !== void 0) {
searchParams.set("style", options.style);
}
if (options.logo !== void 0) {
searchParams.set("logo", options.logo);
}
if (options.logoColor !== void 0) {
const logoColor = validateColor(options.logoColor);
searchParams.set("logoColor", logoColor);
}
if (options.logoSize !== void 0) {
const logoSize = validateLogoSize(options.logoSize);
searchParams.set("logoSize", logoSize);
}
if (options.cacheSeconds !== void 0) {
const cacheSeconds = validateCacheSeconds(options.cacheSeconds);
searchParams.set("cacheSeconds", cacheSeconds.toString());
}
const queryString = searchParams.toString();
if (queryString.length > 0) {
url += `?${queryString}`;
}
return url;
}
// src/generators/build-status.ts
var BUILD_STATUS_COLORS = {
success: "brightgreen",
failure: "red",
pending: "yellow",
error: "red",
cancelled: "lightgrey",
skipped: "lightgrey",
unknown: "lightgrey"
};
var BUILD_STATUS_MESSAGES = {
success: "passing",
failure: "failing",
pending: "pending",
error: "error",
cancelled: "cancelled",
skipped: "skipped",
unknown: "unknown"
};
function buildStatus(options) {
const { status, label = "build", style, color, logo, logoColor, cacheSeconds } = options;
return {
label,
message: BUILD_STATUS_MESSAGES[status],
color: color ?? BUILD_STATUS_COLORS[status],
style,
logo,
logoColor,
cacheSeconds
};
}
// src/generators/coverage.ts
var DEFAULT_THRESHOLDS = {
excellent: 90,
good: 80,
moderate: 60,
poor: 40
};
function getCoverageColor(percentage, thresholds) {
if (percentage >= thresholds.excellent) {
return "brightgreen";
}
if (percentage >= thresholds.good) {
return "green";
}
if (percentage >= thresholds.moderate) {
return "yellow";
}
if (percentage >= thresholds.poor) {
return "orange";
}
return "red";
}
function formatCoverageMessage(percentage) {
const rounded = Math.round(percentage * 10) / 10;
return rounded % 1 === 0 ? `${Math.round(rounded)}%` : `${rounded}%`;
}
function coverage(options) {
const {
percentage,
label = "coverage",
style,
color,
thresholds = {},
logo,
logoColor,
cacheSeconds
} = options;
if (percentage < 0 || percentage > 100) {
throw new Error(`Coverage percentage must be between 0 and 100, got ${percentage}`);
}
const resolvedThresholds = {
...DEFAULT_THRESHOLDS,
...thresholds
};
const message = formatCoverageMessage(percentage);
const badgeColor = color ?? getCoverageColor(percentage, resolvedThresholds);
return {
label,
message,
color: badgeColor,
style,
logo,
logoColor,
cacheSeconds
};
}
// src/generators/license.ts
var LICENSE_CATEGORY_COLORS = {
permissive: "blue",
copyleft: "orange",
"creative-commons": "purple",
proprietary: "lightgrey",
custom: "lightgrey"
};
var COMMON_LICENSES = {
// Permissive licenses
"MIT": {
name: "MIT",
category: "permissive",
url: "https://opensource.org/licenses/MIT"
},
"Apache-2.0": {
name: "Apache 2.0",
category: "permissive",
url: "https://www.apache.org/licenses/LICENSE-2.0"
},
"BSD-2-Clause": {
name: "BSD 2-Clause",
category: "permissive",
url: "https://opensource.org/licenses/BSD-2-Clause"
},
"BSD-3-Clause": {
name: "BSD 3-Clause",
category: "permissive",
url: "https://opensource.org/licenses/BSD-3-Clause"
},
"ISC": {
name: "ISC",
category: "permissive",
url: "https://opensource.org/licenses/ISC"
},
"Unlicense": {
name: "Unlicense",
category: "permissive",
url: "https://unlicense.org/"
},
// Copyleft licenses
"GPL-2.0": {
name: "GPL 2.0",
category: "copyleft",
url: "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html"
},
"GPL-3.0": {
name: "GPL 3.0",
category: "copyleft",
url: "https://www.gnu.org/licenses/gpl-3.0.html"
},
"LGPL-2.1": {
name: "LGPL 2.1",
category: "copyleft",
url: "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html"
},
"LGPL-3.0": {
name: "LGPL 3.0",
category: "copyleft",
url: "https://www.gnu.org/licenses/lgpl-3.0.html"
},
"AGPL-3.0": {
name: "AGPL 3.0",
category: "copyleft",
url: "https://www.gnu.org/licenses/agpl-3.0.html"
},
// Creative Commons licenses
"CC-BY-4.0": {
name: "CC BY 4.0",
category: "creative-commons",
url: "https://creativecommons.org/licenses/by/4.0/"
},
"CC-BY-SA-4.0": {
name: "CC BY-SA 4.0",
category: "creative-commons",
url: "https://creativecommons.org/licenses/by-sa/4.0/"
},
"CC0-1.0": {
name: "CC0 1.0",
category: "creative-commons",
url: "https://creativecommons.org/publicdomain/zero/1.0/"
},
// Proprietary
"UNLICENSED": {
name: "Proprietary",
category: "proprietary"
}
};
function normalizeLicenseId(license2) {
return license2.trim().toUpperCase();
}
function getLicenseInfo(license2) {
const normalized = normalizeLicenseId(license2);
if (normalized in COMMON_LICENSES) {
return COMMON_LICENSES[normalized];
}
const variations = [
license2.trim(),
// Original case
license2.trim().toLowerCase(),
license2.trim().toUpperCase()
];
for (const variation of variations) {
if (variation in COMMON_LICENSES) {
return COMMON_LICENSES[variation];
}
}
return void 0;
}
function license(options) {
const {
license: licenseId,
label = "license",
style,
color,
category,
logo,
logoColor,
cacheSeconds
} = options;
const licenseInfo = getLicenseInfo(licenseId);
const displayName = licenseInfo?.name ?? licenseId;
const resolvedCategory = category ?? licenseInfo?.category ?? "custom";
const badgeColor = color ?? LICENSE_CATEGORY_COLORS[resolvedCategory];
return {
label,
message: displayName,
color: badgeColor,
style,
logo,
logoColor,
cacheSeconds
};
}
// src/generators/social.ts
var SOCIAL_BADGE_COLORS = {
stars: "yellow",
forks: "blue",
watchers: "green",
issues: "red",
followers: "blue",
downloads: "green"
};
var SOCIAL_BADGE_LABELS = {
stars: "stars",
forks: "forks",
watchers: "watchers",
issues: "issues",
followers: "followers",
downloads: "downloads"
};
var SOCIAL_BADGE_LOGOS = {
stars: "github",
forks: "github",
watchers: "github",
issues: "github",
followers: "github"
};
function formatCount(count) {
if (count >= 1e6) {
const millions = count / 1e6;
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
}
if (count >= 1e3) {
const thousands = count / 1e3;
return thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;
}
return count.toString();
}
function social(options) {
const {
type,
repository,
user,
packageName,
count,
label,
style,
color,
logo,
logoColor,
cacheSeconds
} = options;
if ((type === "stars" || type === "forks" || type === "watchers" || type === "issues") && (repository === void 0 || repository === "")) {
throw new Error(`repository is required for ${type} badges`);
}
if (type === "followers" && (user === void 0 || user === "")) {
throw new Error("user is required for followers badges");
}
if (type === "downloads" && (packageName === void 0 || packageName === "")) {
throw new Error("packageName is required for downloads badges");
}
const resolvedLabel = label ?? SOCIAL_BADGE_LABELS[type];
const message = count === void 0 ? "0" : formatCount(count);
const badgeColor = color ?? SOCIAL_BADGE_COLORS[type];
const badgeLogo = logo ?? SOCIAL_BADGE_LOGOS[type];
return {
label: resolvedLabel,
message,
color: badgeColor,
style,
logo: badgeLogo,
logoColor,
cacheSeconds
};
}
// src/generators/version.ts
var VERSION_SOURCE_COLORS = {
npm: "red",
git: "blue",
github: "blue",
custom: "blue"
};
var VERSION_SOURCE_LABELS = {
npm: "npm",
git: "version",
github: "version",
custom: "version"
};
function isValidSemver(version2) {
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i;
return semverPattern.test(version2);
}
function formatVersionMessage(version2, source) {
const cleanVersion = version2.replace(/^v/, "");
if (source === "npm") {
return cleanVersion;
}
if (source === "git" && isValidSemver(cleanVersion)) {
return `v${cleanVersion}`;
}
return cleanVersion;
}
function version(options) {
const {
version: rawVersion,
source = "custom",
packageName,
repository,
label,
style,
color,
logo,
logoColor,
cacheSeconds
} = options;
if (source === "npm" && (packageName === void 0 || packageName === "")) {
throw new Error('packageName is required when source is "npm"');
}
if ((source === "git" || source === "github") && (repository === void 0 || repository === "")) {
throw new Error('repository is required when source is "git" or "github"');
}
let resolvedLabel = label;
if (resolvedLabel === void 0 || resolvedLabel === "") {
if (source === "npm" && packageName !== void 0 && packageName !== "") {
resolvedLabel = packageName;
} else {
resolvedLabel = VERSION_SOURCE_LABELS[source];
}
}
const message = formatVersionMessage(rawVersion, source);
const badgeColor = color ?? VERSION_SOURCE_COLORS[source];
return {
label: resolvedLabel,
message,
color: badgeColor,
style,
logo,
logoColor,
cacheSeconds
};
}
export {
BadgeError,
buildStatus,
coverage,
createBadge,
createBadgeUrl,
encodeText,
license,
sanitizeInput,
social,
validateCacheSeconds,
validateColor,
validateLogoSize,
version
};
//# sourceMappingURL=index.js.map