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

[Proposal] Plugin API #329

Closed
ulivz opened this issue May 2, 2018 · 18 comments
Closed

[Proposal] Plugin API #329

ulivz opened this issue May 2, 2018 · 18 comments
Assignees
Labels
help wanted Extra attention is needed type: question or discussion Question or discussion

Comments

@ulivz
Copy link
Member

ulivz commented May 2, 2018

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

@ulivz
Copy link
Member Author

ulivz commented May 2, 2018

cc @yyx990803 @meteorlxy @ycmjason

@meteorlxy
Copy link
Member

meteorlxy commented May 2, 2018

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. 😈

@ulivz
Copy link
Member Author

ulivz commented May 2, 2018

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 ulivz added help wanted Extra attention is needed type: question or discussion Question or discussion labels May 2, 2018
@meteorlxy
Copy link
Member

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

@ulivz
Copy link
Member Author

ulivz commented May 2, 2018

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.

@meteorlxy
Copy link
Member

meteorlxy commented May 2, 2018

What about a dev branch to work together?

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

@ycmjason
Copy link
Contributor

ycmjason commented May 2, 2018

This is just what I feel 😳, 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! 🎉

@yyx990803
Copy link
Member

  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.

@ulivz
Copy link
Member Author

ulivz commented May 3, 2018

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

@meteorlxy
Copy link
Member

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

@ulivz
Copy link
Member Author

ulivz commented May 3, 2018

@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
    ]
}
  1. 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?

@ycmjason
Copy link
Contributor

ycmjason commented May 3, 2018

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

@ulivz
Copy link
Member Author

ulivz commented May 3, 2018

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

@ycmjason
Copy link
Contributor

ycmjason commented May 3, 2018

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

@ycmjason
Copy link
Contributor

ycmjason commented May 3, 2018

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')))

@ulivz
Copy link
Member Author

ulivz commented May 3, 2018

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. 😁

@meteorlxy
Copy link
Member

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

@dgpgdev
Copy link

dgpgdev commented May 10, 2018

+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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed type: question or discussion Question or discussion
Projects
None yet
Development

No branches or pull requests

5 participants