1 |
|
2 |
|
3 |
|
4 |
|
5 | "use strict";
|
6 |
|
7 | const util = require("util");
|
8 |
|
9 | const Tapable = require("tapable/lib/Tapable");
|
10 | const SyncHook = require("tapable/lib/SyncHook");
|
11 | const AsyncSeriesBailHook = require("tapable/lib/AsyncSeriesBailHook");
|
12 | const AsyncSeriesHook = require("tapable/lib/AsyncSeriesHook");
|
13 | const createInnerContext = require("./createInnerContext");
|
14 |
|
15 | const REGEXP_NOT_MODULE = /^\.$|^\.[\\\/]|^\.\.$|^\.\.[\/\\]|^\/|^[A-Z]:[\\\/]/i;
|
16 | const REGEXP_DIRECTORY = /[\/\\]$/i;
|
17 |
|
18 | const memoryFsJoin = require("memory-fs/lib/join");
|
19 | const memoizedJoin = new Map();
|
20 | const memoryFsNormalize = require("memory-fs/lib/normalize");
|
21 |
|
22 | function withName(name, hook) {
|
23 | hook.name = name;
|
24 | return hook;
|
25 | }
|
26 |
|
27 | function toCamelCase(str) {
|
28 | return str.replace(/-([a-z])/g, str => str.substr(1).toUpperCase());
|
29 | }
|
30 |
|
31 | const deprecatedPushToMissing = util.deprecate((set, item) => {
|
32 | set.add(item);
|
33 | }, "The missing array is now a Set. Use add instead of push.");
|
34 |
|
35 | const deprecatedResolveContextInCallback = util.deprecate((x) => {
|
36 | return x;
|
37 | }, "The callback argument was splitted into resolveContext and callback.");
|
38 |
|
39 | const 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 |
|
43 | class 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 |
|
138 | if(typeof callback !== "function") {
|
139 | callback = deprecatedResolveContextInCallback(resolveContext);
|
140 | }
|
141 |
|
142 | const obj = {
|
143 | context: context,
|
144 | path: path,
|
145 | request: request
|
146 | };
|
147 |
|
148 | const message = "resolve '" + request + "' in '" + path + "'";
|
149 |
|
150 |
|
151 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
295 | module.exports = Resolver;
|