5 Extending Asset-Pipeline - Reference Documentation
Authors:
Version: 3.2.1
5 Extending Asset-Pipeline
The asset-pipeline is extremely extensible and easy to customize to suit one's needs. You might extend the asset-pipeline to handle a new type of asset that may need to be preprocessed before being served to the browser, or you may want to define a new custom directive. This guide will go over the basics of how to perform those tasks with ease.5.1 Asset File Definitions
TheAssetFile
definition is where our journey begins. This is the defining file for various file types. Without this definition, the asset-pipeline will treat an unknown file type as a standard passthrough resource. As an example, lets first look at the CssAssetFile
definition.class CssAssetFile extends AbstractAssetFile { static final String contentType = 'text/css' static extensions = ['css'] static compiledExtension = 'css' static processors = [CssProcessor] String directiveForLine(String line) { line.find(/*=(.*)/) { fullMatch, directive -> return directive } } }
contentType
property is used to match a file definition with an incoming file request. When the browser requests a text/css
content-type file , this file is matched and files matching this definition are scanned. The extensions
list tells asset-pipeline which file extensions to scan through and match. In this case it is just 'css', but in the case of LESS for example, we may be looking for extensions less
, or css.less
.The compiledExtension
property tells asset-pipelines precompiler what the final file extension should be.Finally, the processors
array determines the list of processors that need be run on the file contents before returning a result. This array is executed in order. In this case, we have the CssProcessor
(a processor for converting the relative image paths and replacing with their cache digested version).Directive Definition
AnassetFile
can specify a REGEXP pattern for require directives. These directives are used to bundle assets together. Some file types don't utilize these require directives and simply returning a null value will cancel directive processing.Pattern directivePattern = ~/(?m)*=(.*)/
directiveForLine
that matched on each individual line. This was changed to support a multiline regex pattern for faster processing.The example above shows a match pattern for CSS files. This allows it to match require directives for the following example:/* *= require_self *= require_file example_b *= require_tree . */body { margin-top:25px; }
Processing Data Streams
Processors are used to precompile certain assets, and/or adjust the file path contents. The Processor class itself will get a more in depth explanation in the next section. For now, the part we want to look at is the processedStream function.String processedStream(Boolean precompiler) { def fileText def skipCache = precompiler ?: (!processors || processors.size() == 0) if(baseFile?.encoding || encoding) { fileText = file?.getText(baseFile?.encoding ? baseFile.encoding : encoding) } else { fileText = file?.text } def md5 = AssetHelper.getByteDigest(fileText.bytes) if(!skipCache) { def cache = CacheManager.findCache(file.canonicalPath, md5) if(cache) { return cache } } for(processor in processors) { def processInstance = processor.newInstance(precompiler) fileText = processInstance.process(fileText, this) } if(!skipCache) { CacheManager.createCache(file.canonicalPath,md5,fileText) } return fileText }
processors
variable. This creates a new instance and informs the processor whether this is a developer mode request or being issued by the precompiler (useful for determining if file replacements need to be cache digested or not). The processedStream
method is now a part of the AbstractAssetFile
definition and handles cache management if there are processors.Adding the Asset definiton to the list of AssetFiles
Originally we had to add these classes on startup in both runtime and build phases to theAssetHelper.assetSpecs
array. Thanks to contributions by Graeme Rocher we have been able to simplify this process.
Simply adding a list file @META-INF/asset-pipeline/asset.specs to the classpath will automatically get scanned.Example:
asset.pipeline.HtmlAssetFile asset.pipeline.JsAssetFile asset.pipeline.CssAssetFile
processor.specs
file and goes in the same META-INF/asset-pipeline
folder.
This is a Properties file with the key being the class path of the Processor and the value being a comma delimited list of AssetFile
classes you want the processor added to.Example:
asset.pipeline.CssProcessor=asset.pipeline.CssAssetFile,asset.pipeline.LessAssetFile
5.2 Processors
Processors are where the real power of asset-pipeline comes into play. These are the driving force behind making compileable assets such as LESS, and CoffeeScript first class citizens. Gone is the need to run a compiler on the side, and gone is the delay between making changes in development.A Processor is an implementation of theProcessor
interface via the AbstractProcessor
class. It must have a constructor with an AssetCompiler
argument, and it must have a process method. The rest is up to the developer.
The reason the AssetCompiler is passed is for giving the processor access to manipulate the precompiler phase. If a null precompiler is passed, than development mode is assumed and the processor can infer that. An example use case
for this is the SassProcessor in the SASS/SCSS Asset Pipeline Plugin. Image sprite generation causes additional image files to be created that need added to the list of files to process.class CoffeeScriptProcessor extends AbstractProcessor { Scriptable globalScope ClassLoader classLoader CoffeeScriptProcessor(AssetCompiler precompiler){ super(precompiler) } String process(String input, AssetFile assetFile) { try { def cx = Context.enter() def compileScope = cx.newObject(globalScope) compileScope.setParentScope(globalScope) compileScope.put("coffeeScriptSrc", compileScope, input) def result = cx.evaluateString(compileScope, "CoffeeScript.compile(coffeeScriptSrc)", "CoffeeScript compile command", 0, null) return result } catch (Exception e) { throw new Exception(""" CoffeeScript Engine compilation of coffeescript to javascript failed. $e """) } finally { Context.exit() } } }
CoffeeScriptProcessor
plugin. This plugin takes advantage of RhinoJS to use the CoffeeScript compiler and provide the application with direct support for CoffeeScript files. The process
method takes an input, as well as a reference to the asset file definition, and returns a result. To use your processor simply add it to your 'static processors' array on the AssetFile
definition.The LESSProcessor
was not used in this example as it's more complicated due to supporting the @import LESS directive and cache dependencies on the cache manager. However, it is a great example to look at and highly recommended.