From 6a814ac64a9d525959c77c29bb69a1eb562fa3ec Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 2 Jun 2019 20:37:22 -0700 Subject: [PATCH] refactor(v2): change plugin api (#1547) * misc(v2): new plugin format example * refactor(v2): make all plugins a function returning objects * misc: add CHANGELOG * misc(v2): update CHANGELOG * misc(v2): fix tests * misc(v2): convert swizzle command * misc(v2): convert sitemap back to commonjs --- .../src/index.js | 628 +++++++++--------- .../src/__tests__/index.test.js | 5 +- .../src/index.js | 294 ++++---- .../src/__tests__/index.test.js | 4 +- .../src/index.js | 119 ++-- .../src/__tests__/createSitemap.test.js | 30 + .../src/__tests__/index.test.js | 36 - .../src/createSitemap.js | 33 + .../docusaurus-plugin-sitemap/src/index.js | 70 +- .../docusaurus-preset-classic/src/index.js | 12 +- .../docusaurus-theme-classic/src/index.js | 25 +- .../src/index.js | 25 +- .../docusaurus/{CHANGES.md => CHANGELOG.md} | 10 +- packages/docusaurus/src/commands/swizzle.ts | 7 +- .../custom-site/docusaurus.config.js | 4 +- .../simple-site/docusaurus.config.js | 4 +- .../docusaurus/src/server/plugins/index.ts | 19 +- website/docs/plugins-api.md | 109 +-- 18 files changed, 709 insertions(+), 725 deletions(-) create mode 100644 packages/docusaurus-plugin-sitemap/src/__tests__/createSitemap.test.js delete mode 100644 packages/docusaurus-plugin-sitemap/src/__tests__/index.test.js create mode 100644 packages/docusaurus-plugin-sitemap/src/createSitemap.js rename packages/docusaurus/{CHANGES.md => CHANGELOG.md} (82%) diff --git a/packages/docusaurus-plugin-content-blog/src/index.js b/packages/docusaurus-plugin-content-blog/src/index.js index bd2d814aec..8788118de7 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.js +++ b/packages/docusaurus-plugin-content-blog/src/index.js @@ -31,336 +31,334 @@ const DEFAULT_OPTIONS = { blogTagsPostsComponent: '@theme/BlogTagsPostsPage', }; -class DocusaurusPluginContentBlog { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - this.contentPath = path.resolve(this.context.siteDir, this.options.path); - } +module.exports = function(context, opts) { + const options = {...DEFAULT_OPTIONS, ...opts}; + const contentPath = path.resolve(context.siteDir, options.path); - getName() { - return 'docusaurus-plugin-content-blog'; - } + return { + name: 'docusaurus-plugin-content-blog', - getPathsToWatch() { - const {include = []} = this.options; - const globPattern = include.map( - pattern => `${this.contentPath}/${pattern}`, - ); - return [...globPattern]; - } + getPathsToWatch() { + const {include = []} = options; + const globPattern = include.map(pattern => `${contentPath}/${pattern}`); + return [...globPattern]; + }, - // Fetches blog contents and returns metadata for the necessary routes. - async loadContent() { - const {postsPerPage, include, routeBasePath} = this.options; - const {siteConfig} = this.context; - const blogDir = this.contentPath; + // Fetches blog contents and returns metadata for the necessary routes. + async loadContent() { + const {postsPerPage, include, routeBasePath} = options; + const {siteConfig} = context; + const blogDir = contentPath; - if (!fs.existsSync(blogDir)) { - return null; - } + if (!fs.existsSync(blogDir)) { + return null; + } - const {baseUrl} = siteConfig; - const blogFiles = await globby(include, { - cwd: blogDir, - }); - - const blogPosts = []; - - await Promise.all( - blogFiles.map(async relativeSource => { - const source = path.join(blogDir, relativeSource); - - const blogFileName = path.basename(relativeSource); - // Extract, YYYY, MM, DD from the file name. - const filePathDateArr = blogFileName.split('-'); - const date = new Date( - `${filePathDateArr[0]}-${filePathDateArr[1]}-${ - filePathDateArr[2] - }T06:00:00.000Z`, - ); - - const fileString = await fs.readFile(source, 'utf-8'); - const {frontMatter, excerpt} = parse(fileString); - - blogPosts.push({ - id: blogFileName, - metadata: { - permalink: normalizeUrl([ - baseUrl, - routeBasePath, - fileToUrl(blogFileName), - ]), - source, - description: frontMatter.description || excerpt, - date, - tags: frontMatter.tags, - title: frontMatter.title || blogFileName, - }, - }); - }), - ); - blogPosts.sort((a, b) => b.metadata.date - a.metadata.date); - - // Blog pagination routes. - // Example: `/blog`, `/blog/page/1`, `/blog/page/2` - const totalCount = blogPosts.length; - const numberOfPages = Math.ceil(totalCount / postsPerPage); - const basePageUrl = normalizeUrl([baseUrl, routeBasePath]); - - const blogListPaginated = []; - - function blogPaginationPermalink(page) { - return page > 0 - ? normalizeUrl([basePageUrl, `page/${page + 1}`]) - : basePageUrl; - } - - for (let page = 0; page < numberOfPages; page += 1) { - blogListPaginated.push({ - metadata: { - permalink: blogPaginationPermalink(page), - page: page + 1, - postsPerPage, - totalPages: numberOfPages, - totalCount, - previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null, - nextPage: - page < numberOfPages - 1 ? blogPaginationPermalink(page + 1) : null, - }, - items: blogPosts - .slice(page * postsPerPage, (page + 1) * postsPerPage) - .map(item => item.id), + const {baseUrl} = siteConfig; + const blogFiles = await globby(include, { + cwd: blogDir, }); - } - const blogTags = {}; - const tagsPath = normalizeUrl([basePageUrl, 'tags']); - blogPosts.forEach(blogPost => { - const {tags} = blogPost.metadata; - if (!tags || tags.length === 0) { - // TODO: Extract tags out into a separate plugin. + const blogPosts = []; + + await Promise.all( + blogFiles.map(async relativeSource => { + const source = path.join(blogDir, relativeSource); + + const blogFileName = path.basename(relativeSource); + // Extract, YYYY, MM, DD from the file name. + const filePathDateArr = blogFileName.split('-'); + const date = new Date( + `${filePathDateArr[0]}-${filePathDateArr[1]}-${ + filePathDateArr[2] + }T06:00:00.000Z`, + ); + + const fileString = await fs.readFile(source, 'utf-8'); + const {frontMatter, excerpt} = parse(fileString); + + blogPosts.push({ + id: blogFileName, + metadata: { + permalink: normalizeUrl([ + baseUrl, + routeBasePath, + fileToUrl(blogFileName), + ]), + source, + description: frontMatter.description || excerpt, + date, + tags: frontMatter.tags, + title: frontMatter.title || blogFileName, + }, + }); + }), + ); + blogPosts.sort((a, b) => b.metadata.date - a.metadata.date); + + // Blog pagination routes. + // Example: `/blog`, `/blog/page/1`, `/blog/page/2` + const totalCount = blogPosts.length; + const numberOfPages = Math.ceil(totalCount / postsPerPage); + const basePageUrl = normalizeUrl([baseUrl, routeBasePath]); + + const blogListPaginated = []; + + function blogPaginationPermalink(page) { + return page > 0 + ? normalizeUrl([basePageUrl, `page/${page + 1}`]) + : basePageUrl; + } + + for (let page = 0; page < numberOfPages; page += 1) { + blogListPaginated.push({ + metadata: { + permalink: blogPaginationPermalink(page), + page: page + 1, + postsPerPage, + totalPages: numberOfPages, + totalCount, + previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null, + nextPage: + page < numberOfPages - 1 + ? blogPaginationPermalink(page + 1) + : null, + }, + items: blogPosts + .slice(page * postsPerPage, (page + 1) * postsPerPage) + .map(item => item.id), + }); + } + + const blogTags = {}; + const tagsPath = normalizeUrl([basePageUrl, 'tags']); + blogPosts.forEach(blogPost => { + const {tags} = blogPost.metadata; + if (!tags || tags.length === 0) { + // TODO: Extract tags out into a separate plugin. + // eslint-disable-next-line no-param-reassign + blogPost.metadata.tags = []; + return; + } + // eslint-disable-next-line no-param-reassign - blogPost.metadata.tags = []; + blogPost.metadata.tags = tags.map(tag => { + const normalizedTag = _.kebabCase(tag); + const permalink = normalizeUrl([tagsPath, normalizedTag]); + if (!blogTags[normalizedTag]) { + blogTags[normalizedTag] = { + name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag. + items: [], + permalink, + }; + } + + blogTags[normalizedTag].items.push(blogPost.id); + + return { + label: tag, + permalink, + }; + }); + }); + + const blogTagsListPath = + Object.keys(blogTags).length > 0 ? tagsPath : null; + + return { + blogPosts, + blogListPaginated, + blogTags, + blogTagsListPath, + }; + }, + + async contentLoaded({content: blogContents, actions}) { + if (!blogContents) { return; } - // eslint-disable-next-line no-param-reassign - blogPost.metadata.tags = tags.map(tag => { - const normalizedTag = _.kebabCase(tag); - const permalink = normalizeUrl([tagsPath, normalizedTag]); - if (!blogTags[normalizedTag]) { - blogTags[normalizedTag] = { - name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag. - items: [], - permalink, + const { + blogListComponent, + blogPostComponent, + blogTagsListComponent, + blogTagsPostsComponent, + } = options; + + const {addRoute, createData} = actions; + const { + blogPosts, + blogListPaginated, + blogTags, + blogTagsListPath, + } = blogContents; + + const blogItemsToModules = {}; + // Create routes for blog entries. + const blogItems = await Promise.all( + blogPosts.map(async blogPost => { + const {id, metadata} = blogPost; + const {permalink} = metadata; + const metadataPath = await createData( + `${docuHash(permalink)}.json`, + JSON.stringify(metadata, null, 2), + ); + const temp = { + metadata, + metadataPath, }; - } - blogTags[normalizedTag].items.push(blogPost.id); - - return { - label: tag, - permalink, - }; - }); - }); - - const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null; - - return { - blogPosts, - blogListPaginated, - blogTags, - blogTagsListPath, - }; - } - - async contentLoaded({content: blogContents, actions}) { - if (!blogContents) { - return; - } - - const { - blogListComponent, - blogPostComponent, - blogTagsListComponent, - blogTagsPostsComponent, - } = this.options; - - const {addRoute, createData} = actions; - const { - blogPosts, - blogListPaginated, - blogTags, - blogTagsListPath, - } = blogContents; - - const blogItemsToModules = {}; - // Create routes for blog entries. - const blogItems = await Promise.all( - blogPosts.map(async blogPost => { - const {id, metadata} = blogPost; - const {permalink} = metadata; - const metadataPath = await createData( - `${docuHash(permalink)}.json`, - JSON.stringify(metadata, null, 2), - ); - const temp = { - metadata, - metadataPath, - }; - - blogItemsToModules[id] = temp; - return temp; - }), - ); - - blogItems.forEach((blogItem, index) => { - const prevItem = index > 0 ? blogItems[index - 1] : null; - const nextItem = - index < blogItems.length - 1 ? blogItems[index + 1] : null; - const {metadata, metadataPath} = blogItem; - const {source, permalink} = metadata; - - addRoute({ - path: permalink, - component: blogPostComponent, - exact: true, - modules: { - content: source, - metadata: metadataPath, - prevItem: prevItem && prevItem.metadataPath, - nextItem: nextItem && nextItem.metadataPath, - }, - }); - }); - - // Create routes for blog's paginated list entries. - await Promise.all( - blogListPaginated.map(async listPage => { - const {metadata, items} = listPage; - const {permalink} = metadata; - const pageMetadataPath = await createData( - `${docuHash(permalink)}.json`, - JSON.stringify(metadata, null, 2), - ); - - addRoute({ - path: permalink, - component: blogListComponent, - exact: true, - modules: { - items: items.map(postID => { - const {metadata: postMetadata, metadataPath} = blogItemsToModules[ - postID - ]; - // To tell routes.js this is an import and not a nested object to recurse. - return { - content: { - __import: true, - path: postMetadata.source, - query: { - truncated: true, - }, - }, - metadata: metadataPath, - }; - }), - metadata: pageMetadataPath, - }, - }); - }), - ); - - // Tags. - const tagsModule = {}; - - await Promise.all( - Object.keys(blogTags).map(async tag => { - const {name, items, permalink} = blogTags[tag]; - - tagsModule[tag] = { - allTagsPath: blogTagsListPath, - slug: tag, - name, - count: items.length, - permalink, - }; - - const tagsMetadataPath = await createData( - `${docuHash(permalink)}.json`, - JSON.stringify(tagsModule[tag], null, 2), - ); - - addRoute({ - path: permalink, - component: blogTagsPostsComponent, - exact: true, - modules: { - items: items.map(postID => { - const {metadata: postMetadata, metadataPath} = blogItemsToModules[ - postID - ]; - return { - content: { - __import: true, - path: postMetadata.source, - query: { - truncated: true, - }, - }, - metadata: metadataPath, - }; - }), - metadata: tagsMetadataPath, - }, - }); - }), - ); - - // Only create /tags page if there are tags. - if (Object.keys(blogTags).length > 0) { - const tagsListPath = await createData( - `${docuHash(`${blogTagsListPath}-tags`)}.json`, - JSON.stringify(tagsModule, null, 2), + blogItemsToModules[id] = temp; + return temp; + }), ); - addRoute({ - path: blogTagsListPath, - component: blogTagsListComponent, - exact: true, - modules: { - tags: tagsListPath, - }, - }); - } - } + blogItems.forEach((blogItem, index) => { + const prevItem = index > 0 ? blogItems[index - 1] : null; + const nextItem = + index < blogItems.length - 1 ? blogItems[index + 1] : null; + const {metadata, metadataPath} = blogItem; + const {source, permalink} = metadata; - getThemePath() { - return path.resolve(__dirname, './theme'); - } - - configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) { - return { - module: { - rules: [ - { - test: /(\.mdx?)$/, - include: [this.contentPath], - use: [ - getCacheLoader(isServer), - getBabelLoader(isServer), - '@docusaurus/mdx-loader', - { - loader: path.resolve(__dirname, './markdownLoader.js'), - }, - ], + addRoute({ + path: permalink, + component: blogPostComponent, + exact: true, + modules: { + content: source, + metadata: metadataPath, + prevItem: prevItem && prevItem.metadataPath, + nextItem: nextItem && nextItem.metadataPath, }, - ], - }, - }; - } -} + }); + }); -module.exports = DocusaurusPluginContentBlog; + // Create routes for blog's paginated list entries. + await Promise.all( + blogListPaginated.map(async listPage => { + const {metadata, items} = listPage; + const {permalink} = metadata; + const pageMetadataPath = await createData( + `${docuHash(permalink)}.json`, + JSON.stringify(metadata, null, 2), + ); + + addRoute({ + path: permalink, + component: blogListComponent, + exact: true, + modules: { + items: items.map(postID => { + const { + metadata: postMetadata, + metadataPath, + } = blogItemsToModules[postID]; + // To tell routes.js this is an import and not a nested object to recurse. + return { + content: { + __import: true, + path: postMetadata.source, + query: { + truncated: true, + }, + }, + metadata: metadataPath, + }; + }), + metadata: pageMetadataPath, + }, + }); + }), + ); + + // Tags. + const tagsModule = {}; + + await Promise.all( + Object.keys(blogTags).map(async tag => { + const {name, items, permalink} = blogTags[tag]; + + tagsModule[tag] = { + allTagsPath: blogTagsListPath, + slug: tag, + name, + count: items.length, + permalink, + }; + + const tagsMetadataPath = await createData( + `${docuHash(permalink)}.json`, + JSON.stringify(tagsModule[tag], null, 2), + ); + + addRoute({ + path: permalink, + component: blogTagsPostsComponent, + exact: true, + modules: { + items: items.map(postID => { + const { + metadata: postMetadata, + metadataPath, + } = blogItemsToModules[postID]; + return { + content: { + __import: true, + path: postMetadata.source, + query: { + truncated: true, + }, + }, + metadata: metadataPath, + }; + }), + metadata: tagsMetadataPath, + }, + }); + }), + ); + + // Only create /tags page if there are tags. + if (Object.keys(blogTags).length > 0) { + const tagsListPath = await createData( + `${docuHash(`${blogTagsListPath}-tags`)}.json`, + JSON.stringify(tagsModule, null, 2), + ); + + addRoute({ + path: blogTagsListPath, + component: blogTagsListComponent, + exact: true, + modules: { + tags: tagsListPath, + }, + }); + } + }, + + getThemePath() { + return path.resolve(__dirname, './theme'); + }, + + configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) { + return { + module: { + rules: [ + { + test: /(\.mdx?)$/, + include: [contentPath], + use: [ + getCacheLoader(isServer), + getBabelLoader(isServer), + '@docusaurus/mdx-loader', + { + loader: path.resolve(__dirname, './markdownLoader.js'), + }, + ], + }, + ], + }, + }; + }, + }; +}; diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.js b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.js index 0640f77bd0..0578c86cbb 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.js +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.js @@ -6,7 +6,7 @@ */ import path from 'path'; -import DocusaurusPluginContentDocs from '../index'; +import pluginContentDocs from '../index'; describe('loadDocs', () => { test('simple website', async () => { @@ -17,7 +17,7 @@ describe('loadDocs', () => { url: 'https://docusaurus.io', }; const sidebarPath = path.join(siteDir, 'sidebars.json'); - const plugin = new DocusaurusPluginContentDocs( + const plugin = pluginContentDocs( { siteDir, siteConfig, @@ -41,6 +41,7 @@ describe('loadDocs', () => { title: 'Hello, World !', description: `Hi, Endilie here :)`, }); + expect(docsMetadata['foo/bar']).toEqual({ category: 'Test', id: 'foo/bar', diff --git a/packages/docusaurus-plugin-content-docs/src/index.js b/packages/docusaurus-plugin-content-docs/src/index.js index 12c1e3c2b5..c582a5a4bd 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.js +++ b/packages/docusaurus-plugin-content-docs/src/index.js @@ -25,166 +25,162 @@ const DEFAULT_OPTIONS = { docItemComponent: '@theme/DocItem', }; -class DocusaurusPluginContentDocs { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - this.contentPath = path.resolve(this.context.siteDir, this.options.path); - this.content = {}; - } +module.exports = function(context, opts) { + const options = {...DEFAULT_OPTIONS, ...opts}; + const contentPath = path.resolve(context.siteDir, options.path); + let globalContents = {}; - getName() { - return 'docusaurus-plugin-content-docs'; - } + return { + name: 'docusaurus-plugin-content-docs', - getPathsToWatch() { - const {include = []} = this.options; - const globPattern = include.map( - pattern => `${this.contentPath}/${pattern}`, - ); - return [...globPattern, this.options.sidebarPath]; - } + contentPath, - // Fetches blog contents and returns metadata for the contents. - async loadContent() { - const {include, routeBasePath, sidebarPath} = this.options; - const {siteConfig} = this.context; - const docsDir = this.contentPath; + getPathsToWatch() { + const {include = []} = options; + const globPattern = include.map(pattern => `${contentPath}/${pattern}`); + return [...globPattern, options.sidebarPath]; + }, - if (!fs.existsSync(docsDir)) { - return null; - } + // Fetches blog contents and returns metadata for the contents. + async loadContent() { + const {include, routeBasePath, sidebarPath} = options; + const {siteConfig} = context; + const docsDir = contentPath; - const docsSidebars = loadSidebars(sidebarPath); - - // Build the docs ordering such as next, previous, category and sidebar - const order = createOrder(docsSidebars); - - // Prepare metadata container. - const docs = {}; - - // Metadata for default docs files. - const docsFiles = await globby(include, { - cwd: docsDir, - }); - await Promise.all( - docsFiles.map(async source => { - const metadata = await processMetadata( - source, - docsDir, - order, - siteConfig, - routeBasePath, - ); - docs[metadata.id] = metadata; - }), - ); - - // Get the titles of the previous and next ids so that we can use them. - Object.keys(docs).forEach(currentID => { - const previousID = idx(docs, [currentID, 'previous']); - if (previousID) { - const previousTitle = idx(docs, [previousID, 'title']); - docs[currentID].previous_title = previousTitle || 'Previous'; + if (!fs.existsSync(docsDir)) { + return null; } - const nextID = idx(docs, [currentID, 'next']); - if (nextID) { - const nextTitle = idx(docs, [nextID, 'title']); - docs[currentID].next_title = nextTitle || 'Next'; + + const docsSidebars = loadSidebars(sidebarPath); + + // Build the docs ordering such as next, previous, category and sidebar + const order = createOrder(docsSidebars); + + // Prepare metadata container. + const docs = {}; + + // Metadata for default docs files. + const docsFiles = await globby(include, { + cwd: docsDir, + }); + await Promise.all( + docsFiles.map(async source => { + const metadata = await processMetadata( + source, + docsDir, + order, + siteConfig, + routeBasePath, + ); + docs[metadata.id] = metadata; + }), + ); + + // Get the titles of the previous and next ids so that we can use them. + Object.keys(docs).forEach(currentID => { + const previousID = idx(docs, [currentID, 'previous']); + if (previousID) { + const previousTitle = idx(docs, [previousID, 'title']); + docs[currentID].previous_title = previousTitle || 'Previous'; + } + const nextID = idx(docs, [currentID, 'next']); + if (nextID) { + const nextTitle = idx(docs, [nextID, 'title']); + docs[currentID].next_title = nextTitle || 'Next'; + } + }); + + const sourceToPermalink = {}; + const permalinkToId = {}; + Object.values(docs).forEach(({id, source, permalink}) => { + sourceToPermalink[source] = permalink; + permalinkToId[permalink] = id; + }); + + globalContents = { + docs, + docsDir, + docsSidebars, + sourceToPermalink, + permalinkToId, + }; + + return globalContents; + }, + + async contentLoaded({content, actions}) { + if (!content) { + return; } - }); - const sourceToPermalink = {}; - const permalinkToId = {}; - Object.values(docs).forEach(({id, source, permalink}) => { - sourceToPermalink[source] = permalink; - permalinkToId[permalink] = id; - }); + const {docLayoutComponent, docItemComponent, routeBasePath} = options; + const {addRoute, createData} = actions; - this.content = { - docs, - docsDir, - docsSidebars, - sourceToPermalink, - permalinkToId, - }; + const routes = await Promise.all( + Object.values(content.docs).map(async metadataItem => { + const metadataPath = await createData( + `${docuHash(metadataItem.permalink)}.json`, + JSON.stringify(metadataItem, null, 2), + ); + return { + path: metadataItem.permalink, + component: docItemComponent, + exact: true, + modules: { + content: metadataItem.source, + metadata: metadataPath, + }, + }; + }), + ); - return this.content; - } + const docsBaseRoute = normalizeUrl([ + context.siteConfig.baseUrl, + routeBasePath, + ]); + const docsMetadataPath = await createData( + `${docuHash(docsBaseRoute)}.json`, + JSON.stringify(content, null, 2), + ); - async contentLoaded({content, actions}) { - if (!content) { - return; - } - const {docLayoutComponent, docItemComponent, routeBasePath} = this.options; - const {addRoute, createData} = actions; + addRoute({ + path: docsBaseRoute, + component: docLayoutComponent, + routes, + modules: { + docsMetadata: docsMetadataPath, + }, + }); + }, - const routes = await Promise.all( - Object.values(content.docs).map(async metadataItem => { - const metadataPath = await createData( - `${docuHash(metadataItem.permalink)}.json`, - JSON.stringify(metadataItem, null, 2), - ); - return { - path: metadataItem.permalink, - component: docItemComponent, - exact: true, - modules: { - content: metadataItem.source, - metadata: metadataPath, - }, - }; - }), - ); + getThemePath() { + return path.resolve(__dirname, './theme'); + }, - const docsBaseRoute = normalizeUrl([ - this.context.siteConfig.baseUrl, - routeBasePath, - ]); - const docsMetadataPath = await createData( - `${docuHash(docsBaseRoute)}.json`, - JSON.stringify(content, null, 2), - ); - - addRoute({ - path: docsBaseRoute, - component: docLayoutComponent, - routes, - modules: { - docsMetadata: docsMetadataPath, - }, - }); - } - - getThemePath() { - return path.resolve(__dirname, './theme'); - } - - configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) { - return { - module: { - rules: [ - { - test: /(\.mdx?)$/, - include: [this.contentPath], - use: [ - getCacheLoader(isServer), - getBabelLoader(isServer), - '@docusaurus/mdx-loader', - { - loader: path.resolve(__dirname, './markdown/index.js'), - options: { - siteConfig: this.context.siteConfig, - docsDir: this.content.docsDir, - sourceToPermalink: this.content.sourceToPermalink, + configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) { + return { + module: { + rules: [ + { + test: /(\.mdx?)$/, + include: [contentPath], + use: [ + getCacheLoader(isServer), + getBabelLoader(isServer), + '@docusaurus/mdx-loader', + { + loader: path.resolve(__dirname, './markdown/index.js'), + options: { + siteConfig: context.siteConfig, + docsDir: globalContents.docsDir, + sourceToPermalink: globalContents.sourceToPermalink, + }, }, - }, - ], - }, - ], - }, - }; - } -} - -module.exports = DocusaurusPluginContentDocs; + ], + }, + ], + }, + }; + }, + }; +}; diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.js b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.js index 945d449b37..abf97fbe1e 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.js +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.js @@ -7,7 +7,7 @@ import path from 'path'; -import DocusaurusPluginContentPages from '../index'; +import pluginContentPages from '../index'; describe('docusaurus-plugin-content-pages', () => { test('simple pages', async () => { @@ -17,7 +17,7 @@ describe('docusaurus-plugin-content-pages', () => { url: 'https://docusaurus.io', }; const siteDir = path.join(__dirname, '__fixtures__', 'website'); - const plugin = new DocusaurusPluginContentPages({ + const plugin = pluginContentPages({ siteDir, siteConfig, }); diff --git a/packages/docusaurus-plugin-content-pages/src/index.js b/packages/docusaurus-plugin-content-pages/src/index.js index 6c3d6efd98..8ed0d8363c 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.js +++ b/packages/docusaurus-plugin-content-pages/src/index.js @@ -16,75 +16,70 @@ const DEFAULT_OPTIONS = { include: ['**/*.{js,jsx}'], // Extensions to include. }; -class DocusaurusPluginContentPages { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - this.contentPath = path.resolve(this.context.siteDir, this.options.path); - } +module.exports = function(context, opts) { + const options = {...DEFAULT_OPTIONS, ...opts}; + const contentPath = path.resolve(context.siteDir, options.path); - getName() { - return 'docusaurus-plugin-content-pages'; - } + return { + name: 'docusaurus-plugin-content-pages', - getPathsToWatch() { - const {include = []} = this.options; - const globPattern = include.map( - pattern => `${this.contentPath}/${pattern}`, - ); - return [...globPattern]; - } + contentPath, - async loadContent() { - const {include} = this.options; - const {siteConfig} = this.context; - const pagesDir = this.contentPath; + getPathsToWatch() { + const {include = []} = options; + const globPattern = include.map(pattern => `${contentPath}/${pattern}`); + return [...globPattern]; + }, - if (!fs.existsSync(pagesDir)) { - return null; - } + async loadContent() { + const {include} = options; + const {siteConfig} = context; + const pagesDir = contentPath; - const {baseUrl} = siteConfig; - const pagesFiles = await globby(include, { - cwd: pagesDir, - }); + if (!fs.existsSync(pagesDir)) { + return null; + } - return pagesFiles.map(relativeSource => { - const source = path.join(pagesDir, relativeSource); - const pathName = encodePath(fileToPath(relativeSource)); - // Default Language. - return { - permalink: pathName.replace(/^\//, baseUrl), - source, - }; - }); - } + const {baseUrl} = siteConfig; + const pagesFiles = await globby(include, { + cwd: pagesDir, + }); - async contentLoaded({content, actions}) { - if (!content) { - return; - } + return pagesFiles.map(relativeSource => { + const source = path.join(pagesDir, relativeSource); + const pathName = encodePath(fileToPath(relativeSource)); + // Default Language. + return { + permalink: pathName.replace(/^\//, baseUrl), + source, + }; + }); + }, - const {addRoute, createData} = actions; + async contentLoaded({content, actions}) { + if (!content) { + return; + } - await Promise.all( - content.map(async metadataItem => { - const {permalink, source} = metadataItem; - const metadataPath = await createData( - `${docuHash(permalink)}.json`, - JSON.stringify(metadataItem, null, 2), - ); - addRoute({ - path: permalink, - component: source, - exact: true, - modules: { - metadata: metadataPath, - }, - }); - }), - ); - } -} + const {addRoute, createData} = actions; -module.exports = DocusaurusPluginContentPages; + await Promise.all( + content.map(async metadataItem => { + const {permalink, source} = metadataItem; + const metadataPath = await createData( + `${docuHash(permalink)}.json`, + JSON.stringify(metadataItem, null, 2), + ); + addRoute({ + path: permalink, + component: source, + exact: true, + modules: { + metadata: metadataPath, + }, + }); + }), + ); + }, + }; +}; diff --git a/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemap.test.js b/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemap.test.js new file mode 100644 index 0000000000..f5cf466f09 --- /dev/null +++ b/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemap.test.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import createSitemap from '../createSitemap'; + +describe('createSitemap', () => { + test('simple site', () => { + const sitemap = createSitemap({ + siteConfig: { + url: 'https://example.com', + }, + routesPaths: ['/', '/test'], + }); + expect(sitemap).toContain( + ``, + ); + }); + + test('empty site', () => { + expect(() => { + createSitemap({}); + }).toThrowErrorMatchingInlineSnapshot( + `"Url in docusaurus.config.js cannot be empty/undefined"`, + ); + }); +}); diff --git a/packages/docusaurus-plugin-sitemap/src/__tests__/index.test.js b/packages/docusaurus-plugin-sitemap/src/__tests__/index.test.js deleted file mode 100644 index 383b0af167..0000000000 --- a/packages/docusaurus-plugin-sitemap/src/__tests__/index.test.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import DocusaurusPluginSitemap from '../index'; - -describe('docusaurus-plugin-sitemap', () => { - describe('createSitemap', () => { - test('simple site', async () => { - const context = { - siteConfig: { - url: 'https://example.com', - }, - routesPaths: ['/', '/test'], - }; - const plugin = new DocusaurusPluginSitemap(context, null); - const sitemap = await plugin.createSitemap(context); - expect(sitemap).toContain( - ``, - ); - }); - - test('empty site', async () => { - const context = {}; - const plugin = new DocusaurusPluginSitemap(context, null); - expect( - plugin.createSitemap(context), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Url in docusaurus.config.js cannot be empty/undefined"`, - ); - }); - }); -}); diff --git a/packages/docusaurus-plugin-sitemap/src/createSitemap.js b/packages/docusaurus-plugin-sitemap/src/createSitemap.js new file mode 100644 index 0000000000..f9cc631800 --- /dev/null +++ b/packages/docusaurus-plugin-sitemap/src/createSitemap.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const sitemap = require('sitemap'); + +module.exports = function createSitemap({ + siteConfig = {}, + routesPaths, + options = {}, +}) { + const {url: hostname} = siteConfig; + if (!hostname) { + throw new Error('Url in docusaurus.config.js cannot be empty/undefined'); + } + + const urls = routesPaths.map(routesPath => ({ + url: routesPath, + changefreq: options.changefreq, + priority: options.priority, + })); + + return sitemap + .createSitemap({ + hostname, + cacheTime: options.cacheTime, + urls, + }) + .toString(); +}; diff --git a/packages/docusaurus-plugin-sitemap/src/index.js b/packages/docusaurus-plugin-sitemap/src/index.js index dd724c88f7..eb43d83384 100644 --- a/packages/docusaurus-plugin-sitemap/src/index.js +++ b/packages/docusaurus-plugin-sitemap/src/index.js @@ -6,61 +6,37 @@ */ const fs = require('fs'); -const sitemap = require('sitemap'); const path = require('path'); +const createSitemap = require('./createSitemap'); + const DEFAULT_OPTIONS = { cacheTime: 600 * 1000, // 600 sec - cache purge period changefreq: 'weekly', priority: 0.5, }; -class DocusaurusPluginSitemap { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - } +module.exports = function(context, opts) { + const options = {...DEFAULT_OPTIONS, ...opts}; - getName() { - return 'docusaurus-plugin-sitemap'; - } + return { + name: 'docusaurus-plugin-sitemap', - async createSitemap({siteConfig = {}, routesPaths}) { - const {url: hostname} = siteConfig; - if (!hostname) { - throw new Error(`Url in docusaurus.config.js cannot be empty/undefined`); - } + async postBuild({siteConfig = {}, routesPaths = [], outDir}) { + // Generate sitemap + const generatedSitemap = createSitemap({ + siteConfig, + routesPaths, + options, + }).toString(); - const urls = routesPaths.map(routesPath => ({ - url: routesPath, - changefreq: this.changefreq, - priority: this.priority, - })); - - return sitemap - .createSitemap({ - hostname, - cacheTime: this.cacheTime, - urls, - }) - .toString(); - } - - async postBuild({siteConfig = {}, routesPaths = [], outDir}) { - // Generate sitemap - const generatedSitemap = await this.createSitemap({ - siteConfig, - routesPaths, - }); - - // Write sitemap file - const sitemapPath = path.join(outDir, 'sitemap.xml'); - fs.writeFile(sitemapPath, generatedSitemap, err => { - if (err) { - throw new Error(`Sitemap error: ${err}`); - } - }); - } -} - -module.exports = DocusaurusPluginSitemap; + // Write sitemap file + const sitemapPath = path.join(outDir, 'sitemap.xml'); + fs.writeFile(sitemapPath, generatedSitemap, err => { + if (err) { + throw new Error(`Sitemap error: ${err}`); + } + }); + }, + }; +}; diff --git a/packages/docusaurus-preset-classic/src/index.js b/packages/docusaurus-preset-classic/src/index.js index a3f7ef8f56..ec161e2aab 100644 --- a/packages/docusaurus-preset-classic/src/index.js +++ b/packages/docusaurus-preset-classic/src/index.js @@ -9,27 +9,27 @@ module.exports = function preset(context, opts = {}) { return { themes: [ { - name: '@docusaurus/theme-classic', + module: '@docusaurus/theme-classic', }, { - name: '@docusaurus/theme-search-algolia', + module: '@docusaurus/theme-search-algolia', }, ], plugins: [ { - name: '@docusaurus/plugin-content-docs', + module: '@docusaurus/plugin-content-docs', options: opts.docs, }, { - name: '@docusaurus/plugin-content-blog', + module: '@docusaurus/plugin-content-blog', options: opts.blog, }, { - name: '@docusaurus/plugin-content-pages', + module: '@docusaurus/plugin-content-pages', options: opts.pages, }, { - name: '@docusaurus/plugin-sitemap', + module: '@docusaurus/plugin-sitemap', options: opts.sitemap, }, ], diff --git a/packages/docusaurus-theme-classic/src/index.js b/packages/docusaurus-theme-classic/src/index.js index f47388e6c2..e48bd261fb 100644 --- a/packages/docusaurus-theme-classic/src/index.js +++ b/packages/docusaurus-theme-classic/src/index.js @@ -7,21 +7,12 @@ const path = require('path'); -const DEFAULT_OPTIONS = {}; +module.exports = function() { + return { + name: 'docusaurus-theme-classic', -class DocusaurusThemeClassic { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - } - - getName() { - return 'docusaurus-theme-classic'; - } - - getThemePath() { - return path.resolve(__dirname, './theme'); - } -} - -module.exports = DocusaurusThemeClassic; + getThemePath() { + return path.resolve(__dirname, './theme'); + }, + }; +}; diff --git a/packages/docusaurus-theme-search-algolia/src/index.js b/packages/docusaurus-theme-search-algolia/src/index.js index d7f3838ed0..7245c04d2c 100644 --- a/packages/docusaurus-theme-search-algolia/src/index.js +++ b/packages/docusaurus-theme-search-algolia/src/index.js @@ -7,21 +7,12 @@ const path = require('path'); -const DEFAULT_OPTIONS = {}; +module.exports = function() { + return { + name: 'docusaurus-theme-search-algolia', -class DocusaurusThemeSearchAlgolia { - constructor(context, opts) { - this.options = {...DEFAULT_OPTIONS, ...opts}; - this.context = context; - } - - getName() { - return 'docusaurus-theme-search-algolia'; - } - - getThemePath() { - return path.resolve(__dirname, './theme'); - } -} - -module.exports = DocusaurusThemeSearchAlgolia; + getThemePath() { + return path.resolve(__dirname, './theme'); + }, + }; +}; diff --git a/packages/docusaurus/CHANGES.md b/packages/docusaurus/CHANGELOG.md similarity index 82% rename from packages/docusaurus/CHANGES.md rename to packages/docusaurus/CHANGELOG.md index b527f0c420..c2c4dae5e5 100644 --- a/packages/docusaurus/CHANGES.md +++ b/packages/docusaurus/CHANGELOG.md @@ -1,4 +1,12 @@ -# Breaking Changes +# Docusaurus 2 Changelog + +## 2.0.0-alpha.19 + +- Changed plugin definitions from classes to functions. Refer to the new plugin docs. +- Added sun and moon emoji to the dark mode toggle. +- Add a sensible default for browserslist config. + +## V2 Changelog ### `siteConfig.js` changes diff --git a/packages/docusaurus/src/commands/swizzle.ts b/packages/docusaurus/src/commands/swizzle.ts index 2f9ff185b5..4ca6d4f571 100644 --- a/packages/docusaurus/src/commands/swizzle.ts +++ b/packages/docusaurus/src/commands/swizzle.ts @@ -15,10 +15,9 @@ export async function swizzle( themeName: string, componentName?: string, ): Promise { - const Plugin = importFresh(themeName); - const context = {siteDir}; - const PluginInstance = new Plugin(context); - let fromPath = PluginInstance.getThemePath(); + const plugin = importFresh(themeName); + const pluginInstance = plugin({siteDir}); + let fromPath = pluginInstance.getThemePath(); if (fromPath) { let toPath = path.resolve(siteDir, 'theme'); diff --git a/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/docusaurus.config.js b/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/docusaurus.config.js index 7922779c51..424e97fda1 100644 --- a/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/docusaurus.config.js +++ b/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/docusaurus.config.js @@ -15,14 +15,14 @@ module.exports = { favicon: 'img/docusaurus.ico', plugins: [ { - name: '@docusaurus/plugin-content-docs', + module: '@docusaurus/plugin-content-docs', options: { path: '../docs', sidebarPath: require.resolve('./sidebars.json'), }, }, { - name: '@docusaurus/plugin-content-pages', + module: '@docusaurus/plugin-content-pages', }, ], }; diff --git a/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js b/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js index 86e054660e..e1f80488be 100644 --- a/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js +++ b/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/docusaurus.config.js @@ -15,14 +15,14 @@ module.exports = { favicon: 'img/docusaurus.ico', plugins: [ { - name: '@docusaurus/plugin-content-docs', + module: '@docusaurus/plugin-content-docs', options: { path: '../docs', sidebarPath: require.resolve('./sidebars.json'), }, }, { - name: '@docusaurus/plugin-content-pages', + module: '@docusaurus/plugin-content-pages', }, ], }; diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 695cb5358b..334ab55146 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -19,14 +19,10 @@ export async function loadPlugins({ context: LoadContext; }) { // 1. Plugin Lifecycle - Initialization/Constructor - const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => { - let Plugin; - if (pluginPath && fs.existsSync(pluginPath)) { - Plugin = importFresh(pluginPath); - } else { - Plugin = importFresh(name); - } - return new Plugin(context, options); + const plugins = pluginConfigs.map(({module, options}) => { + // module is any valid module identifier - npm package or locally-resolved path. + const plugin = importFresh(module); + return plugin(context, options); }); // 2. Plugin lifecycle - loadContent @@ -51,8 +47,11 @@ export async function loadPlugins({ if (!plugin.contentLoaded) { return; } - const pluginName = plugin.getName(); - const pluginContentDir = path.join(context.generatedFilesDir, pluginName); + + const pluginContentDir = path.join( + context.generatedFilesDir, + plugin.name, + ); const actions = { addRoute: config => pluginsRouteConfigs.push(config), createData: async (name, content) => { diff --git a/website/docs/plugins-api.md b/website/docs/plugins-api.md index 1773e970ef..010fe16b7d 100644 --- a/website/docs/plugins-api.md +++ b/website/docs/plugins-api.md @@ -3,7 +3,7 @@ id: plugins-api title: Plugins --- -Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides. +Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides. ## Installing a Plugin @@ -19,11 +19,11 @@ Then you add it in your site's `docusaurus.config.js` plugin arrays: module.exports = { plugins: [ { - name: '@docusaurus/plugin-content-pages', + module: '@docusaurus/plugin-content-pages', }, { // Plugin with options - name: '@docusaurus/plugin-content-blog', + module: '@docusaurus/plugin-content-blog', options: { include: ['*.md', '*.mdx'], path: 'blog', @@ -33,81 +33,84 @@ module.exports = { }; ``` -Docusaurus can also load plugins from your local folder, you can do something like below: +Docusaurus can also load plugins from your local directory, you can do something like the following: ```js +const path = require('path'); + module.exports = { plugins: [ { - path: '/path/to/docusaurus-local-plugin', + module: path.resolve(__dirname, '/path/to/docusaurus-local-plugin'), }, ], -} +}; ``` -## Basic Plugin Architecture +## Basic Plugin Definition + +Plugins are modules which export a function that takes in the context, options and returns a plain JavaScript object that has some properties defined. For examples, please refer to several official plugins created. ```js -// A JavaScript class -class DocusaurusPlugin { - constructor(context, options) { - // Initialization hook - - // options are the plugin options set on config file - this.options = {...options}; - - // context are provided from docusaurus. Example: siteConfig can be accessed from context - this.context = context; - } +const DEFAULT_OPTIONS = { + // Some defaults. +}; - getName() { - // plugin name identifier - } +// A JavaScript function that returns an object. +// `context` is provided by Docusaurus. Example: siteConfig can be accessed from context. +// `opts` is the user-defined options. +module.exports = function(context, opts) { + // Merge defaults with user-defined options. + const options = {...DEFAULT_OPTIONS, ...options}; + return { + // Namespace used for directories to cache the intermediate data for each plugin. + name: 'docusaurus-cool-plugin', - async loadContent()) { - // The loadContent hook is executed after siteConfig and env has been loaded - // You can return a JavaScript object that will be passed to contentLoaded hook - } + async loadContent() { + // The loadContent hook is executed after siteConfig and env has been loaded + // You can return a JavaScript object that will be passed to contentLoaded hook + }, - async contentLoaded({content, actions}) { - // contentLoaded hook is done after loadContent hook is done - // actions are set of functional API provided by Docusaurus. e.g: addRoute - } + async contentLoaded({content, actions}) { + // contentLoaded hook is done after loadContent hook is done + // actions are set of functional API provided by Docusaurus. e.g: addRoute + }, - async postBuild(props) { - // after docusaurus finish - } + async postBuild(props) { + // after docusaurus finish + }, - // TODO - async postStart(props) { - // docusaurus finish - } + // TODO + async postStart(props) { + // docusaurus finish + }, - // TODO - afterDevServer(app, server) { - // https://webpack.js.org/configuration/dev-server/#devserverbefore - } + // TODO + afterDevServer(app, server) { + // https://webpack.js.org/configuration/dev-server/#devserverbefore + }, - // TODO - beforeDevServer(app, server) { - // https://webpack.js.org/configuration/dev-server/#devserverafter + // TODO + beforeDevServer(app, server) { + // https://webpack.js.org/configuration/dev-server/#devserverafter + }, - } + configureWebpack(config, isServer) { + // Modify internal webpack config. If returned value is an Object, it + // will be merged into the final config using webpack-merge; + // If the returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument. + }, - configureWebpack(config, isServer) { - // Modify internal webpack config. If returned value is an Object, it will be merged into the final config using webpack-merge; If returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument. - } - - getPathsToWatch() { - // path to watch - } -} + getPathsToWatch() { + // Path to watch + }, + }; +}; ``` - #### References - https://v1.vuepress.vuejs.org/plugin/option-api.html