detect-dependencies.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. 'use strict';
  2. var $ = {
  3. _: require('lodash'),
  4. fs: require('fs'),
  5. lodash: require('lodash'),
  6. path: require('path'),
  7. glob: require('glob'),
  8. propprop: require('propprop')
  9. };
  10. /**
  11. * Detect dependencies of the components from `bower.json`.
  12. *
  13. * @param {object} config the global configuration object.
  14. * @return {object} config
  15. */
  16. function detectDependencies(config) {
  17. var allDependencies = {};
  18. if (config.get('dependencies')) {
  19. $._.assign(allDependencies, config.get('bower.json').dependencies);
  20. }
  21. if (config.get('dev-dependencies')) {
  22. $._.assign(allDependencies, config.get('bower.json').devDependencies);
  23. }
  24. if (config.get('include-self')) {
  25. allDependencies[config.get('bower.json').name] = config.get('bower.json').version;
  26. }
  27. $._.each(allDependencies, gatherInfo(config));
  28. config.set('global-dependencies-sorted', filterExcludedDependencies(
  29. config.get('detectable-file-types').
  30. reduce(function (acc, fileType) {
  31. if (!acc[fileType]) {
  32. acc[fileType] = prioritizeDependencies(config, '.' + fileType);
  33. }
  34. return acc;
  35. }, {}),
  36. config.get('exclude')
  37. ));
  38. return config;
  39. }
  40. /**
  41. * Find the component's JSON configuration file.
  42. *
  43. * @param {object} config the global configuration object
  44. * @param {string} component the name of the component to dig for
  45. * @return {object} the component's config file
  46. */
  47. function findComponentConfigFile(config, component) {
  48. var componentConfigFile;
  49. if (config.get('include-self') && component === config.get('bower.json').name) {
  50. return config.get('bower.json');
  51. }
  52. ['bower.json', '.bower.json', 'component.json', 'package.json'].
  53. forEach(function (configFile) {
  54. configFile = $.path.join(config.get('bower-directory'), component, configFile);
  55. if (!$._.isObject(componentConfigFile) && $.fs.existsSync(configFile)) {
  56. componentConfigFile = JSON.parse($.fs.readFileSync(configFile));
  57. }
  58. });
  59. return componentConfigFile;
  60. }
  61. /**
  62. * Find the main file the component refers to. It's not always `main` :(
  63. *
  64. * @param {object} config the global configuration object
  65. * @param {string} component the name of the component to dig for
  66. * @param {componentConfigFile} the component's config file
  67. * @return {array} the array of paths to the component's primary file(s)
  68. */
  69. function findMainFiles(config, component, componentConfigFile) {
  70. var filePaths = [];
  71. var file;
  72. var self = config.get('include-self') && component === config.get('bower.json').name;
  73. var cwd = self ? config.get('cwd') : $.path.join(config.get('bower-directory'), component);
  74. if ($._.isString(componentConfigFile.main)) {
  75. // start by looking for what every component should have: config.main
  76. filePaths = [componentConfigFile.main];
  77. } else if ($._.isArray(componentConfigFile.main)) {
  78. filePaths = componentConfigFile.main;
  79. } else if ($._.isArray(componentConfigFile.scripts)) {
  80. // still haven't found it. is it stored in config.scripts, then?
  81. filePaths = componentConfigFile.scripts;
  82. } else {
  83. file = $.path.join(config.get('bower-directory'), component, componentConfigFile.name + '.js');
  84. if ($.fs.existsSync(file)) {
  85. filePaths = [componentConfigFile.name + '.js'];
  86. }
  87. }
  88. return $._.unique(filePaths.reduce(function (acc, filePath) {
  89. acc = acc.concat(
  90. $.glob.sync(filePath, { cwd: cwd, root: '/' })
  91. .map(function (path) {
  92. return $.path.join(cwd, path);
  93. })
  94. );
  95. return acc;
  96. }, []));
  97. }
  98. /**
  99. * Store the information our prioritizer will need to determine rank.
  100. *
  101. * @param {object} config the global configuration object
  102. * @return {function} the iterator function, called on every component
  103. */
  104. function gatherInfo(config) {
  105. /**
  106. * The iterator function, which is called on each component.
  107. *
  108. * @param {string} version the version of the component
  109. * @param {string} component the name of the component
  110. * @return {undefined}
  111. */
  112. return function (version, component) {
  113. var dep = config.get('global-dependencies').get(component) || {
  114. main: '',
  115. type: '',
  116. name: '',
  117. dependencies: {}
  118. };
  119. var componentConfigFile = findComponentConfigFile(config, component);
  120. if (!componentConfigFile) {
  121. var error = new Error(component + ' is not installed. Try running `bower install`.');
  122. error.code = 'PKG_NOT_INSTALLED';
  123. config.get('on-error')(error);
  124. return;
  125. }
  126. var overrides = config.get('overrides');
  127. if (overrides && overrides[component]) {
  128. if (overrides[component].dependencies) {
  129. componentConfigFile.dependencies = overrides[component].dependencies;
  130. }
  131. if (overrides[component].main) {
  132. componentConfigFile.main = overrides[component].main;
  133. }
  134. }
  135. var mains = findMainFiles(config, component, componentConfigFile);
  136. var fileTypes = $._.chain(mains).map($.path.extname).unique().value();
  137. dep.main = mains;
  138. dep.type = fileTypes;
  139. dep.name = componentConfigFile.name;
  140. var depIsExcluded = $._.find(config.get('exclude'), function (pattern) {
  141. return $.path.join(config.get('bower-directory'), component).match(pattern);
  142. });
  143. if (dep.main.length === 0 && !depIsExcluded) {
  144. // can't find the main file. this config file is useless!
  145. config.get('on-main-not-found')(component);
  146. return;
  147. }
  148. if (componentConfigFile.dependencies) {
  149. dep.dependencies = componentConfigFile.dependencies;
  150. $._.each(componentConfigFile.dependencies, gatherInfo(config));
  151. }
  152. config.get('global-dependencies').set(component, dep);
  153. };
  154. }
  155. /**
  156. * Compare two dependencies to determine priority.
  157. *
  158. * @param {object} a dependency a
  159. * @param {object} b dependency b
  160. * @return {number} the priority of dependency a in comparison to dependency b
  161. */
  162. function dependencyComparator(a, b) {
  163. var aNeedsB = false;
  164. var bNeedsA = false;
  165. aNeedsB = Object.
  166. keys(a.dependencies).
  167. some(function (dependency) {
  168. return dependency === b.name;
  169. });
  170. if (aNeedsB) {
  171. return 1;
  172. }
  173. bNeedsA = Object.
  174. keys(b.dependencies).
  175. some(function (dependency) {
  176. return dependency === a.name;
  177. });
  178. if (bNeedsA) {
  179. return -1;
  180. }
  181. return 0;
  182. }
  183. /**
  184. * Take two arrays, sort based on their dependency relationship, then merge them
  185. * together.
  186. *
  187. * @param {array} left
  188. * @param {array} right
  189. * @return {array} the sorted, merged array
  190. */
  191. function merge(left, right) {
  192. var result = [];
  193. var leftIndex = 0;
  194. var rightIndex = 0;
  195. while (leftIndex < left.length && rightIndex < right.length) {
  196. if (dependencyComparator(left[leftIndex], right[rightIndex]) < 1) {
  197. result.push(left[leftIndex++]);
  198. } else {
  199. result.push(right[rightIndex++]);
  200. }
  201. }
  202. return result.
  203. concat(left.slice(leftIndex)).
  204. concat(right.slice(rightIndex));
  205. }
  206. /**
  207. * Take an array and slice it in halves, sorting each half along the way.
  208. *
  209. * @param {array} items
  210. * @return {array} the sorted array
  211. */
  212. function mergeSort(items) {
  213. if (items.length < 2) {
  214. return items;
  215. }
  216. var middle = Math.floor(items.length / 2);
  217. return merge(
  218. mergeSort(items.slice(0, middle)),
  219. mergeSort(items.slice(middle))
  220. );
  221. }
  222. /**
  223. * Some dependencies which we know should always come first.
  224. */
  225. var eliteDependencies = [
  226. 'es5-shim',
  227. 'jquery',
  228. 'zepto',
  229. 'modernizr'
  230. ];
  231. /**
  232. * Sort the dependencies in the order we can best determine they're needed.
  233. *
  234. * @param {object} config the global configuration object
  235. * @param {string} fileType the type of file to prioritize
  236. * @return {array} the sorted items of 'path/to/main/files.ext' sorted by type
  237. */
  238. function prioritizeDependencies(config, fileType) {
  239. var eliteDependenciesCaught = [];
  240. var dependencies = mergeSort(
  241. $._.toArray(config.get('global-dependencies').get()).
  242. filter(function (dependency) {
  243. return $._.contains(dependency.type, fileType);
  244. }).
  245. filter(function (dependency) {
  246. if ($._.contains(eliteDependencies, dependency.name)) {
  247. eliteDependenciesCaught.push(dependency.main);
  248. } else {
  249. return true;
  250. }
  251. })
  252. ).map($.propprop('main'));
  253. eliteDependenciesCaught.
  254. forEach(function (dependency) {
  255. dependencies.unshift(dependency);
  256. });
  257. return $._
  258. (dependencies).
  259. flatten().
  260. value().
  261. filter(function (main) {
  262. return $.path.extname(main) === fileType;
  263. });
  264. }
  265. /**
  266. * Excludes dependencies that match any of the patterns.
  267. *
  268. * @param {array} allDependencies array of dependencies to filter
  269. * @param {array} patterns array of patterns to match against
  270. * @return {array} items that don't match any of the patterns
  271. */
  272. function filterExcludedDependencies(allDependencies, patterns) {
  273. return $._.transform(allDependencies, function (result, dependencies, fileType) {
  274. result[fileType] = $._.reject(dependencies, function (dependency) {
  275. return $._.find(patterns, function (pattern) {
  276. return dependency.replace(/\\/g, '/').match(pattern);
  277. });
  278. });
  279. });
  280. }
  281. module.exports = detectDependencies;