├── .gitignore ├── package.json ├── tsconfig.json ├── .github └── workflows │ └── update.yml └── scripts ├── data-to-readme.mts └── search-github-sponsorable-in-japan.mts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-sponsorable-in-japan", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "update:resources": "node --loader ts-node/esm ./scripts/search-github-sponsorable-in-japan.mts", 7 | "update:readme": "node --loader ts-node/esm ./scripts/data-to-readme.mts" 8 | }, 9 | "devDependencies": { 10 | "@octokit/core": "^4.2.0", 11 | "@octokit/plugin-paginate-graphql": "^2.0.1", 12 | "@types/node": "18.14.4", 13 | "markdown-function": "^2.0.0", 14 | "ts-node": "^10.9.2", 15 | "typescript": "^5.3.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Update 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | # daily 9 | - cron: '0 0 * * *' 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | name: Update Resources 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | - name: Install Dependencies 25 | run: npm ci 26 | - name: Update Resources 27 | run: npm run update:resources && npm run update:readme 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Commit Changes 31 | uses: EndBug/add-and-commit@v9 32 | with: 33 | author_name: ${{ github.actor }} 34 | author_email: ${{ github.actor }}@users.noreply.github.com 35 | message: 'docs: update resources' 36 | add: '.' 37 | -------------------------------------------------------------------------------- /scripts/data-to-readme.mts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | import * as path from "path"; 3 | import { fileURLToPath } from "url"; 4 | import { UserNode } from "./search-github-sponsorable-in-japan.mjs"; 5 | import { mdEscape, mdImg, mdLink } from "markdown-function" 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const datadir = path.join(__dirname, "../data"); 10 | const resultJSON: UserNode[] = JSON.parse(await fs.readFile(path.join(datadir, "results.json"), "utf-8")); 11 | const escapeTable = (text?: string) => text ? text.replace(/\|/g, "|").replace(/\r?\n/g, " ") : ""; 12 | const isAccount = (person: UserNode) => person.login !== undefined; 13 | const persons = resultJSON.filter(isAccount).map((person) => { 14 | const firstPin = person.pinnedItems?.edges?.[0]?.node ?? {}; 15 | const firstItem = firstPin.url ? mdLink({ 16 | text: firstPin.name ?? person.login ?? "", 17 | url: firstPin.url 18 | }) : "" 19 | const firstItemDescription = firstPin.description ? mdEscape(firstPin.description ?? "") : "" 20 | return `## ${mdLink({ 21 | text: `${person.name ?? person.login ?? ""}`, 22 | url: person.url, 23 | })} 24 | 25 | | ${mdLink({ text: `@${person.login}`, url: person.url })} | [❤️Sponsor](https://github.com/sponsors/${person.login}) | 26 | | --- | --- | 27 | | | ${escapeTable(mdEscape(person.bio ?? ""))} | 28 | | ${escapeTable(firstItem)} | ${escapeTable(firstItemDescription)} | 29 | 30 | ` 31 | 32 | }).join("\n\n"); 33 | 34 | const OUTPUT = `# GitHub Sponsor-able Users in Japan 35 | 36 | This repository is a list of GitHub users who are living in Japan and are sponsor-able. 37 | 38 | - Total: ${resultJSON.length} 39 | - Search Results: 40 | 41 | ---- 42 | 43 | ${persons} 44 | 45 | ` 46 | const README_FILE = path.join(__dirname, "../README.md"); 47 | await fs.writeFile(README_FILE, OUTPUT); 48 | -------------------------------------------------------------------------------- /scripts/search-github-sponsorable-in-japan.mts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/core"; 2 | import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; 3 | import * as fs from "fs/promises"; 4 | import * as path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const MyOctokit = Octokit.plugin(paginateGraphql); 8 | const octokit = new MyOctokit({ auth: process.env.GITHUB_TOKEN }); 9 | 10 | export type UserNode = { 11 | login: string; 12 | name: string; 13 | url: string; 14 | location: string; 15 | avatarUrl: string; 16 | bio: string; 17 | pinnedItems: PinnedItems; 18 | } 19 | 20 | export type PinnedItems = { 21 | edges: Edge[]; 22 | } 23 | 24 | export type Edge = { 25 | node: Node; 26 | } 27 | 28 | export type Node = { 29 | name: string; 30 | description: string; 31 | url: string; 32 | } 33 | 34 | const query = `query paginate($cursor: String) { 35 | search(type: USER query: "location:Japan is:sponsorable", first: 100, after: $cursor) { 36 | userCount 37 | pageInfo { 38 | hasNextPage 39 | endCursor 40 | } 41 | nodes { 42 | ... on User{ 43 | login, 44 | name 45 | url 46 | location 47 | avatarUrl 48 | bio 49 | pinnedItems(first:1) { 50 | edges { 51 | node { 52 | ... on Repository{ 53 | name 54 | description 55 | url 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }`; 64 | 65 | const results: UserNode[] = []; 66 | for await (const result of octokit.graphql.paginate.iterator(query)) { 67 | // TODO: support "Optional: Opt-in to get featured on github.com/sponsors" 68 | // TODO: support opt-out users 69 | results.push(...result.search.nodes.filter((node: UserNode) => node.login !== undefined)); 70 | console.log(`results: ${results.length}/${result.search.userCount}`); 71 | } 72 | const __filename = fileURLToPath(import.meta.url); 73 | const __dirname = path.dirname(__filename); 74 | const DATA_DIR = path.join(__dirname, "..", "data"); 75 | const RESULT_FILE_PATH = path.join(DATA_DIR, "results.json"); 76 | await fs.writeFile(RESULT_FILE_PATH, JSON.stringify(results, null, 2)); 77 | --------------------------------------------------------------------------------