Vuepress: [Proposal] Plugin API

Created on 2 May 2018  ·  18Comments  ·  Source: vuejs/vuepress

Background

Hey, guys, there is a previous work for plugin support: #240 (_Lifecycle-based Plugin Support_), but it seems only focus on providing Hook, and doesn't provide some useful APIs for plugin to use. so this issue is for that.

Proposal

My proposal is to leverage the plugin style from _webpack_. so for me, a ideal plugin would be like:

class XXXPlugin {
  constructor(pluginOptions) {
    this.options = pluginOptions
  }

  apply(app) {

    // app is the current VuePress app instance.
    // You can access all the app's properties and methods.
    // so to finish this plugin API, we need to rewrite the core as a class.

    ////////////////////////////
    // Basic extension
    ////////////////////////////

    // #1 extend webpack via webpack-chain.
    app.chainWebpack(config => { })

    // #2 extend markdown
    app.extendMarkdown(md => {
      md.use(require('markdown-it-xxx'))
    })

    // #3 add extra temp file.
    // so 'enhanceApp' and 'themeEnhanceApp' can be seperated as plugin.
    app.enhanceAppFiles.add('[relative]', content) // generate file to '.temp/relative'
    app.enhanceAppFiles.add('[absolute]') // copy to '.temp' if 'absolute' exists

    // #4. Write extra file to outDir.
    app.outFiles.add('[relative]', content) // generate file to 'outDir/relative'
    app.outFiles.add('[absolute]') // copy to outDir if 'absolute' exists

    // #5. Add extra data mixins, will be merged to core's dataMixin.
    app.dataMixin.add({
      computed: {
        $themeTitle () { /* ... */ }
      },
      methods: { /* ... */ }
    })

    // #6. enhance dev server ('webpack-serve')
    app.enhanceDevServer(server => {})

    // #7. extend the $page's data
    // so 'lastModified' support can be separated into a plugin.
    app.extendPageData((filepath, path, content, frontmatter) => {
      return {
        lastModified: getLastModified(filepath)
      }
    })

    ////////////////////////////
    // Life Cycle (Browser)
    ////////////////////////////

    // #8 called before creating Vue app. (DEV & BUILD)
    // So 'GA', 'SW' and 'scrollingActiveSidebarLink' could be separated into a plugin.
    // Consider if we should open the Layout's hooks ???
    app.hook.add('beforeCreateApp', ({ Vue, options, router, siteData }) => {})

    ////////////////////////////
    // Life Cycle (Node.js)
    ////////////////////////////

    // #9. called when all options was resolved. (DEV & BUILD)
    // with this hook, all the internal generate-related logic at prepare
    // can be separated into a plugin.
    app.hook.add('ready', () => {})

    // #10. called when webpack finished compiled. (DEV & BUILD)
    app.hook.add('compiled', () => {})

    // #11. called when webpack hot updated (only DEV)
    app.hook.add('updated', () => {})

    // #12. called when all files are generated (only BUILD)
    app.hook.add('generated', () => {})

    // ... we can also provide some useful utils.
  }
}

With this plugin mechanism, maybe we can do a lot of things we want to do.

Feel free to tell me your thoughts. and we can make plugin more _powerful_ !!!

help wanted question or discussion

Most helpful comment

  1. I actually prefer a simple object-based API as @ycmjason suggested. If the plugin needs to take options it can be a function like module.exports = options => pluginObject.

  2. I don't think core needs to be rewritten as a class to support plugins

  3. Note client code and server code have different constraints so they cannot live in the same file.

All 18 comments

cc @yyx990803 @meteorlxy @ycmjason

Looks cool.

Current code of core is a little messy, so I did some refactor work. But it's not a thorogh solution to the problem.

With the help of plugin API, many existed functions could be separated into plugins. We should ensure the core has basic function, and then extract others into plugins.

// so to finish this plugin API, we need to rewrite the core as a class.

Looks like an entire rewrite. No wonder you didn't merge my PRs. 😈

I was going to merge, but I saw @yyx990803 approved it just a few minutes ago, so I let it go. but then, Evan didn't merge it, LOL. 😅

@ulivz Will this plan begin after approved? Or you have had some draft code?

I have started worked for that by following my plan, but writing code behind closed doors is not a good habit to write open source, so I want to hear more about your guys opinions.

Just want it to be better. so welcome to all your ideas here.

What about a dev branch to work together?

(btw so when will the approved pr to be merged :sweat_smile: )

This is just what I feel :flushed:, maybe I haven't read and understood enough, but the idea of using the app seems quite vague. Everything seems to appear in app. Perhaps we could define a little more on what app is.

I probably prefer a config-based way to define the plugin, similar to how we define Vue components:

module.exports = {
  chainWebpack(config) {
     ...
  }

  extendMarkdown(md) {
    md.use(require('markdown-it-xxx'))
  }

  clientHooks: {
    beforeCreateApp(...) {
      ...
    }
  }

  serverHooks: {
    ready() {

    }

    compiled() {

    }

    updated() {

    }

    generated() {

    }
  }
};

Especially with the hooks, if we can make it looks like this, Vue developers might possibly find this more familiar? I personally find this a little bit more intuitive as the config describes what the methods are for. Perhaps bind this to app in the methods too?

Just some thoughts, not sure if you guys have considered yet. Let's make this plugin thing right! :tada:

  1. I actually prefer a simple object-based API as @ycmjason suggested. If the plugin needs to take options it can be a function like module.exports = options => pluginObject.

  2. I don't think core needs to be rewritten as a class to support plugins

  3. Note client code and server code have different constraints so they cannot live in the same file.

Following Vue's convention is a better choice! cool !

What about a VuepressPlugin class to be extended from? So we can predefine the interface

@yyx990803 After discussed at https://vuepress.slack.com/ with @meteorlxy and @ycmjason , we come up with a preliminary solution:

1 API

1.1 Plugin's entry

Now a plugin file would be like this:

const path = require('path')

module.exports = options => ({

  // Specify the client plugin's absolute path.
  // If given, this file will be bundled to client's output
  // and executed at client side.
  client: path.resolve(__dirname, 'client.js'),

  chainWebpack (config) { /* */ },

  enhanceDevServer (server) { /* */ },

  extendMarkdown (md) { /* */ },

  enhanceAppFiles () {
    return {
      // Will be generated to '.temp/enhanceApp.js'
      'enhanceApp.js': '[content]' // string | Buffer
    }
  },

  outFiles () {
    return {
      // Will be generated to 'outDir/CNAME', BUILD only
      'CNAME': options.domain // string | Buffer
    }
  },

  // extend the $page's data
  extendPageData ({ filepath, path /* router url */, content, frontmatter }) {
    return {
      lastModified: getLastModified(filepath)
    }
  },

  // called when all options was resolved. (DEV & BUILD)
  ready () { /* */ },

  // called when webpack finished compiled. (DEV & BUILD)
  compiled () { /* */ },

  // called when dev server hot updated. (DEV only)
  updated () { /* */ },

  // called when all files are generated. (BUILD only)
  generated () { /* */ }

})

Aslo support plain object: module.exports = {}.

1.2 Client entry

And client.js(need to configure its path at plugin's entry) would be like this:

export default {
  // API that vuepress only
  beforeCreateApp ({ Vue, options, router, siteData }) {},
  extendGlobalMixins ({ router, siteData }) {},

  // Mix rest options in Layout component
  mounted () {},
  beforeDestroy () {},
  methods: {}
}

2 Usage

2.1 define at .vuepress/config.js

  1. Function in Array
module.exports = {
    plugins: [
        require('./rssPlugin.js')   
    ]
}
  1. String in Array

    module.exports = {
      plugins: [
          'pluginName', // will to load 'vuepress-plugin-${pluginName}'
          'vuepress-plugin-rss', // also support full name
      ]
    }
    
  2. Array in Array (Babel Style)

This configuration style comes from babel:

module.exports = {
    plugins: [
        [
          'rss',
          {
              option1: '1',
              option2: '2'
              // ...
          }
        ]
    ]
}

Do you think it works?

2.2 define at themeDir/config.js

module.exports = {
  plugins: [
    // ...
  ]
}

Do you think it works?

@ulivz

May I just note that we don't have to keep a single entry. I prefer keeping them as server.js and client.js as this makes things obvious.

We can refer to the server and client files by doing:

// for the server entry
require('vuepress-plugin-blah/server')
// for the client entry
import plugin from 'vuepress-plugin-blah/client';

@ycmjason

@meteorlxy and I originally also want it to be server.js and client.js, but it's hard to use, since we want to use it like:

// .vuepress/config.js
module.exports = {
    plugins: [
        require('./xxxPlugin.js')   
    ]
}

and keep a single entry also doesn't restrict the structure and naming of a plugin.

@ulivz
Lets talk in slack. I think if the plugin is capable of doi~g what enhanceApp does, we could move enhanceApp into .vuepress/custom/server.js which the .vuepress/custom will be treated as a custom plugin.

This way we unify how things are done with plugins.

We could also keep server.js and client.js:

// .vuepress/config.js
module.exports = {
    plugins: [
        './path/to/plugin-dir'
    ]
}

Then we get ther server.js and client.js by doing

const serverPlugins = config.plugins.map(dir => require(path.join(dir, 'server.js')))

Whether to keep a single entry is only a personal preference. just I prefer single entry with only using a plain object or pure function to write a plugin, which is easy to implement and use IMO. and also doesn't restrict the naming and plugin's directory structure.

@yyx990803 Need your opinion here. 😁

In addition, a single "main" entry may be more like a npm package.

+1 @ycmjason client.js and server.js in same diretory.
@meteorlxy main entry is not necessary, just scan directory to find client and/or server js files.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shaodahong picture shaodahong  ·  3Comments

lileiseven picture lileiseven  ·  3Comments

gaomd picture gaomd  ·  3Comments

higuoxing picture higuoxing  ·  3Comments

harryhorton picture harryhorton  ·  3Comments