github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/website/src/plugins/changelog/index.js (about) 1 /** 2 * Copyright (c) Facebook, Inc. and its affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8 import path from 'path' 9 import fs from 'fs-extra' 10 import pluginContentBlog from '@docusaurus/plugin-content-blog' 11 import { aliasedSitePath, docuHash, normalizeUrl } from '@docusaurus/utils' 12 13 /** 14 * Multiple versions may be published on the same day, causing the order to be 15 * the reverse. Therefore, our publish time has a "fake hour" to order them. 16 */ 17 const publishTimes = new Set() 18 /** 19 * @type {Record<string, {name: string, url: string,alias: string, imageURL: string}>} 20 */ 21 const authorsMap = {} 22 23 /** 24 * @param {string} section 25 */ 26 function processSection(section) { 27 const title = section 28 .match(/\n## .*/)?.[0] 29 .trim() 30 .replace('## ', '') 31 .replace(/\[(.*)\]\(.*\) (.*)/, '$1 $2') 32 if (!title) { 33 return null 34 } 35 const content = section 36 .replace(/\n## .*/, '') 37 .trim() 38 .replace('running_woman', 'running') 39 40 let authors = content.match(/## Committers: \d.*/s) 41 if (authors) { 42 authors = authors[0] 43 .match(/- .*/g) 44 .map((line) => line.match(/- (?:(?<name>.*?) \()?\[@(?<alias>.*)\]\((?<url>.*?)\)\)?/).groups) 45 .map((author) => ({ 46 ...author, 47 name: author.name ?? author.alias, 48 imageURL: `https://github.com/${author.alias}.png`, 49 })) 50 .sort((a, b) => a.url.localeCompare(b.url)) 51 52 authors.forEach((author) => { 53 authorsMap[author.alias] = author 54 }) 55 } 56 57 let hour = 20 58 const date = title.match(/ \((?<date>.*)\)/)?.groups.date 59 while (publishTimes.has(`${date}T${hour}:00`)) { 60 hour -= 1 61 } 62 publishTimes.add(`${date}T${hour}:00`) 63 64 return { 65 title: title.replace(/ \(.*\)/, ''), 66 content: `--- 67 mdx: 68 format: md 69 date: ${`${date}T${hour}:00`} 70 ${authors ? `authors:\n${authors.map((author) => ` - '${author.alias}'`).join('\n')}` : ''} 71 --- 72 73 # ${title.replace(/ \(.*\)/, '')} 74 75 <!-- truncate --> 76 77 ${content.replace(/####/g, '##')}`, 78 } 79 } 80 81 /** 82 * @param {import('@docusaurus/types').LoadContext} context 83 * @returns {import('@docusaurus/types').Plugin} 84 */ 85 export default async function ChangelogPlugin(context, options) { 86 const generateDir = path.join(context.generatedFilesDir, 'changelog-plugin/source') 87 const blogPlugin = await pluginContentBlog.default(context, { 88 ...options, 89 path: generateDir, 90 id: 'changelog', 91 blogListComponent: '@theme/ChangelogList', 92 blogPostComponent: '@theme/ChangelogPage', 93 }) 94 const changelogPath = path.join(__dirname, '../../../../CHANGELOG.md') 95 return { 96 ...blogPlugin, 97 name: 'changelog-plugin', 98 async loadContent() { 99 await fs.remove(generateDir) 100 const fileContent = await fs.readFile(changelogPath, 'utf-8') 101 const sections = fileContent 102 .split(/(?=\n## )/) 103 .map(processSection) 104 .filter(Boolean) 105 await Promise.all( 106 sections.map((section) => 107 fs.outputFile(path.join(generateDir, `${section.title}.md`), section.content), 108 ), 109 ) 110 const authorsPath = path.join(generateDir, 'authors.json') 111 await fs.outputFile(authorsPath, JSON.stringify(authorsMap, null, 2)) 112 const content = await blogPlugin.loadContent() 113 content.blogPosts.forEach((post, index) => { 114 const pageIndex = Math.floor(index / options.postsPerPage) 115 const { metadata } = post 116 metadata.listPageLink = normalizeUrl([ 117 context.baseUrl, 118 options.routeBasePath, 119 pageIndex === 0 ? '/' : `/page/${pageIndex + 1}`, 120 ]) 121 }) 122 return content 123 }, 124 configureWebpack(...args) { 125 const config = blogPlugin.configureWebpack(...args) 126 const pluginDataDirRoot = path.join(context.generatedFilesDir, 'changelog-plugin', 'default') 127 // Redirect the metadata path to our folder 128 const mdxLoader = config.module.rules[0].use[0] 129 mdxLoader.options.metadataPath = (mdxPath) => { 130 // Note that metadataPath must be the same/in-sync as 131 // the path from createData for each MDX. 132 const aliasedPath = aliasedSitePath(mdxPath, context.siteDir) 133 return path.join(pluginDataDirRoot, `${docuHash(aliasedPath)}.json`) 134 } 135 return config 136 }, 137 getThemePath() { 138 return './theme' 139 }, 140 getPathsToWatch() { 141 // Don't watch the generated dir 142 return [changelogPath] 143 }, 144 } 145 } 146 147 export const { validateOptions } = pluginContentBlog