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
This commit is contained in:
Yangshun Tay 2019-06-02 20:37:22 -07:00 committed by GitHub
parent 9feb7b2c64
commit 6a814ac64a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 709 additions and 725 deletions

View file

@ -31,30 +31,24 @@ 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}`,
);
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;
const {postsPerPage, include, routeBasePath} = options;
const {siteConfig} = context;
const blogDir = contentPath;
if (!fs.existsSync(blogDir)) {
return null;
@ -126,7 +120,9 @@ class DocusaurusPluginContentBlog {
totalCount,
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
nextPage:
page < numberOfPages - 1 ? blogPaginationPermalink(page + 1) : null,
page < numberOfPages - 1
? blogPaginationPermalink(page + 1)
: null,
},
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
@ -166,7 +162,8 @@ class DocusaurusPluginContentBlog {
});
});
const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null;
const blogTagsListPath =
Object.keys(blogTags).length > 0 ? tagsPath : null;
return {
blogPosts,
@ -174,7 +171,7 @@ class DocusaurusPluginContentBlog {
blogTags,
blogTagsListPath,
};
}
},
async contentLoaded({content: blogContents, actions}) {
if (!blogContents) {
@ -186,7 +183,7 @@ class DocusaurusPluginContentBlog {
blogPostComponent,
blogTagsListComponent,
blogTagsPostsComponent,
} = this.options;
} = options;
const {addRoute, createData} = actions;
const {
@ -252,9 +249,10 @@ class DocusaurusPluginContentBlog {
exact: true,
modules: {
items: items.map(postID => {
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
postID
];
const {
metadata: postMetadata,
metadataPath,
} = blogItemsToModules[postID];
// To tell routes.js this is an import and not a nested object to recurse.
return {
content: {
@ -299,9 +297,10 @@ class DocusaurusPluginContentBlog {
exact: true,
modules: {
items: items.map(postID => {
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
postID
];
const {
metadata: postMetadata,
metadataPath,
} = blogItemsToModules[postID];
return {
content: {
__import: true,
@ -335,11 +334,11 @@ class DocusaurusPluginContentBlog {
},
});
}
}
},
getThemePath() {
return path.resolve(__dirname, './theme');
}
},
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
return {
@ -347,7 +346,7 @@ class DocusaurusPluginContentBlog {
rules: [
{
test: /(\.mdx?)$/,
include: [this.contentPath],
include: [contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
@ -360,7 +359,6 @@ class DocusaurusPluginContentBlog {
],
},
};
}
}
module.exports = DocusaurusPluginContentBlog;
},
};
};

View file

@ -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',

View file

@ -25,31 +25,27 @@ 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',
contentPath,
getPathsToWatch() {
const {include = []} = this.options;
const globPattern = include.map(
pattern => `${this.contentPath}/${pattern}`,
);
return [...globPattern, this.options.sidebarPath];
}
const {include = []} = options;
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
return [...globPattern, options.sidebarPath];
},
// 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;
const {include, routeBasePath, sidebarPath} = options;
const {siteConfig} = context;
const docsDir = contentPath;
if (!fs.existsSync(docsDir)) {
return null;
@ -101,7 +97,7 @@ class DocusaurusPluginContentDocs {
permalinkToId[permalink] = id;
});
this.content = {
globalContents = {
docs,
docsDir,
docsSidebars,
@ -109,14 +105,15 @@ class DocusaurusPluginContentDocs {
permalinkToId,
};
return this.content;
}
return globalContents;
},
async contentLoaded({content, actions}) {
if (!content) {
return;
}
const {docLayoutComponent, docItemComponent, routeBasePath} = this.options;
const {docLayoutComponent, docItemComponent, routeBasePath} = options;
const {addRoute, createData} = actions;
const routes = await Promise.all(
@ -138,7 +135,7 @@ class DocusaurusPluginContentDocs {
);
const docsBaseRoute = normalizeUrl([
this.context.siteConfig.baseUrl,
context.siteConfig.baseUrl,
routeBasePath,
]);
const docsMetadataPath = await createData(
@ -154,11 +151,11 @@ class DocusaurusPluginContentDocs {
docsMetadata: docsMetadataPath,
},
});
}
},
getThemePath() {
return path.resolve(__dirname, './theme');
}
},
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
return {
@ -166,7 +163,7 @@ class DocusaurusPluginContentDocs {
rules: [
{
test: /(\.mdx?)$/,
include: [this.contentPath],
include: [contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
@ -174,9 +171,9 @@ class DocusaurusPluginContentDocs {
{
loader: path.resolve(__dirname, './markdown/index.js'),
options: {
siteConfig: this.context.siteConfig,
docsDir: this.content.docsDir,
sourceToPermalink: this.content.sourceToPermalink,
siteConfig: context.siteConfig,
docsDir: globalContents.docsDir,
sourceToPermalink: globalContents.sourceToPermalink,
},
},
],
@ -184,7 +181,6 @@ class DocusaurusPluginContentDocs {
],
},
};
}
}
module.exports = DocusaurusPluginContentDocs;
},
};
};

View file

@ -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,
});

View file

@ -16,29 +16,25 @@ 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',
contentPath,
getPathsToWatch() {
const {include = []} = this.options;
const globPattern = include.map(
pattern => `${this.contentPath}/${pattern}`,
);
const {include = []} = options;
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
return [...globPattern];
}
},
async loadContent() {
const {include} = this.options;
const {siteConfig} = this.context;
const pagesDir = this.contentPath;
const {include} = options;
const {siteConfig} = context;
const pagesDir = contentPath;
if (!fs.existsSync(pagesDir)) {
return null;
@ -58,7 +54,7 @@ class DocusaurusPluginContentPages {
source,
};
});
}
},
async contentLoaded({content, actions}) {
if (!content) {
@ -84,7 +80,6 @@ class DocusaurusPluginContentPages {
});
}),
);
}
}
module.exports = DocusaurusPluginContentPages;
},
};
};

View file

@ -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(
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
);
});
test('empty site', () => {
expect(() => {
createSitemap({});
}).toThrowErrorMatchingInlineSnapshot(
`"Url in docusaurus.config.js cannot be empty/undefined"`,
);
});
});

View file

@ -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(
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
);
});
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"`,
);
});
});
});

View file

@ -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();
};

View file

@ -6,52 +6,29 @@
*/
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';
}
async createSitemap({siteConfig = {}, routesPaths}) {
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: this.changefreq,
priority: this.priority,
}));
return sitemap
.createSitemap({
hostname,
cacheTime: this.cacheTime,
urls,
})
.toString();
}
return {
name: 'docusaurus-plugin-sitemap',
async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
// Generate sitemap
const generatedSitemap = await this.createSitemap({
const generatedSitemap = createSitemap({
siteConfig,
routesPaths,
});
options,
}).toString();
// Write sitemap file
const sitemapPath = path.join(outDir, 'sitemap.xml');
@ -60,7 +37,6 @@ class DocusaurusPluginSitemap {
throw new Error(`Sitemap error: ${err}`);
}
});
}
}
module.exports = DocusaurusPluginSitemap;
},
};
};

View file

@ -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,
},
],

View file

@ -7,21 +7,12 @@
const path = require('path');
const DEFAULT_OPTIONS = {};
class DocusaurusThemeClassic {
constructor(context, opts) {
this.options = {...DEFAULT_OPTIONS, ...opts};
this.context = context;
}
getName() {
return 'docusaurus-theme-classic';
}
module.exports = function() {
return {
name: 'docusaurus-theme-classic',
getThemePath() {
return path.resolve(__dirname, './theme');
}
}
module.exports = DocusaurusThemeClassic;
},
};
};

View file

@ -7,21 +7,12 @@
const path = require('path');
const DEFAULT_OPTIONS = {};
class DocusaurusThemeSearchAlgolia {
constructor(context, opts) {
this.options = {...DEFAULT_OPTIONS, ...opts};
this.context = context;
}
getName() {
return 'docusaurus-theme-search-algolia';
}
module.exports = function() {
return {
name: 'docusaurus-theme-search-algolia',
getThemePath() {
return path.resolve(__dirname, './theme');
}
}
module.exports = DocusaurusThemeSearchAlgolia;
},
};
};

View file

@ -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

View file

@ -15,10 +15,9 @@ export async function swizzle(
themeName: string,
componentName?: string,
): Promise<void> {
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');

View file

@ -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',
},
],
};

View file

@ -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',
},
],
};

View file

@ -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) => {

View file

@ -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
const DEFAULT_OPTIONS = {
// Some defaults.
};
// options are the plugin options set on config file
this.options = {...options};
// 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};
// context are provided from docusaurus. Example: siteConfig can be accessed from context
this.context = context;
}
return {
// Namespace used for directories to cache the intermediate data for each plugin.
name: 'docusaurus-cool-plugin',
getName() {
// plugin name identifier
}
async loadContent()) {
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 postBuild(props) {
// after docusaurus <build> finish
}
},
// TODO
async postStart(props) {
// docusaurus <start> finish
}
},
// TODO
afterDevServer(app, server) {
// https://webpack.js.org/configuration/dev-server/#devserverbefore
}
},
// 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 returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument.
}
// 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.
},
getPathsToWatch() {
// path to watch
}
}
// Path to watch
},
};
};
```
#### References
- https://v1.vuepress.vuejs.org/plugin/option-api.html