├── .cargo-ok ├── .gitignore ├── .prettierrc ├── src ├── index.ts ├── types.d.ts └── handler.ts ├── test ├── _setup.ts ├── tsconfig.json └── handler.ts ├── wrangler.toml ├── tsconfig.json ├── webpack.config.js ├── .github └── workflows │ └── release.yml ├── LICENSE ├── package.json ├── assets ├── pic2.svg └── pic1.svg └── README.md /.cargo-ok: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | transpiled 4 | worker 5 | .idea 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { handleRequest } from './handler' 2 | 3 | addEventListener('fetch', (event: FetchEvent) => { 4 | event.respondWith(handleRequest(event.request)) 5 | }) 6 | -------------------------------------------------------------------------------- /test/_setup.ts: -------------------------------------------------------------------------------- 1 | // set up global namespace for worker environment 2 | import * as makeServiceWorkerEnv from 'service-worker-mock' 3 | declare var global: any 4 | Object.assign(global, makeServiceWorkerEnv()) 5 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "api-dev" 2 | type = "webpack" 3 | account_id = "3ea01693f23309397d4b82c5e57b0e40" 4 | workers_dev = true 5 | zone_id = "" 6 | webpack_config = "webpack.config.js" 7 | 8 | [env.staging] 9 | name = "my-worker-staging" 10 | 11 | [env.production] 12 | name = "api" 13 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { KVNamespace } from '@cloudflare/workers-types' 2 | 3 | declare global { 4 | const CACHETTL: string 5 | interface GitHub_Star_Fork { 6 | forks: number 7 | stars: number 8 | } 9 | 10 | interface GitHubRepo { 11 | forks: number 12 | stargazers_count: number 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../transpiled", 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "lib": ["esnext", "webworker"] 9 | }, 10 | "include": [ 11 | "./*.ts", 12 | "../node_modules/@cloudflare/workers-types/index.d.ts", 13 | "../src/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "esnext", 6 | "lib": ["esnext", "webworker"], 7 | "alwaysStrict": true, 8 | "strict": true, 9 | "preserveConstEnums": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": [ 15 | "./src/*.ts", 16 | "./test/*.ts", 17 | "./src/**/*.ts", 18 | "./test/**/*.ts", 19 | "./node_modules/@cloudflare/workers-types/index.d.ts" 20 | ], 21 | "exclude": ["node_modules/", "dist/"] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const mode = process.env.NODE_ENV || 'production' 5 | 6 | module.exports = { 7 | output: { 8 | filename: `worker.${mode}.js`, 9 | path: path.join(__dirname, 'dist'), 10 | }, 11 | devtool: 'source-map', 12 | mode, 13 | resolve: { 14 | extensions: ['.ts', '.tsx', '.js'], 15 | plugins: [], 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | loader: 'ts-loader', 22 | options: { 23 | transpileOnly: true, 24 | }, 25 | }, 26 | { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, 27 | ], 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /test/handler.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { handleRequest } from '../src/handler' 3 | 4 | describe('handler returns response with request method', () => { 5 | const methods = [ 6 | 'GET', 7 | 'HEAD', 8 | 'POST', 9 | 'PUT', 10 | 'DELETE', 11 | 'CONNECT', 12 | 'OPTIONS', 13 | 'TRACE', 14 | 'PATCH', 15 | ] 16 | methods.forEach(method => { 17 | it(method, async () => { 18 | const result = await handleRequest(new Request('/', { method })) 19 | const { status, statusText } = result 20 | const text = await result.text() 21 | if (method === 'GET') { 22 | expect(status).not.eq(405) 23 | expect(statusText).to.eq('Should Provide Github Username') 24 | } else { 25 | expect(status).to.eq(405) 26 | expect(text).to.eq('Expected GET') 27 | } 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [10.x, 12.x, 13.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm run build --if-present 23 | - run: npm test 24 | env: 25 | CI: true 26 | release: 27 | needs: build 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@master 31 | - name: Publish to !production! 32 | uses: cloudflare/wrangler-action@1.1.0 33 | with: 34 | apiToken: ${{ secrets.CF_API_TOKEN }} 35 | environment: 'production' 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 fengkx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-star-counter", 3 | "version": "1.0.1", 4 | "description": "An API to count a GitHub user's total stars, working with shield.io.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "dev": "NODE_ENV=development npm run build", 9 | "format": "prettier --write '**/*.{ts,js,css,json,md}'", 10 | "test:clean": "rimraf ./transpiled/src ./transpiled/test", 11 | "test": "npm run test:clean && npm run transpile && mocha --require source-map-support/register --recursive transpiled/test", 12 | "transpile": "tsc --project ./test" 13 | }, 14 | "author": "idealclover & fengkx ", 15 | "license": "MIT OR Apache-2.0", 16 | "devDependencies": { 17 | "@cloudflare/workers-types": "^1.0.1", 18 | "@types/chai": "^4.2.11", 19 | "@types/mocha": "^5.2.7", 20 | "chai": "^4.2.0", 21 | "mocha": "^6.1.4", 22 | "prettier": "^1.18.2", 23 | "rimraf": "^3.0.2", 24 | "service-worker-mock": "^2.0.3", 25 | "source-map-loader": "^0.2.4", 26 | "source-map-support": "^0.5.12", 27 | "ts-loader": "^6.0.4", 28 | "typescript": "^3.5.3", 29 | "webpack": "^4.35.3", 30 | "webpack-cli": "^3.3.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/pic2.svg: -------------------------------------------------------------------------------- 1 | GITHUB FORKS250 -------------------------------------------------------------------------------- /assets/pic1.svg: -------------------------------------------------------------------------------- 1 | GITHUB STARS1321 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub-Star-Counter 2 | 3 | > ✨ City of stars, are you shining just for me? 4 | 5 | ## What Is It 6 | 7 | An API to count a GitHub user's total stars. 8 | 9 | > /user/:username - to show a user's total stars and forks of is repositories. 10 | 11 | For example: [https://api.github-star-counter.workers.dev/user/idealclover](https://api.github-star-counter.workers.dev/user/idealclover) 12 | 13 | Heroku version: [https://github-star-counter.herokuapp.com/user/idealclover](https://github-star-counter.herokuapp.com/user/idealclover) 14 | 15 | Moreover, you can combined it with [shields.io](https://shields.io/) to produce a badge like this: 16 | 17 | ![](https://img.shields.io/badge/dynamic/json?logo=github&label=GitHub%20Stars&style=for-the-badge&query=%24.stars&url=https://api.github-star-counter.workers.dev/user/idealclover) 18 | 19 | ![](https://img.shields.io/badge/dynamic/json?logo=github&label=GitHub%20Forks&style=for-the-badge&query=%24.forks&url=https://api.github-star-counter.workers.dev/user/idealclover) 20 | 21 | Sometimes due to slow network, the pictures above could not show correctly, here are the static version: 22 | 23 | ![](https://github.com/idealclover/GitHub-Star-Counter/raw/master/assets/pic1.svg?sanitize=true) 24 | 25 | ![](https://github.com/idealclover/GitHub-Star-Counter/raw/master/assets/pic2.svg?sanitize=true) 26 | 27 | ## Deploy 28 | 29 | ### On Heroku 30 | 31 | In order to increase the API rate limit, you can register GitHub auth code [here](https://github.com/settings/tokens) 32 | 33 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/idealclover/GitHub-Star-Counter/tree/heroku) 34 | 35 | ### On Cloudflare Worker 36 | 37 | This is a serverless function deployed on Cloudflare Workers. Please check: [Quick Start | Cloudflare Workers](https://developers.cloudflare.com/workers/quickstart) if you want to deploy or contribute. 38 | 39 | ## LICENSE 40 | 41 | Inspired by [yyx990803/starz](https://github.com/yyx990803/starz). 42 | 43 | Thanks for strong support by [fengkx](https://github.com/fengkx)! 44 | 45 | MIT LICENSE. Have fun. 46 | -------------------------------------------------------------------------------- /src/handler.ts: -------------------------------------------------------------------------------- 1 | export async function handleRequest(request: Request): Promise { 2 | const headers = { 3 | 'Access-Control-Allow-Origin': '*', 4 | } 5 | if (request.method !== 'GET') { 6 | return new Response('Expected GET', { status: 405, headers }) 7 | } 8 | const url = new URL(request.url) 9 | const { pathname } = url 10 | if (checkPath(pathname)) { 11 | let username: string 12 | if (pathname.endsWith('/')) { 13 | username = pathname.substring(6, pathname.length - 1) 14 | } else { 15 | username = pathname.substring(6) 16 | } 17 | const resp = await github(`/users/${username}`) 18 | const data = await resp.json() 19 | const pageCount = Math.ceil(data.public_repos / 100) 20 | const pages = [] 21 | for (let i = 1; i <= pageCount; i++) { 22 | pages.push(i) 23 | } 24 | const result: GitHub_Star_Fork = { 25 | stars: 0, 26 | forks: 0, 27 | } 28 | await Promise.all( 29 | pages.map(async p => { 30 | const data = await getRepos(username, p) 31 | result.stars += data.stars 32 | result.forks += data.forks 33 | }), 34 | ) 35 | return new Response(JSON.stringify(result), { 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | ...headers, 39 | }, 40 | }) 41 | } else { 42 | return new Response('/user/:username/', { 43 | status: 400, 44 | statusText: 'Should Provide Github Username', 45 | headers, 46 | }) 47 | } 48 | } 49 | 50 | function checkPath(pathname: string): boolean { 51 | if (!pathname.startsWith('/user/')) { 52 | return false 53 | } 54 | const rest = pathname.substring(6, pathname.length - 1) 55 | return rest.length > 0 && !rest.includes('/') 56 | } 57 | 58 | async function github(path: string): Promise { 59 | const GITHUB_API = 'https://api.github.com' 60 | return fetch(`${GITHUB_API}${path}`, { 61 | headers: { 62 | 'User-Agent': 'GitHub-Start-Counter', 63 | }, 64 | // @ts-ignore 65 | cf: { cacheTTl: 900 }, 66 | }) 67 | } 68 | 69 | async function getRepos(user: string, page: number): Promise { 70 | const path = `/users/${user}/repos?per_page=100&page=${page}` 71 | const resp = await github(path) 72 | const repos: GitHubRepo[] = await resp.json() 73 | return repos.reduce( 74 | (acc, cur) => { 75 | acc.stars += cur.stargazers_count 76 | acc.forks += cur.forks 77 | return acc 78 | }, 79 | { stars: 0, forks: 0 }, 80 | ) 81 | } 82 | --------------------------------------------------------------------------------