Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Plugins/Themes/Presets API #1438

Open
yangshun opened this issue May 7, 2019 · 5 comments

Comments

@yangshun
Copy link
Member

commented May 7, 2019

We've implemented a basic version of plugins, themes and presets and would like to get the opinion from the community regarding the API and some design decisions before proceeding further.

Feel free to check out the packages within https://github.com/facebook/Docusaurus/tree/master/packages to get a sense of what has been done.

Glossary

Plugins

A plugin is a package that exports a class which can be instantiated with configurable options (provided by the user) and its various methods will be invoked by the Docusaurus runtime. Examples of plugin lifecycle methods are:

  • loadContent - Plugins should fetch from data sources (filesystem, remote API, etc)
  • contentLoaded - Plugins should use the data loaded in loadContent and construct the pages/routes that consume the data
  • configureWebpack - To extend the webpack config via webpack-merge.

For example, the in docusaurus-plugin-content-docs:

  1. In loadContent, it loads the doc Markdown files based on the specified directory in options (defaulting to docs).
  2. In contentLoaded, for each doc Markdown file, a route is created: /doc/installation, /doc/getting-started, etc.

Plugin design is very similar to Gatsby and VuePress, but most plugins work like that anyway The main difference here is that Docusaurus plugins currently doesn't allow including other plugins. If plugins are meant to used together, it would have to be done using presets.

In most cases, plugins are there to fetch data and create routes. A plugin could take in components as part of its options and to act as the wrapper for the page.

Themes

Themes are plugins with exactly the same lifecycle methods, but they mostly exist to add component aliases by extending the webpack config, and run after all existing plugins. The reason for the differentiation is so that by running after the pure plugins, the webpack resolve aliases that the theme sets will take precedence.

How Theming is Done

Users can install themes, that provide a set of components, e.g. Navbar, Layout, Footer. Themes are plugins but most (all?) of them shouldn't be using the loadContent lifecyle.

Users can use these components in their code by importing them using the @theme webpack alias:

import Navbar from '@theme/Navbar';

The alias @theme can refer to a few directories, in the following priority:

  1. A user's website/theme directory, which is a special directory that has the higher precedence.
  2. A Docusaurus theme packages's theme directory.
  3. Fallback components provided by Docusaurus core (usually not needed).

Given the following structure

website
├── node_modules
│   └── docusaurus-theme
│       └── theme
│           └── Navbar.js
└── theme
    └── Navbar.js

website/theme/Navbar.js takes precedence whenever @theme/Navbar is imported. This behavior is called component swizzling. In iOS, method swizzling is the process of changing the implementation of an existing selector (method). In the context of a website, component swizzling will mean providing an alternative component that takes precedence over the theme-provided component.

The default Docusaurus theme will provide most components out of the box, and users can swizzle the components by adding a component of the same name in the website/themes directory if they want to modify how it looks and behaves.

Themes are for providing UI components to present the content. Most content plugins would need to be paired with a theme in order to be actually useful. The UI is a separate layer from the data schema, so it makes it easy to swap out the themes for other designs (if someone wants to use Bootstrap for example).

A default Docusaurus blog would have in the config:

// docusaurus.config.js
{
  theme: ['theme-blog'],
  plugins: ['plugin-content-blog'],
}

and if someone wants to use Bootstrap styling:

// docusaurus.config.js
{
  theme: ['theme-blog-bootstrap'],
  plugins: ['plugin-content-blog'],
}

The content plugin remains the same and the only thing they need to change will be the theme.

Presets

A package that exports a config file which can contain a set of themes, plugins, and potentially other presets (TBD). The purpose of presets is to bundle themes and plugins together so that they can be easily shared and upgraded.

RFC

  1. Should presets be able to contain other presets?

I was originally envisioning this sort of hierarchy if we could have presets within presets.

preset-classic
├── theme-classic
│   ├── Navbar.js
│   ├── Layout.js
│   └── Footer.js
├── preset-docs
│   ├── theme-docs
│   │   ├── DocPage.js
│   │   └── DocItem.js
│   └── plugin-content-docs
│       └── index.js
├── preset-blog
│   ├── theme-blog
│   │   ├── BlogPage.js
│   │   └── BlogItem.js
│   └── plugin-content-blog
│       └── index.js
└── plugin-pages
    └── index.js

plugin-content-blog reads the data and creates routes, and theme-blog provide the components for the routes to use.

And if a user simply wants to use a blog, they just have to install preset-blog instead of having to install both theme and plugin separately.

However, this simpler hierarchy could also work:

preset-classic
├── theme-classic
│   ├── Navbar.js
│   ├── Layout.js
│   └── Footer.js
├── plugin-docs
│   ├── theme
│   │   ├── DocPage.js
│   │   └── DocItem.js
│   └── index.js
├── plugin-blog
│   ├── theme
│   │   ├── BlogPage.js
│   │   └── BlogItem.js
│   └── index.js
└── plugin-pages
   └── index.js

The difference here is that plugins can also provide some theme components. However, this blurs the lines between plugins and themes which might not be good (should I now provide components in a plugin or a theme?). I think having a clear separation is great for discoverability of new plugins and communicating what exactly a package does.

  1. Should themes be allowed to include sub-themes and plugins be allowed to include sub-plugins? I feel that allowing this will encourage recursive nesting of plugins and themes which could end up being really messy. By allowing theme/plugin composition via presets, the hierarchy stays relatively flat.

cc frequent Docusaurus users - @JoelMarcey @jaredpalmer @jordwalke @SimenB @chenglou @microbouji @hzoo @markerikson

@yangshun yangshun pinned this issue May 7, 2019
@endiliey endiliey closed this May 7, 2019
@yangshun yangshun reopened this May 7, 2019
@microbouji

This comment has been minimized.

Copy link
Contributor

commented May 8, 2019

I haven't had a chance to look too much into the implementation but here are my thoughts from an API user standpoint:

  1. I also like the first approach with separate plugins and themes. Would the overriding of specific themes work same as for the "main" theme? So if you have:
// docusaurus.config.js

{
  plugins: ['plugin-pages', 'plugin-content-blog'],
  themes: ['theme-classic', 'theme-blog', 'theme-blog-bootstrap'],
}

// or in reality just:
{
  presets: ['preset-classic']
  themes: ['theme-blog-bootstrap'],
}

and

website
├── node_modules
│   ├── theme-classic
│   │   └── Navbar.js
│   ├── theme-blog
│   │   ├── BlogPage.js *
│   │   ├── BlogItem.js
│   │   └── BlogList.js
│   └── theme-blog-bootstrap
│   │   ├── BlogItem.js *
│   │   └── BlogList.js
└── theme
    ├── Navbar.js *
    └── BlogList.js *

the "active" files would be BlogPage.js from theme-blog (because no one is overriding it), BlogItem.js from theme-blog-bootstrap (because of the order in config), and BlogList.js from the local theme folder.

  1. No sub-themes and sub-plugins should be needed if they can be composed as above. theme-blog-bootstrap could have theme-blog in it's peerDependecies effectively becoming a child of it while being published separately.
@endiliey

This comment has been minimized.

Copy link
Collaborator

commented May 8, 2019

@microbouji

Would the overriding of specific themes work same as for the "main" theme? So if you have ....

Yes. Your scenario behavior is correct. The theme plugins are prioritized based on order. So if there is theme-1 and theme-2, theme-2 will
override any components of theme-1 that overlap. But the special theme folder in user’s website always win.

This theming system makes it easy to publish reusable components and override components. For example, theme-classic need a @theme/SearchBar component. By default it will point to a noop module. But by adding theme-search-algolia, @theme/SearchBar become a searchbar based on algolia. Theme-search-offline is also possible

Currently we also introduce a swizzle command with syntax swizzle <themeName> [componentName]that help you copy the relevant files to your special “theme” folder in website.

I also personally like separate plugins and themes.

Currently, another big question Yangshun is asking is whether preset can contain another preset. I personally prefer a flat hierarchy for now, so presets can only contain themes and plugins. This is to avoid cyclic dependency and its much easier to spot what themes are being included in a preset. We can also add this preset include preset in the future as it is backward compatible.

i am also quite against sub-themes and sub-plugins. Sub-presets might be ok: not ok. needs more discussion

@wgao19

This comment has been minimized.

Copy link
Collaborator

commented Jun 27, 2019

Hey @yangshun @endiliey I'm working on the docs for plugins and here's something I feel slightly hairy about:

Looks like, it is possible if not recommended that we pass plugin options via preset options. Meanwhile, it is also possible to just write our own docusaurus.config.js with a bunch of plugins each passing its own options. But some options (such as Google Analytics trackingID can only be specified via themeConfig or from preset options. Personally I feel the wrapping and discrepancy in option passing slightly confusing.

@wgao19

This comment has been minimized.

Copy link
Collaborator

commented Aug 10, 2019

Hey @tobiastimm,

I happen to be working on the docs, thanks for asking about plugins and letting us know the confusing parts!

It is also possible that your plugins include the components in your plugin. Check out @docusaurus/plugin-ideal-image.

@tobiastimm

This comment has been minimized.

Copy link

commented Aug 10, 2019

@wgao19 yeah saw that that's why I've deleted that comment 😄 thanks! I will take a look at this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.