refactor(v2): plugins lifecycle (#1299)

* refactor(v2): plugins lifecycle

* dont allow duplicate plugin & fix typo
This commit is contained in:
Endilie Yacop Sucipto 2019-03-24 06:08:36 +07:00 committed by Yangshun Tay
parent 3a7a253db7
commit 73b89658cc
10 changed files with 127 additions and 136 deletions

View file

@ -41,7 +41,7 @@ class DocusaurusPluginContentBlog {
}
// Fetches blog contents and returns metadata for the contents.
async loadContents() {
async loadContent() {
const {pageCount, include, routeBasePath} = this.options;
const {env, siteConfig} = this.context;
const blogDir = this.contentPath;
@ -109,10 +109,10 @@ class DocusaurusPluginContentBlog {
return blogMetadata;
}
async generateRoutes({metadata, actions}) {
async contentLoaded({content, actions}) {
const {blogPageComponent, blogPostComponent} = this.options;
const {addRoute} = actions;
metadata.forEach(metadataItem => {
content.forEach(metadataItem => {
const {isBlogPage, permalink} = metadataItem;
if (isBlogPage) {
addRoute({

View file

@ -11,7 +11,7 @@ import loadSetup from '../../docusaurus/test/loadSetup';
import DocusaurusPluginContentPages from '../index';
describe('docusaurus-plugin-content-pages', () => {
describe('loadContents', () => {
describe('loadContent', () => {
test.each([
[
'simple',
@ -116,7 +116,7 @@ describe('docusaurus-plugin-content-pages', () => {
siteDir,
siteConfig,
});
const pagesMetadatas = await plugin.loadContents();
const pagesMetadatas = await plugin.loadContent();
const pagesDir = plugin.contentPath;
expect(pagesMetadatas).toEqual(expected(pagesDir));

View file

@ -31,7 +31,7 @@ class DocusaurusPluginContentPages {
return 'docusaurus-plugin-content-pages';
}
async loadContents() {
async loadContent() {
const {include} = this.options;
const {env, siteConfig} = this.context;
const pagesDir = this.contentPath;
@ -88,11 +88,11 @@ class DocusaurusPluginContentPages {
return pagesMetadatas;
}
async generateRoutes({metadata, actions}) {
async contentLoaded({content, actions}) {
const {component} = this.options;
const {addRoute} = actions;
metadata.forEach(metadataItem => {
content.forEach(metadataItem => {
const {permalink, source} = metadataItem;
addRoute({
path: permalink,

View file

@ -46,27 +46,24 @@ module.exports = async function build(siteDir) {
const props = await load(siteDir);
// Apply user webpack config.
const {
outDir,
siteConfig: {configureWebpack},
} = props;
const {outDir, plugins} = props;
const clientConfigObj = createClientConfig(props);
// Remove/clean build folders before building bundles.
clientConfigObj
.plugin('clean')
.use(CleanWebpackPlugin, [outDir, {verbose: false, allowExternal: true}]);
const serverConfigObj = createServerConfig(props);
const clientConfig = applyConfigureWebpack(
configureWebpack,
clientConfigObj.toConfig(),
false,
);
const serverConfig = applyConfigureWebpack(
configureWebpack,
serverConfigObj.toConfig(),
true,
);
let serverConfig = createServerConfig(props).toConfig();
let clientConfig = clientConfigObj.toConfig();
// Plugin lifecycle - configureWebpack
plugins.forEach(({configureWebpack}) => {
if (!configureWebpack) {
return;
}
clientConfig = applyConfigureWebpack(configureWebpack, clientConfig, false);
serverConfig = applyConfigureWebpack(configureWebpack, serverConfig, true);
});
// Build the client bundles first.
// We cannot run them in parallel because the server needs to know

View file

@ -77,7 +77,7 @@ module.exports = async function start(siteDir, cliOptions = {}) {
// Create compiler from generated webpack config.
let config = createClientConfig(props);
const {siteConfig} = props;
const {siteConfig, plugins = []} = props;
config.plugin('html-webpack-plugin').use(HtmlWebpackPlugin, [
{
inject: false,
@ -89,11 +89,13 @@ module.exports = async function start(siteDir, cliOptions = {}) {
]);
config = config.toConfig();
// Apply user webpack config.
const {
siteConfig: {configureWebpack},
} = props;
// Plugin lifecycle - configureWebpack
plugins.forEach(({configureWebpack}) => {
if (!configureWebpack) {
return;
}
config = applyConfigureWebpack(configureWebpack, config, false);
});
const compiler = webpack(config);

View file

@ -14,6 +14,7 @@ const loadDocs = require('./docs');
const loadEnv = require('./env');
const loadTheme = require('./theme');
const loadRoutes = require('./routes');
const loadPlugins = require('./plugins');
const constants = require('../constants');
module.exports = async function load(siteDir) {
@ -23,7 +24,7 @@ module.exports = async function load(siteDir) {
);
fs.ensureDirSync(generatedFilesDir);
// Site Config - @tested
// Site Config
const siteConfig = loadConfig.loadConfig(siteDir);
await generate(
generatedFilesDir,
@ -31,7 +32,7 @@ module.exports = async function load(siteDir) {
`export default ${JSON.stringify(siteConfig, null, 2)};`,
);
// Env - @tested
// Env
const env = loadEnv({siteDir, siteConfig});
await generate(
generatedFilesDir,
@ -73,76 +74,11 @@ module.exports = async function load(siteDir) {
// Process plugins.
const pluginConfigs = siteConfig.plugins || [];
const context = {env, siteDir, siteConfig};
// Initialize plugins.
const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => {
let Plugin;
// If path itself is provided
if (pluginPath && fs.existsSync(pluginPath)) {
// eslint-disable-next-line
Plugin = require(pluginPath);
} else {
// Resolve using node_modules as well.
try {
// eslint-disable-next-line
Plugin = require(name);
} catch (e) {
throw new Error(`'${name}' plugin cannot be found.`);
}
}
return new Plugin(options, context);
const {plugins, pluginRouteConfigs} = await loadPlugins({
pluginConfigs,
context,
});
// Plugin lifecycle - loadContents().
// Currently plugins run lifecycle in parallel and are not order-dependent. We could change
// this in future if there are plugins which need to run in certain order or depend on
// others for data.
const pluginsLoadedMetadata = await Promise.all(
plugins.map(async plugin => {
if (!plugin.loadContents) {
return null;
}
const name = plugin.getName();
const {options} = plugin;
const {metadataKey, metadataFileName} = options;
const metadata = await plugin.loadContents();
const pluginContentPath = path.join(name, metadataFileName);
const pluginContentDir = path.join(generatedFilesDir, name);
fs.ensureDirSync(pluginContentDir);
await generate(
pluginContentDir,
metadataFileName,
JSON.stringify(metadata, null, 2),
);
const contentPath = path.join('@generated', pluginContentPath);
return {
metadataKey,
contentPath,
metadata,
};
}),
);
// Plugin lifecycle - generateRoutes().
const pluginRouteConfigs = [];
const actions = {
addRoute: config => pluginRouteConfigs.push(config),
};
await Promise.all(
plugins.map(async (plugin, index) => {
if (!plugin.generateRoutes) {
return;
}
const loadedMetadata = pluginsLoadedMetadata[index];
await plugin.generateRoutes({
metadata: loadedMetadata.metadata,
actions,
});
}),
);
// Resolve outDir.
const outDir = path.resolve(siteDir, 'build');
@ -167,16 +103,9 @@ module.exports = async function load(siteDir) {
'../core/metadata.template.ejs',
);
const metadataTemplate = fs.readFileSync(metadataTemplateFile).toString();
const pluginMetadataImports = pluginsLoadedMetadata.map(
({metadataKey, contentPath}) => ({
name: metadataKey,
path: contentPath,
}),
);
const metadataFile = ejs.render(metadataTemplate, {
imports: [
...pluginMetadataImports,
{
name: 'docsMetadatas',
path: '@generated/docsMetadatas',

View file

@ -0,0 +1,70 @@
/**
* 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 fs = require('fs-extra');
module.exports = async function loadPlugins({pluginConfigs = [], context}) {
/* 1. Plugin Lifecycle - Initializiation/Constructor */
const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => {
let Plugin;
if (pluginPath && fs.existsSync(pluginPath)) {
// eslint-disable-next-line
Plugin = require(pluginPath);
} else {
try {
// eslint-disable-next-line
Plugin = require(name);
} catch (e) {
throw new Error(`'${name}' plugin cannot be found.`);
}
}
return new Plugin(options, context);
});
// Do not allow plugin with duplicate name
const pluginNames = new Set();
plugins.forEach(plugin => {
const name = plugin.getName();
if (pluginNames.has(name)) {
throw new Error(`Duplicate plugin with name '${name}' found`);
}
pluginNames.add(name);
});
/* 2. Plugin lifecycle - LoadContent */
const pluginsLoadedContent = {};
await Promise.all(
plugins.map(async plugin => {
if (!plugin.loadContent) {
return;
}
const name = plugin.getName();
pluginsLoadedContent[name] = await plugin.loadContent();
}),
);
/* 3. Plugin lifecycle - contentLoaded */
const pluginRouteConfigs = [];
const actions = {
addRoute: config => pluginRouteConfigs.push(config),
};
await Promise.all(
plugins.map(async plugin => {
if (!plugin.contentLoaded) {
return;
}
const name = plugin.getName();
const content = pluginsLoadedContent[name];
await plugin.contentLoaded({content, actions});
}),
);
return {
plugins,
pluginRouteConfigs,
};
};

View file

@ -14,9 +14,12 @@ import DocusaurusContext from '@docusaurus/context';
function BlogPage(props) {
const context = useContext(DocusaurusContext);
const {blogMetadata, language, siteConfig = {}} = context;
const {language, siteConfig = {}} = context;
const {baseUrl, favicon} = siteConfig;
const {modules: BlogPosts} = props;
const {
metadata: {posts = []},
modules: BlogPosts,
} = props;
return (
<Layout>
@ -28,7 +31,7 @@ function BlogPage(props) {
</Head>
<div>
<ul>
{blogMetadata.map(metadata => (
{posts.map(metadata => (
<li key={metadata.permalink}>
<Link to={metadata.permalink}>{metadata.permalink}</Link>
</li>

View file

@ -5,15 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {useContext} from 'react';
import Link from '@docusaurus/Link';
import DocusaurusContext from '@docusaurus/context';
import React from 'react';
import styles from './styles.module.css';
function Footer() {
const context = useContext(DocusaurusContext);
return (
<footer className={styles.footer}>
<section className={styles.footerRow}>
@ -77,19 +73,6 @@ function Footer() {
</li>
</ul>
</div>
{/* This is for v2 development only to know the available pages. */}
<div className={styles.footerColumn}>
<h3 className={styles.footerColumnTitle}>Pages</h3>
<ul className={styles.footerList}>
{context.pagesMetadata.map(metadata => (
<li key={metadata.permalink} className={styles.footerListItem}>
<Link className={styles.footerLink} to={metadata.permalink}>
{metadata.permalink}
</Link>
</li>
))}
</ul>
</div>
</section>
<section className={styles.copyright}>
<span>Copyright © {new Date().getFullYear()} Facebook Inc.</span>

View file

@ -4,7 +4,7 @@ Plugins are one of the best ways to add functionality to our Docusaurus. Plugins
## Installing a Plugin
A plugin is usually a dependency, so you install them like other packages in node using NPM. However, you don't need to install official plugin provided by Docusaurus team because it comes by default.
A plugin is usually a dependency, so you install them like other packages in node using NPM.
```bash
yarn add docusaurus-plugin-name
@ -16,14 +16,14 @@ Then you add it in your site's `docusaurus.config.js` plugin arrays:
module.exports = {
plugins: [
{
name: 'docusaurus-plugin-content-pages',
name: '@docusaurus/plugin-content-pages',
},
{
// Plugin with options
name: 'docusaurus-plugin-content-blog',
name: '@docusaurus/plugin-content-blog',
options: {
include: ['*.md', '*.mdx'],
path: '../v1/website/blog',
path: 'blog',
},
},
],
@ -50,6 +50,7 @@ For examples, please refer to several official plugins created.
// A JavaScript class
class DocusaurusPlugin {
constructor(options, context) {
// Initialization hook
// options are the plugin options set on config file
this.options = {...options};
@ -62,13 +63,19 @@ class DocusaurusPlugin {
// plugin name identifier
}
async loadContents() {
// Content loading hook that runs the first time plugin is loaded
// expect a content data structure to be returned
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 generateRoutes({metadata, actions}) {
// This is routes generation hook
async contentLoaded({content, actions}) {
// loaded hook is done after load hook is done
// actions are set of functional API provided by Docusaurus. e.g: addRoute
}
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() {