File size: 4,762 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
134
135
136
137
138
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Ivan Kopeykin @vankop
*/

"use strict";

const forEachBail = require("./forEachBail");

/** @typedef {import("./Resolver")} Resolver */
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
/** @typedef {{ alias: string | string[], extension: string }} ExtensionAliasOption */

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

	/**
	 * @param {Resolver} resolver the resolver
	 * @returns {void}
	 */
	apply(resolver) {
		const target = resolver.ensureHook(this.target);
		const { extension, alias } = this.options;
		resolver
			.getHook(this.source)
			.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
				// Two modes of operation:
				// - "request" mode: original request specifier still carries the
				//   extension (e.g. user wrote `./foo.js`). We swap the extension
				//   on `request.request` and re-resolve.
				// - "path" mode: the specifier has already been joined into an
				//   absolute `request.path` (e.g. produced by the imports field).
				//   We swap the extension on `request.path` and `request.relativePath`.
				const useRequest = request.request !== undefined;
				const source = useRequest
					? /** @type {string} */ (request.request)
					: request.path;
				if (!source || !source.endsWith(extension)) return callback();
				const isAliasString = typeof alias === "string";
				// Hoist the base (everything before the old extension) out of the
				// per-alias `resolve` callback. For an array `alias`, the callback
				// runs once per candidate extension; the base does not change
				// between iterations, so there's no reason to recompute it.
				const sourceBase = source.slice(0, -extension.length);
				const relativePathBase =
					!useRequest &&
					request.relativePath &&
					request.relativePath.endsWith(extension)
						? request.relativePath.slice(0, -extension.length)
						: null;
				/**
				 * @param {string} alias extension alias
				 * @param {(err?: null | Error, result?: null | ResolveRequest) => void} callback callback
				 * @param {number=} index index
				 * @returns {void}
				 */
				const resolve = (alias, callback, index) => {
					const newValue = `${sourceBase}${alias}`;
					const nextRequest = useRequest
						? {
								...request,
								request: newValue,
								fullySpecified: true,
							}
						: {
								...request,
								path: newValue,
								relativePath:
									relativePathBase !== null
										? `${relativePathBase}${alias}`
										: request.relativePath,
								fullySpecified: true,
							};

					return resolver.doResolve(
						target,
						nextRequest,
						`aliased from extension alias with mapping '${extension}' to '${alias}'`,
						resolveContext,
						(err, result) => {
							// Throw error if we are on the last alias (for multiple aliases) and it failed, always throw if we are not an array or we have only one alias
							if (!isAliasString && index) {
								if (index !== this.options.alias.length) {
									if (resolveContext.log) {
										resolveContext.log(
											`Failed to alias from extension alias with mapping '${extension}' to '${alias}' for '${newValue}': ${err}`,
										);
									}

									return callback(null, result);
								}

								return callback(err, result);
							}
							callback(err, result);
						},
					);
				};
				/**
				 * @param {(null | Error)=} err error
				 * @param {(null | ResolveRequest)=} result result
				 * @returns {void}
				 */
				const stoppingCallback = (err, result) => {
					if (err) return callback(err);
					if (result) return callback(null, result);
					// Listing the source extension among its own aliases keeps the
					// original request valid, so it must still be resolvable in its
					// normal (not fully specified) form: as a directory, or with
					// extensions appended. Only a mapping that drops the source
					// extension is strict.
					if (isAliasString ? alias === extension : alias.includes(extension)) {
						return callback();
					}
					// Don't allow other aliasing or raw request
					return callback(null, null);
				};
				if (isAliasString) {
					resolve(alias, stoppingCallback);
				} else if (alias.length > 1) {
					forEachBail(alias, resolve, stoppingCallback);
				} else {
					resolve(alias[0], stoppingCallback);
				}
			});
	}
};