1import _ from 'lodash'; 2import path from 'path'; 3import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils'; 4 5import { 6 PluginOptions, 7 Release, 8 ReleaseContent, 9} from './types'; 10import { 11 LoadContext, 12 PluginContentLoadedActions, 13 ConfigureWebpackUtils, 14 Plugin, 15} from '@docusaurus/types'; 16import {Configuration, Loader} from 'webpack'; 17import {generateReleases} from './releaseUtils'; 18 19const DEFAULT_OPTIONS: PluginOptions = { 20 path: 'releases', // Path to data on filesystem, relative to site dir. 21 routeBasePath: 'releases', // URL Route. 22 include: ['*.md', '*.mdx'], // Extensions to include. 23 releaseComponent: '@theme/ReleasePage', 24 releaseDownloadComponent: '@theme/ReleaseDownloadPage', 25 remarkPlugins: [], 26 rehypePlugins: [], 27 truncateMarker: /<!--\s*(truncate)\s*-->/, // Regex. 28}; 29 30export default function pluginContentRelease( 31 context: LoadContext, 32 opts: Partial<PluginOptions>, 33): Plugin<ReleaseContent | null> { 34 const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts}; 35 const {siteDir, generatedFilesDir} = context; 36 const contentPath = path.resolve(siteDir, options.path); 37 const dataDir = path.join( 38 generatedFilesDir, 39 'releases', 40 ); 41 let releases: Release[] = []; 42 43 return { 44 name: 'releases', 45 46 getPathsToWatch() { 47 const {include = []} = options; 48 const releasesGlobPattern = include.map(pattern => `${contentPath}/${pattern}`); 49 return [...releasesGlobPattern]; 50 }, 51 52 async loadContent() { 53 // 54 // Releases 55 // 56 57 releases = await generateReleases(contentPath, context, options); 58 59 // Colocate next and prev metadata. 60 releases.forEach((release, index) => { 61 const prevItem = index > 0 ? releases[index - 1] : null; 62 if (prevItem) { 63 release.metadata.prevItem = { 64 title: prevItem.metadata.title, 65 permalink: prevItem.metadata.permalink, 66 }; 67 } 68 69 const nextItem = index < releases.length - 1 ? releases[index + 1] : null; 70 if (nextItem) { 71 release.metadata.nextItem = { 72 title: nextItem.metadata.title, 73 permalink: nextItem.metadata.permalink, 74 }; 75 } 76 }); 77 78 // 79 // Return 80 // 81 82 return { 83 releases, 84 }; 85 }, 86 87 async contentLoaded({ 88 content: releaseContents, 89 actions, 90 }: { 91 content: ReleaseContent; 92 actions: PluginContentLoadedActions; 93 }) { 94 if (!releaseContents) { 95 return; 96 } 97 98 // 99 // Prepare 100 // 101 102 const { 103 releaseComponent, 104 releaseDownloadComponent, 105 } = options; 106 107 const {addRoute, createData} = actions; 108 const {releases} = releaseContents; 109 110 // 111 // Release pages 112 // 113 114 await Promise.all( 115 releases.map(async release => { 116 const {metadata} = release; 117 await createData( 118 // Note that this created data path must be in sync with 119 // metadataPath provided to mdx-loader. 120 `${docuHash(metadata.source)}.json`, 121 JSON.stringify(metadata, null, 2), 122 ); 123 124 addRoute({ 125 path: metadata.permalink, 126 component: releaseComponent, 127 exact: true, 128 modules: { 129 content: metadata.source, 130 }, 131 }); 132 133 let downloadPath = normalizeUrl([metadata.permalink, 'download']); 134 135 addRoute({ 136 path: downloadPath, 137 component: releaseDownloadComponent, 138 exact: true, 139 modules: { 140 content: metadata.source, 141 }, 142 }); 143 }), 144 ); 145 }, 146 147 configureWebpack( 148 _config: Configuration, 149 isServer: boolean, 150 {getBabelLoader, getCacheLoader}: ConfigureWebpackUtils, 151 ) { 152 const {rehypePlugins, remarkPlugins, truncateMarker} = options; 153 return { 154 resolve: { 155 alias: { 156 '~release': dataDir, 157 }, 158 }, 159 module: { 160 rules: [ 161 { 162 test: /(\.mdx?)$/, 163 include: [contentPath], 164 use: [ 165 getCacheLoader(isServer), 166 getBabelLoader(isServer), 167 { 168 loader: '@docusaurus/mdx-loader', 169 options: { 170 remarkPlugins, 171 rehypePlugins, 172 // Note that metadataPath must be the same/in-sync as 173 // the path from createData for each MDX. 174 metadataPath: (mdxPath: string) => { 175 const aliasedSource = aliasedSitePath(mdxPath, siteDir); 176 return path.join( 177 dataDir, 178 `${docuHash(aliasedSource)}.json`, 179 ); 180 }, 181 }, 182 }, 183 { 184 loader: path.resolve(__dirname, './markdownLoader.js'), 185 options: { 186 siteDir, 187 contentPath, 188 truncateMarker, 189 releases, 190 }, 191 }, 192 ].filter(Boolean) as Loader[], 193 }, 194 ], 195 }, 196 }; 197 }, 198 199 injectHtmlTags() { 200 return {} 201 }, 202 }; 203} 204