Later today I am going to be merging our new post rendering engine into master. Swapping out Ember/HTMLBars for a virtual-dom based renderer provides us with a huge performance increase, but it also has the side effect of being incompatible with how plugins were previously made.
While doing this work, it occurred to me that we made a fairly big mistake in the past by not providing a mechanism for versioning the public APIs used by client side code. We used to encourage people to just dump in whatever Javascript they wanted in an Ember initializer, which works great and is fast to develop, but it has major issues if we ever want to deprecate or update those APIs.
The good news is the latest version of Discourse has a new versioned API for plugins and extensibility. The bad news is most plugins will need to be updated.
To ease in this transition, I spent the last two weeks going through the most popular discourse plugins and updating them so that they’d be compatible with both versions of discourse. If your plugin was open source, you might have received a pull request from me! If you are a plugin author, please check your plugin against the tests-passed
of discourse!
I have provided deprecation warnings for old APIs that are migrated to the new system. I’ve also tried wherever possible to always boot discourse, even if code is using old objects or functions. The odds are good that even if you coded directly against an internal Discourse object, your plugin will not stop Discourse from loading (but it will not add or change functionality!)
Using the new API
To use the new client side API, you must ask Discourse for an instance of a PluginAPI
object. You can do this in an initializer by importing withPluginApi
and calling it like so:
// assets/javascripts/discourse/initializers/with-plugin-sample.js.es6
import { withPluginApi } from 'discourse/lib/plugin-api';
export default {
name: 'with-plugin-sample',
initialize() {
withPluginApi('0.1', api => {
api.onPageChange(() => console.log('user navigated!'));
});
}
}
The idea here is you tell Discourse you want an instance of the Plugin API object for a particular version of the API. Since this is the first release, I’ve specified the version 0.1
.
The cool thing here is that Discourse can provide backwards compatible API objects, even if things behind the scene change. We could be up to version 1.3, for example, but still know how to provide an API compatible object for 0.1.
Additionally, if the API you used is so old that we can’t provide a backwards compatibility layer (likely due to a very severe refactor down the line) we just won’t call the function and your plugin code will not be executed at all. The idea here is it’s better to do nothing at all, rather than have a Javscript error that would interrupt Discourse from starting up safely.
Plugin API Documentation
The documentation for the PluginAPI version 0.1 can be found in plugin-api.gjs. I’ve documented the public surface area of the API there. The easiest thing to do when developing a plugin is to open that file and look for the latest hooks and methods you can consume. Also, don’t be afraid to ask questions on meta! All the Discourse developers read this forum daily and we can offer guidance and support.
Providing Backwards Compatiblity
New plugin authors will be able to use the withPluginApi
as specified above. However, for older plugins that must co-exist with versions of Discourse that exist before the new API was added it’s a little more involved.
I have backported the withPluginApi
call to the beta and stable branches of Discourse, so that call is safe to make in all currently supported releases. As mentioned previously, if no api is available for the version you requested, that code will not run, so you can use it to gate the code that works with the new API safely.
However, you’ll still need a way to run your old code, so I’ve added a noApi
option to help with this. This is how I’ve ported all the plugins I’ve done so far. Here’s an example of that approach:
// assets/javascripts/discourse/initializers/backwards-example.js.es6
import { withPluginApi } from 'discourse/lib/plugin-api';
function oldCode() {
// migrate your old plugin code here. It will only be run if no PluginAPI is present
}
function initializePlugin(api) {
// do stuff with plugin API!
}
export default {
name: 'backwards-example',
initialize() {
withPluginApi('0.1', api => initializePlugin(api), { noApi: () => oldCode() });
}
}
In the above example, if version 0.1 of the plugin API is available, initializePlugin
will be called and can use it. If no API is present, oldCode
will be called. You can paste your old plugin code there and it should work. If you want a more involved example, you can see here how I updated the solved plugin to use this approach.
Using the versioned API in a Site Customization
See this write up for information on how to use the plugin API safely within a Site Customization.
Final Thoughts
Again, I’d like to apologize for not having a versioned API in place in the past and the difficulties this will introduce. However, in the long run I believe this change will be worth it, and will drastically prevent developer pain as we continue to update Discourse.