Resolver.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const util = require("util");
  7. const Tapable = require("tapable/lib/Tapable");
  8. const SyncHook = require("tapable/lib/SyncHook");
  9. const AsyncSeriesBailHook = require("tapable/lib/AsyncSeriesBailHook");
  10. const AsyncSeriesHook = require("tapable/lib/AsyncSeriesHook");
  11. const createInnerContext = require("./createInnerContext");
  12. const REGEXP_NOT_MODULE = /^\.$|^\.[\\\/]|^\.\.$|^\.\.[\/\\]|^\/|^[A-Z]:[\\\/]/i;
  13. const REGEXP_DIRECTORY = /[\/\\]$/i;
  14. const memoryFsJoin = require("memory-fs/lib/join");
  15. const memoizedJoin = new Map();
  16. const memoryFsNormalize = require("memory-fs/lib/normalize");
  17. function withName(name, hook) {
  18. hook.name = name;
  19. return hook;
  20. }
  21. function toCamelCase(str) {
  22. return str.replace(/-([a-z])/g, str => str.substr(1).toUpperCase());
  23. }
  24. const deprecatedPushToMissing = util.deprecate((set, item) => {
  25. set.add(item);
  26. }, "Resolver: 'missing' is now a Set. Use add instead of push.");
  27. const deprecatedResolveContextInCallback = util.deprecate((x) => {
  28. return x;
  29. }, "Resolver: The callback argument was splitted into resolveContext and callback.");
  30. const deprecatedHookAsString = util.deprecate((x) => {
  31. return x;
  32. }, "Resolver#doResolve: The type arguments (string) is now a hook argument (Hook). Pass a reference to the hook instead.");
  33. class Resolver extends Tapable {
  34. constructor(fileSystem) {
  35. super();
  36. this.fileSystem = fileSystem;
  37. this.hooks = {
  38. resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])),
  39. noResolve: withName("noResolve", new SyncHook(["request", "error"])),
  40. resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])),
  41. result: new AsyncSeriesHook(["result", "resolveContext"])
  42. };
  43. this._pluginCompat.tap("Resolver: before/after", options => {
  44. if(/^before-/.test(options.name)) {
  45. options.name = options.name.substr(7);
  46. options.stage = -10;
  47. } else if(/^after-/.test(options.name)) {
  48. options.name = options.name.substr(6);
  49. options.stage = 10;
  50. }
  51. });
  52. this._pluginCompat.tap("Resolver: step hooks", options => {
  53. const name = options.name;
  54. const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name);
  55. if(stepHook) {
  56. options.async = true;
  57. this.ensureHook(name);
  58. const fn = options.fn;
  59. options.fn = (request, resolverContext, callback) => {
  60. const innerCallback = (err, result) => {
  61. if(err) return callback(err);
  62. if(result !== undefined) return callback(null, result);
  63. callback();
  64. };
  65. for(const key in resolverContext) {
  66. innerCallback[key] = resolverContext[key];
  67. }
  68. fn.call(this, request, innerCallback);
  69. };
  70. }
  71. });
  72. }
  73. ensureHook(name) {
  74. if(typeof name !== "string") return name;
  75. name = toCamelCase(name);
  76. if(/^before/.test(name)) {
  77. return this.ensureHook(name[6].toLowerCase() + name.substr(7)).withOptions({
  78. stage: -10
  79. });
  80. }
  81. if(/^after/.test(name)) {
  82. return this.ensureHook(name[5].toLowerCase() + name.substr(6)).withOptions({
  83. stage: 10
  84. });
  85. }
  86. const hook = this.hooks[name];
  87. if(!hook) {
  88. return this.hooks[name] = withName(name, new AsyncSeriesBailHook(["request", "resolveContext"]));
  89. }
  90. return hook;
  91. }
  92. getHook(name) {
  93. if(typeof name !== "string") return name;
  94. name = toCamelCase(name);
  95. if(/^before/.test(name)) {
  96. return this.getHook(name[6].toLowerCase() + name.substr(7)).withOptions({
  97. stage: -10
  98. });
  99. }
  100. if(/^after/.test(name)) {
  101. return this.getHook(name[5].toLowerCase() + name.substr(6)).withOptions({
  102. stage: 10
  103. });
  104. }
  105. const hook = this.hooks[name];
  106. if(!hook) {
  107. throw new Error(`Hook ${name} doesn't exist`);
  108. }
  109. return hook;
  110. }
  111. resolveSync(context, path, request) {
  112. let err, result, sync = false;
  113. this.resolve(context, path, request, {}, (e, r) => {
  114. err = e;
  115. result = r;
  116. sync = true;
  117. });
  118. if(!sync) throw new Error("Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!");
  119. if(err) throw err;
  120. return result;
  121. }
  122. resolve(context, path, request, resolveContext, callback) {
  123. // TODO remove in enhanced-resolve 5
  124. // For backward compatiblity START
  125. if(typeof callback !== "function") {
  126. callback = deprecatedResolveContextInCallback(resolveContext);
  127. // resolveContext is a function containing additional properties
  128. // It's now used for resolveContext and callback
  129. }
  130. // END
  131. const obj = {
  132. context: context,
  133. path: path,
  134. request: request
  135. };
  136. const message = "resolve '" + request + "' in '" + path + "'";
  137. // Try to resolve assuming there is no error
  138. // We don't log stuff in this case
  139. return this.doResolve(this.hooks.resolve, obj, message, {
  140. missing: resolveContext.missing,
  141. stack: resolveContext.stack
  142. }, (err, result) => {
  143. if(!err && result) {
  144. return callback(null, result.path === false ? false : result.path + (result.query || ""), result);
  145. }
  146. const localMissing = new Set();
  147. // TODO remove in enhanced-resolve 5
  148. localMissing.push = item => deprecatedPushToMissing(localMissing, item);
  149. const log = [];
  150. return this.doResolve(this.hooks.resolve, obj, message, {
  151. log: msg => {
  152. if(resolveContext.log) {
  153. resolveContext.log(msg);
  154. }
  155. log.push(msg);
  156. },
  157. missing: localMissing,
  158. stack: resolveContext.stack
  159. }, (err, result) => {
  160. if(err) return callback(err);
  161. const error = new Error("Can't " + message);
  162. error.details = log.join("\n");
  163. error.missing = Array.from(localMissing);
  164. this.hooks.noResolve.call(obj, error);
  165. return callback(error);
  166. });
  167. });
  168. }
  169. doResolve(hook, request, message, resolveContext, callback) {
  170. // TODO remove in enhanced-resolve 5
  171. // For backward compatiblity START
  172. if(typeof callback !== "function") {
  173. callback = deprecatedResolveContextInCallback(resolveContext);
  174. // resolveContext is a function containing additional properties
  175. // It's now used for resolveContext and callback
  176. }
  177. if(typeof hook === "string") {
  178. const name = toCamelCase(hook);
  179. hook = deprecatedHookAsString(this.hooks[name]);
  180. if(!hook) {
  181. throw new Error(`Hook "${name}" doesn't exist`);
  182. }
  183. }
  184. // END
  185. if(typeof callback !== "function") throw new Error("callback is not a function " + Array.from(arguments));
  186. if(!resolveContext) throw new Error("resolveContext is not an object " + Array.from(arguments));
  187. const stackLine = hook.name + ": (" + request.path + ") " +
  188. (request.request || "") + (request.query || "") +
  189. (request.directory ? " directory" : "") +
  190. (request.module ? " module" : "");
  191. let newStack;
  192. if(resolveContext.stack) {
  193. newStack = new Set(resolveContext.stack);
  194. if(resolveContext.stack.has(stackLine)) {
  195. // Prevent recursion
  196. const recursionError = new Error("Recursion in resolving\nStack:\n " + Array.from(newStack).join("\n "));
  197. recursionError.recursion = true;
  198. if(resolveContext.log) resolveContext.log("abort resolving because of recursion");
  199. return callback(recursionError);
  200. }
  201. newStack.add(stackLine);
  202. } else {
  203. newStack = new Set([stackLine]);
  204. }
  205. this.hooks.resolveStep.call(hook, request);
  206. if(hook.isUsed()) {
  207. const innerContext = createInnerContext({
  208. log: resolveContext.log,
  209. missing: resolveContext.missing,
  210. stack: newStack
  211. }, message);
  212. return hook.callAsync(request, innerContext, (err, result) => {
  213. if(err) return callback(err);
  214. if(result) return callback(null, result);
  215. callback();
  216. });
  217. } else {
  218. callback();
  219. }
  220. }
  221. parse(identifier) {
  222. if(identifier === "") return null;
  223. const part = {
  224. request: "",
  225. query: "",
  226. module: false,
  227. directory: false,
  228. file: false
  229. };
  230. const idxQuery = identifier.indexOf("?");
  231. if(idxQuery === 0) {
  232. part.query = identifier;
  233. } else if(idxQuery > 0) {
  234. part.request = identifier.slice(0, idxQuery);
  235. part.query = identifier.slice(idxQuery);
  236. } else {
  237. part.request = identifier;
  238. }
  239. if(part.request) {
  240. part.module = this.isModule(part.request);
  241. part.directory = this.isDirectory(part.request);
  242. if(part.directory) {
  243. part.request = part.request.substr(0, part.request.length - 1);
  244. }
  245. }
  246. return part;
  247. }
  248. isModule(path) {
  249. return !REGEXP_NOT_MODULE.test(path);
  250. }
  251. isDirectory(path) {
  252. return REGEXP_DIRECTORY.test(path);
  253. }
  254. join(path, request) {
  255. let cacheEntry;
  256. let pathCache = memoizedJoin.get(path);
  257. if(typeof pathCache === "undefined") {
  258. memoizedJoin.set(path, pathCache = new Map());
  259. } else {
  260. cacheEntry = pathCache.get(request);
  261. if(typeof cacheEntry !== "undefined")
  262. return cacheEntry;
  263. }
  264. cacheEntry = memoryFsJoin(path, request);
  265. pathCache.set(request, cacheEntry);
  266. return cacheEntry;
  267. }
  268. normalize(path) {
  269. return memoryFsNormalize(path);
  270. }
  271. }
  272. module.exports = Resolver;