1 | # Plugins
|
2 |
|
3 | ## Overview
|
4 | Lumbar may be extended through plugins that can inject or modify the behavior at numerous different
|
5 | places in the build process. A chaining pattern is used so each plugin can either return, modify or
|
6 | replace the response from plugins that are later in the chain.
|
7 |
|
8 |
|
9 | ## Installation
|
10 | Plugins can be used in 2 ways. The first allows passing configuration options to the plugin
|
11 | which can be accessed as the first parameter if the plugin exports a function.
|
12 |
|
13 | module.exports = function(options) {
|
14 | return {
|
15 | // plugin methods
|
16 | };
|
17 | };
|
18 |
|
19 | For plugins that do not require instance options, a singleton exports pattern can be used
|
20 |
|
21 | module.exports = {
|
22 | // plugin methods
|
23 | };
|
24 |
|
25 | ## Modes
|
26 | A mode is an operating context or filter so that only plugins that are registered for a given mode are
|
27 | allowed to operate - otherwise they are ignored.
|
28 |
|
29 | The plugin can contribute new modes (or bind to existing modes) by exporting a `mode` value which can either
|
30 | be a string or an array of strings. Lumber operates with 3 defined modes by default:
|
31 |
|
32 | * **scripts**: Compile and copy all javascript artifacts to build
|
33 | * **styles**: Compile and copy all stylus and css artifacts to build
|
34 | * **static**: Copy all static resources to build
|
35 |
|
36 | Lumbar will iterate a build lifecycle for each unique platform, module and mode combination. This
|
37 | allows the plugins to filter build resources and operations to only what is meaningful for their purpose.
|
38 |
|
39 | module.exports = {
|
40 | mode: 'scripts' // operate within the scripts mode along with other core plugins
|
41 | mode: ['scripts', 'foo'] // scripts mode and add a new mode called 'foo'
|
42 | mode: ['foo', 'all'] // add a new 'foo' mode and also operate under all modes
|
43 | // if no mode is defined, the plugin will operate under all modes
|
44 | }
|
45 |
|
46 | It is recommended that, unless necessary, a mode be explicitly defined.
|
47 |
|
48 | ## Plugin API
|
49 |
|
50 | Plugins primary method of interaction with Lumbar it through the `moduleResources`, `resourceList`,
|
51 | `file`, `module`, and `resource` callbacks.
|
52 |
|
53 | Each of these callbacks are implemented as a chain,
|
54 | allowing for a plugin to modify the current context object and determine if subsequent plugins are
|
55 | allowed to operate, via the `next` parameter passed to the callback.
|
56 |
|
57 | Almost all plugin methods are asynchronous and have the same signature - (context, next, complete)
|
58 |
|
59 | * context: provides access to all data that a plugin should need.
|
60 | * next: the chaining callback used to execute the remaining plugins
|
61 | * complete: the completion callback
|
62 |
|
63 | All parameters are described in more detail after the method documentation.
|
64 |
|
65 |
|
66 | ### moduleResources *moduleResources(context, next, complete)*
|
67 |
|
68 | Called when generating a list of all resources that a given module will need. This method may be
|
69 | used to add additional content to the module such as the router declaration for router
|
70 | modules.
|
71 |
|
72 | The expected return value is an array. The contents of the array can be whatever is meaningful to the plugin.
|
73 | Other plugin methods can be used to take action on individual entries in the returned list.
|
74 |
|
75 | #### Resource expansion
|
76 | Each value in the returned array will be expanded if that value represents a directory structure.
|
77 | This is done by either using a simple string or using the `src` attribute. In this case, every
|
78 | child directory and file will automatically be added as a resource entry.
|
79 |
|
80 | For example, if the application structure is:
|
81 |
|
82 | app
|
83 | - lumbar.json
|
84 | - files
|
85 | --- file1.txt
|
86 | --- sub-files
|
87 | ----- file2.txt
|
88 |
|
89 | And a resource entry is returned with the value of
|
90 |
|
91 | {src: "files", foo: "bar"}
|
92 |
|
93 | or
|
94 |
|
95 | "files"
|
96 |
|
97 | The resource entries will be converted to
|
98 |
|
99 | [
|
100 | {dir: "files", foo: "bar"},
|
101 | {src: "files/file1.txt", srcDir: "files", foo: "bar"}
|
102 | {dir: "files/sub-files", srcDir: "files", foo: "bar"}
|
103 | {src: "files/sub-files/file2.txt", srcDir: "files", foo: "bar"}
|
104 | ]
|
105 |
|
106 | The existence of `srcDir` to determine if the resource was auto-generated from a resource entry representing a directory.
|
107 |
|
108 | Any additional attributes that were provided will be added to all created entries as you can
|
109 | see with the `foo` attribute.
|
110 |
|
111 | Note: the `foo` attribute would not be present if the resource
|
112 | entry was just `files` - just `{src: "files", foo: "bar"}`.
|
113 |
|
114 | #### Current behavior
|
115 | Without implementing this method, the resources retrieved will be the serialized JSON value referenced by the mode key on the module.
|
116 |
|
117 | For example, if the plugin has defined a mode called `foo` and a lumbar.json file of:
|
118 |
|
119 | {
|
120 | "modules": {
|
121 | "myModule": {
|
122 |
|
123 | "foo": [
|
124 | "abc", "def"
|
125 | ],
|
126 |
|
127 | "bar": [
|
128 | "ghi", "jkl"
|
129 | ]
|
130 | }
|
131 | }
|
132 | }
|
133 |
|
134 | The resources available to the plugin would be:
|
135 |
|
136 | ["abc", "def"]
|
137 |
|
138 | #### Example
|
139 | If the plugin intends to use the 'bar' value (disregarding the fact that maybe the mode should be 'bar'),
|
140 | a sample moduleResources would be:
|
141 |
|
142 | module.exports = {
|
143 | moduleResources: function(context, next, complete) {
|
144 | complete(undefined, context.module.bar);
|
145 | }
|
146 | }
|
147 |
|
148 | It is also possible to add to the module resources when multiple plugins operate within the same mode.
|
149 | Here is an example of the router plugin:
|
150 |
|
151 | moduleResources: function(context, next, complete) {
|
152 | next(function(err, ret) {
|
153 | if (err) {
|
154 | return complete(err);
|
155 | }
|
156 |
|
157 | // Generate the router if we have the info for it
|
158 | var module = context.module;
|
159 | if (module.routes) {
|
160 | ret.unshift({ routes: module.routes });
|
161 | }
|
162 |
|
163 | complete(undefined, ret);
|
164 | });
|
165 |
|
166 |
|
167 | ### resourceList *resourceList(context, next, complete)*
|
168 |
|
169 | Allows plugins to create multiple resources from a single resource. This is called once for each
|
170 | resource generated from the `moduleResources` callback.
|
171 |
|
172 | This is useful for plugins that expand on specific resources.
|
173 |
|
174 | The expected return value is an array of resource objects. The data associated with these objects may be anything the plugin or other plugins will operate on.
|
175 |
|
176 | Strings will be treated as file or directory includes as will object that define a `src` field.
|
177 | Resources that define a `platform` or `platforms` fields will be filtered based on the current platform being executed.
|
178 |
|
179 | For example, the scope plugin wraps the returned resources add a execution scope.
|
180 |
|
181 | resourceList: function(context, next, complete) {
|
182 | next(function(err, resources) {
|
183 | if (err) {
|
184 | return complete(err);
|
185 | }
|
186 |
|
187 | if (context.config.attributes.scope === 'resource'
|
188 | && !context.resource.global
|
189 | && !context.resource.dir) {
|
190 | resources.unshift(generator('(function() {\n'));
|
191 | resources.push(generator('}).call(this);\n'));
|
192 | }
|
193 | complete(undefined, resources);
|
194 | });
|
195 | }
|
196 |
|
197 |
|
198 | ### file *file(context, next, complete)*
|
199 |
|
200 | Allows plugins to apply file-level changes to the resources. Called once for each file
|
201 | generated, just prior to resources being combined. May alter the `context.resources` field
|
202 | to change the resource list.
|
203 |
|
204 | This could be used, for example, to append JSONP callbacks to a file.
|
205 |
|
206 |
|
207 | ### fileName *fileName(context, next, complete)*
|
208 | Allows for plugins to override the default file name used for output file creation.
|
209 |
|
210 | The return value should be an object with the following attributes:
|
211 |
|
212 | * **path**: the file path relative to the output directory (minus the extension)
|
213 | * **extension**: the file extension
|
214 |
|
215 | For example, the script plugin uses the platform path and module name to create the file name:
|
216 |
|
217 | fileName: function(context, next, complete) {
|
218 | var name = context.module ? context.module.name : context.package;
|
219 | complete(undefined, {path: name, extension: 'js'});
|
220 | }
|
221 |
|
222 |
|
223 | ### module *module(context, next, complete)*
|
224 |
|
225 | Allow plugins to apply module-level changes to the resources. Called once for each module.
|
226 | May alter the resource list associated with the module by altering the `context.moduleResources`
|
227 | field.
|
228 |
|
229 | This can be useful for writing resources to the output directory. For example, this is how the
|
230 | static-output plugin adds the static files to the output directory:
|
231 |
|
232 | module: function(context, next, complete) {
|
233 | next(function(err) {
|
234 | async.forEach(context.moduleResources, function(resource, callback) {
|
235 | var fileContext = context.clone();
|
236 | fileContext.resource = resource;
|
237 | var fileInfo = fu.loadResource(resource, function(err, data) {
|
238 | if (err || !data || !data.content) {
|
239 | return callback(err);
|
240 | }
|
241 |
|
242 | fileContext.outputFile(function(callback) {
|
243 | var ret = {
|
244 | fileName: fileContext.fileName,
|
245 | inputs: fileInfo.inputs || [ fileInfo.name ],
|
246 | fileConfig: context.fileConfig,
|
247 | platform: context.platform,
|
248 | package: context.package,
|
249 | mode: context.mode
|
250 | };
|
251 |
|
252 | fu.writeFile(fileContext.fileName, data.content, function(err) {
|
253 | callback(err, ret);
|
254 | });
|
255 | },
|
256 | callback);
|
257 | });
|
258 | },
|
259 | complete);
|
260 | });
|
261 | }
|
262 |
|
263 |
|
264 | ### resource *resource(context, next, complete)*
|
265 | Allows plugins to include content other than direct file references as well as chain resource modifications.
|
266 |
|
267 | The current resource can be referenced using `context.resource`.
|
268 |
|
269 | In general, the plugin should have one of the following return values:
|
270 |
|
271 | #### Return: callback function
|
272 | This function is used for asynchronous data loading. The callback has the standard `(err, data)` signature
|
273 |
|
274 | * **err** is used to indicate an error
|
275 | * **data** can be a string or buffer representing file contents or a hash with the following values:
|
276 | * **data**: the string or buffer file contents
|
277 | * **noSeparator**: truthy - adds ';;' separator for when content is known to be validated javascript or css, Resources that always end in a complete statement should utilize this field.
|
278 | * **inputs**: a list of files that, if in watch mode, impact the generation of this file
|
279 |
|
280 | For example, this is how the async callback function can be used to write "Hello World!"
|
281 |
|
282 | resource: function(context, next, complete) {
|
283 | complete(undefined, function(context, complete) {
|
284 | if ( *simple* ) {
|
285 | complete(undefined, "Hello World!");
|
286 | } else {
|
287 | var dependantFiles = [...];
|
288 | complete(undefined, {data: "Hello World!", inputs: dependantFiles}
|
289 | }
|
290 | });
|
291 | }
|
292 |
|
293 | #### Return: An object
|
294 | This object should have the following attributes:
|
295 | * **src**: file path relative to the lumbar.json file
|
296 | * **dest**: only applicable for static resources - the destination path relative to the platform
|
297 | * **sourceFile**: file path that, if in watch mode, should be watched to trigger a rebuild. This is not needed if src is defined.
|
298 |
|
299 |
|
300 | ### Method parameters
|
301 | #### Context
|
302 | Each plugin method is passed a `context` parameter which describes the entire state of the build
|
303 | at the point of the call. Plugins are free to modify this structure as they please.
|
304 |
|
305 | The context is cloned at various times during the lumbar lifecycle so any modifications to the context
|
306 | can not be guaranteed to exist outside of the plugin method that made the modification.
|
307 |
|
308 | * **package** : The name of the package currently being operated on.
|
309 | * **platform** : The name of the platform currently being operated on.
|
310 | * **module** : The module currently being operated on, as defined in the JSON file.
|
311 | * **resource** : The resource currently being operated on, if applicable. Definition depends on plugins.
|
312 | * **moduleResources** : All resources associated with the current module, if available.
|
313 | * **resources** : All resources associated with a file. For non-combined cases this is identical to `moduleResources`
|
314 | * **platformPath** : Path to the output path for the current platform
|
315 | * **options** : Options passed to the lumbar initialize call
|
316 | * **config** : Current lumbar configuration. See _config.js_
|
317 | * **combined** : Truthy if the output content is intended to be combined when possible
|
318 |
|
319 | Some utility functions are also available:
|
320 |
|
321 | * **loadResource(resource, callback)**: Async method for retrieving file contents of a resource.
|
322 | * **resource**: the resource that is provided as `context.resource` in the resource method
|
323 | * **callback**: async callback method with the following parameters:
|
324 | * **err**: error if anything went wrong
|
325 | * **data**: buffer or what was returned if the resource provided was a function
|
326 |
|
327 | * **outputFile(writer, callback)**: write content to a file
|
328 | * **writer**:
|
329 | * **callback**:
|
330 | FIXME: Kevin, can you document the parameter usage?
|
331 |
|
332 | #### Next and Complete
|
333 | Each plugin is responsible for completing the plugin chain by calling next() or compete().
|
334 | Next is called to let the other plugins respond while complete is used to stop the plugin
|
335 | chain and directly return a result.
|
336 |
|
337 | The complete callback can be provided as a parameter to next if desired but not necessary.
|
338 |
|
339 | see examples below:
|
340 |
|
341 | module.exports = {
|
342 | moduleResources: function(context, next, complete) {
|
343 | if ( *continue with chain* ) {
|
344 | next();
|
345 |
|
346 | } else if ( *modify plugin result* ) {
|
347 | // define a new complete function
|
348 | function _complete (err, data) {
|
349 | if (err) {
|
350 | // something bad happened
|
351 | complete(err, data);
|
352 | } else {
|
353 | data.push("something new");
|
354 | complete(undefined, data);
|
355 | }
|
356 | }
|
357 | // call next and override the existing complete function
|
358 | next(_complete);
|
359 |
|
360 | } else if ( *stop the plugin chain and return something* ) {
|
361 | var something = [...];
|
362 | complete(undefined, something);
|
363 |
|
364 | } else {
|
365 | // we're asyncronous - *always* make sure to call next or complete!
|
366 | next();
|
367 | }
|
368 | }
|
369 | }
|
370 |
|
371 |
|
372 | ### Lifecycle Pseudocode
|
373 | For an understanding of how these methods work together, see the following *extremely simplified* pseudocode:
|
374 |
|
375 | for each defined platform
|
376 | for each mode {added by `plugin.mode`}
|
377 | for each module in platform {as determined by package}
|
378 | resources = `plugin.moduleResources`
|
379 | for each resource in resources
|
380 | if resource matches `plugin.fileFilter`
|
381 | replace/expand resource if it matches a directory
|
382 | else
|
383 | remove from the list of resources
|
384 |
|
385 | for each resource in resources
|
386 | replace/flatten resource with `plugin.resourceList`
|
387 |
|
388 | call `plugin.module`
|
389 | for each resource in resources
|
390 | resource = 'plugin.resource'
|
391 |
|
392 |
|
393 | ### Caches
|
394 |
|
395 | Each context object defines a variety of caches that are reset at specific points through the
|
396 | build process. This allows plugins to cache any relevant data for specific timeframes. Note
|
397 | that these objects are shared across all plugins so proper naming conventions should be followed
|
398 | to prevent conflicts.
|
399 |
|
400 | * **configCache** : Reset when the configuration file changes
|
401 | * **fileCache** : Reset when the current file processing completes
|
402 | * **moduleCache** : Reset when the current module processing completes
|
403 |
|
404 |
|
405 | ## Warnings
|
406 |
|
407 | As most Lumbar projects are dealing with a large number of files it is quite susceptible to
|
408 | **EMFILE** exceptions under OSX. The current recovery method for this is to utilize async
|
409 | methods and retry methods that fail due to this error. A variety of file methods that are
|
410 | protected from this case have been made available on the `lumbar.fileUtil` object. It
|
411 | is recommended that these methods are used whenever possible while dealing with files throughout
|
412 | the system.
|
413 |
|
414 |
|
415 | ## FileUtils
|
416 |
|
417 | With respect to the previous warning about EMFILE, all file access should be done using fileUtils (fileUtils.js).
|
418 | This should be accessed from the context using the `fileUtil` key. This wraps much of the functionality of `fs`
|
419 | with handling of EMFILE errors.
|
420 |
|
421 | FileUtils also caches files that are referenced to optimize build time.
|
422 |
|
423 | ### resetCache *resetCache(path)*
|
424 | Clear all cached file content
|
425 |
|
426 | * **path**: Clear all or clear for a specific path. falsy or missing input for path will clear all.
|
427 |
|
428 | ### resolvePath *resolvePath(path)*
|
429 | Return a file path that, if relative, is appropriatly qualitied with the build output path based on the 'lookupPath'
|
430 |
|
431 | * **path**: the file path
|
432 |
|
433 | ### readFileSync *readFileSync(path)*
|
434 | Same as fs.readFileSync but uses `resolvePath`
|
435 |
|
436 | * **path**: the file path
|
437 |
|
438 | ### makeRelative *makeRelative(path)*
|
439 | The opposite of resolvePath. This will remove the lookup path if the path has that as a prefix.
|
440 |
|
441 | ### stat *stat(file, callback)*
|
442 | Same as fs.stat but with EMFILE handling
|
443 |
|
444 | * **file**: the file path
|
445 | * **callback**: the asynchronous callback
|
446 |
|
447 | ### readFile *readFile(file, callback)*
|
448 | Same as fs.readFile cacheing. A buffer is returned.
|
449 |
|
450 | * **file**: the file path
|
451 | * **callback**: the asynchronous callback
|
452 |
|
453 | ### readdir *readdir(dir, callback)*
|
454 | same as fs.readdir with cacheing.
|
455 |
|
456 | * **dir**: the directory path
|
457 | * **callback**: the asynchronous callback
|
458 |
|
459 | ### ensureDirs *ensureDirs(pathname, callback)*
|
460 | Ensure that the parent directories for the provided file path exist and create otherwise.
|
461 |
|
462 | * **pathname**: the file path
|
463 | * **callback**: the asynchronous callback
|
464 |
|
465 | ### writeFile *writeFile(file, data, callback)*
|
466 | Same as fs.writefile but will also ensure directories, cache file contents, and handle EMFILE errors gracefully.
|
467 |
|
468 | * **file**: the file path
|
469 | * **data**: the file contents
|
470 | * **callback**: the asynchronous callback
|
471 |
|
472 | ### loadResource *loadResource(resource, callback)*
|
473 | Specifically designed to load a lumbar resource (see the lumbar API `resource` method).
|
474 |
|
475 | * **resource**: the lumbar resource
|
476 | * **callback**: the asynchronous callback
|