mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-05 04:12:53 +02:00
refactor(v2): plugins lifecycle (#1299)
* refactor(v2): plugins lifecycle * dont allow duplicate plugin & fix typo
This commit is contained in:
parent
3a7a253db7
commit
73b89658cc
10 changed files with 127 additions and 136 deletions
|
@ -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({
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
70
packages/docusaurus/lib/load/plugins.js
Normal file
70
packages/docusaurus/lib/load/plugins.js
Normal 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,
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue