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
1044exports . 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