Skip to content

Commit caeb897

Browse files
authored
refactor: use Node.js native type striping (#33)
1 parent e9af5bc commit caeb897

9 files changed

Lines changed: 289 additions & 266 deletions

File tree

.github/dependabot.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ updates:
44
directory: /
55
schedule:
66
interval: monthly
7+
cooldown:
8+
default-days: 5
79
groups:
810
github-actions:
911
patterns:
@@ -14,6 +16,8 @@ updates:
1416
versioning-strategy: widen
1517
schedule:
1618
interval: weekly
19+
cooldown:
20+
default-days: 5
1721
groups:
1822
dependencies:
1923
dependency-type: "production"

.github/workflows/node.js.yml

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
1-
name: Node.js CI
2-
3-
on:
4-
push:
5-
branches: master
6-
pull_request:
7-
8-
permissions:
9-
contents: read
10-
11-
jobs:
12-
test:
13-
runs-on: ubuntu-latest
14-
strategy:
15-
matrix:
16-
node-version: [20.x]
17-
fail-fast: false
18-
steps:
19-
- name: Harden Runner
20-
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
21-
with:
22-
egress-policy: audit
23-
24-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25-
- name: Use Node.js ${{ matrix.node-version }}
26-
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
27-
with:
28-
node-version: ${{ matrix.node-version }}
29-
- name: Install dependencies
30-
run: npm install
31-
- name: Run tests
32-
run: npm run test
33-
automerge:
34-
if: >
35-
github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
36-
needs:
37-
- test
38-
runs-on: ubuntu-latest
39-
permissions:
40-
contents: write
41-
pull-requests: write
42-
steps:
43-
- name: Merge Dependabot PR
44-
uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1
45-
with:
46-
github-token: ${{ secrets.GITHUB_TOKEN }}
1+
name: Node.js CI
2+
3+
on:
4+
push:
5+
branches: master
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node-version: [24.x]
17+
fail-fast: false
18+
steps:
19+
- name: Harden Runner
20+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
21+
with:
22+
egress-policy: audit
23+
24+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25+
- name: Use Node.js ${{ matrix.node-version }}
26+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
27+
with:
28+
node-version: ${{ matrix.node-version }}
29+
- name: Install dependencies
30+
run: npm install --ignore-scripts
31+
- name: Run tests
32+
run: npm run test
33+
automerge:
34+
if: >
35+
github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
36+
needs:
37+
- test
38+
runs-on: ubuntu-latest
39+
permissions:
40+
contents: write
41+
pull-requests: write
42+
steps:
43+
- name: Merge Dependabot PR
44+
uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1
45+
with:
46+
github-token: ${{ secrets.GITHUB_TOKEN }}

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
package-lock=false
2+
ignore-scripts=true
3+
save-exact=true

package.json

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
"exports": "./dist/index.js",
77
"types": "./dist/index.d.ts",
88
"scripts": {
9-
"build": "rimraf ./dist && tsc",
9+
"build": "tsc",
1010
"prepublishOnly": "npm run build",
11-
"test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"",
11+
"test": "node --test ./test/**/*.spec.ts",
1212
"coverage": "c8 -r html npm test",
1313
"lint": "eslint src test"
1414
},
@@ -42,19 +42,16 @@
4242
},
4343
"homepage": "https://github.com/dashlog/fetch-github-repositories#readme",
4444
"devDependencies": {
45-
"@openally/config.eslint": "^2.1.0",
46-
"@openally/config.typescript": "^1.0.3",
45+
"@openally/config.eslint": "2.2.0",
46+
"@openally/config.typescript": "^1.2.1",
4747
"@types/http-link-header": "^1.0.7",
48-
"@types/node": "^22.15.3",
48+
"@types/node": "25.0.3",
4949
"c8": "^10.1.3",
50-
"glob": "^11.0.2",
51-
"rimraf": "^6.0.1",
52-
"tsx": "^4.19.4",
53-
"typescript": "^5.8.3"
50+
"typescript": "5.9.3"
5451
},
5552
"dependencies": {
56-
"@myunisoft/httpie": "^5.0.1",
5753
"combine-async-iterators": "^3.0.0",
58-
"http-link-header": "^1.1.3"
54+
"http-link-header": "^1.1.3",
55+
"undici": "7.16.0"
5956
}
6057
}

src/api.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Import Third-party Dependencies
2+
import { request } from "undici";
3+
import linkHeader from "http-link-header";
4+
import combineAsyncIterators from "combine-async-iterators";
5+
6+
// Import Internal Dependencies
7+
import type {
8+
Repository,
9+
UserOrg
10+
} from "./github.types.ts";
11+
12+
// CONSTANTS
13+
const kGithubURL = new URL("https://api.github.com/");
14+
15+
export interface FetchOptions {
16+
/**
17+
* @default fetch-github-repo
18+
*/
19+
agent?: string;
20+
token?: string | null;
21+
/**
22+
* @default users
23+
*/
24+
kind?: "users" | "orgs";
25+
/**
26+
* Fetch the repositories of all orgs for a given user
27+
* @default false
28+
*/
29+
fetchUserOrgs?: boolean;
30+
}
31+
32+
export async function* fetchLazy(
33+
namespace: string,
34+
options: FetchOptions = Object.create(null)
35+
): AsyncIterableIterator<Repository> {
36+
if (typeof namespace !== "string") {
37+
throw new TypeError("namespace argument must be typeof string");
38+
}
39+
40+
const {
41+
agent = "fetch-github-repo",
42+
token = null,
43+
kind = "users",
44+
fetchUserOrgs = false
45+
} = options;
46+
47+
const headers = {
48+
"User-Agent": agent,
49+
Accept: "application/vnd.github.v3.raw",
50+
...(typeof token === "string" ? { Authorization: `token ${token}` } : {})
51+
};
52+
53+
let currURL: string | null = `${kind}/${namespace}/repos`;
54+
while (currURL) {
55+
const { body, headers: currHeaders } = await request(
56+
new URL(currURL, kGithubURL),
57+
{ headers }
58+
);
59+
yield* (await body.json() as Repository[]);
60+
61+
currURL = getNextPageUrl(currHeaders);
62+
}
63+
64+
if (kind === "users" && fetchUserOrgs) {
65+
const { body } = await request(
66+
new URL(`users/${namespace}/orgs`, kGithubURL),
67+
{ headers }
68+
);
69+
const data = await body.json() as UserOrg[];
70+
71+
const iterators = data.map(
72+
(row) => fetchLazy(row.login, { agent, token, kind: "orgs" })
73+
);
74+
for await (const repo of combineAsyncIterators(...iterators)) {
75+
yield repo;
76+
}
77+
}
78+
}
79+
80+
export async function fetch(
81+
namespace: string,
82+
options: FetchOptions = Object.create(null)
83+
): Promise<Repository[]> {
84+
const repositories: Repository[] = [];
85+
for await (const repo of fetchLazy(namespace, options)) {
86+
repositories.push(repo);
87+
}
88+
89+
return repositories;
90+
}
91+
92+
function getNextPageUrl(
93+
headers: Record<string, string | string[] | undefined>
94+
): string | null {
95+
const linkValue = headers.link;
96+
if (linkValue === undefined) {
97+
return null;
98+
}
99+
100+
const { refs } = linkHeader.parse(linkValue.toString());
101+
102+
return refs.find(
103+
(row) => row.rel === "next" || row.rel === "last"
104+
)?.uri ?? null;
105+
}

src/github.types.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
export interface Repository {
2+
id: number;
3+
node_id: string;
4+
name: string;
5+
full_name: string;
6+
private: boolean;
7+
owner: {
8+
login: string;
9+
id: number;
10+
node_id: string;
11+
avatar_url: string;
12+
gravatar_id: string;
13+
url: string;
14+
html_url: string;
15+
followers_url: string;
16+
following_url: string;
17+
gists_url: string;
18+
starred_url: string;
19+
subscriptions_url: string;
20+
organizations_url: string;
21+
repos_url: string;
22+
events_url: string;
23+
received_events_url: string;
24+
type: "User" | "Organization";
25+
site_admin: boolean;
26+
};
27+
html_url: string;
28+
description: string;
29+
fork: boolean;
30+
url: string;
31+
forks_url: string;
32+
keys_url: string;
33+
collaborators_url: string;
34+
teams_url: string;
35+
hooks_url: string;
36+
issue_events_url: string;
37+
events_url: string;
38+
assignees_url: string;
39+
branches_url: string;
40+
tags_url: string;
41+
blobs_url: string;
42+
git_tags_url: string;
43+
git_refs_url: string;
44+
trees_url: string;
45+
statuses_url: string;
46+
languages_url: string;
47+
stargazers_url: string;
48+
contributors_url: string;
49+
subscribers_url: string;
50+
subscription_url: string;
51+
commits_url: string;
52+
git_commits_url: string;
53+
comments_url: string;
54+
issue_comment_url: string;
55+
contents_url: string;
56+
compare_url: string;
57+
merges_url: string;
58+
archive_url: string;
59+
downloads_url: string;
60+
issues_url: string;
61+
pulls_url: string;
62+
milestones_url: string;
63+
notifications_url: string;
64+
labels_url: string;
65+
releases_url: string;
66+
deployments_url: string;
67+
created_at: string;
68+
updated_at: string;
69+
pushed_at: string;
70+
git_url: string;
71+
ssh_url: string;
72+
clone_url: string;
73+
svn_url: string;
74+
homepage: string;
75+
size: number;
76+
stargazers_count: number;
77+
watchers_count: number;
78+
language: null | string;
79+
has_issues: boolean;
80+
has_projects: boolean;
81+
has_downloads: boolean;
82+
has_wiki: boolean;
83+
has_pages: boolean;
84+
forks_count: number;
85+
mirror_url: string;
86+
archived: boolean;
87+
disabled: boolean;
88+
open_issues_count: number;
89+
license: {
90+
key: string;
91+
name: string;
92+
spdx_id: string;
93+
url: string;
94+
node_id: string;
95+
} | null;
96+
forks: number;
97+
open_issues: number;
98+
watchers: number;
99+
default_branch: string;
100+
permissions?: {
101+
admin: true;
102+
push: true;
103+
pull: true;
104+
};
105+
}
106+
107+
export interface UserOrg {
108+
login: string;
109+
id: number;
110+
node_id: string;
111+
url: string;
112+
repos_url: string;
113+
events_url: string;
114+
hooks_url: string;
115+
issues_url: string;
116+
members_url: string;
117+
public_members_url: string;
118+
avatar_url: string;
119+
description: string;
120+
}

0 commit comments

Comments
 (0)