join-function.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /*
  2. * MIT License http://opensource.org/licenses/MIT
  3. * Author: Ben Holloway @bholloway
  4. */
  5. 'use strict';
  6. var path = require('path'),
  7. fs = require('fs'),
  8. compose = require('compose-function'),
  9. Iterator = require('es6-iterator');
  10. var PACKAGE_NAME = require('../package.json').name;
  11. var simpleJoin = compose(path.normalize, path.join);
  12. /**
  13. * The default join function iterates over possible base paths until a suitable join is found.
  14. *
  15. * The first base path is used as fallback for the case where none of the base paths can locate the actual file.
  16. *
  17. * @type {function}
  18. */
  19. exports.defaultJoin = createJoinForPredicate(
  20. function predicate(_, uri, base, i, next) {
  21. var absolute = simpleJoin(base, uri);
  22. return fs.existsSync(absolute) ? absolute : next((i === 0) ? absolute : null);
  23. },
  24. 'defaultJoin'
  25. );
  26. /**
  27. * Define a join function by a predicate that tests possible base paths from an iterator.
  28. *
  29. * The `predicate` is of the form:
  30. *
  31. * ```
  32. * function(filename, uri, base, i, next):string|null
  33. * ```
  34. *
  35. * Given the uri and base it should either return:
  36. * - an absolute path success
  37. * - a call to `next(null)` as failure
  38. * - a call to `next(absolute)` where absolute is placeholder and the iterator continues
  39. *
  40. * The value given to `next(...)` is only used if success does not eventually occur.
  41. *
  42. * The `file` value is typically unused but useful if you would like to differentiate behaviour.
  43. *
  44. * You can write a much simpler function than this if you have specific requirements.
  45. *
  46. * @param {function} predicate A function that tests values
  47. * @param {string} [name] Optional name for the resulting join function
  48. */
  49. function createJoinForPredicate(predicate, name) {
  50. /**
  51. * A factory for a join function with logging.
  52. *
  53. * @param {string} filename The current file being processed
  54. * @param {{debug:function|boolean,root:string}} options An options hash
  55. */
  56. function join(filename, options) {
  57. var log = createDebugLogger(options.debug);
  58. /**
  59. * Join function proper.
  60. *
  61. * For absolute uri only `uri` will be provided. In this case we substitute any `root` given in options.
  62. *
  63. * @param {string} uri A uri path, relative or absolute
  64. * @param {string|Iterator.<string>} [baseOrIteratorOrAbsent] Optional absolute base path or iterator thereof
  65. * @return {string} Just the uri where base is empty or the uri appended to the base
  66. */
  67. return function joinProper(uri, baseOrIteratorOrAbsent) {
  68. var iterator =
  69. (typeof baseOrIteratorOrAbsent === 'undefined') && new Iterator([options.root ]) ||
  70. (typeof baseOrIteratorOrAbsent === 'string' ) && new Iterator([baseOrIteratorOrAbsent]) ||
  71. baseOrIteratorOrAbsent;
  72. var result = runIterator([]);
  73. log(createJoinMsg, [filename, uri, result, result.isFound]);
  74. return (typeof result.absolute === 'string') ? result.absolute : uri;
  75. function runIterator(accumulator) {
  76. var nextItem = iterator.next();
  77. var base = !nextItem.done && nextItem.value;
  78. if (typeof base === 'string') {
  79. var element = predicate(filename, uri, base, accumulator.length, next);
  80. if ((typeof element === 'string') && path.isAbsolute(element)) {
  81. return Object.assign(
  82. accumulator.concat(base),
  83. {isFound: true, absolute: element}
  84. );
  85. } else if (Array.isArray(element)) {
  86. return element;
  87. } else {
  88. throw new Error('predicate must return an absolute path or the result of calling next()');
  89. }
  90. } else {
  91. return accumulator;
  92. }
  93. function next(fallback) {
  94. return runIterator(Object.assign(
  95. accumulator.concat(base),
  96. (typeof fallback === 'string') && {absolute: fallback}
  97. ));
  98. }
  99. }
  100. };
  101. }
  102. function toString() {
  103. return '[Function: ' + name + ']';
  104. }
  105. return Object.assign(join, name && {
  106. valueOf : toString,
  107. toString: toString
  108. });
  109. }
  110. exports.createJoinForPredicate = createJoinForPredicate;
  111. /**
  112. * Format a debug message.
  113. *
  114. * @param {string} file The file being processed by webpack
  115. * @param {string} uri A uri path, relative or absolute
  116. * @param {Array.<string>} bases Absolute base paths up to and including the found one
  117. * @param {boolean} isFound Indicates the last base was correct
  118. * @return {string} Formatted message
  119. */
  120. function createJoinMsg(file, uri, bases, isFound) {
  121. return [PACKAGE_NAME + ': ' + pathToString(file) + ': ' + uri]
  122. .concat(bases.map(pathToString).filter(Boolean))
  123. .concat(isFound ? 'FOUND' : 'NOT FOUND')
  124. .join('\n ');
  125. /**
  126. * If given path is within `process.cwd()` then show relative posix path, otherwise show absolute posix path.
  127. *
  128. * @param {string} absolute An absolute path
  129. * @return {string} A relative or absolute path
  130. */
  131. function pathToString(absolute) {
  132. if (!absolute) {
  133. return null;
  134. } else {
  135. var relative = path.relative(process.cwd(), absolute)
  136. .split(path.sep);
  137. return ((relative[0] === '..') ? absolute.split(path.sep) : ['.'].concat(relative).filter(Boolean))
  138. .join('/');
  139. }
  140. }
  141. }
  142. exports.createJoinMsg = createJoinMsg;
  143. /**
  144. * A factory for a log function predicated on the given debug parameter.
  145. *
  146. * The logging function created accepts a function that formats a message and parameters that the function utilises.
  147. * Presuming the message function may be expensive we only call it if logging is enabled.
  148. *
  149. * The log messages are de-duplicated based on the parameters, so it is assumed they are simple types that stringify
  150. * well.
  151. *
  152. * @param {function|boolean} debug A boolean or debug function
  153. * @return {function(function, array)} A logging function possibly degenerate
  154. */
  155. function createDebugLogger(debug) {
  156. var log = !!debug && ((typeof debug === 'function') ? debug : console.log);
  157. var cache = {};
  158. return log ? actuallyLog : noop;
  159. function noop() {}
  160. function actuallyLog(msgFn, params) {
  161. var key = JSON.stringify(params);
  162. if (!cache[key]) {
  163. cache[key] = true;
  164. log(msgFn.apply(null, params));
  165. }
  166. }
  167. }
  168. exports.createDebugLogger = createDebugLogger;