Skip to content

Commit caacd02

Browse files
committed
Add contributors to pages
1 parent 1fc8539 commit caacd02

File tree

7 files changed

+756
-294
lines changed

7 files changed

+756
-294
lines changed

gatsby-node.js

Lines changed: 146 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
1-
const {resolve, join: _join, relative} = require('path')
2-
const join = (...paths) => _join(...paths).replace(/\\/g, '/')
1+
const {resolve, join, relative} = require('path')
2+
const {Octokit: CoreOctokit} = require('@octokit/rest')
3+
const {throttling} = require('@octokit/plugin-throttling')
4+
const {retry} = require('@octokit/plugin-retry')
35

4-
const SHOW_CONTRIBUTORS = false
5-
const REPO = {
6-
url: 'https://github.com/npm/documentation',
7-
defaultBranch: 'main',
6+
const PROD = process.env.NODE_ENV === 'production'
7+
const REPO_URL = 'https://github.com/npm/documentation'
8+
const NWO = new URL(REPO_URL).pathname.slice(1)
9+
const REPO_BRANCH = 'main'
10+
const CWD = process.cwd()
11+
const TEST_CONTRIBUTORS = [
12+
{
13+
author: {login: 'mona'},
14+
commit: {author: {date: new Date('2023-03-21').toJSON()}},
15+
html_url: REPO_URL,
16+
},
17+
]
18+
19+
const createOctokit = ({reporter}) => {
20+
const Octokit = CoreOctokit.plugin(throttling).plugin(retry)
21+
return new Octokit({
22+
log: {
23+
debug: () => {},
24+
info: reporter.info,
25+
warn: reporter.warn,
26+
error: reporter.error,
27+
},
28+
auth: process.env.GITHUB_TOKEN,
29+
throttle: {
30+
onRateLimit: (retryAfter, options, {log}, retryCount) => {
31+
log.warn(`Request quota exhausted for request ${options.method} ${options.url}`)
32+
if (retryCount < 2) {
33+
log.info(`Retrying after ${retryAfter} seconds`)
34+
return true
35+
}
36+
},
37+
onSecondaryRateLimit: (_, options, {log}) => {
38+
log.warn(`SecondaryRateLimit detected for request ${options.method} ${options.url}`)
39+
},
40+
},
41+
})
842
}
943

1044
exports.onCreateNode = ({node, actions, getNode}) => {
@@ -60,8 +94,8 @@ exports.createSchemaCustomization = ({actions: {createTypes}}) => {
6094
`)
6195
}
6296

63-
exports.createPages = async ({graphql, actions}) => {
64-
const {data} = await graphql(`
97+
exports.createPages = async ({graphql, actions, reporter}) => {
98+
const response = await graphql(`
6599
{
66100
allMdx {
67101
nodes {
@@ -93,140 +127,152 @@ exports.createPages = async ({graphql, actions}) => {
93127
}
94128
`)
95129

130+
if (response.errors) {
131+
reporter.panic('Error getting allMdx', response.errors)
132+
return
133+
}
134+
135+
const octokit = createOctokit({reporter})
136+
96137
// Turn every MDX file into a page.
97-
return Promise.all(data.allMdx.nodes.map(node => createPage(node, actions)))
138+
return Promise.all(
139+
response.data.allMdx.nodes.map(async node => {
140+
try {
141+
node.fields ||= {}
142+
node.frontmatter ||= {}
143+
node.frontmatter.redirect_from ||= []
144+
node.tableOfContents ||= {}
145+
node.tableOfContents.items ||= []
146+
return await createPage(node, {actions, reporter, octokit})
147+
} catch (err) {
148+
reporter.panic(`Error creating page: ${JSON.stringify(node, null, 2)}`, err)
149+
}
150+
}),
151+
)
98152
}
99153

100-
async function createPage(
154+
const createPage = async (
101155
{
102156
id,
103157
internal: {contentFilePath},
104-
fields: {slug} = {},
158+
fields: {slug},
105159
frontmatter = {},
106160
tableOfContents = {},
107161
parent: {relativeDirectory, name: parentName},
108162
},
109-
actions,
110-
) {
163+
{actions, reporter, octokit},
164+
) => {
165+
const path = relative(CWD, contentFilePath)
111166
// sites can programmatically override slug, that takes priority
112167
// then a slug specified in frontmatter
113168
// finally, we'll just use the path on disk
114-
const pagePath = slug ?? frontmatter.slug ?? join(relativeDirectory, parentName === 'index' ? '/' : parentName)
169+
const pageSlug =
170+
slug ?? frontmatter.slug ?? join(relativeDirectory, parentName === 'index' ? '/' : parentName).replace(/\\/g, '/')
115171

116-
const relativePath = relative(process.cwd(), contentFilePath)
117-
118-
const editUrl = getEditUrl(REPO, relativePath, frontmatter)
119-
120-
const contributors = SHOW_CONTRIBUTORS ? await fetchContributors(REPO, relativePath, frontmatter) : {}
121-
122-
// Fix some old CLI pages which have mismatched headings at the top level.
123-
// All top level headings should be the same level.
124-
const toc = tableOfContents.items?.reduce((acc, item) => {
125-
if (!item.url && Array.isArray(item.items)) {
126-
acc.push(...item.items)
127-
} else {
128-
acc.push(item)
129-
}
130-
return acc
131-
}, [])
172+
const context = {
173+
mdxId: id,
174+
tableOfContents: getTableOfConents(tableOfContents),
175+
repositoryUrl: REPO_URL,
176+
}
177+
// edit_on_github: false in frontmatter will not include editUrl and contributors
178+
// on the page. this is used for policy pages as well as some index pages that don't
179+
// have any editable content
180+
if (frontmatter.edit_on_github !== false) {
181+
context.editUrl = getRepo(path, frontmatter).replace(`https://github.com/{nwo}/edit/{branch}/{path}`)
182+
Object.assign(context, await fetchContributors(path, frontmatter, {reporter, octokit}))
183+
}
132184

133185
actions.createPage({
134-
path: pagePath,
186+
path: pageSlug,
135187
component: contentFilePath,
136-
context: {
137-
mdxId: id,
138-
editUrl,
139-
contributors,
140-
tableOfContents: toc,
141-
repositoryUrl: REPO.url,
142-
},
188+
context,
143189
})
144190

145-
for (const from of frontmatter.redirect_from ?? []) {
191+
for (const from of frontmatter.redirect_from) {
146192
actions.createRedirect({
147193
fromPath: from,
148-
toPath: `/${pagePath}`,
194+
toPath: `/${pageSlug}`,
149195
isPermanent: true,
150196
redirectInBrowser: true,
151197
})
152198

153-
if (pagePath.startsWith('cli/') && !from.endsWith('index')) {
199+
if (pageSlug.startsWith('cli/') && !from.endsWith('index')) {
154200
actions.createRedirect({
155201
fromPath: `${from}.html`,
156-
toPath: `/${pagePath}`,
202+
toPath: `/${pageSlug}`,
157203
isPermanent: true,
158204
redirectInBrowser: true,
159205
})
160206
}
161207
}
162208
}
163209

164-
function getGitHubData(repo, overrideData, filePath) {
165-
const gh = {
166-
nwo: new URL(repo.url).pathname.slice(1).split('/'),
167-
branch: 'master',
168-
}
169-
170-
if (overrideData.github_repo) {
171-
gh.nwo = overrideData.github_repo
172-
}
173-
174-
if (overrideData.github_branch) {
175-
gh.branch = overrideData.github_branch
176-
} else if (repo.defaultBranch) {
177-
gh.branch = repo.defaultBranch
178-
}
210+
const getTableOfConents = ({items}) => {
211+
// Fix some old CLI pages which have mismatched headings at the top level.
212+
// All top level headings should be the same level.
213+
const tableOfContents = items.reduce((acc, item) => {
214+
if (!item.url && Array.isArray(item.items)) {
215+
acc.push(...item.items)
216+
} else {
217+
acc.push(item)
218+
}
219+
return acc
220+
}, [])
179221

180-
if (overrideData.github_path) {
181-
gh.path = overrideData.github_path
182-
} else {
183-
gh.path = filePath
222+
if (tableOfContents.length) {
223+
return tableOfContents
184224
}
185-
186-
return gh
187225
}
188226

189-
function getEditUrl(repo, filePath, overrideData = {}) {
190-
if (overrideData.edit_on_github === false) {
191-
return null
227+
const getRepo = (path, fm) => {
228+
const result = {
229+
nwo: NWO,
230+
branch: REPO_BRANCH,
231+
...(fm.github_repo ? {nwo: fm.github_repo} : {}),
232+
...(fm.github_branch ? {branch: fm.github_branch} : {}),
233+
path: fm.github_path || path,
192234
}
193-
194-
const {nwo, branch, path} = getGitHubData(repo, overrideData, filePath)
195-
return `https://github.com/${nwo}/edit/${branch}/${path}`
235+
const [owner, repo] = result.nwo.split('/')
236+
result.owner = owner
237+
result.repo = repo
238+
result.replace = str => str.replace(/\{([a-z]+)\}/g, (_, name) => result[name])
239+
return result
196240
}
197241

198-
const CONTRIBUTOR_CACHE = new Map()
199-
200-
async function fetchContributors(repo, filePath, overrideData = {}) {
201-
if (!process.env.GITHUB_TOKEN) {
202-
console.warn('Skipping fetching contributors because no github token was set')
203-
return
204-
}
205-
206-
const gh = getGitHubData(repo, overrideData, filePath)
207-
const key = JSON.stringify(gh)
242+
let warnOnNoContributors = true
243+
const fetchContributors = async (path, fm, {reporter, octokit}) => {
244+
const noAuth = (await octokit.auth()).type === 'unauthenticated'
245+
if (noAuth) {
246+
const msg = `Cannot fetch contributors without GitHub authentication.`
247+
if (PROD) {
248+
reporter.panic(msg)
249+
return
250+
}
208251

209-
const cached = CONTRIBUTOR_CACHE.get(key)
210-
if (cached) {
211-
return cached
252+
if (warnOnNoContributors) {
253+
warnOnNoContributors = false
254+
reporter.warn(`${msg} Pages will be include test contributor data.`)
255+
}
212256
}
213257

214258
try {
215-
const resp = await fetch(
216-
`https://api.github.com/repos/${gh.nwo}/commits?path=${gh.path}&sha=${gh.branch}&per_page=100`,
217-
{
218-
headers: {
219-
Authorization: `token ${process.env.GITHUB_TOKEN}`,
220-
},
221-
},
222-
)
259+
const repo = getRepo(path, fm)
260+
const resp = noAuth
261+
? {data: TEST_CONTRIBUTORS}
262+
: await octokit.rest.repos.listCommits({
263+
repo: repo.repo,
264+
owner: repo.owner,
265+
path: repo.path,
266+
sha: repo.branch,
267+
per_page: 100,
268+
})
223269

224-
const logins = new Set()
270+
const contributors = new Set()
225271
let latestCommit = null
226272

227-
for (const item of await resp.json().then(r => r.data)) {
273+
for (const item of resp.data) {
228274
if (item.author?.login) {
229-
logins.add(item.author.login)
275+
contributors.add(item.author.login)
230276
if (!latestCommit) {
231277
latestCommit = {
232278
login: item.author.login,
@@ -237,11 +283,12 @@ async function fetchContributors(repo, filePath, overrideData = {}) {
237283
}
238284
}
239285

240-
const result = {logins: [...logins], latestCommit}
241-
CONTRIBUTOR_CACHE.set(key, result)
242-
return result
243-
} catch (error) {
244-
console.error(`[ERROR] Unable to fetch contributors for ${filePath}. ${error.message}`)
245-
return []
286+
return {
287+
contributors: [...contributors],
288+
latestCommit,
289+
}
290+
} catch (err) {
291+
reporter[PROD ? 'panic' : 'error'](`Error fetching contributors for ${path}`, err)
292+
return
246293
}
247294
}

0 commit comments

Comments
 (0)