UNPKG

@bfra.me/badge-config

Version:

TypeScript API for generating shields.io badge URLs with preset generators

555 lines (548 loc) 15.4 kB
// 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