UNPKG

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