UNPKG

10.9 kBJavaScriptView Raw
1var _ = require('underscore'),
2 async = require('async'),
3 bower = require('bower'),
4 config = require('./config'),
5 fs = require('fs'),
6 fu = require('./fileUtil'),
7 path = require('path'),
8 resources = require('./util/resources');
9
10function Libraries(options) {
11 this.options = options;
12 this.mixins = [];
13 this.configs = [];
14}
15
16Libraries.prototype.initialize = function(context, callback) {
17 this.mixins = [];
18 this.originalConfig = _.clone(context.config.attributes);
19
20 function normalize(libraries) {
21 if (_.isString(libraries)) {
22 return [libraries];
23 } else {
24 return _.map(libraries, function (name) {
25 if (_.isString(name)) {
26 return path.normalize(name);
27 } else {
28 return name;
29 }
30 });
31 }
32 }
33
34 var commandLineLibraries = normalize(this.options.libraries || []),
35 configLibraries = normalize(context.config.attributes.libraries || context.config.attributes.mixins || []),
36 bowerLibraries = this.bowerLibraries(context) || [],
37
38 allLibraries = _.union(commandLineLibraries, configLibraries, bowerLibraries);
39
40 delete context.config.attributes.mixins;
41
42 async.forEachSeries(allLibraries, _.bind(this.load, this, context), callback);
43};
44
45Libraries.prototype.bowerLibraries = function(context) {
46 try {
47 fs.statSync(fu.resolvePath('bower.json'));
48
49 var bowerDir = bower.config.directory,
50 possibleModules = fs.readdirSync(bowerDir);
51
52 return possibleModules
53 .map(function(name) {
54 return path.normalize(path.join(bowerDir, name));
55 })
56 .filter(function(name) {
57 try {
58 fs.statSync(path.join(name, 'lumbar.json'));
59 return true;
60 } catch (err) {
61 /* NOP */
62 }
63 });
64 } catch (err) {
65 context.event.emit('debug', err);
66 }
67};
68
69Libraries.prototype.load = function(context, libraryConfig, callback) {
70 // Allow mixins to be passed directly
71 var root = libraryConfig.root,
72 configPath,
73 self = this;
74
75 // Or as a file reference
76 if (!_.isObject(libraryConfig)) {
77 root = root || libraryConfig;
78
79 // If we have a dir then pull lumbar.json from that
80 try {
81 var stat = fs.statSync(fu.resolvePath(libraryConfig));
82 if (stat.isDirectory()) {
83 libraryConfig = libraryConfig + '/lumbar.json';
84 } else if (root === libraryConfig) {
85 // If we are a file the root should be the file's directory unless explicitly passed
86 root = path.dirname(root);
87 }
88 } catch (err) {
89 return callback(err);
90 }
91
92 configPath = fu.resolvePath(libraryConfig);
93 libraryConfig = config.readConfig(configPath);
94 }
95
96 // To make things easy force root to be a dir
97 if (root && !/\/$/.test(root)) {
98 root = root + '/';
99 }
100
101 if (!libraryConfig.name) {
102 return callback(new Error('Mixin with root "' + root + '" is missing a name.'));
103 }
104
105 var mixins = libraryConfig.mixins,
106 toRegister = {};
107 delete libraryConfig.mixins;
108
109 function mapMixin(mixin, name) {
110 // Only register once, giving priority to an explicitly defined mixin
111 if (!toRegister[name]) {
112 toRegister[name] = {
113 serialize: function() {
114 return {name: this.name, library: this.parent.name};
115 },
116 name: name,
117 attributes: mixin,
118 parent: libraryConfig,
119 root: root
120 };
121 }
122 }
123
124 // Read each of the mixins that are defined in the config
125 _.each(mixins, mapMixin, this);
126
127 // Make mixin modules accessible as normal mixins as well
128 _.each(libraryConfig.modules, mapMixin, this);
129
130 // After we've pulled everything in register
131 _.each(toRegister, function(mixin, name) {
132 this.mixins[name] = this.mixins[name] || [];
133 var list = this.mixins[name];
134 list.push(mixin);
135 }, this);
136
137 // Run all of the plugins that are concerned with this.
138 libraryConfig.root = root;
139 libraryConfig.path = configPath;
140 context.loadedLibrary = libraryConfig;
141 context.plugins.loadMixin(context, function(err) {
142 delete libraryConfig.root;
143
144 // And then splat everything else into our config
145 _.defaults(context.config.attributes, _.omit(context.loadedLibrary, 'name', 'path'));
146
147 libraryConfig.serialize = function() {
148 return { library: this.name };
149 };
150
151 libraryConfig.root = root;
152 self.configs.push(libraryConfig);
153
154 callback(err);
155 });
156};
157
158Libraries.prototype.findDecl = function(mixins, mixinName) {
159 if (!mixinName.name) {
160 mixinName = {name: mixinName};
161 }
162
163 return _.find(mixins, function(mixinDecl) {
164 return (mixinDecl.name || mixinDecl) === mixinName.name
165 && (!mixinDecl.library || mixinDecl.library === mixinName.library);
166 });
167};
168
169Libraries.prototype.moduleMixins = function(module) {
170 // Perform any nested mixin lookup
171 var mixins = _.clone(module.mixins || []),
172 processed = {};
173 for (var i = 0; i < mixins.length; i++) {
174 var firstInclude = mixins[i],
175 mixinConfig = firstInclude.name && firstInclude,
176 mixin = this.getMixin(firstInclude),
177 added = [i, 0];
178
179 // Save a config object off for propagation to included mixins
180 if (mixinConfig) {
181 mixinConfig = _.omit(mixinConfig, 'overrides', 'name', 'library');
182 }
183
184 if (!mixin) {
185 throw new Error('Unable to find mixin "' + ((firstInclude && firstInclude.name) || firstInclude) + '"');
186 }
187
188 // Check if we need to include any modules that this defined
189 var processedName = mixin.name + '_' + (mixin.parent && mixin.parent.name);
190 if (!processed[processedName]) {
191 processed[processedName] = true;
192
193 _.each(mixin.attributes.mixins, function(mixinInclude) {
194 // Apply any attributes that were applied to the mixin config here
195 if (mixinConfig) {
196 mixinInclude = mixinInclude.name ? _.clone(mixinInclude) : {name: mixinInclude};
197 _.extend(mixinInclude, mixinConfig);
198 }
199
200 // Save the library that caused the include so we can lookup the root and reverse
201 // any overrides in the future.
202 if (firstInclude.overrides) {
203 mixinInclude.overrideLibrary = _.extend({root: mixin.parent.root}, firstInclude);
204 } else {
205 mixinInclude.overrideLibrary = mixin.parent;
206 }
207
208 if (!this.findDecl(mixins, mixinInclude)) {
209 added.push(mixinInclude);
210 }
211 }, this);
212 }
213
214 // If we've found any new mixins insert them at the current spot and iterate
215 // over those items
216 if (added.length > 2) {
217 mixins.splice.apply(mixins, added);
218 i--;
219 }
220 }
221
222 // Extend the module with each of the mixins content, giving priority to the module
223 return _.map(mixins.reverse(), function(mixin) {
224 var mixinConfig = mixin.name && mixin,
225 name = mixin;
226 if (mixinConfig) {
227 mixinConfig = _.clone(mixinConfig);
228 delete mixinConfig.library;
229 delete mixinConfig.container;
230 }
231 mixin = _.extend(
232 {},
233 this.getMixin(name),
234 mixinConfig);
235 if (!mixin.attributes) {
236 throw new Error('Mixin "' + (name.name || name) + '" is not defined.');
237 }
238
239 // Save a distinct instance of the config for resource extension
240 if (mixinConfig) {
241 mixinConfig = _.clone(mixinConfig);
242 delete mixinConfig.overrides;
243 delete mixinConfig.name;
244 }
245
246 return {
247 library: mixin,
248 mixinConfig: mixinConfig
249 };
250 }, this);
251};
252
253Libraries.prototype.mapFiles = function(value, library, config) {
254 var files = _.map(value, function(resource) {
255 return this.mapFile(resource, library, config);
256 }, this);
257 files = _.filter(files, function(file) { return file; });
258
259 return files;
260};
261Libraries.prototype.mapFile = function(resource, library, config) {
262 // If explicitly declared the resource library takes precedence
263 if (_.isString(resource.library || resource.mixin)) {
264 library = this.getConfig(resource.library || resource.mixin);
265 if (!library) {
266 throw new Error('Mixin "' + (resource.library || resource.mixin) + '" not found');
267 }
268 delete resource.mixin;
269 }
270
271 return resources.map(resource, library, config);
272};
273
274Libraries.prototype.mapPathToLibrary = function(src, library) {
275 return resources.pathToLibrary(src, library);
276};
277
278Libraries.prototype.getMixin = function(name) {
279 var mixins = (this.mixins && this.mixins[name.name || name]) || [],
280 library = name.library || name.container;
281 if (mixins.length > 1 && !library) {
282 throw new Error(
283 'Duplicate mixins found for "' + (name.name || name) + '"'
284 + _.map(mixins, function(mixin) {
285 return ' parent: "' + mixin.parent.name + '"';
286 }).join(''));
287 }
288
289 if (library) {
290 if (name.name === undefined) {
291 var found = _.find(this.configs, function(config) {
292 return config.name === library;
293 });
294 if (!found) {
295 throw new Error('Unable to find library "' + library + '"');
296 }
297 return found;
298 }
299
300 var found = _.find(mixins, function(mixin) {
301 return mixin.parent.name === library;
302 });
303 if (found) {
304 return found;
305 } else {
306 throw new Error('Mixin named "' + name.name + '" not found in library "' + library + '"');
307 }
308 } else if (mixins.length === 1) {
309 return mixins[0];
310 }
311};
312Libraries.prototype.getConfig = function(name) {
313 return _.find(this.configs, function(config) { return config.name === name; });
314};
315
316Libraries.prototype.mergeHash = function(hashName, input, mixin, output) {
317 if (mixin[hashName]) {
318 // Close the value to make sure that we are not overriding anything
319 if (!output[hashName] || output[hashName] === input[hashName]) {
320 output[hashName] = _.clone(input[hashName] || {});
321 }
322 _.each(mixin[hashName], function(value, key) {
323 if (!input[hashName] || !(key in input[hashName])) {
324 output[hashName][key] = value;
325 }
326 });
327 return true;
328 }
329};
330Libraries.prototype.mergeFiles = function(fieldName, input, mixinData, output, library) {
331 if (mixinData[fieldName]) {
332 mixinData = _.isArray(mixinData[fieldName]) ? mixinData[fieldName] : [mixinData[fieldName]];
333
334 var configData = input[fieldName] || [];
335 if (!output[fieldName] || configData === output[fieldName]) {
336 output[fieldName] = _.clone(configData);
337 }
338 if (!_.isArray(configData)) {
339 configData = [configData];
340 }
341 if (!_.isArray(output[fieldName])) {
342 output[fieldName] = [output[fieldName]];
343 }
344
345 // Insert point is at the start of the upstream list, which we are
346 // assuming occurs at length postions from the end.
347 _.each(mixinData, function(value) {
348 //Make the include relative to the mixin
349 value = (library.root || '') + value;
350
351 output[fieldName].splice(
352 output[fieldName].length - configData.length,
353 0,
354 {src: value, library: library});
355 });
356
357 return true;
358 }
359};
360
361module.exports = Libraries;