File size: 4,291 Bytes
f119c28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const forEachBail = require("./forEachBail");
const { getPathsCached } = require("./getPaths");
const { PathType, getType } = require("./util/path");

/** @typedef {import("./Resolver")} Resolver */
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */

module.exports = class SymlinkPlugin {
	/**
	 * @param {string | ResolveStepHook} source source
	 * @param {string | ResolveStepHook} target target
	 */
	constructor(source, target) {
		this.source = source;
		this.target = target;
	}

	/**
	 * @param {Resolver} resolver the resolver
	 * @returns {void}
	 */
	apply(resolver) {
		const target = resolver.ensureHook(this.target);
		const fs = resolver.fileSystem;
		resolver
			.getHook(this.source)
			.tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
				if (request.ignoreSymlinks) return callback();
				const pathsResult = getPathsCached(
					fs,
					/** @type {string} */ (request.path),
				);
				const { paths, segments } = pathsResult;
				// `pathsResult.segments` is shared across callers via the cache.
				// The only place we need to mutate is `pathSegments[idx] = result`
				// when `fs.readlink` succeeds — which is rare (the vast majority
				// of paths contain no symlinks, e.g. every resolve on
				// `cache-predicate`'s no-symlink fixture). Defer the copy until
				// we actually see a symlink so the common no-symlink path stays
				// allocation-free.
				/** @type {string[] | null} */
				let pathSegments = null;

				let containsSymlink = false;
				let idx = -1;
				forEachBail(
					paths,
					/**
					 * @param {string} path path
					 * @param {(err?: null | Error, result?: null | number) => void} callback callback
					 * @returns {void}
					 */
					(path, callback) => {
						idx++;
						if (resolveContext.fileDependencies) {
							resolveContext.fileDependencies.add(path);
						}
						fs.readlink(path, (err, result) => {
							if (!err && result) {
								// First symlink seen — take our own copy now, so
								// the cached `segments` array stays pristine for
								// sibling resolves.
								if (pathSegments === null) {
									pathSegments = [...segments];
								}
								pathSegments[idx] = /** @type {string} */ (result);
								containsSymlink = true;
								// Shortcut when absolute symlink found
								const resultType = getType(result.toString());
								if (
									resultType === PathType.AbsoluteWin ||
									resultType === PathType.AbsolutePosix
								) {
									return callback(null, idx);
								}
							}
							callback();
						});
					},
					/**
					 * @param {null | Error=} err error
					 * @param {null | number=} idx result
					 * @returns {void}
					 */
					(err, idx) => {
						if (!containsSymlink) return callback();
						// `containsSymlink === true` implies we took a copy in
						// `pathSegments` already, so it's non-null. The copy is
						// our own, so `slice` to trim is fine and spreading to
						// "unshare" is no longer necessary.
						const own = /** @type {string[]} */ (pathSegments);
						const resultSegments =
							typeof idx === "number" ? own.slice(0, idx + 1) : own;
						const result = resultSegments.reduceRight((a, b) =>
							resolver.join(a, b),
						);
						/** @type {ResolveRequest} */
						const obj = {
							...request,
							path: result,
						};
						resolver.doResolve(
							target,
							obj,
							`resolved symlink to ${result}`,
							resolveContext,
							(err, innerResult) => {
								if (err) return callback(err);
								// The symlink-resolved (real) path is authoritative. If
								// resolving it produced a result, use it. If it did not —
								// e.g. a `restrictions` rule rejected the real target —
								// stop here with no result instead of letting the next
								// plugin report the original in-root symlink path, which
								// would leave the symlink unresolved and bypass
								// `restrictions`.
								if (innerResult) return callback(null, innerResult);
								return callback(null, null);
							},
						);
					},
				);
			});
	}
};