pathcache.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. var path = require('path');
  2. var fs = require('fs');
  3. var Evaluator = require('stylus/lib/visitor/evaluator');
  4. var loaderUtils = require('loader-utils');
  5. var nodes = require('stylus/lib/nodes');
  6. var utils = require('stylus/lib/utils');
  7. var when = require('when');
  8. var whenNodefn = require('when/node/function');
  9. var listImports = require('./listimports');
  10. module.exports = PathCache;
  11. var readFile = whenNodefn.lift(fs.readFile);
  12. // A cache of import paths of a stylus file resolved to their location on disk
  13. // before the stylus file is rendered. With a special evaluator this lets us
  14. // webpack's resolver.
  15. function PathCache(contexts, sources, imports) {
  16. this.contexts = contexts;
  17. this.sources = sources;
  18. this.imports = imports;
  19. // Non relative paths are simpler and looked up in this as a fallback
  20. // to this.context.
  21. this.simpleContext = {};
  22. for (var dirname in this.contexts) {
  23. for (var path in this.contexts[dirname]) {
  24. this.simpleContext[path] = this.contexts[dirname][path];
  25. }
  26. }
  27. }
  28. // Return a promise for a PathCache.
  29. PathCache.create = function(contexts, sources, imports) {
  30. return when(new PathCache(contexts, sources, imports));
  31. };
  32. PathCache.createFromFile = resolveFileDeep;
  33. // Create a list of ways to resolve paths.
  34. PathCache.resolvers = resolvers;
  35. PathCache.resolvers.reduce = reduceResolvers;
  36. // Lookup the path in this cache.
  37. PathCache.prototype.find = function(path, dirname) {
  38. if (this.contexts[dirname] && this.contexts[dirname][path]) {
  39. return this.contexts[dirname][path].path;
  40. } else if (this.simpleContext[path]) {
  41. return this.simpleContext[path].path;
  42. } else if (/.styl$/.test(path)) {
  43. // A user can specify @import 'something.styl' but if they specify
  44. // @import 'something' stylus adds .styl, we drop that here to see if we
  45. // looked for it without .styl.
  46. return this.find(path.replace(/.styl$/, ''), dirname);
  47. } else {
  48. return undefined;
  49. }
  50. };
  51. // Return if the path in this cache is an index file.
  52. PathCache.prototype.isIndex = function(path, dirname) {
  53. if (this.contexts[dirname] && this.contexts[dirname][path]) {
  54. return this.contexts[dirname][path].index;
  55. } else {
  56. return undefined;
  57. }
  58. };
  59. // Return an array of all imports the original file depends on.
  60. PathCache.prototype.allDeps = function() {
  61. var deps = [];
  62. for (var dirname in this.contexts) {
  63. for (var path in this.contexts[dirname]) {
  64. if (this.contexts[dirname][path]) {
  65. deps = deps.concat(this.contexts[dirname][path].path);
  66. }
  67. }
  68. }
  69. return deps;
  70. };
  71. // Create an array of ways to resolve a path.
  72. //
  73. // The resolved paths may be a path or an object specifying path and index
  74. // members. The index member is used later by stylus, we store it at this point.
  75. function resolvers(options, webpackResolver) {
  76. var evaluator = new Evaluator(nodes.null, options);
  77. var whenWebpackResolver = whenNodefn.lift(webpackResolver);
  78. // Stylus's normal resolver for single files.
  79. var stylusFile = function(context, path) {
  80. // Stylus adds .styl to paths for normal "paths" lookup if it isn't there.
  81. if (!/.styl$/.test(path)) {
  82. path += '.styl';
  83. }
  84. var paths = options.paths.concat(context);
  85. var found = utils.find(path, paths, options.filename)
  86. if (found) {
  87. return normalizePaths(found);
  88. }
  89. };
  90. // Stylus's normal resolver for node_modules packages. Cannot locate paths
  91. // inside a package.
  92. var stylusIndex = function(context, path) {
  93. // Stylus calls the argument name. If it exists it should match the name
  94. // of a module in node_modules.
  95. if (!path) {
  96. return null;
  97. }
  98. var paths = options.paths.concat(context);
  99. var found = utils.lookupIndex(path, paths, options.filename);
  100. if (found) {
  101. return {path: normalizePaths(found), index: true};
  102. }
  103. };
  104. // Fallback to resolving with webpack's configured resovler.
  105. var webpackResolve = function(context, path) {
  106. // Follow the webpack stylesheet idiom of '~path' meaning a path in a
  107. // modules folder and a unprefixed 'path' meaning a relative path like
  108. // './path'.
  109. path = loaderUtils.urlToRequest(path, options.root);
  110. // First try with a '.styl' extension.
  111. return whenWebpackResolver(context, path + '.styl')
  112. // If the user adds ".styl" to resolve.extensions, webpack can find
  113. // index files like stylus but it uses all of webpack's configuration,
  114. // by default for example the module could be web_modules.
  115. .catch(function() { return whenWebpackResolver(context, path); })
  116. .catch(function() { return null; })
  117. .then(function(result) {
  118. return Array.isArray(result) && result[1] && result[1].path || result
  119. });
  120. };
  121. if (options.preferPathResolver === 'webpack') {
  122. return [
  123. webpackResolve,
  124. stylusFile,
  125. stylusIndex
  126. ];
  127. }
  128. else {
  129. return [
  130. stylusFile,
  131. stylusIndex,
  132. webpackResolve
  133. ];
  134. }
  135. }
  136. function reduceResolvers(resolvers, context, path) {
  137. return when
  138. .reduce(resolvers, function(result, resolver) {
  139. return result ? result : resolver(context, path);
  140. }, undefined);
  141. }
  142. // Run resolvers on one path and return an object with the found path under a
  143. // key of the original path.
  144. //
  145. // Example:
  146. // resolving the path
  147. // 'a/file'
  148. // returns an object
  149. // {'a/file': {path: ['node_modules/a/file'], index: true}}
  150. function resolveOne(resolvers, context, path) {
  151. return reduceResolvers(resolvers, context, path)
  152. .then(function(result) {
  153. result = typeof result === 'string' ? [result] : result;
  154. result = Array.isArray(result) ? {path: result, index: false} : result;
  155. var map = {};
  156. map[path] = result;
  157. return map;
  158. });
  159. }
  160. // Run the resolvers on an array of paths and return an object like resolveOne.
  161. function resolveMany(resolvers, context, paths) {
  162. return when
  163. .map(paths, resolveOne.bind(null, resolvers, context))
  164. .then(function(maps) {
  165. return maps.reduce(function(map, resolvedPaths) {
  166. Object.keys(resolvedPaths).forEach(function(path) {
  167. map[path] = resolvedPaths[path];
  168. });
  169. return map;
  170. }, {});
  171. });
  172. }
  173. // Load a file at fullPath, resolve all of it's imports and report for those.
  174. function resolveFileDeep(helpers, parentCache, source, fullPath) {
  175. var resolvers = helpers.resolvers;
  176. var readFile = helpers.readFile;
  177. var contexts = parentCache.contexts;
  178. var sources = parentCache.sources;
  179. contexts = contexts || {};
  180. var nestResolve = resolveFileDeep.bind(null, helpers, parentCache, null);
  181. var context = path.dirname(fullPath);
  182. readFile = whenNodefn.lift(readFile);
  183. return when
  184. .resolve(source || sources[fullPath] || readFile(fullPath))
  185. // Cast the buffer from the cached input file system to a string.
  186. .then(String)
  187. // Store the source so that the evaluator doesn't need to touch the
  188. // file system.
  189. .then(function(_source) {
  190. sources[fullPath] = _source;
  191. return _source;
  192. })
  193. // Make sure the stylus functions/index.styl source is stored.
  194. .then(partial(ensureFunctionsSource, sources))
  195. // List imports and use its cache. The source file is translated into a
  196. // list of imports. Where the source file came from isn't important for the
  197. // list. The where is added by resolveMany with the context and resolvers.
  198. .then(partialRight(listImports, { cache: parentCache.imports }))
  199. .then(resolveMany.bind(null, resolvers, context))
  200. .then(function(newPaths) {
  201. // Contexts are the full path since multiple could be in the same folder
  202. // but different deps.
  203. contexts[context] = merge(contexts[context] || {}, newPaths);
  204. return when.map(Object.keys(newPaths), function(key) {
  205. var found = newPaths[key] && newPaths[key].path;
  206. if (found) {
  207. return when.map(found, nestResolve);
  208. }
  209. });
  210. })
  211. .then(function() {
  212. return PathCache.create(contexts, sources, parentCache.imports);
  213. });
  214. }
  215. // Resolve functions in a promise wrapper to catch any errors from resolving.
  216. var functionsPath =
  217. new when.Promise(function(resolve) {
  218. resolve(require.resolve('stylus/lib/functions/index.styl'));
  219. })
  220. .catch(function() { return ''; });
  221. var functionsSource = functionsPath
  222. .then(readFile)
  223. .catch(function(error) {
  224. // Ignore error if functions/index.styl doesn't exist.
  225. if (error.code !== 'ENOENT') {
  226. throw error;
  227. }
  228. return '';
  229. })
  230. .then(String);
  231. function ensureFunctionsSource(sources, source) {
  232. if (!sources[functionsPath]) {
  233. return functionsSource
  234. .then(function(functionsSource) {
  235. if (functionsSource) {
  236. sources[functionsPath] = functionsSource;
  237. }
  238. })
  239. // Pass through the source given to this function.
  240. .yield(source);
  241. }
  242. // Pass through the source given to this function.
  243. return source;
  244. }
  245. var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
  246. function merge(a, b) {
  247. var key;
  248. for (key in b) {
  249. a[key] = b[key];
  250. }
  251. return a;
  252. }
  253. function partial(fn) {
  254. var args = slice(arguments, 1);
  255. return function() {
  256. return fn.apply(this, args.concat(slice(arguments)));
  257. };
  258. }
  259. function partialRight(fn) {
  260. var args = slice(arguments, 1);
  261. return function() {
  262. return fn.apply(this, slice(arguments).concat(args));
  263. };
  264. }
  265. function normalizePaths(paths) {
  266. for(var i in paths) {
  267. paths[i] = path.normalize(paths[i]);
  268. }
  269. return paths;
  270. }