UNPKG

36 kBJavaScriptView Raw
1/*
2 * Copyright 2013 Yahoo! Inc. All rights reserved.
3 * Copyrights licensed under the BSD License.
4 * See the accompanying LICENSE.txt file for terms.
5 */
6
7
8/*jslint nomen:true, node:true */
9"use strict";
10
11
12var libfs = require('fs'),
13 libpath = require('path'),
14 walk = require('walk'),
15 libsemver = require('semver'),
16 Module = require("module").Module,
17 Bundle = require('./bundle.js'),
18 debug = require('debug')('locator'),
19 DEFAULT_RULESET = 'main',
20 DEFAULT_RULESETS = require('./rulesets.js'),
21 DEFAULT_SELECTOR = '{}',
22 DEFAULT_MAX_PACKAGES_DEPTH = 9999,
23 DEFAULT_VERSION = '1.0.0', // for bundles w/out a version or parent
24 PATH_SEP = libpath.sep;
25
26function mix(target, source, overwrite) {
27 for (var prop in source) {
28 if (source.hasOwnProperty(prop) && (overwrite || !target.hasOwnProperty(prop))) {
29 target[prop] = source[prop];
30 }
31 }
32 return target;
33}
34
35/**
36 * The Locator walks the filesystem and gives semantic meaning to
37 * files in the application.
38 * @module Locator
39 */
40
41
42/**
43 * @class Locator
44 * @constructor
45 * @param {object} [options] Options for how the configuration files are located.
46 * @param {string} [options.applicationDirectory] Where the application will be found.
47 * If not given it defaults to the current working directory.
48 * @param {[string]} [options.exclude] folder names that should not be analyzed
49 * @param {integer} options.maxPackageDepth Maximum depth in `node_modules/` to walk.
50 * Defaults to 9999.
51 * @param {function} options.rulesetFn Function hook to compute rules per bundle dynamically.
52 * This hook allow to analyze the pkg, and produce the proper rules when needed.
53 *
54 * @example (note constructor "BundleLocator" is exported as "Locator")
55 * var locOpts = {
56 * applicationDirectory: __dirname,
57 * exclude: ['build'],
58 * maxPackageDepth: 5
59 * },
60 * locator = new Locator(locOpts);
61 */
62function BundleLocator(options) {
63 this._options = options || {};
64 if (this._options.applicationDirectory) {
65 this._options.applicationDirectory = libpath.resolve(process.cwd(), this._options.applicationDirectory);
66 } else {
67 this._options.applicationDirectory = process.cwd();
68 }
69 this._options.maxPackageDepth = this._options.maxPackageDepth || DEFAULT_MAX_PACKAGES_DEPTH;
70 this._options.exclude = this._options.exclude || [];
71
72 this._cacheRules = {}; // rulename + package directory: rules
73
74 this._bundles = {};
75 this._bundlePaths = {}; // path: name
76 this._bundleUpdates = {}; // name: object describing why the update happened
77}
78
79
80BundleLocator.prototype = {
81
82
83 /**
84 * Parses the directory to turn it into a bundle.
85 * @method parseBundle
86 * @param {string} dir The directory for the bundle.
87 * @param {object} [options] Options for processing the bundle.
88 *
89 * @example (note constructor "BundleLocator" is exported as "Locator")
90 * var locator = new Locator();
91 * var appBundle = locator.parseBundle(__dirname, {});
92 *
93 * @return {Bundle} The bundle.
94 */
95 parseBundle: function (dir, options) {
96 var self = this,
97 bundleSeeds;
98
99 // Normalize the root directory before storing its value. If it is
100 // stored before normalizing, other paths created afterwards may be
101 // normalized and fail comparisons against the root directory
102 dir = libpath.normalize(dir);
103
104 this._rootDirectory = dir;
105
106 bundleSeeds = this._walkNPMTree(dir);
107 bundleSeeds = this._filterBundleSeeds(bundleSeeds);
108 bundleSeeds.forEach(function (bundleSeed) {
109 var opts = (bundleSeed.baseDirectory === dir) ? options : {};
110 self._walkBundle(bundleSeed, opts);
111 });
112 this._rootBundleName = this._bundlePaths[libfs.realpathSync(dir)];
113 return this._bundles[this._rootBundleName];
114 },
115
116
117 /**
118 * Returns the named bundle, no matter how deeply found.
119 * @method getBundle
120 * @param {string} name The name of the bundle to retrieve.
121 * @return {Bundle|undefined} The named bundle, or undefined if not found.
122 */
123 getBundle: function (name) {
124 return this._bundles[name];
125 },
126
127
128 /**
129 * Returns the root of the bundles. This is the root bundle that was parsed
130 * by the call to `parseBundle()`.
131 * @method getRootBundle
132 * @return {Bundle} The root bundle.
133 */
134 getRootBundle: function () {
135 return this._bundles[this._rootBundleName];
136 },
137
138
139 /**
140 * Utility method for listing all files in a bundle.
141 * @method getBundleFiles
142 * @param {string} bundleName The name of the bundle.
143 * @param {object} filter Filter for deciding which files to return.
144 * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which files to return.
145 * @return {[string]} An array of filesystem paths.
146 */
147 getBundleFiles: function (bundleName, filter) {
148 var bundle,
149 files = [];
150 bundle = this._bundles[bundleName];
151 if (!bundle) {
152 throw new Error('Unknown bundle "' + bundleName + '"');
153 }
154 Object.keys(bundle.files).forEach(function (fullpath) {
155 var res = {
156 ext: libpath.extname(fullpath).substr(1)
157 };
158 if (this._filterResource(res, filter)) {
159 files.push(fullpath);
160 }
161 }, this);
162 return files;
163 },
164
165
166 /**
167 * Utility method for listing all resources in a bundle.
168 * @method getBundleResources
169 * @param {string} bundleName The name of the bundle.
170 * @param {object} filter Filter for deciding which resources to return.
171 * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which resources to return.
172 * @param {string|[string]} [filter.types] The resources types for which resources to return.
173 * @return {[string]} An array of filesystem paths.
174 */
175 getBundleResources: function (bundleName, filter) {
176 var bundle = this._bundles[bundleName];
177 if (!bundle) {
178 throw new Error('Unknown bundle "' + bundleName + '"');
179 }
180 return this._walkBundleResources(bundle, filter);
181 },
182
183
184 /**
185 * Returns a list of resources in all the bundles.
186 * @method getAllResources
187 * @param {object} filter Filter for deciding which resources to list.
188 * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) to return.
189 * @param {string|[string]} [filter.types] The resources types to return.
190 * @return {[LocatorResource]} An array of resources.
191 */
192 getAllResources: function (filter) {
193 var self = this,
194 ress = [];
195 Object.keys(this._bundles).forEach(function (bundleName) {
196 var bundle = self._bundles[bundleName];
197 self._walkBundleResources(bundle, filter).forEach(function (res) {
198 ress.push(res);
199 });
200 });
201 return ress;
202 },
203 listAllResources: function (filter) {
204 debug('`listAllResources()` is deprecated in favor of `getAllResources()`.');
205 return this.getAllResources(filter);
206 },
207
208
209 /**
210 * Returns a list of all located bundle names.
211 * The names are not ordered.
212 * @method getBundleNames
213 * @param {Function} [filter] function to execute on each bundle.
214 * If no `filter` is supplied, all bundle names will be returned. The
215 * function will receive the following argument:
216 * @param {Object} filter.bundle the current bundle being iterated on
217 * @param {boolean} filter.return Return true to indicate that the
218 * bundle name should be returned in the list. Otherise the bundle
219 * name will be skipped.
220 * @return {Array} list of bundles
221 */
222 getBundleNames: function (filter) {
223 var bundleName,
224 bundles = this._bundles,
225 bundleNames = [];
226 if ('function' !== typeof filter) {
227 return Object.keys(this._bundles);
228 }
229 for (bundleName in bundles) {
230 if (bundles.hasOwnProperty(bundleName)) {
231 if (filter(bundles[bundleName])) {
232 bundleNames.push(bundleName);
233 }
234 }
235 }
236 return bundleNames;
237 },
238 listBundleNames: function (filter) {
239 debug('`listBundleNames()` is deprecated in favor of `getBundleNames()`.');
240 return this.getBundleNames(filter);
241 },
242
243
244 /**
245 * Returns the name of the bundle to which the path belongs.
246 * @private
247 * @method _getBundleNameByPath
248 * @param {string} findPath The path to consider.
249 * @return {string} The name the bundle to which the findPath most closely matches.
250 */
251 _getBundleNameByPath: function (findPath) {
252 // FUTURE OPTIMIZATION: use a more complicated datastructure for faster lookups
253 var found = {}, // length: path
254 longest;
255 // expands path in case of symlinks
256 findPath = libfs.realpathSync(findPath);
257 // searchs based on expanded path
258 Object.keys(this._bundlePaths).forEach(function (bundlePath) {
259 if (0 === findPath.indexOf(bundlePath) &&
260 (findPath.length === bundlePath.length ||
261 libpath.sep === findPath.charAt(bundlePath.length))) {
262 found[bundlePath.length] = bundlePath;
263 }
264 });
265 longest = Math.max.apply(Math, Object.keys(found));
266 return this._bundlePaths[found[longest]];
267 },
268
269
270 /**
271 * Creates the seed of a potential bundle.
272 *
273 * The seed includes:
274 * <dl>
275 * <dt> baseDirectory {string} </dt>
276 * <dd> The directory of the bundle. </dd>
277 * <dt> name {string} </dt>
278 * <dd> The name of the bundle. </dd>
279 * <dt> version {string} </dt>
280 * <dd> The version of the bundle. </dd>
281 * <dt> npmDepth {integer} </dt>
282 * <dd> The depth in the NPM package dependency tree. </dd>
283 * <dt> options {object} </dt>
284 * <dd> Options for the bundle, taken in part from the `"locator"` section of `package.json` </dd>
285 * </dl>
286 * @private
287 * @method _makeBundleSeed
288 * @param {string} baseDirectory Full path to the bundle.
289 * @param {string} name The name of the bundle.
290 * Might be overrriden by the name in package.json (if it exists).
291 * @param {string} version The version to use if not specified in the package.json.
292 * Might be overriden by the version in package.json (if it exists).
293 * @param {object} [pkg] Contents of the bundles package.json.
294 * @param {object} [options] Additional options to apply. Lower priority
295 * than those found in package.json.
296 * @return {object} The bundle seed, as described above.
297 */
298 _makeBundleSeed: function (baseDirectory, name, version, pkg, options) {
299 var seed;
300 seed = {
301 baseDirectory: baseDirectory,
302 name: name,
303 version: version
304 };
305 if (pkg) {
306 seed.name = (pkg.locator && pkg.locator.name ? pkg.locator.name : pkg.name);
307 seed.version = pkg.version;
308 seed.options = pkg.locator;
309 seed.pkg = pkg;
310 }
311 if (options) {
312 if (seed.options) {
313 // merge options under seed.options
314 mix(seed.options, options);
315 } else {
316 seed.options = options;
317 }
318 }
319 return seed;
320 },
321
322 /**
323 * Makes a bundle out of a directory.
324 * @private
325 * @method _makeBundle
326 * @param {object} seed The seed bundle, @see _makeBundleSeed()
327 * @param {Bundle} parent Parent bundle. Only the root bundle doesn't have a parent.
328 * @return {Bundle} The new bundle
329 */
330 _makeBundle: function (seed, parent) {
331 var bundle,
332 ruleset = this._loadRuleset(seed),
333 msg;
334
335 if (seed.options.location) {
336 // This is fairly legacy, and we might be able to remove it.
337 seed.baseDirectory = libpath.resolve(seed.baseDirectory, seed.options.location);
338 }
339
340 if (!ruleset) {
341 msg = 'Bundle "' + seed.name + '" has unknown ruleset ' + JSON.stringify(seed.options.ruleset);
342 if (seed.options.rulesets) {
343 msg += ' in rulesets ' + JSON.stringify(seed.options.rulesets);
344 }
345 throw new Error(msg);
346 }
347
348 bundle = new Bundle(seed.baseDirectory, seed.options);
349 bundle.name = seed.name;
350 bundle.version = seed.version;
351 bundle.type = ruleset._name;
352 this._bundles[bundle.name] = bundle;
353 this._bundlePaths[libfs.realpathSync(bundle.baseDirectory)] = bundle.name;
354
355 // wire into parent
356 if (parent) {
357 if (!parent.bundles) {
358 parent.bundles = {};
359 }
360 parent.bundles[bundle.name] = bundle;
361 }
362 return bundle;
363 },
364
365
366 /**
367 * Turns the path into a resource in the associated bundle, if applicable.
368 * @private
369 * @method _processFile
370 * @param {string} fullPath the path to the file to be processed
371 */
372 _processFile: function (fullPath) {
373 var bundleName,
374 bundle,
375 ruleset,
376 relativePath,
377 pathParts,
378 subBundleSeed,
379 res;
380
381 bundleName = this._getBundleNameByPath(fullPath);
382 bundle = this._bundles[bundleName];
383 if (bundle.baseDirectory === fullPath.substr(0, bundle.baseDirectory.length)) {
384 relativePath = fullPath.substr(bundle.baseDirectory.length + 1);
385 }
386
387 // This mainly happens during watch(), since we skip node_modules
388 // in _walkBundle().
389 if (relativePath.indexOf('node_modules') === 0) {
390 pathParts = relativePath.split(libpath.sep);
391 while (pathParts[0] === 'node_modules' && pathParts.length >= 2) {
392 pathParts.shift();
393 bundleName = pathParts.shift();
394 }
395 relativePath = pathParts.join(libpath.sep);
396 bundle = this._bundles[bundleName];
397
398 // The package's directory is not a resource (... and is mostly uninteresting).
399 if (!relativePath) {
400 return;
401 }
402
403 // unknown bundle
404 if (!bundle) {
405 return;
406 }
407 }
408
409 ruleset = this._loadRuleset(bundle);
410 if (ruleset._skip && this._ruleSkip(fullPath, relativePath, ruleset._skip)) {
411 return;
412 }
413 if (ruleset._bundles) {
414 subBundleSeed = this._ruleBundles(fullPath, relativePath, ruleset._bundles, bundle);
415 if (subBundleSeed) {
416 // sub-bundle inherits options.rulesets from parent
417 if (!subBundleSeed.options) {
418 subBundleSeed.options = {};
419 }
420 if (!subBundleSeed.options.rulesets) {
421 subBundleSeed.options.rulesets = bundle.options.rulesets;
422 }
423 this._makeBundle(subBundleSeed, bundle);
424 return;
425 }
426 }
427
428 // This is the base "meta" for a file. If a rule matches we'll
429 // augment this.
430 res = {
431 bundleName: bundleName,
432 fullPath: fullPath,
433 relativePath: relativePath,
434 ext: libpath.extname(fullPath).substr(1)
435 };
436
437 this._onFile(res, ruleset);
438 },
439
440
441 /**
442 * Processes the "_skip" rule to decide if the path should be skipped.
443 * @private
444 * @method _ruleSkip
445 * @param {string} fullPath The full path to the file.
446 * @param {string} relativePath The path relative to the bundle.
447 * @param {object} rule The skip rule to process.
448 * @return {boolean} True if the path should be skipped, false otherwise.
449 */
450 _ruleSkip: function (fullPath, relativePath, rule) {
451 var r, regex;
452
453 relativePath = BundleLocator._toUnixPath(relativePath);
454
455 for (r = 0; r < rule.length; r += 1) {
456 regex = rule[r];
457 if (regex.test(relativePath)) {
458 return true;
459 }
460 }
461 return false;
462 },
463
464
465 /**
466 * Processes the "_bundles" rule looking for child bundles.
467 * Returns a "bundle seed" as described by _makeBundleSeed().
468 *
469 * @private
470 * @method _ruleBundles
471 * @param {string} fullPath The full path to the file.
472 * @param {string} relativePath The path relative to the parent bundle.
473 * @param {object} rule The bundles rule.
474 * @param {Bundle} parent The parent bundle.
475 * @return {object|undefined} The processing options if the path is a child bundle.
476 */
477 _ruleBundles: function (fullPath, relativePath, rule, parent) {
478 var r,
479 matches,
480 defaultVersion = DEFAULT_VERSION,
481 pkg;
482 if (parent) {
483 defaultVersion = parent.version;
484 }
485
486 relativePath = BundleLocator._toUnixPath(relativePath);
487
488 for (r = 0; r < rule.length; r += 1) {
489 matches = relativePath.match(rule[r].regex);
490 if (matches) {
491 try {
492 pkg = require(libpath.resolve(fullPath, 'package.json'));
493 } catch (packageErr) {
494 // It's OK for a sub-bundle to not have a package.json.
495 }
496 return this._makeBundleSeed(fullPath, libpath.normalize(matches[1]), defaultVersion, pkg, rule[r].options);
497 }
498 }
499 },
500
501
502 /**
503 * Handles the file.
504 * @private
505 * @method _onFile
506 * @param {object} res Metadata about the file.
507 * @param {object} ruleset Rules to attempt on the file.
508 */
509 _onFile: function (res, ruleset) {
510 var bundle = this._bundles[res.bundleName],
511 ruleName,
512 rule,
513 relativePath = BundleLocator._toUnixPath(res.relativePath),
514 match;
515
516 bundle.files[res.fullPath] = true;
517
518 for (ruleName in ruleset) {
519 if (ruleset.hasOwnProperty(ruleName)) {
520 // Rules that start with "_" are special directives,
521 // and have already been handle by the time we get here.
522 if ('_' !== ruleName.charAt(0)) {
523 rule = ruleset[ruleName];
524 match = relativePath.match(rule.regex);
525 if (match) {
526 res.name = match[rule.nameKey || 1];
527 res.type = ruleName;
528 if (rule.subtypeKey) {
529 res.subtype = match[rule.subtypeKey] || '';
530 }
531 if (rule.selectorKey && match[rule.selectorKey]) {
532 res.selector = match[rule.selectorKey];
533 } else {
534 res.selector = DEFAULT_SELECTOR;
535 }
536 // file will become a resource after the first match
537 return this._onResource(res);
538 }
539 }
540 }
541 }
542 },
543
544
545 /**
546 * Handles the resource.
547 * @private
548 * @method _onResource
549 * @param {object} res The resource.
550 */
551 _onResource: function (res) {
552 var bundle = this._bundles[res.bundleName],
553 type = res.type,
554 subtype,
555 selector = res.selector,
556 name = res.name;
557
558 if (!bundle.resources[selector]) {
559 bundle.resources[selector] = {};
560 }
561 if (!bundle.resources[selector][type]) {
562 bundle.resources[selector][type] = {};
563 }
564 if (res.hasOwnProperty('subtype')) {
565 subtype = res.subtype;
566 if (!bundle.resources[selector][type][subtype]) {
567 bundle.resources[selector][type][subtype] = {};
568 }
569 bundle.resources[selector][type][subtype][name] = res;
570 } else {
571 bundle.resources[selector][type][name] = res;
572 }
573 },
574
575
576 /**
577 * Determines whether a resource is filtered or not.
578 * @private
579 * @method _filterResource
580 * @param {LocatorResource} res The resource to filter.
581 * @param {object} filter The filter.
582 * @return {boolean} Whether resource passes the filter.
583 */
584 _filterResource: function (res, filter) {
585 if (!filter || Object.keys(filter).length === 0) {
586 return true;
587 }
588 var prop;
589 for (prop in filter) {
590 if ('extensions' === prop) {
591 // sugar for users
592 if ('string' === typeof filter.extensions) {
593 filter.extensions = filter.extensions.split(',');
594 }
595 if (!filter.extensions || filter.extensions.indexOf(res.ext) === -1) {
596 return false;
597 }
598 } else if ('types' === prop) {
599 // sugar for users
600 if ('string' === typeof filter.types) {
601 filter.types = filter.types.split(',');
602 }
603 if (!filter.types || filter.types.indexOf(res.type) === -1) {
604 return false;
605 }
606 } else {
607 return false; // unknown filters should fail to pass
608 }
609 }
610 return true;
611 },
612
613
614 /**
615 * Walks the resources in the bundle, optionally applying the filter along the way.
616 * @private
617 * @method _walkBundleResources
618 * @param {Bundle} bundle The bundle containing resources.
619 * @param {object} filter Filter for deciding which resources to return.
620 * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which resources to return.
621 * @param {string|[string]} [filter.types] The resources types for which resources to return.
622 * @return {[LocatorResource]} A collection of filtered resources.
623 */
624 _walkBundleResources: function (bundle, filter) {
625 var self = this,
626 ruleset = this._loadRuleset(bundle),
627 ress = [];
628 Object.keys(bundle.resources).forEach(function (selector) {
629 Object.keys(bundle.resources[selector]).forEach(function (resType) {
630 var rule = ruleset[resType];
631 if (rule.subtypeKey) {
632 Object.keys(bundle.resources[selector][resType]).forEach(function (subtype) {
633 Object.keys(bundle.resources[selector][resType][subtype]).forEach(function (name) {
634 var res = bundle.resources[selector][resType][subtype][name];
635 if (self._filterResource(res, filter)) {
636 ress.push(res);
637 }
638 });
639 });
640 } else {
641 Object.keys(bundle.resources[selector][resType]).forEach(function (name) {
642 var res = bundle.resources[selector][resType][name];
643 if (self._filterResource(res, filter)) {
644 ress.push(res);
645 }
646 });
647 }
648 });
649 });
650 return ress;
651 },
652
653
654 /**
655 * Walks a directory and returns a list of metadata about locator packages
656 * installed in that directory (including the package for the directory itself).
657 * @private
658 * @method _walkNPMTree
659 * @param {string} dir Directory to walk.
660 * @param {integer} _depth [internal] Depth of the directory being walked.
661 * Internally used for recursion.
662 * @return {Array} information about locator packages in the directory.
663 * If the directory is not an NPM package then it returns undefined value.
664 */
665 _walkNPMTree: function (dir, _depth) {
666 var self = this,
667 pkg,
668 seed,
669 seeds = [],
670 subdirs;
671
672 _depth = _depth || 0;
673
674 try {
675 pkg = require(libpath.resolve(dir, 'package.json'));
676 // top level package doesn't require a locator flag in package.json
677 if ((0 === _depth) && (!pkg.locator)) {
678 pkg.locator = {};
679 }
680 seed = this._makeBundleSeed(dir, libpath.basename(dir), DEFAULT_VERSION, pkg);
681 if (seed.options) {
682 seed.npmDepth = _depth;
683 seeds.push(seed);
684 }
685 } catch (packageErr) {
686 // Some build environments leave extraneous directories in
687 // node_modules and we should ignore them gracefully.
688 // (trello board:Modown card:124)
689 if ('MODULE_NOT_FOUND' !== packageErr.code) {
690 throw packageErr;
691 }
692 return seeds;
693 }
694
695 if (_depth < this._options.maxPackageDepth) {
696 try {
697 subdirs = libfs.readdirSync(libpath.join(dir, 'node_modules'));
698 } catch (readdirErr) {
699 if ('ENOENT' === readdirErr.code) {
700 // missing node_modules/ directory is OK
701 return seeds;
702 }
703 throw readdirErr;
704 }
705 subdirs.forEach(function (subdir) {
706 var subpkgResults;
707 if ('.' === subdir.substring(0, 1)) {
708 return;
709 }
710 subpkgResults = self._walkNPMTree(libpath.join(dir, 'node_modules', subdir), _depth + 1);
711 // merge in found packages
712 if (subpkgResults && subpkgResults.length) {
713 seeds = seeds.concat(subpkgResults);
714 }
715 });
716 }
717 return seeds;
718 },
719
720
721 /**
722 * Figures out which seed to use from the list of available packages.
723 * Select by depth, then by semver.
724 * @private
725 * @method _dedupeSeeds
726 * @param {object} pkgDepths List of seed by deep.
727 * @return {object} The metas of the selected package.
728 */
729 _dedupeSeeds: function (pkgDepths) {
730 // pkgDepths -> depth: [metas]
731 var depths,
732 minDepth,
733 maxDepth,
734 seeds;
735 depths = Object.keys(pkgDepths);
736 minDepth = Math.min.apply(Math, depths);
737 maxDepth = Math.max.apply(Math, depths);
738 seeds = pkgDepths[minDepth];
739 if (1 === seeds.length) {
740 if (minDepth !== maxDepth) {
741 debug('multiple "' + seeds[0].name + '" packages found, using version ' +
742 seeds[0].version + ' from ' + seeds[0].baseDirectory);
743 }
744 return seeds[0];
745 }
746 seeds.sort(function (a, b) {
747 return libsemver.rcompare(a.version, b.version);
748 });
749 debug('multiple "' + seeds[0].name + '" packages found, using version ' +
750 seeds[0].version + ' from ' + seeds[0].baseDirectory);
751 return seeds[0];
752 },
753
754 /**
755 * Figures out which bundles to use from the list.
756 * The returned list is sorted first by NPM package depth then by name.
757 * @private
758 * @method _filterBundleSeeds
759 * @param {array} all List of all bundle seeds from NPM packages.
760 * @return {array} The metas of the packages to actually use.
761 */
762 _filterBundleSeeds: function (all) {
763 var byDepth = {}; // name: depth: [metas]
764 all.forEach(function (seed) {
765 if (!byDepth[seed.name]) {
766 byDepth[seed.name] = {};
767 }
768 if (!byDepth[seed.name][seed.npmDepth]) {
769 byDepth[seed.name][seed.npmDepth] = [];
770 }
771 byDepth[seed.name][seed.npmDepth].push(seed);
772 });
773 return Object.keys(byDepth).map(function (name) {
774 return this._dedupeSeeds(byDepth[name]);
775 }, this);
776 },
777
778
779 /**
780 * Creates a bundle from an NPM package, and queues up files in the package.
781 * @private
782 * @method _walkBundle
783 * @param {object} bundleSeed Metadata about the package. See the docs for _makeBundleSeed()
784 * for format of this metadata.
785 * @return {Bundle} The bundle made from the NPM package.
786 */
787 _walkBundle: function (bundleSeed) {
788 var self = this,
789 parentName,
790 parent,
791 bundle,
792 filters;
793 // TODO -- merge options (second arg) over bundleSeed.options
794
795 parentName = this._getBundleNameByPath(libpath.dirname(bundleSeed.baseDirectory));
796 parent = this._bundles[parentName];
797
798 bundle = this._makeBundle(bundleSeed, parent);
799 this._bundles[bundle.name] = bundle;
800 filters = this._options.exclude.concat(['node_modules', /^\./]);
801 // adding the bundle dir itself for BC
802 this._processFile(bundle.baseDirectory);
803 walk.walkSync(bundle.baseDirectory, {
804 filters: [],
805 listeners: {
806 directories: function (root, dirStatsArray, next) {
807 var i, dirStats, exclude;
808 function filterDir(filter) {
809 if (dirStats.name.match(filter)) {
810 return true;
811 }
812 }
813 for (i = dirStatsArray.length - 1; i >= 0; i -= 1) {
814 dirStats = dirStatsArray[i];
815 exclude = filters.some(filterDir);
816 if (exclude) {
817 // the sync walk api is pretty bad, it requires to
818 // mutate the actual dir array
819 dirStatsArray.splice(i, 1);
820 } else {
821 self._processFile(libpath.join(root, dirStats.name));
822 }
823 }
824 next();
825 },
826 file: function(root, fileStats, next) {
827 self._processFile(libpath.join(root, fileStats.name));
828 next();
829 },
830 errors: function(root, nodeStatsArray, next) {
831 next();
832 }
833 }
834 });
835
836 return bundle;
837 },
838
839
840 /**
841 * Loads the rulesets for the bundle (or seed).
842 * @private
843 * @method _loadRuleset
844 * @param {Bundle|object} bundle The bundle (or bundle seed, see _makeBundleSeed())
845 * to load the ruleset for.
846 * @return {object} The ruleset, or undefined if we couldn't load it.
847 */
848 _loadRuleset: function (bundle) {
849 var name = (bundle.options && bundle.options.ruleset) || DEFAULT_RULESET,
850 cacheKey = name + '@' + bundle.baseDirectory,
851 rulesetsPath,
852 rulesets,
853 dir,
854 rules;
855
856 rules = this._cacheRules[cacheKey];
857 if (rules) {
858 return rules;
859 }
860 if (bundle.options && bundle.options.rulesets) {
861 try {
862 rulesetsPath = libpath.resolve(bundle.baseDirectory, bundle.options.rulesets);
863 rulesets = require(rulesetsPath);
864 } catch (errLocal) {
865 if ('MODULE_NOT_FOUND' !== errLocal.code) {
866 throw errLocal;
867 }
868 }
869 if (!rulesets) {
870 dir = bundle.baseDirectory;
871 while (dir) {
872 try {
873 rulesetsPath = libpath.resolve(dir, bundle.options.rulesets);
874 rulesets = require(rulesetsPath);
875 break;
876 } catch (errDir) {
877 if ('MODULE_NOT_FOUND' !== errDir.code) {
878 throw errDir;
879 }
880 }
881 try {
882 rulesetsPath = libpath.resolve(dir, 'node_modules', bundle.options.rulesets);
883 rulesets = require(rulesetsPath);
884 break;
885 } catch (errDep) {
886 if ('MODULE_NOT_FOUND' !== errDep.code) {
887 throw errDep;
888 }
889 }
890
891 // not found, iterate
892 dir = libpath.dirname(dir);
893 if ('node_modules' === libpath.basename(dir)) {
894 dir = libpath.dirname(dir);
895 }
896 if (this._rootDirectory && dir.length < this._rootDirectory.length) {
897 // if we can find the ruleset anywhere near in the filesystem
898 // we should try to rely on npm lookup process
899 try {
900 rulesetsPath = Module._resolveFilename(bundle.options.rulesets,
901 Module._cache[__filename]);
902 rulesets = require(rulesetsPath);
903 } catch (errLocalMod) {
904 return;
905 }
906 }
907 }
908 }
909 // a ruleset pkg can contain multiple rulesets
910 rules = rulesets[name];
911 } else if (this._options.rulesetFn) {
912 // using the rulesetFn() hook to produce custom rules
913 rules = this._options.rulesetFn(bundle);
914 } else {
915 rules = DEFAULT_RULESETS[name];
916 }
917 if (rules) {
918 rules._name = name;
919 this._cacheRules[cacheKey] = rules;
920 }
921 return rules;
922 },
923
924
925 /**
926 * Creates a new object with the certain keys excluded.
927 * This is used instead of "delete" since that has performance implications in V8.
928 * @private
929 * @method _objectExclude
930 * @param {object} srcObject The original object.
931 * @param {array} excludeKeys The keys to exclude from the results.
932 * @return {object} A version of the original object with the keys excluded.
933 */
934 _objectExclude: function (srcObject, excludeKeys) {
935 var destObject = {},
936 key;
937 for (key in srcObject) {
938 if (srcObject.hasOwnProperty(key)) {
939 if (-1 === excludeKeys.indexOf(key)) {
940 destObject[key] = srcObject[key];
941 }
942 }
943 }
944 return destObject;
945 }
946
947
948};
949
950/**
951 * Rules are written in regular expressions that test paths as if they
952 * were unix-based paths. Replacing path separators with unix style
953 * separators ensures that these rules apply in other OSs too.
954 *
955 * @method toUnixPath
956 * @param {String} path A path string
957 * @return {String} A path with Unix style separators
958 * @static
959 * @private
960 */
961BundleLocator._toUnixPath = '/' !== libpath.sep ? function toUnixPath(path) {
962 return path.split(PATH_SEP).join('/');
963} : function toUnixPath(path) {
964 return path;
965};
966
967
968/**
969 * This object represents a resource on the filesystem.
970 * @class LocatorResource
971 */
972/**
973 * The name of the bundle to which the resource belongs.
974 * @property bundleName
975 * @type string
976 */
977/**
978 * The full filesystem path to the resources.
979 * @property fullPath
980 * @type string
981 */
982/**
983 * The filesystem path relative to the bundle.
984 * @property relativePath
985 * @type string
986 */
987/**
988 * The filesystem extension of the resource.
989 * Does not include the dot.
990 * @property ext
991 * @type string
992 */
993/**
994 * The name of the resource.
995 * @property name
996 * @type string
997 */
998/**
999 * The type of the resource.
1000 * @property type
1001 * @type string
1002 */
1003/**
1004 * The subtype of the resource, if it has one.
1005 * @property subtype
1006 * @type string
1007 * @optional
1008 */
1009/**
1010 * The selector for the resource.
1011 * @property selector
1012 * @type string
1013 */
1014
1015
1016module.exports = BundleLocator;