Skip to content

BladeRunnerJS Plugin Development Guide

Dominic Chambers edited this page Jan 15, 2014 · 12 revisions

BladeRunnerJS supports the following plug-in interfaces:

  • ModelObserverPlugin (observe events on the model)
  • CommandPlugin (run commands through 'brjs')
  • MinifierPlugin (source-map friendly minifier support)
  • TestPlugin (support new test runners -- not yet available)
  • ContentPlugin (serve browser requests)
  • TagHandlerPlugin (replace logical page tags)
  • AssetLocationPlugin (support new asset directory structures, e.g. Node.js directory structure)
  • AssetPlugin (support new asset file types, e.g. Node.js classes)

All plug-ins extend Plugin, which has the following interface:

public interface Plugin {
	public void setBRJS(BRJS brjs);
	boolean instanceOf(Class<? extends Plugin> otherPluginCLass);
	Class<?> getPluginClass();
}

The setBRJS() method is used to provide access to the model for all plug-ins. The need for the instanceOf() and getPluginClass() methods is explained later in the Circular Dependencies section, but it's important to note that developers shouldn't be implementing these methods themselves, and should instead extend one of AbstractModelObserverPlugin, AbstractCommandPlugin, AbstractMinifierPlugin, AbstractTestPlugin, AbstractContentPlugin, AbstractTagHandlerPlugin, AbstractAssetLocationPlugin or AbstractAssetPlugin rather than attempting to implement the full interfaces themselves.

ModelObserverPlugin

Model observer plug-ins listen to the model using the AbstractNode.addObserver(EventObserver observer) method. Any node within the model can be observed, including BRJS itself if all events need to be observed. They have the following interface:

public interface ModelObserverPlugin extends Plugin {
}

CommandPlugin

The main advantage to creating a command plug-in rather than providing an external command or script is that command plug-ins get access to the BladeRunnerJS model. They have the following interface:

public interface CommandPlugin extends Plugin {
	String getCommandName();
	String getCommandDescription();
	String getCommandUsage();
	String getCommandHelp();
	void doCommand(String[] args) throws CommandArgumentsException, CommandOperationException;
}

Command plug-ins can either implement CommandPlugin themselves, or extend ArgsParsingCommandPlugin, if they'd like to use the same library BladeRunnerJS uses internally for all command-line argument parsing.

MinifierPlugin

Minifier plug-ins allow alternate minifier implementations to be made available for JavaScript & CSS minification.

Minifier plug-ins may or may not choose to support source-maps, with the following levels of support being possible:

  • No source-map support: Minifiers do not themselves support source-maps, and do not interfere with instances of ContentPlugin that are capable of generating source-maps.
  • Single-level source-map support: Minifiers support source-map generation for the minified content they generate, but neither preserve nor interfere with source-maps that can be generated from instances of ContentPlugin that are capable of generating source-maps.
  • Multi-level source-map support Minifiers support source-map generation for the minified content while preserving any source-maps generated from instances of ContentPlugin that are capable of generating source-maps.

They have the following interface:

public interface MinifierPlugin extends Plugin {
	List<String> getSettingNames();
	void minify(String settingName, List<InputSource> inputSources, Writer writer) throws IOException;
	void generateSourceMap(String minifierLevel, List<InputSource> inputSources, Writer writer) throws IOException;
}

TestPlugin

To Be Defined...

ContentPlugin

Content plug-ins allow generated content to be returned by the application server in response to requests from the web browser. Where content plug-ins differ from similar mechanisms employed on most other application servers is that they are required to support flat-file export, and so must therefore have the following properties:

  • Determinism: The same response must always be returned given the same request and the same set of files on disk.
  • Discoverability: The complete list of valid content paths must be provided by content plug-ins on demand using getValidDevContentPaths() & getValidProdContentPaths(), such that any requests from the browser that are not included in this list are considered to be an error.

They have the following interface:

public interface ContentPlugin extends Plugin {
	String getRequestPrefix();
	String getGroupName();
	ContentPathParser getContentPathParser();
	void writeContent(ParsedContentPath contentPath, BundleSet bundleSet, OutputStream os) throws BundlerProcessingException;
	List<String> getValidDevContentPaths(BundleSet bundleSet, String locale) throws BundlerProcessingException;
	List<String> getValidProdContentPaths(BundleSet bundleSet, String locale) throws BundlerProcessingException;
}

Content plug-ins are provided a BundleSet object that may or may not be of use to them. Bundle-sets, are defined below.

TagHandlerPlugin

Tag handler plug-ins allow BladeRunnerJS tags within index pages (e.g. <@bundle.js/@>) to be replaced with arbitrary content.

Tag handlers are often used to cause the browser to make requests to a ContentPlugin, using the ContentPlugin.getContentPathParser() and ContentPathParser.createRequest() methods to help generate valid content paths. Similar to ContentPlugin instances, tag-handler plug-ins should be deterministic, in that they generate the same content each time they are run.

They have the following interface:

public interface TagHandlerPlugin extends Plugin {
	String getTagName();
	String getGroupName();
	void writeDevTagContent(Map<String, String> tagAttributes, BundleSet bundleSet, String locale, Writer writer) throws IOException;
	void writeProdTagContent(Map<String, String> tagAttributes, BundleSet bundleSet, String locale, Writer writer) throws IOException;
}

Tag-handler plug-ins are provided a BundleSet object that may or may not be of use to them. Bundle-sets, are defined below.

Composite Content & Tag-Handler Plugins

The ContentPlugin and TagHandlerPlugin both have a getGroupName() method that allows related plug-ins to be grouped together using composition. This mechanism make it possible for a single logical tag to be used to include all assets of a particular type, or for a single browser request to cause all assets to be downloaded. The built-in BRJS bundler plug-ins use Mime-Types as their grouping keys, but user plug-ins are free to make use of any other grouping keys too.

AssetLocationPlugin & AssetPlugin

Asset location plug-ins and asset plug-ins are used to build the BundleSet that is used by ContentPlugin & TagHandlerPlugin instances. Put simply, a bundle-set is the set of all assets that should be sent to the browser for a given request; whether they will all be sent depends on the logical tags that have been used in the index page.

However, before we can understand bundle-sets fully, we need some definitions:

  • Asset container: a deep directory structure containing assets, linked assets and source modules -- examples include libraries, aspects, bladesets and blades.
  • Asset location: an asset container partitioning mechanism that ensures that only the assets within an asset location, and not the entire set of assets within the asset container, are bundled when a source module within the same asset location is required.
  • Asset: A file on disk that will be bundled if a source module in the same location has been required; it's also possible that assets correspond to multiple files on disk, or no files at all.
  • Linked Asset: A bundlable asset that can also contain references to other source modules that should be bundled if this asset is also to be bundled.
  • Source module: A JavaScript linked-asset that is addressable via a unique require path. Source modules can also contain order dependent dependencies that must appear before the dependent source module itself, in addition to the plain dependencies possible with regular linked-assets.

Bundle-sets are built using the following process:

  1. Requests received by the server are analysed to determine the bundlable-node the request is being targetted at.
  2. The list of asset-containers that should be scanned for the given bundlable-node is determined.
  3. With the help of the various AssetLocationPlugin instances available, a list of asset-locations is determined for each asset-container.
  4. With the help of the various AssetPlugin instances available, a list of assets is determined for each asset-location.

They have the following interfaces:

public interface AssetLocationPlugin extends Plugin {
	List<AssetLocation> getAssetLocations(AssetContainer assetContainer);
}

and:

public interface AssetPlugin extends Plugin {
	List<SourceModule> getSourceModules(AssetLocation assetLocation);
	List<LinkedAsset> getLinkedAssets(AssetLocation assetLocation);
	List<Asset> getAssets(AssetLocation assetLocation);
}

Accessing Plug-ins Via The Model

Plug-ins can be accessed directly via BRJS.plugins(), or indirectly using one of the many methods that makes use of plug-ins under the covers, for example:

  • BRJS.runCommand(String... args)
  • BundlableNode.getSourceModule(String requirePath)
  • BundlableNode.getBundleSet()

Target Plug-ins for 1.0

Here are some of the more interesting plug-ins that will be available with the 1.0 release, with the plug-in interfaces they need to implement indicated:

  • In-built Jetty web server (CommandPlugin)
  • Flat-file export support (CommandPlugin)
  • WAR export support (CommandPlugin)
  • Templated project and blade creation (CommandPlugin)
  • BladeRunnerJS Thirdparty Library Support (AssetLocationPlugin, AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • Node.js Thirdparty Library Support (AssetLocationPlugin, AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • BladeRunnerJS Conformant Library Support (AssetLocationPlugin -- class & asset support is dependent on other plug-ins)
  • Node.js style class support (AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • Namespaced style class support (AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • CSS bundling (AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • HTML template bundling (AssetPlugin & ContentPlugin)
  • XML config bundling (AssetPlugin & ContentPlugin)
  • I18N internatiionalization support (AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • Composite Javascript Bundling (ContentPlugin & TagHandlerPlugin)
  • Composite CSS Bundling (ContentPlugin & TagHandlerPlugin)
  • Fast concatenating minification (MinifierPlugin)
  • Closure Compiler minification with single-level source map support (MinifierPlugin)
  • Aliasing IoC mechanism (ContentPlugin & AssetPlugin)
  • Js-test-driver test runner for fast browser-based testing (TestPlugin)
  • Selenium test runner for pointy-clicky browser-based tests (TestPlugin)

Bundler Plug-in Relationships

Unlike other plug-ins where you will normally implement just a single interface, bundlers are implemented by creating all, or a subset, of the following four plug-in types:

      AssetLocationPlugin + AssetPlugin + ContentPlugin + TagHandlerPlugin

These plug-ins work together in the following way when bundling JavaScript for example:

  • The AssetLocationPlugin recognizes JavaScript locations by virtue of there being either a manifest-file or a recognizer-file.
  • The AssetPlugin will only 'recognize' js source files within supported asset locations (using getJsStyle()), so is dependent on an AssetLocationPlugin.
  • The ContentPlugin will only serve assets it recognizes (unless it's a composite), so we need a corresponding AssetPlugin.
  • The TagHandlerPlugin plug-in will only generate requests for ContentPlugin instances it's associated with (unless it's a composite).

Most Wanted List

These are plug-ins we're not planning to write for 1.0, but which we believe our plug-in interfaces are capable of supporting, and for which we'd love to see contributions:

  • EcmaScript6 support so we can write standards based code now (AssetPlugin, ContentPlugin & TagHandlerPlugin -- depends on the BRJS conformant library plug-in)
  • TypeScript for strongly typed JavaScript, including sourcemap support (AssetPlugin, ContentPlugin & TagHandlerPlugin -- depends on the BRJS conformant library plug-in)
  • Bower style library support (AssetLocationPlugin, AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • Bower package management support (CommandPlugin)
  • JsHint to provide JavaScript linting support (TagHandlerPlugin & ContentPlugin)
  • LessCSS to allow leaner style-sheets (AssetPlugin, ContentPlugin & TagHandlerPlugin)
  • UglifyJS version 2 minification, with multi-level source map support (MinifierPlugin)
  • Karma test runner for reliable browser based testing (TestPlugin)
  • Node.js test runner for reliable non-browser based testing (TestPlugin)
  • App-Cache support as an alternative to HTTP caching (TagHandlerPlugin & ContentPlugin)
  • Live-editing support from within Chrome (TagHandlerPlugin & ContentPlugin)
  • Live-reload support to aid mobile development on multiple devices (TagHandlerPlugin & ContentPlugin)
  • Heroku/etc deployment support (CommandPlugin)

We'll provide quick responsive support for anybody attempting to write any of these plug-ins that runs into problems in the core code base.

Plug-in Interfaces

The plug-in interfaces are defined as follows:

Plug-in Registration

Plug-ins are registered using the SPI Mechanism introduced in Java 6. All plug-ins available on the class-path will be automatically discovered, and be usable from within BladeRunnerJS. At present this can only be done by dropping plug-ins into the 'conf/java' directory, but by 1.0 we'll support plug-ins that get pulled in much the same way as libraries can, with the potential to do this using a package management tool like Bower.

Circular Dependencies

Because plug-ins form part of the model, yet use the model to initialize themselves, an untenable circular dependency exists. To overcome this problem, each concrete plugin instance is wrapped inside a Virtual Proxy that delays plug-in initialization until somebody attempts to actually use the plug-in.

Since code interested in interacting with a subset of the plug-ins will often need to query all of them to locate the ones it needs, certain identifier methods are proxied through before the object's setBRJS() method has been invoked, which plug-in authors must be aware of. Another consequence of wrapping all plug-ins in a virtual proxy is that the instanceof operator and the getClass() method does not work as expected. The Plugin.instanceOf() and Plugin.getPluginClass() methods are provided to overcome these deficiencies (see BRJS Initialization Problem for more details).

Further Reading

Clone this wiki locally