From efeb58b443e36010a664640d2cf0fcf07219eae4 Mon Sep 17 00:00:00 2001 From: South Drifter Date: Sun, 23 Feb 2025 18:00:53 +0000 Subject: [PATCH 1/3] [refactor] simplify Type & Logic based on LinkeDOM [add] VS Code extensions [optimize] replace Tab with Space for Indent --- .editorconfig | 2 +- .npmrc | 1 + .prettierrc | 5 +- .vscode/extensions.json | 17 +++ package.json | 7 +- pnpm-lock.yaml | 126 ++++++++++++++++++++-- src/index.ts | 224 +++++++++++++++++++--------------------- src/types.ts | 38 +++---- tsconfig.json | 6 +- 9 files changed, 268 insertions(+), 158 deletions(-) create mode 100644 .npmrc create mode 100644 .vscode/extensions.json diff --git a/.editorconfig b/.editorconfig index a727df3..aca6ecd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ root = true [*] -indent_style = tab +indent_style = space end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..8638f02 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = false diff --git a/.prettierrc b/.prettierrc index 5c7b5d3..1bcb475 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,5 @@ { - "printWidth": 140, + "printWidth": 100, "singleQuote": true, - "semi": true, - "useTabs": true + "semi": true } diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4c35d65 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,17 @@ +{ + "recommendations": [ + "yzhang.markdown-all-in-one", + "redhat.vscode-yaml", + "akamud.vscode-caniuse", + "visualstudioexptteam.intellicode-api-usage-examples", + "pflannery.vscode-versionlens", + "christian-kohler.npm-intellisense", + "esbenp.prettier-vscode", + "rangav.vscode-thunder-client", + "cweijan.vscode-database-client2", + "eamodio.gitlens", + "github.vscode-pull-request-github", + "github.vscode-github-actions", + "github.copilot" + ] +} diff --git a/package.json b/package.json index 7db154d..e32c1d8 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,13 @@ "test": "vitest", "cf-typegen": "wrangler types" }, + "dependencies": { + "linkedom": "^0.18.9" + }, "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.6.4", + "@cloudflare/vitest-pool-workers": "^0.6.16", "@cloudflare/workers-types": "^4.20250214.0", - "typescript": "^5.5.2", + "typescript": "~5.7.3", "vitest": "~2.1.9", "wrangler": "^3.109.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fc7856..710ba3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,21 +1,25 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false importers: .: + dependencies: + linkedom: + specifier: ^0.18.9 + version: 0.18.9 devDependencies: '@cloudflare/vitest-pool-workers': - specifier: ^0.6.4 + specifier: ^0.6.16 version: 0.6.16(@cloudflare/workers-types@4.20250214.0)(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.9) '@cloudflare/workers-types': specifier: ^4.20250214.0 version: 4.20250214.0 typescript: - specifier: ^5.5.2 + specifier: ~5.7.3 version: 5.7.3 vitest: specifier: ~2.1.9 @@ -655,6 +659,9 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -691,6 +698,16 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} @@ -717,6 +734,27 @@ packages: devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -759,9 +797,18 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + linkedom@0.18.9: + resolution: {integrity: sha512-Pfvhwjs46nBrcQdauQjMXDJZqj6VwN7KStT84xQqmIgD9bPH6UVJ/ESW8y4VHVF2h7di0/P+f4Iln4U5emRcmg==} + loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -801,6 +848,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + ohash@1.1.4: resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} @@ -823,8 +873,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - postcss@8.5.2: - resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} printable-characters@1.0.42: @@ -914,6 +964,9 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + undici@5.28.5: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} @@ -1447,6 +1500,8 @@ snapshots: blake3-wasm@2.1.5: {} + boolbase@1.0.0: {} + cac@6.7.14: {} chai@5.2.0: @@ -1485,6 +1540,18 @@ snapshots: cookie@0.5.0: {} + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + cssom@0.5.0: {} + data-uri-to-buffer@2.0.2: {} debug@4.4.0: @@ -1500,6 +1567,28 @@ snapshots: devalue@4.3.3: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + entities@4.5.0: {} + + entities@6.0.0: {} + es-module-lexer@1.6.0: {} esbuild@0.17.19: @@ -1575,9 +1664,26 @@ snapshots: glob-to-regexp@0.4.1: {} + html-escaper@3.0.3: {} + + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.0 + is-arrayish@0.3.2: optional: true + linkedom@0.18.9: + dependencies: + css-select: 5.1.0 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 10.0.0 + uhyphen: 0.2.0 + loupe@3.1.3: {} magic-string@0.25.9: @@ -1637,6 +1743,10 @@ snapshots: nanoid@3.3.8: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + ohash@1.1.4: {} path-to-regexp@6.3.0: {} @@ -1655,7 +1765,7 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - postcss@8.5.2: + postcss@8.5.3: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -1772,6 +1882,8 @@ snapshots: ufo@1.5.4: {} + uhyphen@0.2.0: {} + undici@5.28.5: dependencies: '@fastify/busboy': 2.1.1 @@ -1805,7 +1917,7 @@ snapshots: vite@5.4.14: dependencies: esbuild: 0.21.5 - postcss: 8.5.2 + postcss: 8.5.3 rollup: 4.34.8 optionalDependencies: fsevents: 2.3.3 diff --git a/src/index.ts b/src/index.ts index ed078d4..af23027 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,126 +1,118 @@ -import { UsageResponse, AchievementsResponse } from './types'; +import { CacheStorage, Request as CF_Req, Response as CF_Res } from '@cloudflare/workers-types'; +import { parseHTML } from 'linkedom'; + +import { Achievement, AchievementsResponse, UsageResponse } from './types'; // 创建 CORS 头部 const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', }; // 创建 JSON 响应的辅助函数 -function createJsonResponse(data: any, status = 200, additionalHeaders = {}) { - return new Response(JSON.stringify(data, null, 2), { - status, - headers: { - 'Content-Type': 'application/json;charset=UTF-8', - ...corsHeaders, - ...additionalHeaders, - }, - }); -} +const createJsonResponse = (data: any, status = 200, additionalHeaders = {}) => + new Response(JSON.stringify(data, null, 2), { + status, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + ...corsHeaders, + ...additionalHeaders, + }, + }); // 创建错误响应的辅助函数 -function createErrorResponse(message: string, status = 500) { - return new Response(message, { - status, - headers: corsHeaders, - }); +const createErrorResponse = (message: string, status = 500) => + new Response(message, { status, headers: corsHeaders }); + +const usageOf = (origin: string): UsageResponse => ({ + description: 'GitHub Achievements API - 获取用户的 GitHub 成就信息', + author: { + name: 'Leo Wang', + github: 'https://github.com/wangrunlin', + }, + repository: 'https://github.com/wangrunlin/github-achievements-api', + usage: { + endpoint: `${origin}/`, + example: `${origin}/wangrunlin`, + }, + response: { + total: { + raw: '成就总数(不计算等级)', + weighted: '成就总数(计算等级)', + }, + achievements: [ + { + type: '成就类型', + tier: '成就等级(若无等级则为1)', + image: '成就图标 URL', + }, + ], + }, +}); + +class HTTPError extends Error { + constructor(message: string, public readonly response: Response) { + super(message); + } } -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - const url = new URL(request.url); - const username = url.pathname.split('/')[1]; - - if (!username) { - const usage: UsageResponse = { - description: 'GitHub Achievements API - 获取用户的 GitHub 成就信息', - author: { - name: 'Leo Wang', - github: 'https://github.com/wangrunlin', - }, - repository: 'https://github.com/wangrunlin/github-achievements-api', - usage: { - endpoint: `${url.origin}/`, - example: `${url.origin}/wangrunlin`, - }, - response: { - total: { - raw: '成就总数(不计算等级)', - weighted: '成就总数(计算等级)', - }, - achievements: [ - { - type: '成就类型', - tier: '成就等级(若无等级则为1)', - }, - ], - }, - }; - - return createJsonResponse(usage); - } - - try { - const cacheKey = `${url.origin}/cache/${username}`; - const cache = caches.default; - const skipCache = url.searchParams.has('nocache'); - let response = skipCache ? null : await cache.match(cacheKey); - - if (!response) { - const githubUrl = `https://github.com/${username}`; - const githubResponse = await fetch(githubUrl); - - if (!githubResponse.ok) { - return createErrorResponse(`Failed to fetch GitHub achievements: ${githubResponse.statusText}`, githubResponse.status); - } - - const html = await githubResponse.text(); - const achievementsSection = html.match(/
[\s\S]*?<\/div>/); - - if (!achievementsSection) { - return createJsonResponse({ total: 0, achievements: [] }); - } - - const achievements: { type: string; tier?: number; image?: string }[] = []; - const pattern = new RegExp( - `]*href="/${username}\\?achievement=([^&]+)[^>]*>\\s*]*>.*?(?:class="Label[^>]*achievement-tier-label[^>]*>x(\\d+))?(?:)?`, - 'gs' - ); - - let match; - while ((match = pattern.exec(achievementsSection[0])) !== null) { - const [, type, image, tier] = match; - achievements.push({ - type: type.trim(), - tier: tier ? parseInt(tier) : 1, - image - }); - } - - const rawTotal = achievements.length; - const weightedTotal = achievements.reduce((sum, { tier = 1 }) => sum + tier, 0); - - const result: AchievementsResponse = { - total: { - raw: rawTotal, - weighted: weightedTotal, - }, - achievements: achievements.sort((a, b) => (b.tier || 1) - (a.tier || 1)), - }; - - response = createJsonResponse(result, 200, { - 'Cache-Control': skipCache ? 'no-store' : 'public, max-age=3600', - }); - - if (!skipCache) { - ctx.waitUntil(cache.put(new Request(cacheKey), response.clone())); - } - } - - return response; - } catch (error: unknown) { - return createErrorResponse(`Error: ${error}`); - } - }, -} satisfies ExportedHandler; +const loadAchievements = async (username: string) => { + const githubResponse = await globalThis.fetch(`https://github.com/${username}?tab=achievements`); + + if (!githubResponse.ok) throw new HTTPError(githubResponse.statusText, githubResponse); + + const { document } = parseHTML(await githubResponse.text()); + + const achievements = [...document.querySelectorAll('.achievement-card')].map((card) => { + const type = card.querySelector('h3')!.textContent!.trim(), + tier = card.querySelector('.achievement-tier-label')?.textContent?.trim().slice(1) || '1', + image = card.querySelector('.achievement-badge-card')!.src; + + return { type, tier: +tier, image } as Achievement; + }); + + return { + total: { + raw: achievements.length, + weighted: achievements.reduce((sum, { tier }) => sum + tier, 0), + }, + achievements: achievements.sort((a, b) => b.tier - a.tier), + } as AchievementsResponse; +}; + +const fetch: ExportedHandler['fetch'] = async (request, env, ctx) => { + const { origin, pathname, searchParams } = new URL(request.url); + const [, username] = pathname.split('/'); + + if (!username) return createJsonResponse(usageOf(origin)); + + try { + const cacheKey = `${origin}/cache/${username}`; + const cache = (caches as unknown as CacheStorage).default; + const skipCache = searchParams.has('nocache'); + const cacheResponse = !skipCache && (await cache.match(cacheKey)); + + if (cacheResponse) return cacheResponse as unknown as Response; + + const result = await loadAchievements(username); + + const response = createJsonResponse(result, 200, { + 'Cache-Control': skipCache ? 'no-store' : 'public, max-age=3600', + }); + if (!skipCache) + ctx.waitUntil( + cache.put(new Request(cacheKey) as unknown as CF_Req, response.clone() as unknown as CF_Res) + ); + return response; + } catch (error: unknown) { + return error instanceof HTTPError + ? createErrorResponse( + `Failed to fetch GitHub achievements: ${error.response.statusText}`, + error.response.status + ) + : createErrorResponse(`Error: ${error}`); + } +}; + +export default { fetch } satisfies ExportedHandler; diff --git a/src/types.ts b/src/types.ts index 6ebf718..0343109 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,33 +1,21 @@ export interface UsageResponse { description: string; - author: { - name: string; - github: string; - }; + author: Record<'name' | 'github', string>; repository: string; - usage: { - endpoint: string; - example: string; - }; + usage: Record<'endpoint' | 'example', string>; response: { - total: { - raw: string; - weighted: string; - }; - achievements: { - type: string; - tier: string; - }[]; + total: Record; + achievements: Record[]; }; } +export interface Achievement { + type: string; + image: string; + tier: number; +} + export interface AchievementsResponse { - total: { - raw: number; - weighted: number; - }; - achievements: Array<{ - type: string; - tier?: number; - }>; -} \ No newline at end of file + total: Record<'raw' | 'weighted', number>; + achievements: Achievement[]; +} diff --git a/tsconfig.json b/tsconfig.json index 33bdb79..ec6e874 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2021", /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "lib": ["es2021"], + "lib": ["es2021", "DOM", "DOM.Iterable"], /* Specify what JSX code is generated. */ "jsx": "react-jsx", @@ -14,9 +14,7 @@ /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "Bundler", /* Specify type package names to be included without being referenced in a source file. */ - "types": [ - "@cloudflare/workers-types/2023-07-01" - ], + "types": ["@cloudflare/workers-types/2023-07-01"], /* Enable importing .json files */ "resolveJsonModule": true, From a877535893278e906d09ed4a380bd4e1c7833cdc Mon Sep 17 00:00:00 2001 From: TechQuery Date: Mon, 24 Feb 2025 15:22:56 +0800 Subject: [PATCH 2/3] [fix] Unit tests [optimize] run Test in Git hook --- .husky/pre-commit | 1 + .prettierrc | 5 - package.json | 52 +++--- pnpm-lock.yaml | 426 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 18 +- test/index.spec.ts | 162 +++++++++-------- test/mock.ts | 405 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 957 insertions(+), 112 deletions(-) create mode 100644 .husky/pre-commit delete mode 100644 .prettierrc create mode 100644 test/mock.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..72c4429 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 1bcb475..0000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "printWidth": 100, - "singleQuote": true, - "semi": true -} diff --git a/package.json b/package.json index e32c1d8..94afeda 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,34 @@ { - "name": "github-achievements-api", - "version": "0.1.0", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "start": "wrangler dev", - "test": "vitest", - "cf-typegen": "wrangler types" - }, - "dependencies": { - "linkedom": "^0.18.9" - }, - "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.6.16", - "@cloudflare/workers-types": "^4.20250214.0", - "typescript": "~5.7.3", - "vitest": "~2.1.9", - "wrangler": "^3.109.2" - } + "name": "github-achievements-api", + "version": "0.1.0", + "private": true, + "scripts": { + "prepare": "husky", + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "test": "lint-staged && vitest", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "linkedom": "^0.18.9" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.6.16", + "@cloudflare/workers-types": "^4.20250214.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "prettier": "^3.5.2", + "typescript": "~5.7.3", + "vitest": "~2.1.9", + "wrangler": "^3.109.2" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "semi": true + }, + "lint-staged": { + "*.{md,json,jsonc,yml,js,mjs,ts}": "prettier --write" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 710ba3d..b5416b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,15 @@ importers: '@cloudflare/workers-types': specifier: ^4.20250214.0 version: 4.20250214.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^15.4.3 + version: 15.4.3 + prettier: + specifier: ^3.5.2 + version: 3.5.2 typescript: specifier: ~5.7.3 version: 5.7.3 @@ -646,6 +655,18 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} @@ -662,6 +683,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -670,6 +695,10 @@ packages: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -677,6 +706,14 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -691,6 +728,13 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -698,6 +742,10 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} @@ -747,6 +795,9 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -755,6 +806,10 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -778,6 +833,13 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -786,14 +848,26 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -803,12 +877,57 @@ packages: htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + linkedom@0.18.9: resolution: {integrity: sha512-Pfvhwjs46nBrcQdauQjMXDJZqj6VwN7KStT84xQqmIgD9bPH6UVJ/ESW8y4VHVF2h7di0/P+f4Iln4U5emRcmg==} + lint-staged@15.4.3: + resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -818,11 +937,26 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + miniflare@3.20250204.1: resolution: {integrity: sha512-B4PQi/Ai4d0ZTWahQwsFe5WAfr1j8ISMYxJZTc56g2/btgbX+Go099LmojAZY/fMRLhIYsglcStW8SeW3f/afA==} engines: {node: '>=16.13'} @@ -848,12 +982,32 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} ohash@1.1.4: resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -870,6 +1024,15 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -877,9 +1040,21 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + prettier@3.5.2: + resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + engines: {node: '>=14'} + hasBin: true + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -904,12 +1079,32 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -935,6 +1130,22 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -953,6 +1164,10 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1035,6 +1250,11 @@ packages: jsdom: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1070,6 +1290,10 @@ packages: '@cloudflare/workers-types': optional: true + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -1082,6 +1306,11 @@ packages: utf-8-validate: optional: true + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + youch@3.2.3: resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} @@ -1490,6 +1719,14 @@ snapshots: acorn@8.14.0: {} + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.1.0: {} + + ansi-styles@6.2.1: {} + as-table@1.0.55: dependencies: printable-characters: 1.0.42 @@ -1502,6 +1739,10 @@ snapshots: boolbase@1.0.0: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + cac@6.7.14: {} chai@5.2.0: @@ -1512,10 +1753,21 @@ snapshots: loupe: 3.1.3 pathval: 2.0.0 + chalk@5.4.1: {} + check-error@2.1.1: {} cjs-module-lexer@1.4.3: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -1536,10 +1788,20 @@ snapshots: color-string: 1.9.1 optional: true + colorette@2.0.20: {} + + commander@13.1.0: {} + confbox@0.1.8: {} cookie@0.5.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css-select@5.1.0: dependencies: boolbase: 1.0.0 @@ -1585,10 +1847,14 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + emoji-regex@10.4.0: {} + entities@4.5.0: {} entities@6.0.0: {} + environment@1.1.0: {} + es-module-lexer@1.6.0: {} esbuild@0.17.19: @@ -1650,18 +1916,40 @@ snapshots: dependencies: '@types/estree': 1.0.6 + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + exit-hook@2.2.1: {} expect-type@1.1.0: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + fsevents@2.3.3: optional: true + get-east-asian-width@1.3.0: {} + get-source@2.0.12: dependencies: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + get-stream@8.0.1: {} + glob-to-regexp@0.4.1: {} html-escaper@3.0.3: {} @@ -1673,9 +1961,27 @@ snapshots: domutils: 3.2.2 entities: 6.0.0 + human-signals@5.0.0: {} + + husky@9.1.7: {} + is-arrayish@0.3.2: optional: true + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-number@7.0.0: {} + + is-stream@3.0.0: {} + + isexe@2.0.0: {} + + lilconfig@3.1.3: {} + linkedom@0.18.9: dependencies: css-select: 5.1.0 @@ -1684,6 +1990,38 @@ snapshots: htmlparser2: 10.0.0 uhyphen: 0.2.0 + lint-staged@15.4.3: + dependencies: + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.0 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.7.0 + transitivePeerDependencies: + - supports-color + + listr2@8.2.5: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + loupe@3.1.3: {} magic-string@0.25.9: @@ -1694,8 +2032,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + merge-stream@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime@3.0.0: {} + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + miniflare@3.20250204.1: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -1743,12 +2092,28 @@ snapshots: nanoid@3.3.8: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 ohash@1.1.4: {} + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + path-key@3.1.1: {} + + path-key@4.0.0: {} + path-to-regexp@6.3.0: {} pathe@1.1.2: {} @@ -1759,6 +2124,10 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: {} + + pidtree@0.6.0: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -1771,8 +2140,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@3.5.2: {} + printable-characters@1.0.42: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + rollup-plugin-inject@3.0.2: dependencies: estree-walker: 0.6.1 @@ -1841,13 +2219,31 @@ snapshots: '@img/sharp-win32-x64': 0.33.5 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 optional: true + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -1865,6 +2261,20 @@ snapshots: stoppable@1.1.0: {} + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-final-newline@3.0.0: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1875,6 +2285,10 @@ snapshots: tinyspy@3.0.2: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tslib@2.8.1: optional: true @@ -1955,6 +2369,10 @@ snapshots: - supports-color - terser + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -2014,8 +2432,16 @@ snapshots: - bufferutil - utf-8-validate + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + ws@8.18.0: {} + yaml@2.7.0: {} + youch@3.2.3: dependencies: cookie: 0.5.0 diff --git a/src/index.ts b/src/index.ts index af23027..3986160 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,10 @@ const usageOf = (origin: string): UsageResponse => ({ }); class HTTPError extends Error { - constructor(message: string, public readonly response: Response) { + constructor( + message: string, + public readonly response: Response, + ) { super(message); } } @@ -64,8 +67,10 @@ const loadAchievements = async (username: string) => { const { document } = parseHTML(await githubResponse.text()); - const achievements = [...document.querySelectorAll('.achievement-card')].map((card) => { - const type = card.querySelector('h3')!.textContent!.trim(), + const achievements = [ + ...document.querySelectorAll('.js-achievement-card-details'), + ].map((card) => { + const type = card.dataset.achievementSlug, tier = card.querySelector('.achievement-tier-label')?.textContent?.trim().slice(1) || '1', image = card.querySelector('.achievement-badge-card')!.src; @@ -102,14 +107,17 @@ const fetch: ExportedHandler['fetch'] = async (request, env, ctx) => { }); if (!skipCache) ctx.waitUntil( - cache.put(new Request(cacheKey) as unknown as CF_Req, response.clone() as unknown as CF_Res) + cache.put( + new Request(cacheKey) as unknown as CF_Req, + response.clone() as unknown as CF_Res, + ), ); return response; } catch (error: unknown) { return error instanceof HTTPError ? createErrorResponse( `Failed to fetch GitHub achievements: ${error.response.statusText}`, - error.response.status + error.response.status, ) : createErrorResponse(`Error: ${error}`); } diff --git a/test/index.spec.ts b/test/index.spec.ts index c1a9c79..21514fe 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -4,100 +4,98 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import worker from '../src/index'; import { UsageResponse, AchievementsResponse } from '../src/types'; +import { mockGitHubResponse } from './mock'; // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. const IncomingRequest = Request; -// Mock GitHub 响应 -const mockGitHubResponse = ` - -`; - // Mock fetch 函数 const originalFetch = globalThis.fetch; + beforeEach(() => { - globalThis.fetch = vi.fn().mockImplementation((url: string) => { - if (url.includes('non-existent-user')) { - return Promise.resolve(new Response('Not Found', { status: 404 })); - } - return Promise.resolve(new Response(mockGitHubResponse, { status: 200 })); - }); + globalThis.fetch = vi + .fn() + .mockImplementation(async (url: string) => + url.includes('non-existent-user') + ? new Response('Not Found', { status: 404 }) + : new Response(mockGitHubResponse, { status: 200 }), + ); }); afterEach(() => { - globalThis.fetch = originalFetch; + globalThis.fetch = originalFetch; }); describe('GitHub Achievements API', () => { - // 测试 API 使用说明 - it('responds with usage information when no username provided', async () => { - const request = new IncomingRequest('http://example.com/'); - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - await waitOnExecutionContext(ctx); - - const data = (await response.json()) as UsageResponse; - expect(response.headers.get('Content-Type')).toBe('application/json;charset=UTF-8'); - expect(data).toHaveProperty('description'); - expect(data).toHaveProperty('usage'); - expect(data).toHaveProperty('response'); - }); - - // 测试获取用户成就 - it('fetches achievements for a valid username', async () => { - const username = 'wangrunlin'; - const request = new IncomingRequest(`http://example.com/${username}`); - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - await waitOnExecutionContext(ctx); - - const data = (await response.json()) as AchievementsResponse; - expect(response.headers.get('Content-Type')).toBe('application/json;charset=UTF-8'); - expect(data).toHaveProperty('total'); - expect(data.total.raw).toBe(5); // 5个成就 - expect(data.total.weighted).toBe(8); // 1 + 3 + 1 + 1 + 2 = 8 - expect(data.achievements).toHaveLength(5); - - // 验证每个成就的存在性和等级 - const achievementsMap = new Map(data.achievements.map((a) => [a.type, a.tier])); - - expect(achievementsMap.get('starstruck')).toBe(1); - expect(achievementsMap.get('pair-extraordinaire')).toBe(3); - expect(achievementsMap.get('yolo')).toBe(1); - expect(achievementsMap.get('quickdraw')).toBe(1); - expect(achievementsMap.get('pull-shark')).toBe(2); - }); - - // 测试无效用户名 - it('handles non-existent username gracefully', async () => { - const request = new IncomingRequest('http://example.com/non-existent-user-123456'); - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - await waitOnExecutionContext(ctx); - - expect(response.status).toBe(404); - }); - - // 测试缓存功能 - it('uses cache for repeated requests', async () => { - const username = 'wangrunlin'; - const request = new IncomingRequest(`http://example.com/${username}`); - const ctx = createExecutionContext(); - - // 第一次请求 - const response1 = await worker.fetch(request, env, ctx); - await waitOnExecutionContext(ctx); - const data1 = (await response1.json()) as AchievementsResponse; - - // 第二次请求 - const response2 = await worker.fetch(request, env, ctx); - await waitOnExecutionContext(ctx); - const data2 = (await response2.json()) as AchievementsResponse; - - // 验证两次请求返回相同的数据 - expect(JSON.stringify(data1)).toBe(JSON.stringify(data2)); - // 验证 fetch 只被调用一次(第二次使用了缓存) - expect(fetch).toHaveBeenCalledTimes(1); - }); + // 测试 API 使用说明 + it('responds with usage information when no username provided', async () => { + const request = new IncomingRequest('http://example.com/'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + const data = (await response.json()) as UsageResponse; + expect(response.headers.get('Content-Type')).toBe('application/json;charset=UTF-8'); + expect(data).toHaveProperty('description'); + expect(data).toHaveProperty('usage'); + expect(data).toHaveProperty('response'); + }); + + // 测试获取用户成就 + it('fetches achievements for a valid username', async () => { + const username = 'wangrunlin'; + const request = new IncomingRequest(`http://example.com/${username}`); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + const data = (await response.json()) as AchievementsResponse; + expect(response.headers.get('Content-Type')).toBe('application/json;charset=UTF-8'); + expect(data).toHaveProperty('total'); + expect(data.total.raw).toBe(5); // 5个成就 + expect(data.total.weighted).toBe(8); // 1 + 3 + 1 + 1 + 2 = 8 + expect(data.achievements).toHaveLength(5); + + // 验证每个成就的存在性和等级 + const achievementsMap = new Map(data.achievements.map((a) => [a.type, a.tier])); + + expect(achievementsMap.get('starstruck')).toBe(1); + expect(achievementsMap.get('pair-extraordinaire')).toBe(3); + expect(achievementsMap.get('yolo')).toBe(1); + expect(achievementsMap.get('quickdraw')).toBe(1); + expect(achievementsMap.get('pull-shark')).toBe(2); + }); + + // 测试无效用户名 + it('handles non-existent username gracefully', async () => { + const request = new IncomingRequest('http://example.com/non-existent-user-123456'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(404); + }); + + // 测试缓存功能 + it('uses cache for repeated requests', async () => { + const username = 'wangrunlin'; + const request = new IncomingRequest(`http://example.com/${username}`); + const ctx = createExecutionContext(); + + // 第一次请求 + const response1 = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + const data1 = (await response1.json()) as AchievementsResponse; + + // 第二次请求 + const response2 = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + const data2 = (await response2.json()) as AchievementsResponse; + + // 验证两次请求返回相同的数据 + expect(JSON.stringify(data1)).toBe(JSON.stringify(data2)); + // 验证 fetch 只被调用一次(第二次使用了缓存) + expect(fetch).toHaveBeenCalledTimes(1); + }); }); diff --git a/test/mock.ts b/test/mock.ts new file mode 100644 index 0000000..618bf00 --- /dev/null +++ b/test/mock.ts @@ -0,0 +1,405 @@ +// Mock GitHub 响应 +export const mockGitHubResponse = `
+

Earned achievements

+ +
+ + Achievement: Starstruck + +
+

Starstruck

+
+
+ + +
+
+
+ + + Loading + +
+
+
+
+
+ +
+ + Achievement: Pair Extraordinaire + +
+

Pair Extraordinaire

+ x3 +
+
+ + +
+
+
+ + + Loading + +
+
+
+
+
+ +
+ + Achievement: YOLO + +
+

YOLO

+
+
+ + +
+
+
+ + + Loading + +
+
+
+
+
+ +
+ + Achievement: Quickdraw + +
+

Quickdraw

+
+
+ + +
+
+
+ + + Loading + +
+
+
+
+
+ +
+ + Achievement: Pull Shark + +
+

Pull Shark

+ x2 +
+
+ + +
+
+
+ + + Loading + +
+
+
+
+
+
+`; From 75b38c9a8fe77328afcce5561869978dfedd08a8 Mon Sep 17 00:00:00 2001 From: TechQuery Date: Mon, 24 Feb 2025 19:42:33 +0800 Subject: [PATCH 3/3] [optimize] simplify Test Action steps [optimize] format Read Me documents --- .github/workflows/test.yml | 35 +++------ README.md | 146 ++++++++++++++++++++++--------------- README_zh.md | 146 ++++++++++++++++++++++--------------- package.json | 2 +- 4 files changed, 191 insertions(+), 138 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6136aa..9fb6d25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,11 @@ name: Test - on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] + branches: + - main jobs: test: @@ -12,34 +13,22 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: + - 20.x + - 22.x steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v3 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + cache: pnpm - name: Install dependencies run: pnpm install diff --git a/README.md b/README.md index 9fbc7d7..69ea880 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,31 @@ Pull Shark Achievement

-[![License](https://img.shields.io/github/license/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE) -[![GitHub package.json version](https://img.shields.io/github/package-json/v/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/blob/main/package.json) -[![GitHub last commit](https://img.shields.io/github/last-commit/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/commits) -[![Test Status](https://img.shields.io/github/actions/workflow/status/wangrunlin/github-achievements-api/test.yml?label=test)](https://github.com/wangrunlin/github-achievements-api/actions) -[![Node Version](https://img.shields.io/node/v/github-achievements-api)](https://nodejs.org) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.5.2-blue.svg)](https://www.typescriptlang.org/) -[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com) -[![GitHub stars](https://img.shields.io/github/stars/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/network) -[![GitHub issues](https://img.shields.io/github/issues/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/issues) -[![Visitors](https://visitor-badge.laobi.icu/badge?page_id=wangrunlin.github-achievements-api)](https://github.com/wangrunlin/github-achievements-api) -[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-orange)](https://ko-fi.com/wangrunlin) - -English | [简体中文](README_zh.md) +[![License](https://img.shields.io/github/license/wangrunlin/github-achievements-api)][1] +[![GitHub package.json version](https://img.shields.io/github/package-json/v/wangrunlin/github-achievements-api)][2] +[![GitHub last commit](https://img.shields.io/github/last-commit/wangrunlin/github-achievements-api)][3] +[![Test Status](https://img.shields.io/github/actions/workflow/status/wangrunlin/github-achievements-api/test.yml?label=test)][4] +[![Node Version](https://img.shields.io/node/v/github-achievements-api)][5] +[![TypeScript](https://img.shields.io/badge/TypeScript-5.5.2-blue.svg)][6] +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)][7] +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)][8] +[![GitHub stars](https://img.shields.io/github/stars/wangrunlin/github-achievements-api)][9] +[![GitHub forks](https://img.shields.io/github/forks/wangrunlin/github-achievements-api)][10] +[![GitHub issues](https://img.shields.io/github/issues/wangrunlin/github-achievements-api)][11] +[![Visitors](https://visitor-badge.laobi.icu/badge?page_id=wangrunlin.github-achievements-api)][12] +[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-orange)][13] + +English | [简体中文][14] A simple API service for retrieving GitHub user achievements information. Built with Cloudflare Workers. ## Live Demo -- [https://github-achievements-api.wangrunlin.workers.dev](https://github-achievements-api.wangrunlin.workers.dev) +- [https://github-achievements-api.wangrunlin.workers.dev][15] ## Deploy to Cloudflare Workers -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api) +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)][16] ## Features @@ -56,25 +56,28 @@ GET https://.workers.dev/wangrunlin ```json { - "total": { - "raw": 5, // Raw achievement count (without tiers) - "weighted": 8 // Weighted achievement count (with tiers) - }, - "achievements": [ - { - "type": "pair-extraordinaire", - "tier": 3 - }, - { - "type": "pull-shark", - "tier": 2 - }, - { - "type": "quickdraw", - "tier": 1 - } - // ... - ] + "total": { + "raw": 5, // Raw achievement count (without tiers) + "weighted": 8 // Weighted achievement count (with tiers) + }, + "achievements": [ + { + "type": "pair-extraordinaire", + "tier": 3, + "image": "https://some.cdn.com/path/to/pair-extraordinaire.png" + }, + { + "type": "pull-shark", + "tier": 2, + "image": "https://some.cdn.com/path/to/pull-shark.png" + }, + { + "type": "quickdraw", + "tier": 1, + "image": "https://some.cdn.com/path/to/quickdraw.png" + } + // ... + ] } ``` @@ -102,7 +105,7 @@ Example error response: ```json { - "error": "Failed to fetch GitHub achievements: Not Found" + "error": "Failed to fetch GitHub achievements: Not Found" } ``` @@ -110,7 +113,7 @@ Example error response: - [ ] Add support for achievement descriptions - [ ] Add support for achievement dates -- [ ] Add support for achievement images +- [x] Add support for achievement images - [ ] Add API key authentication - [ ] Add more detailed statistics - [ ] Add support for organization achievements @@ -120,13 +123,13 @@ Example error response: Support this project by becoming a sponsor. Your logo will show up here with a link to your website. -[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/wangrunlin) +[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)][17] -[Other sponsorship options](https://alin.run/sponsor) +[Other sponsorship options][18] ## Who's using GitHub Achievements API? -Are you using this API? [Let us know](https://github.com/wangrunlin/github-achievements-api/issues/new) and we'll add your logo here! +Are you using this API? [Let us know][19] and we'll add your logo here! ## Local Development @@ -155,13 +158,13 @@ pnpm test ## Deployment -1. Login to Cloudflare +1. Login to Cloudflare ```bash pnpm dlx wrangler login ``` -2. Deploy Worker +2. Deploy Worker ```bash pnpm deploy @@ -182,21 +185,21 @@ MIT Issues and Pull Requests are welcome! -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request ## Author -[Leo Wang](https://github.com/wangrunlin) +[Leo Wang][20] ## Available Achievements Here are all the achievements currently available on GitHub: -[View more details about GitHub Achievements](https://github.com/drknzz/GitHub-Achievements) +[View more details about GitHub Achievements][21] | Achievement | Name | Description | Max Tiers | | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------- | --------- | @@ -214,10 +217,39 @@ Here are all the achievements currently available on GitHub: Thanks to these awesome projects and resources: -- [GitHub](https://github.com) - For providing the achievement system -- [Cloudflare Workers](https://workers.cloudflare.com) - For the serverless platform -- [GitHub Achievements List](https://github.com/drknzz/GitHub-Achievements) - For the comprehensive achievements documentation -- [TypeScript](https://www.typescriptlang.org) - For the typed JavaScript -- [Vitest](https://vitest.dev) - For the testing framework -- [Wrangler](https://developers.cloudflare.com/workers/wrangler/) - For the development & deployment tool -- [pnpm](https://pnpm.io) - For the fast package manager +- [GitHub][22] - For providing the achievement system +- [Cloudflare Workers][23] - For the serverless platform +- [GitHub Achievements List][24] - For the comprehensive achievements documentation +- [TypeScript][25] - For the typed JavaScript +- [Vitest][26] - For the testing framework +- [Wrangler][27] - For the development & deployment tool +- [pnpm][28] - For the fast package manager + +[1]: https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE +[2]: https://github.com/wangrunlin/github-achievements-api/blob/main/package.json +[3]: https://github.com/wangrunlin/github-achievements-api/commits +[4]: https://github.com/wangrunlin/github-achievements-api/actions +[5]: https://nodejs.org +[6]: https://www.typescriptlang.org/ +[7]: https://github.com/prettier/prettier +[8]: https://makeapullrequest.com +[9]: https://github.com/wangrunlin/github-achievements-api/stargazers +[10]: https://github.com/wangrunlin/github-achievements-api/network +[11]: https://github.com/wangrunlin/github-achievements-api/issues +[12]: https://github.com/wangrunlin/github-achievements-api +[13]: https://ko-fi.com/wangrunlin +[14]: README_zh.md +[15]: https://github-achievements-api.wangrunlin.workers.dev +[16]: https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api +[17]: https://ko-fi.com/wangrunlin +[18]: https://alin.run/sponsor +[19]: https://github.com/wangrunlin/github-achievements-api/issues/new +[20]: https://github.com/wangrunlin +[21]: https://github.com/drknzz/GitHub-Achievements +[22]: https://github.com +[23]: https://workers.cloudflare.com +[24]: https://github.com/drknzz/GitHub-Achievements +[25]: https://www.typescriptlang.org +[26]: https://vitest.dev +[27]: https://developers.cloudflare.com/workers/wrangler/ +[28]: https://pnpm.io diff --git a/README_zh.md b/README_zh.md index b865465..1a4bc06 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,31 +4,31 @@ Pull Shark 成就

-[![License](https://img.shields.io/github/license/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE) -[![GitHub package.json version](https://img.shields.io/github/package-json/v/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/blob/main/package.json) -[![GitHub last commit](https://img.shields.io/github/last-commit/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/commits) -[![Test Status](https://img.shields.io/github/actions/workflow/status/wangrunlin/github-achievements-api/test.yml?label=test)](https://github.com/wangrunlin/github-achievements-api/actions) -[![Node Version](https://img.shields.io/node/v/github-achievements-api)](https://nodejs.org) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.5.2-blue.svg)](https://www.typescriptlang.org/) -[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com) -[![GitHub stars](https://img.shields.io/github/stars/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/network) -[![GitHub issues](https://img.shields.io/github/issues/wangrunlin/github-achievements-api)](https://github.com/wangrunlin/github-achievements-api/issues) -[![Visitors](https://visitor-badge.laobi.icu/badge?page_id=wangrunlin.github-achievements-api)](https://github.com/wangrunlin/github-achievements-api) -[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-orange)](https://ko-fi.com/wangrunlin) - -[English](README.md) | 简体中文 +[![License](https://img.shields.io/github/license/wangrunlin/github-achievements-api)][1] +[![GitHub package.json version](https://img.shields.io/github/package-json/v/wangrunlin/github-achievements-api)][2] +[![GitHub last commit](https://img.shields.io/github/last-commit/wangrunlin/github-achievements-api)][3] +[![Test Status](https://img.shields.io/github/actions/workflow/status/wangrunlin/github-achievements-api/test.yml?label=test)][4] +[![Node Version](https://img.shields.io/node/v/github-achievements-api)][5] +[![TypeScript](https://img.shields.io/badge/TypeScript-5.5.2-blue.svg)][6] +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)][7] +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)][8] +[![GitHub stars](https://img.shields.io/github/stars/wangrunlin/github-achievements-api)][9] +[![GitHub forks](https://img.shields.io/github/forks/wangrunlin/github-achievements-api)][10] +[![GitHub issues](https://img.shields.io/github/issues/wangrunlin/github-achievements-api)][11] +[![Visitors](https://visitor-badge.laobi.icu/badge?page_id=wangrunlin.github-achievements-api)][12] +[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-orange)][13] + +[English][14] | 简体中文 一个简单的 API 服务,用于获取 GitHub 用户的成就信息。基于 Cloudflare Workers 构建。 ## 在线使用 -- [https://github-achievements-api.wangrunlin.workers.dev](https://github-achievements-api.wangrunlin.workers.dev) +- [https://github-achievements-api.wangrunlin.workers.dev][15] ## 部署到 Cloudflare Workers -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api) +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)][16] ## 功能特点 @@ -56,25 +56,28 @@ GET https://.workers.dev/wangrunlin ```json { - "total": { - "raw": 5, // 原始成就数量(不计算等级) - "weighted": 8 // 加权成就数量(计算等级) - }, - "achievements": [ - { - "type": "pair-extraordinaire", - "tier": 3 - }, - { - "type": "pull-shark", - "tier": 2 - }, - { - "type": "quickdraw", - "tier": 1 - } - // ... - ] + "total": { + "raw": 5, // 原始成就数量(不计算等级) + "weighted": 8 // 加权成就数量(计算等级) + }, + "achievements": [ + { + "type": "pair-extraordinaire", + "tier": 3, + "image": "https://some.cdn.com/path/to/pair-extraordinaire.png" + }, + { + "type": "pull-shark", + "tier": 2, + "image": "https://some.cdn.com/path/to/pull-shark.png" + }, + { + "type": "quickdraw", + "tier": 1, + "image": "https://some.cdn.com/path/to/quickdraw.png" + } + // ... + ] } ``` @@ -102,7 +105,7 @@ GET https://.workers.dev/wangrunlin ```json { - "error": "Failed to fetch GitHub achievements: Not Found" + "error": "Failed to fetch GitHub achievements: Not Found" } ``` @@ -110,7 +113,7 @@ GET https://.workers.dev/wangrunlin - [ ] 添加成就描述支持 - [ ] 添加成就获得日期支持 -- [ ] 添加成就图片支持 +- [x] 添加成就图片支持 - [ ] 添加 API 密钥认证 - [ ] 添加更详细的统计信息 - [ ] 添加组织成就支持 @@ -120,13 +123,13 @@ GET https://.workers.dev/wangrunlin 成为赞助商来支持这个项目。您的 logo 将会出现在这里并链接到您的网站。 -[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/wangrunlin) +[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)][17] -[其他赞助方式](https://alin.run/sponsor) +[其他赞助方式][18] ## 谁在使用 GitHub Achievements API? -您正在使用这个 API 吗?[告诉我们](https://github.com/wangrunlin/github-achievements-api/issues/new),我们会在这里添加您的 logo! +您正在使用这个 API 吗?[告诉我们][19],我们会在这里添加您的 logo! ## 本地开发 @@ -155,13 +158,13 @@ pnpm test ## 部署 -1. 登录到 Cloudflare +1. 登录到 Cloudflare ```bash pnpm dlx wrangler login ``` -2. 部署 Worker +2. 部署 Worker ```bash pnpm deploy @@ -182,21 +185,21 @@ MIT 欢迎提交 Issue 和 Pull Request! -1. Fork 本仓库 -2. 创建你的特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交你的更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启一个 Pull Request +1. Fork 本仓库 +2. 创建你的特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交你的更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启一个 Pull Request ## 作者 -[Leo Wang](https://github.com/wangrunlin) +[Leo Wang][20] ## 可获得的成就 以下是目前 GitHub 上可获得的所有成就: -[查看更多 GitHub 成就相关信息](https://github.com/drknzz/GitHub-Achievements) +[查看更多 GitHub 成就相关信息][21] | 成就图标 | 名称 | 描述 | 最高等级 | | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------- | -------- | @@ -214,10 +217,39 @@ MIT 感谢这些优秀的项目和资源: -- [GitHub](https://github.com) - 提供成就系统 -- [Cloudflare Workers](https://workers.cloudflare.com) - 提供无服务器平台 -- [GitHub Achievements List](https://github.com/drknzz/GitHub-Achievements) - 提供完整的成就文档 -- [TypeScript](https://www.typescriptlang.org) - 提供类型化的 JavaScript -- [Vitest](https://vitest.dev) - 提供测试框架 -- [Wrangler](https://developers.cloudflare.com/workers/wrangler/) - 提供开发和部署工具 -- [pnpm](https://pnpm.io) - 提供快速的包管理器 +- [GitHub][22] - 提供成就系统 +- [Cloudflare Workers][23] - 提供无服务器平台 +- [GitHub Achievements List][24] - 提供完整的成就文档 +- [TypeScript][25] - 提供类型化的 JavaScript +- [Vitest][26] - 提供测试框架 +- [Wrangler][27] - 提供开发和部署工具 +- [pnpm][28] - 提供快速的包管理器 + +[1]: https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE +[2]: https://github.com/wangrunlin/github-achievements-api/blob/main/package.json +[3]: https://github.com/wangrunlin/github-achievements-api/commits +[4]: https://github.com/wangrunlin/github-achievements-api/actions +[5]: https://nodejs.org +[6]: https://www.typescriptlang.org/ +[7]: https://github.com/prettier/prettier +[8]: https://makeapullrequest.com +[9]: https://github.com/wangrunlin/github-achievements-api/stargazers +[10]: https://github.com/wangrunlin/github-achievements-api/network +[11]: https://github.com/wangrunlin/github-achievements-api/issues +[12]: https://github.com/wangrunlin/github-achievements-api +[13]: https://ko-fi.com/wangrunlin +[14]: README.md +[15]: https://github-achievements-api.wangrunlin.workers.dev +[16]: https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api +[17]: https://ko-fi.com/wangrunlin +[18]: https://alin.run/sponsor +[19]: https://github.com/wangrunlin/github-achievements-api/issues/new +[20]: https://github.com/wangrunlin +[21]: https://github.com/drknzz/GitHub-Achievements +[22]: https://github.com +[23]: https://workers.cloudflare.com +[24]: https://github.com/drknzz/GitHub-Achievements +[25]: https://www.typescriptlang.org +[26]: https://vitest.dev +[27]: https://developers.cloudflare.com/workers/wrangler/ +[28]: https://pnpm.io diff --git a/package.json b/package.json index 94afeda..e7f8b1d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "deploy": "wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", - "test": "lint-staged && vitest", + "test": "lint-staged && vitest --run", "cf-typegen": "wrangler types" }, "dependencies": {