├── .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 |
--------------------------------------------------------------------------------