Extending
You can add additional functionality to bud.js by writing a bud.js extension.
Extension shape
Extensions can be provided as either a plain JS object or a class.
const MyExtension = {
register: async(bud) {}
}
Extensions writen using JS classes should extend the Extension
base class (exported from @roots/bud-framework/extension
).
import {Extension} from '@roots/bud-framework/extension'
class MyExtension extends Extension {
public async register() {}
}
The rest of this document assumes that extensions are being authored as a class and are being exported from an ESM package.
Interface
label
The extension label
serves as a handle for other extensions or the user config. It should be resolvable as a module signifier.
For instance if your extension is published to npm as bud-extension-name
then the label
should match:
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension-name'
}
dependsOn
Extensions may depend on other other extensions. For instance, if you are authoring an extension that manages PostCSS plugins then your extension
depends on the presence of @roots/bud-postcss
. To ensure dependencies are available, you may list their label
s in a dependsOn
public property.
The dependsOn
property is expressed as a Set
:
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension-name'
public dependsOn = new Set(['@roots/bud-postcss'])
}
dependsOnOptional
Extensions may depend on other extensions on an optional basis. bud.js will attempt to register the extension if it is available. Otherwise, it will silently proceed.
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public dependsOnOptional = new Set(['@roots/bud-postcss'])
}
options
Extension options can be set in the options
property.
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public options = {
option: 'value',
}
}
The options
property is treated specially.
Each option value can be expressed as either the literal value itself or a function receives the bud.js instance object and returns the value.
This is useful when you can't know the value up front (as is the case with user paths):
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public options = {
option: (bud: Bud) => bud.path('@src'),
}
}
Now, if the user makes a change to the @src
path, the reference will be updated in the extension.
The only "gotcha" here is that if you have an extension option which is itself a function. Since bud.js will try to call the function to resolve the option value you may need to wrap it in another function:
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
/**
* This is the function we want to use an an option value
*/
const callback = (prop: string): string => 'hello, world!'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public options = {
/**
* Rather than passing the function directly, we wrap it in another function.
* This way, **bud.js** will not call the function when resolving the option value.
*/
prop: () => callback,
}
}
There are some additional methods available for working with extension options:
setOption
extension.setOption('foo', value)
setOptions
extension.setOptions({
foo: `literal`,
bar: (bud: Bud) => () => `callback`.
})
init
Async callback. Called first. Useful to avoid needing to deal with super
and the constructor.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async init(bud) {
// do something
}
}
register
Async callback. Try to do things in this method, when possible.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async register(bud) {
// do something
}
}
boot
Async callback. Called after register
. Good for business which requires another
extension to have already had register
called on it.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async boot(bud) {
// do something
}
}
configAfter
Async callback. Called after user configuration has been processed.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async configAfter(bud) {
// do something
}
}
buildBefore
Async callback. Called before final configuration is built.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async buildBefore(bud) {
// do something
}
}
buildAfter
Async callback. Called after final configuration is built.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async buildBefore(bud) {
// do something
}
}
make
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
import {bind} from '@roots/bud-framework/extension/decorators'
import Plugin from 'some-webpack-plugin'
export default class MyExtension extends Extension {
public label = 'bud-extension'
@bind
public async make() {
return new Plugin(this.options)
}
public options = {
option: (bud: Bud) => bud.path('@src'),
}
}
plugin
A plugin constructor. Will be passed the extension options. Used in lieue of make.
A plugin is defined as a newable class or function returning an object with an apply
method. Here is how bud.js types it:
/**
* Webpack plugin.
*/
export interface ApplyPlugin {
/**
* Loose defined
*/
[key: string]: any
/**
* Apply callback
*
* @see {@link https://webpack.js.org/contribute/writing-a-plugin/#basic-plugin-architecture}
*/
apply: (compiler?: Compiler) => unknown
}
/**
* Newable function or class that returns
* an {@link ApplyPlugin} instance.
*/
export interface ApplyPluginConstructor {
new (...args: any[]): ApplyPlugin
}
And here is how you might use it:
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
import Plugin from 'some-webpack-plugin'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public plugin = Plugin
public options = {
option: (bud: Bud) => bud.path('@src'),
}
}
apply
An apply
method indicates to bud.js that the extension is doing double duty as a compiler plugin and a bud.js extension.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
import Plugin from 'some-webpack-plugin'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public apply() {
// see webpack documentation. this is treated exactly the same as a pure webpack plugin.
}
}
When present bud.js will pass the extension to the compiler.
when
A callback that returns a boolean. The extension will be registered regardless of the value returned by this function, but certain methods
will not be called if this function returns false
.
This function is passed the bud.js instance.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async when(bud) {
return bud.isProduction && this.options.set.size > 0
}
}
If Extension.when
returns false
the following methods will not be called:
Extension.enabled
(a simple boolean
) will always take precedence over Extension.when
if it is set.
import {Bud} from '@roots/bud-framework'
import {Extension} from '@roots/bud-framework/extension'
export default class MyExtension extends Extension {
public label = 'bud-extension'
public async register(bud) {
// this overrides `when`
this.enabled = true
}
/**
* This function will never be called because {@link Extension.enabled}
* was set to `true` in {@link Extension.register}.
*/
public async when(bud) {
return bud.isProduction && options.set.size > 0
}
}
This is by design; it makes it more straight-forward for the consuming developer to reason about enabling or disabling particular extensions should they wish to override your implementation.