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 | ;
|
10 |
|
11 |
|
12 | var 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 |
|
26 | function 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 | */
|
62 | function 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 |
|
80 | BundleLocator.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 | */
|
961 | BundleLocator._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 |
|
1016 | module.exports = BundleLocator;
|