UNPKG

17.8 kBMarkdownView Raw
1# Plugins
2
3## Overview
4Lumbar may be extended through plugins that can inject or modify the behavior at numerous different
5places in the build process. A chaining pattern is used so each plugin can either return, modify or
6replace the response from plugins that are later in the chain.
7
8
9## Installation
10Plugins can be used in 2 ways. The first allows passing configuration options to the plugin
11which 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
19For 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
26A mode is an operating context or filter so that only plugins that are registered for a given mode are
27allowed to operate - otherwise they are ignored.
28
29The plugin can contribute new modes (or bind to existing modes) by exporting a `mode` value which can either
30be 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
36Lumbar will iterate a build lifecycle for each unique platform, module and mode combination. This
37allows 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
46It is recommended that, unless necessary, a mode be explicitly defined.
47
48## Plugin API
49
50Plugins primary method of interaction with Lumbar it through the `moduleResources`, `resourceList`,
51`file`, `module`, and `resource` callbacks.
52
53Each of these callbacks are implemented as a chain,
54allowing for a plugin to modify the current context object and determine if subsequent plugins are
55allowed to operate, via the `next` parameter passed to the callback.
56
57Almost 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
63All parameters are described in more detail after the method documentation.
64
65
66### moduleResources *moduleResources(context, next, complete)*
67
68Called when generating a list of all resources that a given module will need. This method may be
69used to add additional content to the module such as the router declaration for router
70modules.
71
72The expected return value is an array. The contents of the array can be whatever is meaningful to the plugin.
73Other plugin methods can be used to take action on individual entries in the returned list.
74
75#### Resource expansion
76Each value in the returned array will be expanded if that value represents a directory structure.
77This is done by either using a simple string or using the `src` attribute. In this case, every
78child directory and file will automatically be added as a resource entry.
79
80For example, if the application structure is:
81
82 app
83 - lumbar.json
84 - files
85 --- file1.txt
86 --- sub-files
87 ----- file2.txt
88
89And a resource entry is returned with the value of
90
91 {src: "files", foo: "bar"}
92
93or
94
95 "files"
96
97The 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
106The existence of `srcDir` to determine if the resource was auto-generated from a resource entry representing a directory.
107
108Any additional attributes that were provided will be added to all created entries as you can
109see with the `foo` attribute.
110
111Note: the `foo` attribute would not be present if the resource
112entry was just `files` - just `{src: "files", foo: "bar"}`.
113
114#### Current behavior
115Without implementing this method, the resources retrieved will be the serialized JSON value referenced by the mode key on the module.
116
117For 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
134The resources available to the plugin would be:
135
136 ["abc", "def"]
137
138#### Example
139If the plugin intends to use the 'bar' value (disregarding the fact that maybe the mode should be 'bar'),
140a sample moduleResources would be:
141
142 module.exports = {
143 moduleResources: function(context, next, complete) {
144 complete(undefined, context.module.bar);
145 }
146 }
147
148It is also possible to add to the module resources when multiple plugins operate within the same mode.
149Here 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
169Allows plugins to create multiple resources from a single resource. This is called once for each
170resource generated from the `moduleResources` callback.
171
172This is useful for plugins that expand on specific resources.
173
174The 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
176Strings will be treated as file or directory includes as will object that define a `src` field.
177Resources that define a `platform` or `platforms` fields will be filtered based on the current platform being executed.
178
179For 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
200Allows plugins to apply file-level changes to the resources. Called once for each file
201generated, just prior to resources being combined. May alter the `context.resources` field
202to change the resource list.
203
204This could be used, for example, to append JSONP callbacks to a file.
205
206
207### fileName *fileName(context, next, complete)*
208Allows for plugins to override the default file name used for output file creation.
209
210The 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
215For 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
225Allow plugins to apply module-level changes to the resources. Called once for each module.
226May alter the resource list associated with the module by altering the `context.moduleResources`
227field.
228
229This can be useful for writing resources to the output directory. For example, this is how the
230static-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)*
265Allows plugins to include content other than direct file references as well as chain resource modifications.
266
267The current resource can be referenced using `context.resource`.
268
269In general, the plugin should have one of the following return values:
270
271#### Return: callback function
272This 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
280For 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
294This 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
302Each plugin method is passed a `context` parameter which describes the entire state of the build
303at the point of the call. Plugins are free to modify this structure as they please.
304
305The context is cloned at various times during the lumbar lifecycle so any modifications to the context
306can 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
319Some 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**:
330FIXME: Kevin, can you document the parameter usage?
331
332#### Next and Complete
333Each plugin is responsible for completing the plugin chain by calling next() or compete().
334Next is called to let the other plugins respond while complete is used to stop the plugin
335chain and directly return a result.
336
337The complete callback can be provided as a parameter to next if desired but not necessary.
338
339see 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
373For 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
395Each context object defines a variety of caches that are reset at specific points through the
396build process. This allows plugins to cache any relevant data for specific timeframes. Note
397that these objects are shared across all plugins so proper naming conventions should be followed
398to 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
407As 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
409methods and retry methods that fail due to this error. A variety of file methods that are
410protected from this case have been made available on the `lumbar.fileUtil` object. It
411is recommended that these methods are used whenever possible while dealing with files throughout
412the system.
413
414
415## FileUtils
416
417With respect to the previous warning about EMFILE, all file access should be done using fileUtils (fileUtils.js).
418This should be accessed from the context using the `fileUtil` key. This wraps much of the functionality of `fs`
419with handling of EMFILE errors.
420
421FileUtils also caches files that are referenced to optimize build time.
422
423### resetCache *resetCache(path)*
424Clear 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)*
429Return 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)*
434Same as fs.readFileSync but uses `resolvePath`
435
436* **path**: the file path
437
438### makeRelative *makeRelative(path)*
439The opposite of resolvePath. This will remove the lookup path if the path has that as a prefix.
440
441### stat *stat(file, callback)*
442Same as fs.stat but with EMFILE handling
443
444* **file**: the file path
445* **callback**: the asynchronous callback
446
447### readFile *readFile(file, callback)*
448Same 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)*
454same as fs.readdir with cacheing.
455
456* **dir**: the directory path
457* **callback**: the asynchronous callback
458
459### ensureDirs *ensureDirs(pathname, callback)*
460Ensure 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)*
466Same 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)*
473Specifically designed to load a lumbar resource (see the lumbar API `resource` method).
474
475* **resource**: the lumbar resource
476* **callback**: the asynchronous callback