astro-contributors 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/components/AllContributors.astro +60 -90
- package/components/ContributorList.astro +11 -7
- package/components/FacePile.astro +35 -26
- package/index.ts +1 -1
- package/libs/all-contributors.ts +46 -0
- package/libs/contributions.ts +40 -0
- package/libs/github.ts +84 -0
- package/libs/link.ts +9 -0
- package/libs/style.ts +15 -0
- package/package.json +26 -20
- package/libs/getAllContributorsRc.ts +0 -47
- package/libs/getContributors.ts +0 -105
- package/libs/util-server.ts +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# astro-contributors
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#125](https://github.com/trueberryless-org/astro-contributors/pull/125) [`2d416dd`](https://github.com/trueberryless-org/astro-contributors/commit/2d416dd74e5c50c5b3149d98b0807d53affa416f) Thanks [@trueberryless-bot](https://github.com/trueberryless-bot)! - Adds a `formatLink` prop to the `ContributorList` and `AllContributors` components to customize the link of each contributor, e.g. to link to their commits in your repository instead of their GitHub profile.
|
|
8
|
+
|
|
9
|
+
See the [`ContributorList` documentation](https://astro-contributors.netlify.app/components/contributor-list/#formatlink) for more details.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#125](https://github.com/trueberryless-org/astro-contributors/pull/125) [`2d416dd`](https://github.com/trueberryless-org/astro-contributors/commit/2d416dd74e5c50c5b3149d98b0807d53affa416f) Thanks [@trueberryless-bot](https://github.com/trueberryless-bot)! - Fixes the `PUBLIC_GITHUB_TOKEN` environment variable being ignored by GitHub when it contains a plain personal access token. Tokens in the `username:token` format are still sent using Basic authentication.
|
|
14
|
+
|
|
15
|
+
- [#125](https://github.com/trueberryless-org/astro-contributors/pull/125) [`2d416dd`](https://github.com/trueberryless-org/astro-contributors/commit/2d416dd74e5c50c5b3149d98b0807d53affa416f) Thanks [@trueberryless-bot](https://github.com/trueberryless-bot)! - Refactors the component internals and improves the warning logged when the contributors of a GitHub repository cannot be fetched.
|
|
16
|
+
|
|
3
17
|
## 0.9.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -10,7 +24,7 @@
|
|
|
10
24
|
|
|
11
25
|
### Minor Changes
|
|
12
26
|
|
|
13
|
-
- [`64612b7`](https://github.com/trueberryless-org/astro-contributors/commit/64612b7348c6df8c29ed914c8afb20308b4c6389) Thanks [@trueberryless](https://github.com/trueberryless)! - Add support for All Contributors standard. Check out the [new component documentation](https://astro-contributors.
|
|
27
|
+
- [`64612b7`](https://github.com/trueberryless-org/astro-contributors/commit/64612b7348c6df8c29ed914c8afb20308b4c6389) Thanks [@trueberryless](https://github.com/trueberryless)! - Add support for All Contributors standard. Check out the [new component documentation](https://astro-contributors.netlify.app/all-contributors/) for details.
|
|
14
28
|
|
|
15
29
|
## 0.7.0
|
|
16
30
|
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present, trueberryless
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# `astro-contributors`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Astro components to display all contributors to your project.
|
|
4
4
|
|
|
5
5
|
## Documentation
|
|
6
6
|
|
|
7
7
|
Want to get started immediately?
|
|
8
8
|
|
|
9
|
-
Check out the `astro-contributors` getting started guide.
|
|
9
|
+
Check out the [`astro-contributors` getting started guide](https://astro-contributors.netlify.app/getting-started/).
|
|
10
10
|
|
|
11
11
|
## License
|
|
12
12
|
|
|
@@ -1,109 +1,79 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { getAllContributorsFromRc } from "../libs/
|
|
2
|
+
import { getAllContributorsFromRc } from "../libs/all-contributors";
|
|
3
|
+
import { getContributionEmoji } from "../libs/contributions";
|
|
4
|
+
import { type FormatLink, getContributorLink } from "../libs/link";
|
|
5
|
+
import { getStyleVariables } from "../libs/style";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
interface Props {
|
|
8
|
+
badgeBgColor?: string;
|
|
9
|
+
badgeTextColor?: string;
|
|
6
10
|
bgColor?: string;
|
|
7
11
|
borderColor?: string;
|
|
12
|
+
focusColor?: string;
|
|
13
|
+
formatLink?: FormatLink;
|
|
8
14
|
textColor?: string;
|
|
9
|
-
badgeBgColor?: string;
|
|
10
|
-
badgeTextColor?: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const {
|
|
14
|
-
|
|
18
|
+
badgeBgColor,
|
|
19
|
+
badgeTextColor,
|
|
15
20
|
bgColor,
|
|
16
21
|
borderColor,
|
|
22
|
+
focusColor,
|
|
23
|
+
formatLink,
|
|
17
24
|
textColor,
|
|
18
|
-
|
|
19
|
-
badgeTextColor,
|
|
20
|
-
} = Astro.props as Props;
|
|
21
|
-
|
|
22
|
-
const styleOverride = {
|
|
23
|
-
...(focusColor && { "--ac-focus-color": focusColor }),
|
|
24
|
-
...(bgColor && { "--ac-bg-color": bgColor }),
|
|
25
|
-
...(borderColor && { "--ac-border-color": borderColor }),
|
|
26
|
-
...(textColor && { "--ac-text-color": textColor }),
|
|
27
|
-
...(badgeBgColor && { "--ac-badge-bg-color": badgeBgColor }),
|
|
28
|
-
...(badgeTextColor && { "--ac-badge-text-color": badgeTextColor }),
|
|
29
|
-
};
|
|
25
|
+
} = Astro.props;
|
|
30
26
|
|
|
31
27
|
const contributors = getAllContributorsFromRc();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
content: "🖋",
|
|
41
|
-
data: "🔣",
|
|
42
|
-
design: "🎨",
|
|
43
|
-
doc: "📖",
|
|
44
|
-
eventOrganizing: "📋",
|
|
45
|
-
example: "💡",
|
|
46
|
-
financial: "💵",
|
|
47
|
-
fundingFinding: "🔍",
|
|
48
|
-
ideas: "🤔",
|
|
49
|
-
infra: "🚇",
|
|
50
|
-
maintenance: "🚧",
|
|
51
|
-
mentoring: "🧑🏫",
|
|
52
|
-
platform: "📦",
|
|
53
|
-
plugin: "🔌",
|
|
54
|
-
projectManagement: "📆",
|
|
55
|
-
question: "💬",
|
|
56
|
-
research: "🔬",
|
|
57
|
-
review: "👀",
|
|
58
|
-
security: "🛡️",
|
|
59
|
-
tool: "🔧",
|
|
60
|
-
translation: "🌍",
|
|
61
|
-
test: "⚠️",
|
|
62
|
-
tutorial: "✅",
|
|
63
|
-
talk: "📢",
|
|
64
|
-
userTesting: "📓",
|
|
65
|
-
video: "📹",
|
|
66
|
-
};
|
|
28
|
+
const style = getStyleVariables({
|
|
29
|
+
"--ac-focus-color": focusColor,
|
|
30
|
+
"--ac-bg-color": bgColor,
|
|
31
|
+
"--ac-border-color": borderColor,
|
|
32
|
+
"--ac-text-color": textColor,
|
|
33
|
+
"--ac-badge-bg-color": badgeBgColor,
|
|
34
|
+
"--ac-badge-text-color": badgeTextColor,
|
|
35
|
+
});
|
|
67
36
|
---
|
|
68
37
|
|
|
69
|
-
{
|
|
70
|
-
contributors
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</div>
|
|
38
|
+
{contributors.length > 0 ? (
|
|
39
|
+
<div class="all-contributors-grid" style={style}>
|
|
40
|
+
{contributors.map((contributor) => (
|
|
41
|
+
<a
|
|
42
|
+
href={getContributorLink(
|
|
43
|
+
contributor.login,
|
|
44
|
+
contributor.profile,
|
|
45
|
+
formatLink
|
|
46
|
+
)}
|
|
47
|
+
class="contributor-card"
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
>
|
|
51
|
+
<img
|
|
52
|
+
src={contributor.avatar_url}
|
|
53
|
+
alt={contributor.name}
|
|
54
|
+
title={contributor.name}
|
|
55
|
+
width="64"
|
|
56
|
+
height="64"
|
|
57
|
+
loading="lazy"
|
|
58
|
+
/>
|
|
59
|
+
<div class="contributor-info">
|
|
60
|
+
<span class="contributor-name">{contributor.name}</span>
|
|
61
|
+
<div class="contributor-badges">
|
|
62
|
+
{contributor.contributions.map((type) => (
|
|
63
|
+
<span class="badge" title={`Contribution: ${type}`}>
|
|
64
|
+
{getContributionEmoji(type)} {type}
|
|
65
|
+
</span>
|
|
66
|
+
))}
|
|
99
67
|
</div>
|
|
100
|
-
</
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
68
|
+
</div>
|
|
69
|
+
</a>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
) : (
|
|
73
|
+
<p>
|
|
74
|
+
No <code>.all-contributorsrc</code> file found or no contributors listed.
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
107
77
|
|
|
108
78
|
<style>
|
|
109
79
|
.all-contributors-grid {
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { getGitHubContributors } from "../libs/github";
|
|
3
|
+
import type { FormatLink } from "../libs/link";
|
|
4
|
+
|
|
3
5
|
import FacePile from "./FacePile.astro";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
interface Props {
|
|
8
|
+
focusColor?: string;
|
|
9
|
+
formatLink?: FormatLink;
|
|
6
10
|
githubRepo?: `${string}/${string}`;
|
|
7
11
|
ignore?: string[];
|
|
8
|
-
focusColor?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const {
|
|
15
|
+
focusColor,
|
|
16
|
+
formatLink,
|
|
12
17
|
githubRepo = "withastro/docs",
|
|
13
18
|
ignore,
|
|
14
|
-
|
|
15
|
-
} = Astro.props as Props;
|
|
19
|
+
} = Astro.props;
|
|
16
20
|
|
|
17
|
-
const contributors = await
|
|
21
|
+
const contributors = await getGitHubContributors(githubRepo, ignore);
|
|
18
22
|
---
|
|
19
23
|
|
|
20
|
-
<FacePile {contributors} {focusColor} />
|
|
24
|
+
<FacePile {contributors} {focusColor} {formatLink} />
|
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type GitHubContributor,
|
|
4
|
+
getGitHubAvatarUrl,
|
|
5
|
+
getGitHubProfileUrl,
|
|
6
|
+
} from "../libs/github";
|
|
7
|
+
import { type FormatLink, getContributorLink } from "../libs/link";
|
|
8
|
+
import { getStyleVariables } from "../libs/style";
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
contributors:
|
|
6
|
-
focusColor?: string;
|
|
10
|
+
interface Props {
|
|
11
|
+
contributors: GitHubContributor[];
|
|
12
|
+
focusColor?: string | undefined;
|
|
13
|
+
formatLink?: FormatLink | undefined;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
const { contributors, focusColor } = Astro.props
|
|
10
|
-
|
|
16
|
+
const { contributors, focusColor, formatLink } = Astro.props;
|
|
17
|
+
|
|
18
|
+
const style = getStyleVariables({ "--avatar-focus-color": focusColor });
|
|
11
19
|
---
|
|
12
20
|
|
|
13
|
-
<div style={
|
|
21
|
+
<div style={style}>
|
|
14
22
|
<ul class="avatar-list">
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
{contributors.map((contributor) => (
|
|
24
|
+
<li>
|
|
25
|
+
<a
|
|
26
|
+
href={getContributorLink(
|
|
27
|
+
contributor.login,
|
|
28
|
+
getGitHubProfileUrl(contributor.login),
|
|
29
|
+
formatLink
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<img
|
|
33
|
+
alt={contributor.login}
|
|
34
|
+
title={contributor.login}
|
|
35
|
+
width="3rem"
|
|
36
|
+
height="3rem"
|
|
37
|
+
src={getGitHubAvatarUrl(contributor.id)}
|
|
38
|
+
loading="lazy"
|
|
39
|
+
/>
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
31
43
|
</ul>
|
|
32
44
|
</div>
|
|
33
45
|
|
|
@@ -39,7 +51,6 @@ const styleOverride = focusColor ? { "--avatar-focus-color": focusColor } : {};
|
|
|
39
51
|
--avatar-outline-width: 1px;
|
|
40
52
|
--avatar-outline-offset: 0.08em;
|
|
41
53
|
|
|
42
|
-
/* Fallback chain: Global Var -> Component Prop -> Starlight Variable -> Default Blue */
|
|
43
54
|
--local-accent: var(--avatar-focus-color, var(--sl-color-accent, #3b82f6));
|
|
44
55
|
|
|
45
56
|
display: flex;
|
|
@@ -70,7 +81,6 @@ const styleOverride = focusColor ? { "--avatar-focus-color": focusColor } : {};
|
|
|
70
81
|
border-radius: 50%;
|
|
71
82
|
width: var(--avatar-size);
|
|
72
83
|
height: var(--avatar-size);
|
|
73
|
-
/* Hide alt/title if Avatar image fails to load. */
|
|
74
84
|
text-decoration: none;
|
|
75
85
|
color: transparent;
|
|
76
86
|
}
|
|
@@ -80,7 +90,6 @@ const styleOverride = focusColor ? { "--avatar-focus-color": focusColor } : {};
|
|
|
80
90
|
height: 100%;
|
|
81
91
|
object-fit: cover;
|
|
82
92
|
background-color: var(--local-accent);
|
|
83
|
-
/* Indicates the contributor image boundaries for forced colors users, transparent is changed to a solid color */
|
|
84
93
|
outline: 1px solid transparent;
|
|
85
94
|
}
|
|
86
95
|
|
package/index.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getAllContributorsFromRc() {
|
|
5
|
+
const rcPath = findAllContributorsRcPath(process.cwd());
|
|
6
|
+
|
|
7
|
+
if (!rcPath) return [];
|
|
8
|
+
|
|
9
|
+
return parseAllContributorsRc(fs.readFileSync(rcPath, "utf-8"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findAllContributorsRcPath(directory: string): string | undefined {
|
|
13
|
+
const rcPath = path.join(directory, ".all-contributorsrc");
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(rcPath)) return rcPath;
|
|
16
|
+
|
|
17
|
+
const parentDirectory = path.dirname(directory);
|
|
18
|
+
|
|
19
|
+
return parentDirectory === directory
|
|
20
|
+
? undefined
|
|
21
|
+
: findAllContributorsRcPath(parentDirectory);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseAllContributorsRc(content: string) {
|
|
25
|
+
const rc: unknown = JSON.parse(content);
|
|
26
|
+
|
|
27
|
+
return isAllContributorsRc(rc) ? rc.contributors : [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAllContributorsRc(rc: unknown): rc is AllContributorsRc {
|
|
31
|
+
if (typeof rc !== "object" || rc === null) return false;
|
|
32
|
+
|
|
33
|
+
return Array.isArray((rc as { contributors?: unknown }).contributors);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AllContributorsContributor {
|
|
37
|
+
avatar_url: string;
|
|
38
|
+
contributions: string[];
|
|
39
|
+
login: string;
|
|
40
|
+
name: string;
|
|
41
|
+
profile: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface AllContributorsRc {
|
|
45
|
+
contributors: AllContributorsContributor[];
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const CONTRIBUTION_EMOJIS = new Map(
|
|
2
|
+
Object.entries({
|
|
3
|
+
a11y: "♿️",
|
|
4
|
+
audio: "🔊",
|
|
5
|
+
blog: "📝",
|
|
6
|
+
bug: "🐛",
|
|
7
|
+
business: "💼",
|
|
8
|
+
code: "💻",
|
|
9
|
+
content: "🖋",
|
|
10
|
+
data: "🔣",
|
|
11
|
+
design: "🎨",
|
|
12
|
+
doc: "📖",
|
|
13
|
+
eventOrganizing: "📋",
|
|
14
|
+
example: "💡",
|
|
15
|
+
financial: "💵",
|
|
16
|
+
fundingFinding: "🔍",
|
|
17
|
+
ideas: "🤔",
|
|
18
|
+
infra: "🚇",
|
|
19
|
+
maintenance: "🚧",
|
|
20
|
+
mentoring: "🧑🏫",
|
|
21
|
+
platform: "📦",
|
|
22
|
+
plugin: "🔌",
|
|
23
|
+
projectManagement: "📆",
|
|
24
|
+
question: "💬",
|
|
25
|
+
research: "🔬",
|
|
26
|
+
review: "👀",
|
|
27
|
+
security: "🛡️",
|
|
28
|
+
talk: "📢",
|
|
29
|
+
test: "⚠️",
|
|
30
|
+
tool: "🔧",
|
|
31
|
+
translation: "🌍",
|
|
32
|
+
tutorial: "✅",
|
|
33
|
+
userTesting: "📓",
|
|
34
|
+
video: "📹",
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export function getContributionEmoji(type: string) {
|
|
39
|
+
return CONTRIBUTION_EMOJIS.get(type) ?? "✨";
|
|
40
|
+
}
|
package/libs/github.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const PAGE_SIZE = 100;
|
|
2
|
+
|
|
3
|
+
export async function getGitHubContributors(
|
|
4
|
+
repo: string,
|
|
5
|
+
ignore: string[] = []
|
|
6
|
+
) {
|
|
7
|
+
const contributors = await fetchGitHubContributors(repo);
|
|
8
|
+
|
|
9
|
+
return contributors.filter(({ login }) => !ignore.includes(login));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getGitHubProfileUrl(login: string) {
|
|
13
|
+
return `https://github.com/${login}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getGitHubAvatarUrl(id: number) {
|
|
17
|
+
return `https://avatars.githubusercontent.com/u/${id}?s=64`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fetchGitHubContributors(
|
|
21
|
+
repo: string,
|
|
22
|
+
page = 1
|
|
23
|
+
): Promise<GitHubContributor[]> {
|
|
24
|
+
try {
|
|
25
|
+
const contributors = await fetchGitHubContributorsPage(repo, page);
|
|
26
|
+
|
|
27
|
+
if (contributors.length < PAGE_SIZE) return contributors;
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
...contributors,
|
|
31
|
+
...(await fetchGitHubContributors(repo, page + 1)),
|
|
32
|
+
];
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (page > 1) throw error;
|
|
35
|
+
warnGitHubFetchError(repo, error);
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchGitHubContributorsPage(repo: string, page: number) {
|
|
41
|
+
const response = await fetch(getGitHubContributorsUrl(repo, page), {
|
|
42
|
+
headers: getGitHubHeaders(import.meta.env.PUBLIC_GITHUB_TOKEN),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Request to fetch the GitHub contributors failed with status ${response.status}.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (await response.json()) as GitHubContributor[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getGitHubContributorsUrl(repo: string, page: number) {
|
|
55
|
+
return `https://api.github.com/repos/${repo}/contributors?per_page=${PAGE_SIZE}&page=${page}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getGitHubHeaders(token: string | undefined): Record<string, string> {
|
|
59
|
+
const headers = { "User-Agent": "astro-docs/1.0" };
|
|
60
|
+
|
|
61
|
+
if (!token) return headers;
|
|
62
|
+
|
|
63
|
+
return { ...headers, Authorization: getGitHubAuthorization(token) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getGitHubAuthorization(token: string) {
|
|
67
|
+
if (!token.includes(":")) return `Bearer ${token}`;
|
|
68
|
+
|
|
69
|
+
return `Basic ${Buffer.from(token, "binary").toString("base64")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function warnGitHubFetchError(repo: string, error: unknown) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
|
|
75
|
+
console.warn(
|
|
76
|
+
`[astro-contributors] Failed to fetch the contributors of the "${repo}" GitHub repository.\n${message}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface GitHubContributor {
|
|
81
|
+
contributions: number;
|
|
82
|
+
id: number;
|
|
83
|
+
login: string;
|
|
84
|
+
}
|
package/libs/link.ts
ADDED
package/libs/style.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function getStyleVariables(
|
|
2
|
+
variables: Record<`--${string}`, string | undefined>
|
|
3
|
+
) {
|
|
4
|
+
const definedVariables = Object.entries(variables).filter(isDefinedVariable);
|
|
5
|
+
|
|
6
|
+
return definedVariables.length > 0
|
|
7
|
+
? Object.fromEntries(definedVariables)
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isDefinedVariable(
|
|
12
|
+
entry: [string, string | undefined]
|
|
13
|
+
): entry is [string, string] {
|
|
14
|
+
return typeof entry[1] === "string" && entry[1].length > 0;
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-contributors",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Display a list of all contributors to your project",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"starlight",
|
|
7
|
-
"docs",
|
|
8
|
-
"contributors",
|
|
9
|
-
"component"
|
|
10
|
-
],
|
|
11
|
-
"homepage": "https://astro-contributors.trueberryless.org",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/trueberryless-org/astro-contributors/issues"
|
|
14
|
-
},
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "https://github.com/trueberryless-org/astro-contributors.git",
|
|
18
|
-
"directory": "packages/astro-contributors"
|
|
19
|
-
},
|
|
3
|
+
"version": "0.10.0",
|
|
20
4
|
"license": "MIT",
|
|
21
|
-
"
|
|
5
|
+
"description": "Astro components to display all contributors to your project.",
|
|
6
|
+
"author": "trueberryless <trueberryless@gmail.com> (https://felixs.dev)",
|
|
22
7
|
"type": "module",
|
|
23
8
|
"exports": {
|
|
24
9
|
".": "./index.ts",
|
|
25
10
|
"./package.json": "./package.json"
|
|
26
11
|
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"astro": "^7.3.5"
|
|
14
|
+
},
|
|
27
15
|
"peerDependencies": {
|
|
28
16
|
"astro": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
29
17
|
},
|
|
30
18
|
"engines": {
|
|
31
|
-
"node": ">=22.
|
|
19
|
+
"node": ">=22.12.0"
|
|
32
20
|
},
|
|
33
21
|
"publishConfig": {
|
|
34
22
|
"access": "public"
|
|
35
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"astro",
|
|
27
|
+
"astro-component",
|
|
28
|
+
"withastro",
|
|
29
|
+
"starlight",
|
|
30
|
+
"contributors",
|
|
31
|
+
"all-contributors",
|
|
32
|
+
"github",
|
|
33
|
+
"documentation"
|
|
34
|
+
],
|
|
35
|
+
"homepage": "https://astro-contributors.netlify.app",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/trueberryless-org/astro-contributors.git",
|
|
39
|
+
"directory": "packages/astro-contributors"
|
|
40
|
+
},
|
|
41
|
+
"bugs": "https://github.com/trueberryless-org/astro-contributors/issues"
|
|
36
42
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface RcContributor {
|
|
5
|
-
login: string;
|
|
6
|
-
name: string;
|
|
7
|
-
avatar_url: string;
|
|
8
|
-
profile: string;
|
|
9
|
-
contributions: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface AllContributorsRc {
|
|
13
|
-
contributors: RcContributor[];
|
|
14
|
-
[key: string]: any;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function findAllContributorsRc(): AllContributorsRc | null {
|
|
18
|
-
let currentDir = process.cwd();
|
|
19
|
-
|
|
20
|
-
while (currentDir) {
|
|
21
|
-
const filePath = path.join(currentDir, ".all-contributorsrc");
|
|
22
|
-
|
|
23
|
-
if (fs.existsSync(filePath)) {
|
|
24
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
25
|
-
const parsed = JSON.parse(content) as unknown;
|
|
26
|
-
if (
|
|
27
|
-
typeof parsed === "object" &&
|
|
28
|
-
parsed !== null &&
|
|
29
|
-
Array.isArray((parsed as { contributors?: unknown }).contributors)
|
|
30
|
-
) {
|
|
31
|
-
return parsed as AllContributorsRc;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const parentDir = path.dirname(currentDir);
|
|
37
|
-
if (parentDir === currentDir) break;
|
|
38
|
-
currentDir = parentDir;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function getAllContributorsFromRc(): RcContributor[] {
|
|
45
|
-
const data = findAllContributorsRc();
|
|
46
|
-
return Array.isArray(data?.contributors) ? data.contributors : [];
|
|
47
|
-
}
|
package/libs/getContributors.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { cachedFetch } from "./util-server";
|
|
2
|
-
|
|
3
|
-
export interface Contributor {
|
|
4
|
-
login: string;
|
|
5
|
-
id: number;
|
|
6
|
-
contributions: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface Commit {
|
|
10
|
-
author: {
|
|
11
|
-
login: string;
|
|
12
|
-
id: number;
|
|
13
|
-
};
|
|
14
|
-
commit: {
|
|
15
|
-
message: string;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const printError = (e: Error) =>
|
|
20
|
-
console.warn(
|
|
21
|
-
`[error] /src/util/getContributors.ts\n ${e?.message ?? e}`
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
async function recursiveFetch(endpoint: string, page = 1) {
|
|
25
|
-
try {
|
|
26
|
-
const queryParam = endpoint.includes("?") ? "&" : "?";
|
|
27
|
-
const pageSize = 100;
|
|
28
|
-
const url = `https://api.github.com/${endpoint}${queryParam}per_page=${pageSize}&page=${page}`;
|
|
29
|
-
|
|
30
|
-
const token = import.meta.env.PUBLIC_GITHUB_TOKEN;
|
|
31
|
-
|
|
32
|
-
const res = await cachedFetch(
|
|
33
|
-
url,
|
|
34
|
-
{
|
|
35
|
-
method: "GET",
|
|
36
|
-
headers: {
|
|
37
|
-
Authorization:
|
|
38
|
-
token && `Basic ${Buffer.from(token, "binary").toString("base64")}`,
|
|
39
|
-
"User-Agent": "astro-docs/1.0",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
{ duration: "15m" }
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const data = await res.json();
|
|
46
|
-
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Request to fetch endpoint failed. Reason: ${res.statusText}
|
|
50
|
-
Message: ${data?.message}`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Fetch more data recursively if there are more than GitHub’s per-page response limit.
|
|
55
|
-
if (data.length === pageSize) {
|
|
56
|
-
const rest = await recursiveFetch(endpoint, page + 1);
|
|
57
|
-
data.push(...rest);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return data;
|
|
61
|
-
} catch (e) {
|
|
62
|
-
printError(e as Error);
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function getAllContributors(repo: string, ignore: string[] = []) {
|
|
68
|
-
const endpoint = `repos/${repo}/contributors`;
|
|
69
|
-
const contributors: Contributor[] = await recursiveFetch(endpoint);
|
|
70
|
-
|
|
71
|
-
return contributors.filter(({ login }) => !ignore.includes(login));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function getTranslationContributors(
|
|
75
|
-
paths: string[],
|
|
76
|
-
repo: string
|
|
77
|
-
) {
|
|
78
|
-
const ignoredCommitKeywords =
|
|
79
|
-
/(en-only|typo|broken link|i18nReady|i18nIgnore)/i;
|
|
80
|
-
const contributors: Contributor[] = [];
|
|
81
|
-
|
|
82
|
-
for (const path of paths) {
|
|
83
|
-
const endpoint = `repos/${repo}/commits?path=${path}`;
|
|
84
|
-
const commits: Commit[] = await recursiveFetch(endpoint);
|
|
85
|
-
|
|
86
|
-
for (const { author, commit } of commits) {
|
|
87
|
-
if (ignoredCommitKeywords.test(commit.message) || !author) continue;
|
|
88
|
-
const contributor = contributors.find(
|
|
89
|
-
(contributor) => contributor.id === author.id
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (!contributor) {
|
|
93
|
-
contributors.push({
|
|
94
|
-
id: author.id,
|
|
95
|
-
login: author.login,
|
|
96
|
-
contributions: 1,
|
|
97
|
-
});
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
contributor.contributions += 1;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return contributors.sort((a, b) => b.contributions - a.contributions);
|
|
105
|
-
}
|
package/libs/util-server.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
This file contains server-side utilities using features that don't work in a browser.
|
|
3
|
-
Do not import this file from a hydrated client-side component.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type CachedFetchOptions = {
|
|
7
|
-
duration?: string;
|
|
8
|
-
verbose?: boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export async function cachedFetch(
|
|
12
|
-
url: string,
|
|
13
|
-
fetchOptions = {},
|
|
14
|
-
{ duration = "5m", verbose = false }: CachedFetchOptions = {}
|
|
15
|
-
) {
|
|
16
|
-
let status = 200;
|
|
17
|
-
let statusText = "OK";
|
|
18
|
-
let buffer: Buffer | undefined;
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const response = await fetch(url, fetchOptions);
|
|
22
|
-
status = response.status;
|
|
23
|
-
statusText = response.statusText;
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`Bad response for ${url} (${status}): ${statusText}`);
|
|
26
|
-
}
|
|
27
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
-
buffer = Buffer.from(arrayBuffer);
|
|
29
|
-
} catch (e: unknown) {
|
|
30
|
-
const error = e as Error;
|
|
31
|
-
const msg: string = error?.message || error.toString();
|
|
32
|
-
const matches = msg.match(/^Bad response for (.*) \(.*?\): (.*)$/);
|
|
33
|
-
status = parseInt(matches?.[2] || "") || 404;
|
|
34
|
-
statusText = matches?.[3] || msg;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
ok: status >= 200 && status <= 299,
|
|
39
|
-
status,
|
|
40
|
-
statusText,
|
|
41
|
-
body: buffer,
|
|
42
|
-
json: () => buffer && JSON.parse(buffer.toString()),
|
|
43
|
-
text: () => buffer?.toString(),
|
|
44
|
-
};
|
|
45
|
-
}
|