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