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/.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/.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/.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
deleted file mode 100644
index 5c7b5d3..0000000
--- a/.prettierrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "printWidth": 140,
- "singleQuote": true,
- "semi": true,
- "useTabs": 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/README.md b/README.md
index 9fbc7d7..69ea880 100644
--- a/README.md
+++ b/README.md
@@ -4,31 +4,31 @@
-[](https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE)
-[](https://github.com/wangrunlin/github-achievements-api/blob/main/package.json)
-[](https://github.com/wangrunlin/github-achievements-api/commits)
-[](https://github.com/wangrunlin/github-achievements-api/actions)
-[](https://nodejs.org)
-[](https://www.typescriptlang.org/)
-[](https://github.com/prettier/prettier)
-[](https://makeapullrequest.com)
-[](https://github.com/wangrunlin/github-achievements-api/stargazers)
-[](https://github.com/wangrunlin/github-achievements-api/network)
-[](https://github.com/wangrunlin/github-achievements-api/issues)
-[](https://github.com/wangrunlin/github-achievements-api)
-[](https://ko-fi.com/wangrunlin)
-
-English | [简体中文](README_zh.md)
+[][1]
+[][2]
+[][3]
+[][4]
+[][5]
+[][6]
+[][7]
+[][8]
+[][9]
+[][10]
+[][11]
+[][12]
+[][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
-[](https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api)
+[][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.
-[](https://ko-fi.com/wangrunlin)
+[][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 @@
-[](https://github.com/wangrunlin/github-achievements-api/blob/main/LICENSE)
-[](https://github.com/wangrunlin/github-achievements-api/blob/main/package.json)
-[](https://github.com/wangrunlin/github-achievements-api/commits)
-[](https://github.com/wangrunlin/github-achievements-api/actions)
-[](https://nodejs.org)
-[](https://www.typescriptlang.org/)
-[](https://github.com/prettier/prettier)
-[](https://makeapullrequest.com)
-[](https://github.com/wangrunlin/github-achievements-api/stargazers)
-[](https://github.com/wangrunlin/github-achievements-api/network)
-[](https://github.com/wangrunlin/github-achievements-api/issues)
-[](https://github.com/wangrunlin/github-achievements-api)
-[](https://ko-fi.com/wangrunlin)
-
-[English](README.md) | 简体中文
+[][1]
+[][2]
+[][3]
+[][4]
+[][5]
+[][6]
+[][7]
+[][8]
+[][9]
+[][10]
+[][11]
+[][12]
+[][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
-[](https://deploy.workers.cloudflare.com/?url=https://github.com/wangrunlin/github-achievements-api)
+[][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 将会出现在这里并链接到您的网站。
-[](https://ko-fi.com/wangrunlin)
+[][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 7db154d..e7f8b1d 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +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"
- },
- "devDependencies": {
- "@cloudflare/vitest-pool-workers": "^0.6.4",
- "@cloudflare/workers-types": "^4.20250214.0",
- "typescript": "^5.5.2",
- "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 --run",
+ "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 8fc7856..b5416b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,21 +1,34 @@
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
+ 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.5.2
+ specifier: ~5.7.3
version: 5.7.3
vitest:
specifier: ~2.1.9
@@ -642,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==}
@@ -655,6 +680,13 @@ 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==}
+
+ 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'}
@@ -663,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'}
@@ -670,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'}
@@ -684,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==}
@@ -691,6 +742,20 @@ 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==}
+
+ 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 +782,34 @@ 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==}
+
+ 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'}
+
+ entities@6.0.0:
+ 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==}
@@ -740,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'}
@@ -748,20 +848,86 @@ 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==}
+ html-escaper@3.0.3:
+ resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
+
+ 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==}
@@ -771,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'}
@@ -801,9 +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==}
@@ -820,16 +1024,37 @@ 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==}
- 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}
+ 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.
@@ -854,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'}
@@ -885,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==}
@@ -903,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==}
@@ -914,6 +1179,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'}
@@ -982,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'}
@@ -1017,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'}
@@ -1029,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==}
@@ -1437,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
@@ -1447,6 +1737,12 @@ snapshots:
blake3-wasm@2.1.5: {}
+ boolbase@1.0.0: {}
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
cac@6.7.14: {}
chai@5.2.0:
@@ -1457,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
@@ -1481,10 +1788,32 @@ 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
+ 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 +1829,32 @@ 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
+
+ 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:
@@ -1561,23 +1916,112 @@ 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: {}
+
+ htmlparser2@10.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ 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
+ cssom: 0.5.0
+ html-escaper: 3.0.3
+ 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:
@@ -1588,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
@@ -1637,8 +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: {}
@@ -1649,20 +2124,33 @@ snapshots:
picocolors@1.1.1: {}
+ picomatch@2.3.1: {}
+
+ pidtree@0.6.0: {}
+
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
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
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
@@ -1731,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: {}
@@ -1755,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: {}
@@ -1765,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
@@ -1772,6 +2296,8 @@ snapshots:
ufo@1.5.4: {}
+ uhyphen@0.2.0: {}
+
undici@5.28.5:
dependencies:
'@fastify/busboy': 2.1.1
@@ -1805,7 +2331,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
@@ -1843,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
@@ -1902,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 ed078d4..3986160 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,126 +1,126 @@
-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('.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;
+
+ 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/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
+
+
+
+
+
+
+
Starstruck
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pair Extraordinaire
+ x3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
YOLO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quickdraw
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pull Shark
+ x2
+
+
+
+
+
+
+
+
+
+`;
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,