patch-1
@reggi/path-to-regexp
dependabot/npm_and_yarn/main/copy-to-clipboard-4.0.2
dependabot/npm_and_yarn/main/eslint-10.4.0
dependabot/npm_and_yarn/main/npmcli/eslint-config-7.0.0
dependabot/npm_and_yarn/main/proc-log-7.0.0
dependabot/npm_and_yarn/npm_and_yarn-826852524d
dependabot/npm_and_yarn/npm_and_yarn-ab9a7f4bc2
deprecate-totp-2fa
dhei/classic-tokens
gat-bypass-2fa-docs
jpg619/fix-accessibility-content-flow
jpg619/version-bump-tar-2
kartykp/gat-bypass-2fa-docs
kartykp/upgrade-path-to-regex
main
maitxn/version-bump-tar
patch-1
reggi/cache-based-on-version
reggi/dev-engines
reggi/fix-transform-prettier
reggi/overrides
update-search-sensitivity
| 1 | import {join, relative} from 'path' |
| 2 | import {Octokit as CoreOctokit} from '@octokit/rest' |
| 3 | import {throttling} from '@octokit/plugin-throttling' |
| 4 | import {retry} from '@octokit/plugin-retry' |
| 5 | import webpackConfig from './webpack.config.js' |
| 6 | |
| 7 | const CI = !!process.env.CI |
| 8 | const CWD = process.cwd() |
| 9 | const SRC = join(CWD, 'src') |
| 10 | const REPO_URL = 'https://github.com/npm/documentation' |
| 11 | const NWO = new URL(REPO_URL).pathname.slice(1) |
| 12 | const REPO_BRANCH = 'main' |
| 13 | const TEST_CONTRIBUTORS = [ |
| 14 | { |
| 15 | author: {login: 'mona'}, |
| 16 | commit: {author: {date: new Date('2023-03-21').toJSON()}}, |
| 17 | html_url: REPO_URL, |
| 18 | }, |
| 19 | ] |
| 20 | |
| 21 | const createOctokit = ({reporter}) => { |
| 22 | const Octokit = CoreOctokit.plugin(throttling).plugin(retry) |
| 23 | return new Octokit({ |
| 24 | log: { |
| 25 | debug: () => {}, |
| 26 | info: reporter.info, |
| 27 | warn: reporter.warn, |
| 28 | error: reporter.error, |
| 29 | }, |
| 30 | auth: process.env.GITHUB_TOKEN, |
| 31 | throttle: { |
| 32 | onRateLimit: (retryAfter, options, {log}, retryCount) => { |
| 33 | log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) |
| 34 | if (retryCount < 2) { |
| 35 | log.info(`Retrying after ${retryAfter} seconds`) |
| 36 | return true |
| 37 | } |
| 38 | }, |
| 39 | onSecondaryRateLimit: (_, options, {log}) => { |
| 40 | log.warn(`SecondaryRateLimit detected for request ${options.method} ${options.url}`) |
| 41 | }, |
| 42 | }, |
| 43 | }) |
| 44 | } |
| 45 | |
| 46 | export const onCreateNode = ({node, actions, getNode}) => { |
| 47 | if (node.internal.type === 'Mdx') { |
| 48 | const {name, relativeDirectory: dir} = getNode(node.parent) |
| 49 | |
| 50 | // These paths are unchanged: |
| 51 | // - directory indexes |
| 52 | // - all cli paths |
| 53 | // - all policies paths |
| 54 | if (name === 'index' || dir.startsWith('cli/') || dir.startsWith('policies')) { |
| 55 | return |
| 56 | } |
| 57 | |
| 58 | // otherwise, omit the directory path and use the filename as the slug |
| 59 | actions.createNodeField({ |
| 60 | name: 'slug', |
| 61 | node, |
| 62 | value: name, |
| 63 | }) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | export const onCreateWebpackConfig = ({stage, actions}) => { |
| 68 | actions.setWebpackConfig({ |
| 69 | ...webpackConfig, |
| 70 | }) |
| 71 | |
| 72 | if (stage === `build-javascript`) { |
| 73 | actions.setWebpackConfig({ |
| 74 | devtool: false, |
| 75 | }) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | export const createSchemaCustomization = ({actions: {createTypes}}) => { |
| 80 | createTypes(` |
| 81 | type Mdx implements Node { |
| 82 | frontmatter: MdxFrontmatter |
| 83 | fields: MdxFields |
| 84 | } |
| 85 | type MdxFrontmatter { |
| 86 | edit_on_github: Boolean, |
| 87 | github_branch: String, |
| 88 | github_path: String, |
| 89 | github_repo: String, |
| 90 | redirect_from: [String], |
| 91 | slug: String, |
| 92 | title: String |
| 93 | } |
| 94 | type MdxFields { |
| 95 | slug: String |
| 96 | } |
| 97 | `) |
| 98 | } |
| 99 | |
| 100 | export const createPages = async ({graphql, actions, reporter}) => { |
| 101 | const response = await graphql(` |
| 102 | { |
| 103 | allMdx { |
| 104 | nodes { |
| 105 | id |
| 106 | internal { |
| 107 | contentFilePath |
| 108 | } |
| 109 | fields { |
| 110 | slug |
| 111 | } |
| 112 | frontmatter { |
| 113 | edit_on_github |
| 114 | github_branch |
| 115 | github_path |
| 116 | github_repo |
| 117 | redirect_from |
| 118 | slug |
| 119 | title |
| 120 | } |
| 121 | tableOfContents |
| 122 | parent { |
| 123 | ... on File { |
| 124 | relativeDirectory |
| 125 | name |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | `) |
| 132 | |
| 133 | if (response.errors) { |
| 134 | reporter.panic('Error getting allMdx', response.errors) |
| 135 | return |
| 136 | } |
| 137 | |
| 138 | const octokit = createOctokit({reporter}) |
| 139 | |
| 140 | // Turn every MDX file into a page. |
| 141 | return Promise.all( |
| 142 | response.data.allMdx.nodes.map(async node => { |
| 143 | try { |
| 144 | node.fields ||= {} |
| 145 | node.frontmatter ||= {} |
| 146 | node.frontmatter.redirect_from ||= [] |
| 147 | node.tableOfContents ||= {} |
| 148 | node.tableOfContents.items ||= [] |
| 149 | return await createPage(node, {actions, reporter, octokit}) |
| 150 | } catch (err) { |
| 151 | reporter.panic(`Error creating page: ${JSON.stringify(node, null, 2)}`, err) |
| 152 | } |
| 153 | }), |
| 154 | ) |
| 155 | } |
| 156 | |
| 157 | const createPage = async ( |
| 158 | { |
| 159 | id, |
| 160 | internal: {contentFilePath}, |
| 161 | fields: {slug}, |
| 162 | frontmatter = {}, |
| 163 | tableOfContents = {}, |
| 164 | parent: {relativeDirectory, name: parentName}, |
| 165 | }, |
| 166 | {actions, reporter, octokit}, |
| 167 | ) => { |
| 168 | const path = relative(CWD, contentFilePath) |
| 169 | // sites can programmatically override slug, that takes priority |
| 170 | // then a slug specified in frontmatter |
| 171 | // finally, we'll just use the path on disk |
| 172 | const pageSlug = |
| 173 | slug ?? frontmatter.slug ?? join(relativeDirectory, parentName === 'index' ? '/' : parentName).replace(/\\/g, '/') |
| 174 | |
| 175 | const context = { |
| 176 | mdxId: id, |
| 177 | tableOfContents: getTableOfConents(tableOfContents), |
| 178 | } |
| 179 | // edit_on_github: false in frontmatter will not include editUrl and contributors |
| 180 | // on the page. this is used for policy pages as well as some index pages that don't |
| 181 | // have any editable content |
| 182 | if (frontmatter.edit_on_github !== false) { |
| 183 | context.editUrl = getRepo(path, frontmatter).replace(`https://github.com/{nwo}/edit/{branch}/{path}`) |
| 184 | Object.assign(context, await fetchContributors(path, frontmatter, {reporter, octokit})) |
| 185 | } |
| 186 | |
| 187 | actions.createPage({ |
| 188 | path: pageSlug, |
| 189 | component: `${join(SRC, 'head.js')}?__contentFilePath=${contentFilePath}`, |
| 190 | context, |
| 191 | }) |
| 192 | |
| 193 | for (const from of frontmatter.redirect_from) { |
| 194 | actions.createRedirect({ |
| 195 | fromPath: from, |
| 196 | toPath: `/${pageSlug}`, |
| 197 | isPermanent: true, |
| 198 | redirectInBrowser: true, |
| 199 | }) |
| 200 | |
| 201 | if (pageSlug.startsWith('cli/') && !from.endsWith('index')) { |
| 202 | actions.createRedirect({ |
| 203 | fromPath: `${from}.html`, |
| 204 | toPath: `/${pageSlug}`, |
| 205 | isPermanent: true, |
| 206 | redirectInBrowser: true, |
| 207 | }) |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | const getTableOfConents = ({items}) => { |
| 213 | // Fix some old CLI pages which have mismatched headings at the top level. |
| 214 | // All top level headings should be the same level. |
| 215 | const tableOfContents = items.reduce((acc, item) => { |
| 216 | if (!item.url && Array.isArray(item.items)) { |
| 217 | acc.push(...item.items) |
| 218 | } else { |
| 219 | acc.push(item) |
| 220 | } |
| 221 | return acc |
| 222 | }, []) |
| 223 | |
| 224 | if (tableOfContents.length) { |
| 225 | return tableOfContents |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | const getRepo = (path, fm) => { |
| 230 | const result = { |
| 231 | nwo: NWO, |
| 232 | branch: REPO_BRANCH, |
| 233 | ...(fm.github_repo ? {nwo: fm.github_repo} : {}), |
| 234 | ...(fm.github_branch ? {branch: fm.github_branch} : {}), |
| 235 | path: fm.github_path || path, |
| 236 | } |
| 237 | const [owner, repo] = result.nwo.split('/') |
| 238 | result.owner = owner |
| 239 | result.repo = repo |
| 240 | result.replace = str => str.replace(/\{([a-z]+)\}/g, (_, name) => result[name]) |
| 241 | return result |
| 242 | } |
| 243 | |
| 244 | let warnOnNoContributors = true |
| 245 | const fetchContributors = async (path, fm, {reporter, octokit}) => { |
| 246 | const noAuth = (await octokit.auth()).type === 'unauthenticated' |
| 247 | if (noAuth) { |
| 248 | const msg = `Cannot fetch contributors without GitHub authentication.` |
| 249 | if (CI) { |
| 250 | reporter.panic(msg) |
| 251 | return |
| 252 | } |
| 253 | |
| 254 | if (warnOnNoContributors) { |
| 255 | warnOnNoContributors = false |
| 256 | reporter.warn(`${msg} Pages will be include test contributor data.`) |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | try { |
| 261 | const repo = getRepo(path, fm) |
| 262 | const resp = noAuth |
| 263 | ? {data: TEST_CONTRIBUTORS} |
| 264 | : await octokit.rest.repos.listCommits({ |
| 265 | repo: repo.repo, |
| 266 | owner: repo.owner, |
| 267 | path: repo.path, |
| 268 | sha: repo.branch, |
| 269 | per_page: 100, |
| 270 | }) |
| 271 | |
| 272 | const contributors = new Set() |
| 273 | let latestCommit = null |
| 274 | |
| 275 | for (const item of resp.data) { |
| 276 | if (item.author?.login) { |
| 277 | contributors.add(item.author.login) |
| 278 | if (!latestCommit) { |
| 279 | latestCommit = { |
| 280 | login: item.author.login, |
| 281 | date: item.commit.author.date, |
| 282 | url: item.html_url, |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | return { |
| 289 | contributors: [...contributors], |
| 290 | latestCommit, |
| 291 | } |
| 292 | } catch (err) { |
| 293 | reporter[CI ? 'panic' : 'error'](`Error fetching contributors for ${path}`, err) |
| 294 | } |
| 295 | } |