diff --git a/dist/gh-profile-card.min.js b/dist/gh-profile-card.min.js index 755b314..5aa812f 100644 --- a/dist/gh-profile-card.min.js +++ b/dist/gh-profile-card.min.js @@ -1,6 +1,6 @@ /** * github-profile-card - 3.2.0 | MIT -* (c) 2014 - 2025 Piotr Lewandowski | https://piotrl.github.io/github-profile-card/ +* (c) 2014 - 2026 Piotr Lewandowski | https://piotrl.github.io/github-profile-card/ */ (() => { @@ -9,111 +9,181 @@ constructor(storage) { this.storage = storage; this.cacheName = "github-request-cache"; - this.requestCache = this.getCache() || {}; + this.requestCache = this.initializeCache(); } get(key) { return this.requestCache[key]; } add(url, entry) { this.requestCache[url] = entry; - this.storage.setItem(this.cacheName, JSON.stringify(this.requestCache)); + this.saveCache(); } - getCache() { - return JSON.parse(this.storage.getItem(this.cacheName)); + initializeCache() { + try { + const cacheData = this.storage.getItem(this.cacheName); + if (!cacheData) { + return {}; + } + const cache = JSON.parse(cacheData); + return cache && typeof cache === "object" ? cache : {}; + } catch (error) { + console.error("Failed to parse cache:", error); + try { + this.storage.removeItem(this.cacheName); + } catch (cleanupError) { + console.error("Failed to clear corrupted cache:", cleanupError); + } + return {}; + } + } + saveCache() { + try { + this.storage.setItem(this.cacheName, JSON.stringify(this.requestCache)); + } catch (error) { + console.error("Failed to save cache:", error); + this.clearExpiredEntries(/* @__PURE__ */ new Date()); + try { + this.storage.setItem(this.cacheName, JSON.stringify(this.requestCache)); + } catch (retryError) { + console.error("Failed to save cache after cleanup:", retryError); + } + } + } + clearExpiredEntries(currentDate) { + let hasChanges = false; + for (const [url, entry] of Object.entries(this.requestCache)) { + if (entry.lastModified) { + const entryDate = new Date(entry.lastModified); + if (isNaN(entryDate.getTime()) || entryDate < currentDate) { + delete this.requestCache[url]; + hasChanges = true; + } + } + } + if (hasChanges) { + this.saveCache(); + } } }; // src/gh-data-loader.ts + var API_HOST = "https://api.github.com"; var GitHubApiLoader = class { constructor() { - this.apiBase = "https://api.github.com"; this.cache = new CacheStorage(window.localStorage); } - loadUserData(username, callback) { - const request = this.apiGet(`${this.apiBase}/users/${username}`); - request.success((profile) => { - this.apiGet(profile.repos_url).success((repositories) => { - callback({ profile, repositories }, null); - }); - }); - request.error((result, request2) => { - const error = this.identifyError(result, request2); - callback(null, error); - }); + async loadUserData(username) { + if (typeof username !== "string") { + throw new Error("Invalid username provided"); + } + const sanitizedUsername = username.trim(); + if (!sanitizedUsername) { + throw new Error("Username cannot be empty"); + } + const profile = await this.fetch( + `${API_HOST}/users/${sanitizedUsername}` + ); + const repositories = await this.fetch(profile.repos_url); + return { profile, repositories }; } loadRepositoriesLanguages(repositories, callback) { + if (!repositories || repositories.length === 0) { + callback([]); + return; + } const languagesUrls = this.extractLangURLs(repositories); const langStats = []; - let requestsAmount = languagesUrls.length; + const requestsAmount = languagesUrls.length; + let completedRequests = 0; + if (requestsAmount === 0) { + callback([]); + return; + } + const handleCompletion = () => { + completedRequests++; + if (completedRequests === requestsAmount) { + callback(langStats); + } + }; languagesUrls.forEach((repoLangUrl) => { - const request = this.apiGet(repoLangUrl); - request.error(() => requestsAmount--); - request.success((repoLangs) => { - langStats.push(repoLangs); - if (langStats.length === requestsAmount) { - callback(langStats); - } + this.fetch(repoLangUrl).then((repoLangs) => { + langStats.push(repoLangs || {}); + handleCompletion(); + }).catch((error) => { + console.warn("Failed to load languages for repository:", error); + langStats.push({}); + handleCompletion(); }); }); } - identifyError(result, request) { + async identifyError(response) { + let result; + try { + result = await response.json(); + } catch { + result = { message: "Failed to parse error response" }; + } const error = { - message: result.message + message: result.message || `HTTP ${response.status}: ${response.statusText}` }; - if (request.status === 404) { + if (response.status === 404) { error.isWrongUser = true; } - const limitRequests = request.getResponseHeader("X-RateLimit-Remaining"); + const limitRequests = response.headers.get("X-RateLimit-Remaining"); if (Number(limitRequests) === 0) { - const resetTime = request.getResponseHeader("X-RateLimit-Reset"); - error.resetDate = new Date(Number(resetTime) * 1e3); - error.message = error.message.split("(")[0]; + const resetTime = response.headers.get("X-RateLimit-Reset"); + if (resetTime) { + error.resetDate = new Date(Number(resetTime) * 1e3); + error.message = error.message.split("(")[0]; + } } return error; } extractLangURLs(profileRepositories) { - return profileRepositories.map((repository) => repository.languages_url); + return profileRepositories.filter((repo) => repo && repo.languages_url).map((repository) => repository.languages_url); } - apiGet(url) { - const request = this.buildRequest(url); - return { - success: (callback) => { - request.addEventListener("load", () => { - if (request.status === 304) { - callback(this.cache.get(url).data, request); - } - if (request.status === 200) { - const response = JSON.parse(request.responseText); - this.cache.add(url, { - lastModified: request.getResponseHeader("Last-Modified"), - data: response - }); - callback(response, request); - } - }); - }, - error: (callback) => { - request.addEventListener("load", () => { - if (request.status !== 200 && request.status !== 304) { - callback(JSON.parse(request.responseText), request); - } - }); - } - }; + async fetch(url) { + if (typeof url !== "string") { + throw new Error("Invalid URL provided for fetch"); + } + const cache = this.cache.get(url); + let response; + try { + response = await fetch(url, { + headers: this.buildHeaders(cache) + }); + } catch (networkError) { + throw new Error(`Network error: ${networkError.message}`); + } + if (response.status === 304 && cache) { + return cache.data; + } + if (response.status !== 200) { + throw await this.identifyError(response); + } + let jsonResponse; + try { + jsonResponse = await response.json(); + } catch { + throw new Error("Failed to parse API response as JSON"); + } + const lastModified = response.headers.get("Last-Modified"); + if (lastModified) { + this.cache.add(url, { + lastModified, + data: jsonResponse + }); + } + return jsonResponse; } - buildRequest(url) { - const request = new XMLHttpRequest(); - request.open("GET", url); - this.buildApiHeaders(request, url); - request.send(); - return request; - } - buildApiHeaders(request, url) { - request.setRequestHeader("Accept", "application/vnd.github.v3+json"); - const urlCache = this.cache.get(url); - if (urlCache) { - request.setRequestHeader("If-Modified-Since", urlCache.lastModified); + buildHeaders(cache) { + const headers = { + Accept: "application/vnd.github.v3+json" + }; + if (cache?.lastModified) { + headers["If-Modified-Since"] = cache.lastModified; } + return headers; } }; @@ -131,26 +201,27 @@ const $name = document.createElement("a"); $name.href = profileUrl; $name.className = "name"; - $name.appendChild(document.createTextNode(name)); + $name.appendChild(document.createTextNode(name || "")); return $name; } function createAvatar(avatarUrl) { const $avatar = document.createElement("img"); $avatar.src = avatarUrl; $avatar.className = "avatar"; + $avatar.alt = "GitHub avatar"; return $avatar; } function createFollowButton(username, followUrl) { const $followButton = document.createElement("a"); $followButton.href = followUrl; $followButton.className = "follow-button"; - $followButton.innerHTML = "Follow @" + username; + $followButton.textContent = `Follow @${username}`; return $followButton; } function createFollowers(followersAmount) { const $followers = document.createElement("span"); $followers.className = "followers"; - $followers.innerHTML = "" + followersAmount; + $followers.textContent = String(followersAmount); return $followers; } function createFollowContainer(children) { @@ -163,21 +234,27 @@ // src/gh-dom-operator.ts var DOMOperator = class { static clearChildren($parent) { - while ($parent.hasChildNodes()) { - $parent.removeChild($parent.firstChild); - } + $parent.textContent = ""; } static createError(error, username) { const $error = document.createElement("div"); $error.className = "error"; - $error.innerHTML = `${error.message}`; + const $message = document.createElement("span"); + $message.textContent = error.message; + $error.appendChild($message); if (error.isWrongUser) { - $error.innerHTML = `Not found user: ${username}`; + $message.textContent = `Not found user: ${username}`; } if (error.resetDate) { - let remainingTime = error.resetDate.getMinutes() - (/* @__PURE__ */ new Date()).getMinutes(); - remainingTime = remainingTime < 0 ? 60 + remainingTime : remainingTime; - $error.innerHTML += `Come back after ${remainingTime} minutes`; + const currentTime = (/* @__PURE__ */ new Date()).getTime(); + const resetTime = error.resetDate.getTime(); + const remainingMinutes = Math.ceil( + (resetTime - currentTime) / (1e3 * 60) + ); + const $remainingTime = document.createElement("span"); + $remainingTime.className = "remain"; + $remainingTime.textContent = `Come back after ${remainingMinutes} minutes`; + $error.appendChild($remainingTime); } return $error; } @@ -195,10 +272,19 @@ return $langsList; } static createTopLanguagesList(langs) { - return Object.keys(langs).map((language) => ({ + const sortedLanguages = Object.keys(langs).map((language) => ({ name: language, stat: langs[language] - })).sort((a, b) => b.stat - a.stat).slice(0, 3).map((lang) => `
  • ${lang.name}
  • `).reduce((list, nextElement) => list + nextElement); + })).sort((a, b) => b.stat - a.stat).slice(0, 3); + return sortedLanguages.map((lang) => { + const escapedName = this.escapeHtml(lang.name); + return `
  • ${escapedName}
  • `; + }).join(""); + } + static escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; } static createRepositoriesHeader(headerText) { const $repositoriesHeader = document.createElement("span"); @@ -216,12 +302,19 @@ const updated = new Date(repository.updated_at); const $repoLink = document.createElement("a"); $repoLink.href = repository.html_url; - $repoLink.title = repository.description; - $repoLink.innerHTML = ` - ${repository.name} - Updated: ${updated.toLocaleDateString()} - ${repository.stargazers_count} - `; + $repoLink.title = repository.description || ""; + const $repoName = document.createElement("span"); + $repoName.className = "repo-name"; + $repoName.textContent = repository.name; + const $updated = document.createElement("span"); + $updated.className = "updated"; + $updated.textContent = `Updated: ${updated.toLocaleDateString()}`; + const $star = document.createElement("span"); + $star.className = "star"; + $star.textContent = String(repository.stargazers_count); + $repoLink.appendChild($repoName); + $repoLink.appendChild($updated); + $repoLink.appendChild($star); return $repoLink; } }; @@ -230,15 +323,18 @@ var GitHubCardWidget = class { constructor(options = {}) { this.apiLoader = new GitHubApiLoader(); + this.userData = null; this.$template = this.findTemplate(options.template); this.extractHtmlConfig(options, this.$template); this.options = this.completeConfiguration(options); } - init() { - this.apiLoader.loadUserData(this.options.username, (data, err) => { - this.userData = data; + async init() { + try { + this.userData = await this.apiLoader.loadUserData(this.options.username); + this.render(this.options); + } catch (err) { this.render(this.options, err); - }); + } } refresh(options) { this.options = this.completeConfiguration(options); @@ -246,7 +342,7 @@ } completeConfiguration(options) { const defaultConfig = { - username: null, + username: "", template: "#github-card", sortBy: "stars", // possible: 'stars', 'updateTime' @@ -254,53 +350,62 @@ maxRepos: 5, hideTopLanguages: false }; - for (const key in defaultConfig) { - defaultConfig[key] = options[key] || defaultConfig[key]; - } - return defaultConfig; + return { + ...defaultConfig, + ...options + }; } findTemplate(templateCssSelector = "#github-card") { const $template = document.querySelector( templateCssSelector ); if (!$template) { - throw `No template found for selector: ${templateCssSelector}`; + throw new Error(`No template found for selector: ${templateCssSelector}`); } $template.className = "gh-profile-card"; return $template; } extractHtmlConfig(widgetConfig, $template) { - widgetConfig.username = widgetConfig.username || $template.dataset["username"]; - widgetConfig.sortBy = widgetConfig.sortBy || $template.dataset["sortBy"]; - widgetConfig.headerText = widgetConfig.headerText || $template.dataset["headerText"]; - widgetConfig.maxRepos = widgetConfig.maxRepos || parseInt($template.dataset["maxRepos"], 10); - widgetConfig.hideTopLanguages = widgetConfig.hideTopLanguages || $template.dataset["hideTopLanguages"] === "true"; + const dataset = $template.dataset; + widgetConfig.username = widgetConfig.username || dataset.username; + widgetConfig.sortBy = widgetConfig.sortBy || dataset.sortBy; + widgetConfig.headerText = widgetConfig.headerText || dataset.headerText; + if (dataset.maxRepos) { + const parsedMaxRepos = parseInt(dataset.maxRepos, 10); + if (!isNaN(parsedMaxRepos)) { + widgetConfig.maxRepos = widgetConfig.maxRepos || parsedMaxRepos; + } + } + widgetConfig.hideTopLanguages = widgetConfig.hideTopLanguages || dataset.hideTopLanguages === "true"; if (!widgetConfig.username) { - throw "Not provided username"; + throw new Error("Username is required but not provided"); } } render(options, error) { const $root = this.$template; DOMOperator.clearChildren($root); if (error) { - const $errorSection = DOMOperator.createError(error, options.username); + const $errorSection = DOMOperator.createError( + error, + options.username || "" + ); $root.appendChild($errorSection); return; } const repositories = this.userData.repositories; - this.sortRepositories(repositories, options.sortBy); + this.sortRepositories(repositories, options.sortBy || "stars"); const $profile = DOMOperator.createProfile(this.userData.profile); if (!options.hideTopLanguages) { $profile.appendChild(this.createTopLanguagesSection(repositories)); } $root.appendChild($profile); - if (options.maxRepos > 0) { + if ((options.maxRepos || 0) > 0) { const $reposHeader = DOMOperator.createRepositoriesHeader( - options.headerText + options.headerText || "Repositories" ); const $reposList = DOMOperator.createRepositoriesList( repositories, - options.maxRepos + options.maxRepos || 5 ); $reposList.insertBefore($reposHeader, $reposList.firstChild); $root.appendChild($reposList); @@ -308,11 +413,16 @@ } createTopLanguagesSection(repositories) { const $topLanguages = DOMOperator.createTopLanguagesSection(); + if (!repositories || repositories.length === 0) { + return $topLanguages; + } this.apiLoader.loadRepositoriesLanguages( repositories.slice(0, 10), (langStats) => { - const languagesRank = this.groupLanguagesUsage(langStats); - $topLanguages.innerHTML = DOMOperator.createTopLanguagesList(languagesRank); + if (langStats.length > 0) { + const languagesRank = this.groupLanguagesUsage(langStats); + $topLanguages.innerHTML = DOMOperator.createTopLanguagesList(languagesRank); + } } ); return $topLanguages; @@ -320,16 +430,22 @@ groupLanguagesUsage(langStats) { const languagesRank = {}; langStats.forEach((repoLangs) => { - for (const language in repoLangs) { - languagesRank[language] = languagesRank[language] || 0; - languagesRank[language] += repoLangs[language]; + if (repoLangs && typeof repoLangs === "object") { + Object.entries(repoLangs).forEach(([language, bytes]) => { + if (typeof bytes === "number" && bytes > 0) { + languagesRank[language] = (languagesRank[language] || 0) + bytes; + } + }); } }); return languagesRank; } - sortRepositories(repos, sortyBy) { + sortRepositories(repos, sortBy) { + if (!repos || repos.length === 0) { + return; + } repos.sort((firstRepo, secondRepo) => { - if (sortyBy === "stars") { + if (sortBy === "stars") { const starDifference = secondRepo.stargazers_count - firstRepo.stargazers_count; if (starDifference !== 0) { return starDifference; @@ -339,7 +455,12 @@ }); } dateDifference(first, second) { - return new Date(first).getTime() - new Date(second).getTime(); + const firstDate = new Date(first); + const secondDate = new Date(second); + if (isNaN(firstDate.getTime()) || isNaN(secondDate.getTime())) { + return 0; + } + return firstDate.getTime() - secondDate.getTime(); } }; @@ -549,8 +670,14 @@ document.addEventListener("DOMContentLoaded", () => { const $defaultTemplate = document.querySelector("#github-card"); if ($defaultTemplate) { - const widget = new GitHubCardWidget(); - widget.init(); + try { + const widget = new GitHubCardWidget(); + widget.init().catch((error) => { + console.error("Failed to initialize GitHub Card widget:", error); + }); + } catch (error) { + console.error("Failed to construct GitHub Card widget:", error); + } } }); })(); diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..365c613 --- /dev/null +++ b/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@5.82.1/schema.json", + "entry": ["src/gh-widget-init.ts"], + "project": ["src/**/*.ts"], + "ignore": ["demo/**", "dist/**", "src/**/*.spec.ts"], + "ignoreDependencies": ["sass"] +} diff --git a/package-lock.json b/package-lock.json index 8d75179..4fef88e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "eslint-plugin-prettier": "5.5.1", "jest": "30.0.4", "jest-environment-jsdom": "30.0.4", + "knip": "5.82.1", "prettier": "3.6.2", "sass": "1.25.0", "ts-jest": "29.4.0", @@ -709,21 +710,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", - "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.3", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, "license": "MIT", "optional": true, @@ -732,9 +733,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", - "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, @@ -1966,6 +1967,306 @@ "node": ">= 8" } }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.16.4.tgz", + "integrity": "sha512-6XUHilmj8D6Ggus+sTBp64x/DUQ7LgC/dvTDdUOt4iMQnDdSep6N1mnvVLIiG+qM5tRnNHravNzBJnUlYwRQoA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.16.4.tgz", + "integrity": "sha512-5ODwd1F5mdkm6JIg1CNny9yxIrCzrkKpxmqas7Alw23vE0Ot8D4ykqNBW5Z/nIZkXVEo5VDmnm0sMBBIANcpeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.16.4.tgz", + "integrity": "sha512-egwvDK9DMU4Q8F4BG74/n4E22pQ0lT5ukOVB6VXkTj0iG2fnyoStHoFaBnmDseLNRA4r61Mxxz8k940CIaJMDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.16.4.tgz", + "integrity": "sha512-HMkODYrAG4HaFNCpaYzSQFkxeiz2wzl+smXwxeORIQVEo1WAgUrWbvYT/0RNJg/A8z2aGMGK5KWTUr2nX5GiMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.16.4.tgz", + "integrity": "sha512-mkcKhIdSlUqnndD928WAVVFMEr1D5EwHOBGHadypW0PkM0h4pn89ZacQvU7Qs/Z2qquzvbyw8m4Mq3jOYI+4Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.16.4.tgz", + "integrity": "sha512-ZJvzbmXI/cILQVcJL9S2Fp7GLAIY4Yr6mpGb+k6LKLUSEq85yhG+rJ9eWCqgULVIf2BFps/NlmPTa7B7oj8jhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.16.4.tgz", + "integrity": "sha512-iZUB0W52uB10gBUDAi79eTnzqp1ralikCAjfq7CdokItwZUVJXclNYANnzXmtc0Xr0ox+YsDsG2jGcj875SatA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.16.4.tgz", + "integrity": "sha512-qNQk0H6q1CnwS9cnvyjk9a+JN8BTbxK7K15Bb5hYfJcKTG1hfloQf6egndKauYOO0wu9ldCMPBrEP1FNIQEhaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.16.4.tgz", + "integrity": "sha512-wEXSaEaYxGGoVSbw0i2etjDDWcqErKr8xSkTdwATP798efsZmodUAcLYJhN0Nd4W35Oq6qAvFGHpKwFrrhpTrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.16.4.tgz", + "integrity": "sha512-CUFOlpb07DVOFLoYiaTfbSBRPIhNgwc/MtlYeg3p6GJJw+kEm/vzc9lohPSjzF2MLPB5hzsJdk+L/GjrTT3UPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.16.4.tgz", + "integrity": "sha512-d8It4AH8cN9ReK1hW6ZO4x3rMT0hB2LYH0RNidGogV9xtnjLRU+Y3MrCeClLyOSGCibmweJJAjnwB7AQ31GEhg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.16.4.tgz", + "integrity": "sha512-d09dOww9iKyEHSxuOQ/Iu2aYswl0j7ExBcyy14D6lJ5ijQSP9FXcJYJsJ3yvzboO/PDEFjvRuF41f8O1skiPVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.16.4.tgz", + "integrity": "sha512-lhjyGmUzTWHduZF3MkdUSEPMRIdExnhsqv8u1upX3A15epVn6YVwv4msFQPJl1x1wszkACPeDHGOtzHsITXGdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.16.4.tgz", + "integrity": "sha512-ZtqqiI5rzlrYBm/IMMDIg3zvvVj4WO/90Dg/zX+iA8lWaLN7K5nroXb17MQ4WhI5RqlEAgrnYDXW+hok1D9Kaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.16.4.tgz", + "integrity": "sha512-LM424h7aaKcMlqHnQWgTzO+GRNLyjcNnMpqm8SygEtFRVW693XS+XGXYvjORlmJtsyjo84ej1FMb3U2HE5eyjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.16.4.tgz", + "integrity": "sha512-8w8U6A5DDWTBv3OUxSD9fNk37liZuEC5jnAc9wQRv9DeYKAXvuUtBfT09aIZ58swaci0q1WS48/CoMVEO6jdCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.16.4.tgz", + "integrity": "sha512-hnjb0mDVQOon6NdfNJ1EmNquonJUjoYkp7UyasjxVa4iiMcApziHP4czzzme6WZbp+vzakhVv2Yi5ACTon3Zlw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.16.4.tgz", + "integrity": "sha512-+i0XtNfSP7cfnh1T8FMrMm4HxTeh0jxKP/VQCLWbjdUxaAQ4damho4gN9lF5dl0tZahtdszXLUboBFNloSJNOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.16.4.tgz", + "integrity": "sha512-ePW1islJrv3lPnef/iWwrjrSpRH8kLlftdKf2auQNWvYLx6F0xvcnv9d+r/upnVuttoQY9amLnWJf+JnCRksTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.16.4.tgz", + "integrity": "sha512-qnjQhjHI4TDL3hkidZyEmQRK43w2NHl6TP5Rnt/0XxYuLdEgx/1yzShhYidyqWzdnhGhSPTM/WVP2mK66XLegA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -2364,9 +2665,9 @@ "peer": true }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -4440,6 +4741,16 @@ "bser": "2.1.1" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4554,6 +4865,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5759,6 +6086,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5767,9 +6104,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5883,6 +6220,74 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "5.82.1", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.82.1.tgz", + "integrity": "sha512-1nQk+5AcnkqL40kGQXfouzAEXkTR+eSrgo/8m1d0BMei4eAzFwghoXC4gOKbACgBiCof7hE8wkBVDsEvznf85w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "^1.2.3", + "fast-glob": "^3.3.3", + "formatly": "^0.3.0", + "jiti": "^2.6.0", + "js-yaml": "^4.1.1", + "minimist": "^1.2.8", + "oxc-resolver": "^11.15.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.1", + "smol-toml": "^1.5.2", + "strip-json-comments": "5.0.3", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4 <7" + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6054,6 +6459,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -6190,6 +6605,38 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-resolver": { + "version": "11.16.4", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.16.4.tgz", + "integrity": "sha512-nvJr3orFz1wNaBA4neRw7CAn0SsjgVaEw1UHpgO/lzVW12w+nsFnvU/S6vVX3kYyFaZdxZheTExi/fa8R8PrZA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.16.4", + "@oxc-resolver/binding-android-arm64": "11.16.4", + "@oxc-resolver/binding-darwin-arm64": "11.16.4", + "@oxc-resolver/binding-darwin-x64": "11.16.4", + "@oxc-resolver/binding-freebsd-x64": "11.16.4", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.16.4", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.16.4", + "@oxc-resolver/binding-linux-arm64-gnu": "11.16.4", + "@oxc-resolver/binding-linux-arm64-musl": "11.16.4", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.16.4", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.16.4", + "@oxc-resolver/binding-linux-riscv64-musl": "11.16.4", + "@oxc-resolver/binding-linux-s390x-gnu": "11.16.4", + "@oxc-resolver/binding-linux-x64-gnu": "11.16.4", + "@oxc-resolver/binding-linux-x64-musl": "11.16.4", + "@oxc-resolver/binding-openharmony-arm64": "11.16.4", + "@oxc-resolver/binding-wasm32-wasi": "11.16.4", + "@oxc-resolver/binding-win32-arm64-msvc": "11.16.4", + "@oxc-resolver/binding-win32-ia32-msvc": "11.16.4", + "@oxc-resolver/binding-win32-x64-msvc": "11.16.4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7218,6 +7665,19 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7976,6 +8436,16 @@ "node": ">=18" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -8329,6 +8799,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index a40aee6..f4d5b79 100644 --- a/package.json +++ b/package.json @@ -7,24 +7,26 @@ "devDependencies": { "@eslint/js": "9.31.0", "@jest/globals": "30.0.4", + "esbuild": "0.25.1", + "esbuild-sass-plugin": "3.3.1", "eslint": "9.31.0", - "eslint-plugin-jest": "28.8.3", "eslint-config-prettier": "10.1.5", + "eslint-plugin-jest": "28.8.3", "eslint-plugin-prettier": "5.5.1", "jest": "30.0.4", "jest-environment-jsdom": "30.0.4", + "knip": "5.82.1", "prettier": "3.6.2", "sass": "1.25.0", "ts-jest": "29.4.0", "typescript": "5.8.3", - "typescript-eslint": "8.36.0", - "esbuild": "0.25.1", - "esbuild-sass-plugin": "3.3.1" + "typescript-eslint": "8.36.0" }, "scripts": { "build": "node build.js", "build:check": "npm run build && npm run lint && npm run format:check && npm run test", "lint": "eslint .", + "lint:knip": "knip", "format": "prettier --write {*,src/*,demo/*}", "format:check": "prettier --check {*,src/*,demo/*}", "test": "jest", diff --git a/src/testing/cache-mock.ts b/src/testing/cache-mock.ts index f1507bf..7cd86bd 100644 --- a/src/testing/cache-mock.ts +++ b/src/testing/cache-mock.ts @@ -20,32 +20,3 @@ export function createCacheMock(): jest.Mocked { export function setupEmptyCache(mockCache: jest.Mocked): void { mockCache.get.mockReturnValue(undefined); } - -/** - * Sets up cache mock with cached data - */ -export function setupCacheWithData( - mockCache: jest.Mocked, - url: string, - data: any, - lastModified: string = 'Mon, 18 Mar 2019 20:40:35 GMT', -): void { - mockCache.get.mockImplementation((requestedUrl: string) => { - if (requestedUrl === url) { - return { - lastModified, - data, - }; - } - return undefined; - }); -} - -/** - * Resets all cache mock calls - */ -export function resetCacheMock(mockCache: jest.Mocked): void { - mockCache.get.mockClear(); - mockCache.add.mockClear(); - mockCache.clearExpiredEntries.mockClear(); -} diff --git a/src/testing/fetch-mock.ts b/src/testing/fetch-mock.ts index 7e57cd1..37931c7 100644 --- a/src/testing/fetch-mock.ts +++ b/src/testing/fetch-mock.ts @@ -54,15 +54,6 @@ export function createErrorResponse( }; } -/** - * Creates a 304 Not Modified response mock - */ -export function createNotModifiedResponse(): MockResponse { - return { - status: 304, - }; -} - /** * Creates a network error for fetch */ diff --git a/src/testing/mock-github-data.ts b/src/testing/mock-github-data.ts index b27e2ff..4c94e8f 100644 --- a/src/testing/mock-github-data.ts +++ b/src/testing/mock-github-data.ts @@ -180,27 +180,3 @@ export const mockLanguageStats = [ { TypeScript: 1000, JavaScript: 500 }, { Python: 800, TypeScript: 200 }, ]; - -/** - * Creates a mock repository with custom properties - */ -export function createMockRepository( - overrides: Partial = {}, -): ApiRepository { - return { - ...mockRepositories[0], - ...overrides, - }; -} - -/** - * Creates a mock profile with custom properties - */ -export function createMockProfile( - overrides: Partial = {}, -): ApiProfile { - return { - ...mockProfile, - ...overrides, - }; -} diff --git a/src/testing/test-utils.ts b/src/testing/test-utils.ts index fc8886c..1e5324f 100644 --- a/src/testing/test-utils.ts +++ b/src/testing/test-utils.ts @@ -25,30 +25,3 @@ export function cleanupTestEnvironment(): void { jest.clearAllMocks(); resetFetchMock(); } - -export function setupTestDOM(html: string): void { - document.body.innerHTML = html; -} - -export function setupCommonTestHooks(): { - getLoader: () => GitHubApiLoader; - getMockCache: () => jest.Mocked; -} { - let loader: GitHubApiLoader; - let mockCache: jest.Mocked; - - beforeEach(() => { - const env = setupTestEnvironment(); - loader = env.loader; - mockCache = env.mockCache; - }); - - afterEach(() => { - cleanupTestEnvironment(); - }); - - return { - getLoader: () => loader, - getMockCache: () => mockCache, - }; -}