1 | /*
|
---|
2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
---|
3 | Author Tobias Koppers @sokra
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | const { AsyncSeriesBailHook, AsyncSeriesHook, SyncHook } = require("tapable");
|
---|
9 | const createInnerContext = require("./createInnerContext");
|
---|
10 | const { parseIdentifier } = require("./util/identifier");
|
---|
11 | const {
|
---|
12 | normalize,
|
---|
13 | cachedJoin: join,
|
---|
14 | getType,
|
---|
15 | PathType
|
---|
16 | } = require("./util/path");
|
---|
17 |
|
---|
18 | /** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
|
---|
19 |
|
---|
20 | /**
|
---|
21 | * @typedef {Object} FileSystemStats
|
---|
22 | * @property {function(): boolean} isDirectory
|
---|
23 | * @property {function(): boolean} isFile
|
---|
24 | */
|
---|
25 |
|
---|
26 | /**
|
---|
27 | * @typedef {Object} FileSystemDirent
|
---|
28 | * @property {Buffer | string} name
|
---|
29 | * @property {function(): boolean} isDirectory
|
---|
30 | * @property {function(): boolean} isFile
|
---|
31 | */
|
---|
32 |
|
---|
33 | /**
|
---|
34 | * @typedef {Object} PossibleFileSystemError
|
---|
35 | * @property {string=} code
|
---|
36 | * @property {number=} errno
|
---|
37 | * @property {string=} path
|
---|
38 | * @property {string=} syscall
|
---|
39 | */
|
---|
40 |
|
---|
41 | /**
|
---|
42 | * @template T
|
---|
43 | * @callback FileSystemCallback
|
---|
44 | * @param {PossibleFileSystemError & Error | null | undefined} err
|
---|
45 | * @param {T=} result
|
---|
46 | */
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * @typedef {Object} FileSystem
|
---|
50 | * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readFile
|
---|
51 | * @property {(function(string, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void) & function(string, object, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void} readdir
|
---|
52 | * @property {((function(string, FileSystemCallback<object>): void) & function(string, object, FileSystemCallback<object>): void)=} readJson
|
---|
53 | * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readlink
|
---|
54 | * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void=} lstat
|
---|
55 | * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} stat
|
---|
56 | */
|
---|
57 |
|
---|
58 | /**
|
---|
59 | * @typedef {Object} SyncFileSystem
|
---|
60 | * @property {function(string, object=): Buffer | string} readFileSync
|
---|
61 | * @property {function(string, object=): (Buffer | string)[] | FileSystemDirent[]} readdirSync
|
---|
62 | * @property {(function(string, object=): object)=} readJsonSync
|
---|
63 | * @property {function(string, object=): Buffer | string} readlinkSync
|
---|
64 | * @property {function(string, object=): FileSystemStats=} lstatSync
|
---|
65 | * @property {function(string, object=): FileSystemStats} statSync
|
---|
66 | */
|
---|
67 |
|
---|
68 | /**
|
---|
69 | * @typedef {Object} ParsedIdentifier
|
---|
70 | * @property {string} request
|
---|
71 | * @property {string} query
|
---|
72 | * @property {string} fragment
|
---|
73 | * @property {boolean} directory
|
---|
74 | * @property {boolean} module
|
---|
75 | * @property {boolean} file
|
---|
76 | * @property {boolean} internal
|
---|
77 | */
|
---|
78 |
|
---|
79 | /**
|
---|
80 | * @typedef {Object} BaseResolveRequest
|
---|
81 | * @property {string | false} path
|
---|
82 | * @property {string=} descriptionFilePath
|
---|
83 | * @property {string=} descriptionFileRoot
|
---|
84 | * @property {object=} descriptionFileData
|
---|
85 | * @property {string=} relativePath
|
---|
86 | * @property {boolean=} ignoreSymlinks
|
---|
87 | * @property {boolean=} fullySpecified
|
---|
88 | */
|
---|
89 |
|
---|
90 | /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
|
---|
91 |
|
---|
92 | /**
|
---|
93 | * String with special formatting
|
---|
94 | * @typedef {string} StackEntry
|
---|
95 | */
|
---|
96 |
|
---|
97 | /** @template T @typedef {{ add: (T) => void }} WriteOnlySet */
|
---|
98 |
|
---|
99 | /**
|
---|
100 | * Resolve context
|
---|
101 | * @typedef {Object} ResolveContext
|
---|
102 | * @property {WriteOnlySet<string>=} contextDependencies
|
---|
103 | * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
|
---|
104 | * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
|
---|
105 | * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
|
---|
106 | * @property {(function(string): void)=} log log function
|
---|
107 | */
|
---|
108 |
|
---|
109 | /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
|
---|
110 |
|
---|
111 | /**
|
---|
112 | * @param {string} str input string
|
---|
113 | * @returns {string} in camel case
|
---|
114 | */
|
---|
115 | function toCamelCase(str) {
|
---|
116 | return str.replace(/-([a-z])/g, str => str.substr(1).toUpperCase());
|
---|
117 | }
|
---|
118 |
|
---|
119 | class Resolver {
|
---|
120 | /**
|
---|
121 | * @param {ResolveStepHook} hook hook
|
---|
122 | * @param {ResolveRequest} request request
|
---|
123 | * @returns {StackEntry} stack entry
|
---|
124 | */
|
---|
125 | static createStackEntry(hook, request) {
|
---|
126 | return (
|
---|
127 | hook.name +
|
---|
128 | ": (" +
|
---|
129 | request.path +
|
---|
130 | ") " +
|
---|
131 | (request.request || "") +
|
---|
132 | (request.query || "") +
|
---|
133 | (request.fragment || "") +
|
---|
134 | (request.directory ? " directory" : "") +
|
---|
135 | (request.module ? " module" : "")
|
---|
136 | );
|
---|
137 | }
|
---|
138 |
|
---|
139 | /**
|
---|
140 | * @param {FileSystem} fileSystem a filesystem
|
---|
141 | * @param {ResolveOptions} options options
|
---|
142 | */
|
---|
143 | constructor(fileSystem, options) {
|
---|
144 | this.fileSystem = fileSystem;
|
---|
145 | this.options = options;
|
---|
146 | this.hooks = {
|
---|
147 | /** @type {SyncHook<[ResolveStepHook, ResolveRequest], void>} */
|
---|
148 | resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
|
---|
149 | /** @type {SyncHook<[ResolveRequest, Error]>} */
|
---|
150 | noResolve: new SyncHook(["request", "error"], "noResolve"),
|
---|
151 | /** @type {ResolveStepHook} */
|
---|
152 | resolve: new AsyncSeriesBailHook(
|
---|
153 | ["request", "resolveContext"],
|
---|
154 | "resolve"
|
---|
155 | ),
|
---|
156 | /** @type {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} */
|
---|
157 | result: new AsyncSeriesHook(["result", "resolveContext"], "result")
|
---|
158 | };
|
---|
159 | }
|
---|
160 |
|
---|
161 | /**
|
---|
162 | * @param {string | ResolveStepHook} name hook name or hook itself
|
---|
163 | * @returns {ResolveStepHook} the hook
|
---|
164 | */
|
---|
165 | ensureHook(name) {
|
---|
166 | if (typeof name !== "string") {
|
---|
167 | return name;
|
---|
168 | }
|
---|
169 | name = toCamelCase(name);
|
---|
170 | if (/^before/.test(name)) {
|
---|
171 | return /** @type {ResolveStepHook} */ (this.ensureHook(
|
---|
172 | name[6].toLowerCase() + name.substr(7)
|
---|
173 | ).withOptions({
|
---|
174 | stage: -10
|
---|
175 | }));
|
---|
176 | }
|
---|
177 | if (/^after/.test(name)) {
|
---|
178 | return /** @type {ResolveStepHook} */ (this.ensureHook(
|
---|
179 | name[5].toLowerCase() + name.substr(6)
|
---|
180 | ).withOptions({
|
---|
181 | stage: 10
|
---|
182 | }));
|
---|
183 | }
|
---|
184 | const hook = this.hooks[name];
|
---|
185 | if (!hook) {
|
---|
186 | return (this.hooks[name] = new AsyncSeriesBailHook(
|
---|
187 | ["request", "resolveContext"],
|
---|
188 | name
|
---|
189 | ));
|
---|
190 | }
|
---|
191 | return hook;
|
---|
192 | }
|
---|
193 |
|
---|
194 | /**
|
---|
195 | * @param {string | ResolveStepHook} name hook name or hook itself
|
---|
196 | * @returns {ResolveStepHook} the hook
|
---|
197 | */
|
---|
198 | getHook(name) {
|
---|
199 | if (typeof name !== "string") {
|
---|
200 | return name;
|
---|
201 | }
|
---|
202 | name = toCamelCase(name);
|
---|
203 | if (/^before/.test(name)) {
|
---|
204 | return /** @type {ResolveStepHook} */ (this.getHook(
|
---|
205 | name[6].toLowerCase() + name.substr(7)
|
---|
206 | ).withOptions({
|
---|
207 | stage: -10
|
---|
208 | }));
|
---|
209 | }
|
---|
210 | if (/^after/.test(name)) {
|
---|
211 | return /** @type {ResolveStepHook} */ (this.getHook(
|
---|
212 | name[5].toLowerCase() + name.substr(6)
|
---|
213 | ).withOptions({
|
---|
214 | stage: 10
|
---|
215 | }));
|
---|
216 | }
|
---|
217 | const hook = this.hooks[name];
|
---|
218 | if (!hook) {
|
---|
219 | throw new Error(`Hook ${name} doesn't exist`);
|
---|
220 | }
|
---|
221 | return hook;
|
---|
222 | }
|
---|
223 |
|
---|
224 | /**
|
---|
225 | * @param {object} context context information object
|
---|
226 | * @param {string} path context path
|
---|
227 | * @param {string} request request string
|
---|
228 | * @returns {string | false} result
|
---|
229 | */
|
---|
230 | resolveSync(context, path, request) {
|
---|
231 | /** @type {Error | null | undefined} */
|
---|
232 | let err = undefined;
|
---|
233 | /** @type {string | false | undefined} */
|
---|
234 | let result = undefined;
|
---|
235 | let sync = false;
|
---|
236 | this.resolve(context, path, request, {}, (e, r) => {
|
---|
237 | err = e;
|
---|
238 | result = r;
|
---|
239 | sync = true;
|
---|
240 | });
|
---|
241 | if (!sync) {
|
---|
242 | throw new Error(
|
---|
243 | "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!"
|
---|
244 | );
|
---|
245 | }
|
---|
246 | if (err) throw err;
|
---|
247 | if (result === undefined) throw new Error("No result");
|
---|
248 | return result;
|
---|
249 | }
|
---|
250 |
|
---|
251 | /**
|
---|
252 | * @param {object} context context information object
|
---|
253 | * @param {string} path context path
|
---|
254 | * @param {string} request request string
|
---|
255 | * @param {ResolveContext} resolveContext resolve context
|
---|
256 | * @param {function(Error | null, (string|false)=, ResolveRequest=): void} callback callback function
|
---|
257 | * @returns {void}
|
---|
258 | */
|
---|
259 | resolve(context, path, request, resolveContext, callback) {
|
---|
260 | if (!context || typeof context !== "object")
|
---|
261 | return callback(new Error("context argument is not an object"));
|
---|
262 | if (typeof path !== "string")
|
---|
263 | return callback(new Error("path argument is not a string"));
|
---|
264 | if (typeof request !== "string")
|
---|
265 | return callback(new Error("path argument is not a string"));
|
---|
266 | if (!resolveContext)
|
---|
267 | return callback(new Error("resolveContext argument is not set"));
|
---|
268 |
|
---|
269 | const obj = {
|
---|
270 | context: context,
|
---|
271 | path: path,
|
---|
272 | request: request
|
---|
273 | };
|
---|
274 |
|
---|
275 | const message = `resolve '${request}' in '${path}'`;
|
---|
276 |
|
---|
277 | const finishResolved = result => {
|
---|
278 | return callback(
|
---|
279 | null,
|
---|
280 | result.path === false
|
---|
281 | ? false
|
---|
282 | : `${result.path.replace(/#/g, "\0#")}${
|
---|
283 | result.query ? result.query.replace(/#/g, "\0#") : ""
|
---|
284 | }${result.fragment || ""}`,
|
---|
285 | result
|
---|
286 | );
|
---|
287 | };
|
---|
288 |
|
---|
289 | const finishWithoutResolve = log => {
|
---|
290 | /**
|
---|
291 | * @type {Error & {details?: string}}
|
---|
292 | */
|
---|
293 | const error = new Error("Can't " + message);
|
---|
294 | error.details = log.join("\n");
|
---|
295 | this.hooks.noResolve.call(obj, error);
|
---|
296 | return callback(error);
|
---|
297 | };
|
---|
298 |
|
---|
299 | if (resolveContext.log) {
|
---|
300 | // We need log anyway to capture it in case of an error
|
---|
301 | const parentLog = resolveContext.log;
|
---|
302 | const log = [];
|
---|
303 | return this.doResolve(
|
---|
304 | this.hooks.resolve,
|
---|
305 | obj,
|
---|
306 | message,
|
---|
307 | {
|
---|
308 | log: msg => {
|
---|
309 | parentLog(msg);
|
---|
310 | log.push(msg);
|
---|
311 | },
|
---|
312 | fileDependencies: resolveContext.fileDependencies,
|
---|
313 | contextDependencies: resolveContext.contextDependencies,
|
---|
314 | missingDependencies: resolveContext.missingDependencies,
|
---|
315 | stack: resolveContext.stack
|
---|
316 | },
|
---|
317 | (err, result) => {
|
---|
318 | if (err) return callback(err);
|
---|
319 |
|
---|
320 | if (result) return finishResolved(result);
|
---|
321 |
|
---|
322 | return finishWithoutResolve(log);
|
---|
323 | }
|
---|
324 | );
|
---|
325 | } else {
|
---|
326 | // Try to resolve assuming there is no error
|
---|
327 | // We don't log stuff in this case
|
---|
328 | return this.doResolve(
|
---|
329 | this.hooks.resolve,
|
---|
330 | obj,
|
---|
331 | message,
|
---|
332 | {
|
---|
333 | log: undefined,
|
---|
334 | fileDependencies: resolveContext.fileDependencies,
|
---|
335 | contextDependencies: resolveContext.contextDependencies,
|
---|
336 | missingDependencies: resolveContext.missingDependencies,
|
---|
337 | stack: resolveContext.stack
|
---|
338 | },
|
---|
339 | (err, result) => {
|
---|
340 | if (err) return callback(err);
|
---|
341 |
|
---|
342 | if (result) return finishResolved(result);
|
---|
343 |
|
---|
344 | // log is missing for the error details
|
---|
345 | // so we redo the resolving for the log info
|
---|
346 | // this is more expensive to the success case
|
---|
347 | // is assumed by default
|
---|
348 |
|
---|
349 | const log = [];
|
---|
350 |
|
---|
351 | return this.doResolve(
|
---|
352 | this.hooks.resolve,
|
---|
353 | obj,
|
---|
354 | message,
|
---|
355 | {
|
---|
356 | log: msg => log.push(msg),
|
---|
357 | stack: resolveContext.stack
|
---|
358 | },
|
---|
359 | (err, result) => {
|
---|
360 | if (err) return callback(err);
|
---|
361 |
|
---|
362 | return finishWithoutResolve(log);
|
---|
363 | }
|
---|
364 | );
|
---|
365 | }
|
---|
366 | );
|
---|
367 | }
|
---|
368 | }
|
---|
369 |
|
---|
370 | doResolve(hook, request, message, resolveContext, callback) {
|
---|
371 | const stackEntry = Resolver.createStackEntry(hook, request);
|
---|
372 |
|
---|
373 | let newStack;
|
---|
374 | if (resolveContext.stack) {
|
---|
375 | newStack = new Set(resolveContext.stack);
|
---|
376 | if (resolveContext.stack.has(stackEntry)) {
|
---|
377 | /**
|
---|
378 | * Prevent recursion
|
---|
379 | * @type {Error & {recursion?: boolean}}
|
---|
380 | */
|
---|
381 | const recursionError = new Error(
|
---|
382 | "Recursion in resolving\nStack:\n " +
|
---|
383 | Array.from(newStack).join("\n ")
|
---|
384 | );
|
---|
385 | recursionError.recursion = true;
|
---|
386 | if (resolveContext.log)
|
---|
387 | resolveContext.log("abort resolving because of recursion");
|
---|
388 | return callback(recursionError);
|
---|
389 | }
|
---|
390 | newStack.add(stackEntry);
|
---|
391 | } else {
|
---|
392 | newStack = new Set([stackEntry]);
|
---|
393 | }
|
---|
394 | this.hooks.resolveStep.call(hook, request);
|
---|
395 |
|
---|
396 | if (hook.isUsed()) {
|
---|
397 | const innerContext = createInnerContext(
|
---|
398 | {
|
---|
399 | log: resolveContext.log,
|
---|
400 | fileDependencies: resolveContext.fileDependencies,
|
---|
401 | contextDependencies: resolveContext.contextDependencies,
|
---|
402 | missingDependencies: resolveContext.missingDependencies,
|
---|
403 | stack: newStack
|
---|
404 | },
|
---|
405 | message
|
---|
406 | );
|
---|
407 | return hook.callAsync(request, innerContext, (err, result) => {
|
---|
408 | if (err) return callback(err);
|
---|
409 | if (result) return callback(null, result);
|
---|
410 | callback();
|
---|
411 | });
|
---|
412 | } else {
|
---|
413 | callback();
|
---|
414 | }
|
---|
415 | }
|
---|
416 |
|
---|
417 | /**
|
---|
418 | * @param {string} identifier identifier
|
---|
419 | * @returns {ParsedIdentifier} parsed identifier
|
---|
420 | */
|
---|
421 | parse(identifier) {
|
---|
422 | const part = {
|
---|
423 | request: "",
|
---|
424 | query: "",
|
---|
425 | fragment: "",
|
---|
426 | module: false,
|
---|
427 | directory: false,
|
---|
428 | file: false,
|
---|
429 | internal: false
|
---|
430 | };
|
---|
431 |
|
---|
432 | const parsedIdentifier = parseIdentifier(identifier);
|
---|
433 |
|
---|
434 | if (!parsedIdentifier) return part;
|
---|
435 |
|
---|
436 | [part.request, part.query, part.fragment] = parsedIdentifier;
|
---|
437 |
|
---|
438 | if (part.request.length > 0) {
|
---|
439 | part.internal = this.isPrivate(identifier);
|
---|
440 | part.module = this.isModule(part.request);
|
---|
441 | part.directory = this.isDirectory(part.request);
|
---|
442 | if (part.directory) {
|
---|
443 | part.request = part.request.substr(0, part.request.length - 1);
|
---|
444 | }
|
---|
445 | }
|
---|
446 |
|
---|
447 | return part;
|
---|
448 | }
|
---|
449 |
|
---|
450 | isModule(path) {
|
---|
451 | return getType(path) === PathType.Normal;
|
---|
452 | }
|
---|
453 |
|
---|
454 | isPrivate(path) {
|
---|
455 | return getType(path) === PathType.Internal;
|
---|
456 | }
|
---|
457 |
|
---|
458 | /**
|
---|
459 | * @param {string} path a path
|
---|
460 | * @returns {boolean} true, if the path is a directory path
|
---|
461 | */
|
---|
462 | isDirectory(path) {
|
---|
463 | return path.endsWith("/");
|
---|
464 | }
|
---|
465 |
|
---|
466 | join(path, request) {
|
---|
467 | return join(path, request);
|
---|
468 | }
|
---|
469 |
|
---|
470 | normalize(path) {
|
---|
471 | return normalize(path);
|
---|
472 | }
|
---|
473 | }
|
---|
474 |
|
---|
475 | module.exports = Resolver;
|
---|