Skip to content

Commit a867aa4

Browse files
alan-agius4clydin
authored andcommitted
refactor(@ngtools/webpack): simplify resolution flow by using generators
With this change we refactor the paths-plugin resolution flow by using generators which makes the code more readable and easier to follow.
1 parent f4fed58 commit a867aa4

File tree

1 file changed

+138
-142
lines changed

1 file changed

+138
-142
lines changed

packages/ngtools/webpack/src/paths-plugin.ts

+138-142
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88

99
import * as path from 'path';
1010
import { CompilerOptions } from 'typescript';
11-
12-
import type { Configuration } from 'webpack';
11+
import type { Resolver } from 'webpack';
1312

1413
// eslint-disable-next-line @typescript-eslint/no-empty-interface
1514
export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'paths' | 'baseUrl'> {}
1615

17-
// Extract Resolver type from Webpack types since it is not directly exported
18-
type Resolver = Exclude<Exclude<Configuration['resolve'], undefined>['resolver'], undefined>;
16+
// Extract ResolverRequest type from Webpack types since it is not directly exported
17+
type ResolverRequest = NonNullable<Parameters<Parameters<Resolver['resolve']>[4]>[2]>;
1918

20-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21-
type DoResolveValue = any;
19+
interface PathPluginResolverRequest extends ResolverRequest {
20+
context?: {
21+
issuer?: string;
22+
};
23+
typescriptPathMapped?: boolean;
24+
}
2225

2326
interface PathPattern {
2427
starIndex: number;
@@ -106,178 +109,171 @@ export class TypeScriptPathsPlugin {
106109

107110
// To support synchronous resolvers this hook cannot be promise based.
108111
// Webpack supports synchronous resolution with `tap` and `tapAsync` hooks.
109-
resolver.getHook('described-resolve').tapAsync(
110-
'TypeScriptPathsPlugin',
111-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
112-
(request: any, resolveContext, callback) => {
113-
// Preprocessing of the options will ensure that `patterns` is either undefined or has elements to check
114-
if (!this.patterns) {
115-
callback();
116-
117-
return;
118-
}
119-
120-
if (!request || request.typescriptPathMapped) {
121-
callback();
112+
resolver
113+
.getHook('described-resolve')
114+
.tapAsync(
115+
'TypeScriptPathsPlugin',
116+
(request: PathPluginResolverRequest, resolveContext, callback) => {
117+
// Preprocessing of the options will ensure that `patterns` is either undefined or has elements to check
118+
if (!this.patterns) {
119+
callback();
122120

123-
return;
124-
}
121+
return;
122+
}
125123

126-
const originalRequest = request.request || request.path;
127-
if (!originalRequest) {
128-
callback();
124+
if (!request || request.typescriptPathMapped) {
125+
callback();
129126

130-
return;
131-
}
127+
return;
128+
}
132129

133-
// Only work on Javascript/TypeScript issuers.
134-
if (!request.context.issuer || !request.context.issuer.match(/\.[cm]?[jt]sx?$/)) {
135-
callback();
130+
const originalRequest = request.request || request.path;
131+
if (!originalRequest) {
132+
callback();
136133

137-
return;
138-
}
134+
return;
135+
}
139136

140-
switch (originalRequest[0]) {
141-
case '.':
142-
case '/':
143-
// Relative or absolute requests are not mapped
137+
// Only work on Javascript/TypeScript issuers.
138+
if (!request?.context?.issuer?.match(/\.[cm]?[jt]sx?$/)) {
144139
callback();
145140

146141
return;
147-
case '!':
148-
// Ignore all webpack special requests
149-
if (originalRequest.length > 1 && originalRequest[1] === '!') {
142+
}
143+
144+
switch (originalRequest[0]) {
145+
case '.':
146+
case '/':
147+
// Relative or absolute requests are not mapped
150148
callback();
151149

152150
return;
153-
}
154-
break;
155-
}
151+
case '!':
152+
// Ignore all webpack special requests
153+
if (originalRequest.length > 1 && originalRequest[1] === '!') {
154+
callback();
155+
156+
return;
157+
}
158+
break;
159+
}
160+
161+
// A generator is used to limit the amount of replacements requests that need to be created.
162+
// For example, if the first one resolves, any others are not needed and do not need
163+
// to be created.
164+
const requests = this.createReplacementRequests(request, originalRequest);
156165

157-
// A generator is used to limit the amount of replacements that need to be created.
158-
// For example, if the first one resolves, any others are not needed and do not need
159-
// to be created.
160-
const replacements = findReplacements(originalRequest, this.patterns);
161-
const basePath = this.baseUrl ?? '';
166+
const tryResolve = () => {
167+
const next = requests.next();
168+
if (next.done) {
169+
callback();
170+
171+
return;
172+
}
162173

163-
const attemptResolveRequest = (request: DoResolveValue): Promise<DoResolveValue | null> => {
164-
return new Promise((resolve, reject) => {
165174
resolver.doResolve(
166175
target,
167-
request,
176+
next.value,
168177
'',
169178
resolveContext,
170-
(error: Error | null, result: DoResolveValue) => {
179+
(error: Error | null | undefined, result: ResolverRequest | null | undefined) => {
171180
if (error) {
172-
reject(error);
181+
callback(error);
173182
} else if (result) {
174-
resolve(result);
183+
callback(undefined, result);
175184
} else {
176-
resolve(null);
185+
tryResolve();
177186
}
178187
},
179188
);
180-
});
181-
};
182-
183-
const tryNextReplacement = () => {
184-
const next = replacements.next();
185-
if (next.done) {
186-
callback();
187-
188-
return;
189-
}
190-
191-
const targetPath = path.resolve(basePath, next.value);
192-
// If there is no extension. i.e. the target does not refer to an explicit
193-
// file, then this is a candidate for module/package resolution.
194-
const canBeModule = path.extname(targetPath) === '';
195-
196-
// Resolution in the target location, preserving the original request.
197-
// This will work with the `resolve-in-package` resolution hook, supporting
198-
// package exports for e.g. locally-built APF libraries.
199-
const potentialRequestAsPackage = {
200-
...request,
201-
path: targetPath,
202-
typescriptPathMapped: true,
203189
};
204190

205-
// Resolution in the original callee location, but with the updated request
206-
// to point to the mapped target location.
207-
const potentialRequestAsFile = {
208-
...request,
209-
request: targetPath,
210-
typescriptPathMapped: true,
211-
};
212-
213-
let resultPromise = attemptResolveRequest(potentialRequestAsFile);
214-
215-
// If the request can be a module, we configure the resolution to try package/module
216-
// resolution if the file resolution did not have a result.
217-
if (canBeModule) {
218-
resultPromise = resultPromise.then(
219-
(result) => result ?? attemptResolveRequest(potentialRequestAsPackage),
220-
);
221-
}
191+
tryResolve();
192+
},
193+
);
194+
}
222195

223-
// If we have a result, complete. If not, and no error, try the next replacement.
224-
resultPromise
225-
.then((res) => (res === null ? tryNextReplacement() : callback(undefined, res)))
226-
.catch((error) => callback(error));
227-
};
196+
*findReplacements(originalRequest: string): IterableIterator<string> {
197+
if (!this.patterns) {
198+
return;
199+
}
228200

229-
tryNextReplacement();
230-
},
231-
);
232-
}
233-
}
201+
// check if any path mapping rules are relevant
202+
for (const { starIndex, prefix, suffix, potentials } of this.patterns) {
203+
let partial;
234204

235-
function* findReplacements(
236-
originalRequest: string,
237-
patterns: PathPattern[],
238-
): IterableIterator<string> {
239-
// check if any path mapping rules are relevant
240-
for (const { starIndex, prefix, suffix, potentials } of patterns) {
241-
let partial;
242-
243-
if (starIndex === -1) {
244-
// No star means an exact match is required
245-
if (prefix === originalRequest) {
246-
partial = '';
247-
}
248-
} else if (starIndex === 0 && !suffix) {
249-
// Everything matches a single wildcard pattern ("*")
250-
partial = originalRequest;
251-
} else if (!suffix) {
252-
// No suffix means the star is at the end of the pattern
253-
if (originalRequest.startsWith(prefix)) {
254-
partial = originalRequest.slice(prefix.length);
255-
}
256-
} else {
257-
// Star was in the middle of the pattern
258-
if (originalRequest.startsWith(prefix) && originalRequest.endsWith(suffix)) {
259-
partial = originalRequest.substring(prefix.length, originalRequest.length - suffix.length);
205+
if (starIndex === -1) {
206+
// No star means an exact match is required
207+
if (prefix === originalRequest) {
208+
partial = '';
209+
}
210+
} else if (starIndex === 0 && !suffix) {
211+
// Everything matches a single wildcard pattern ("*")
212+
partial = originalRequest;
213+
} else if (!suffix) {
214+
// No suffix means the star is at the end of the pattern
215+
if (originalRequest.startsWith(prefix)) {
216+
partial = originalRequest.slice(prefix.length);
217+
}
218+
} else {
219+
// Star was in the middle of the pattern
220+
if (originalRequest.startsWith(prefix) && originalRequest.endsWith(suffix)) {
221+
partial = originalRequest.substring(
222+
prefix.length,
223+
originalRequest.length - suffix.length,
224+
);
225+
}
260226
}
261-
}
262227

263-
// If request was not matched, move on to the next pattern
264-
if (partial === undefined) {
265-
continue;
266-
}
228+
// If request was not matched, move on to the next pattern
229+
if (partial === undefined) {
230+
continue;
231+
}
267232

268-
// Create the full replacement values based on the original request and the potentials
269-
// for the successfully matched pattern.
270-
for (const { hasStar, prefix, suffix } of potentials) {
271-
let replacement = prefix;
233+
// Create the full replacement values based on the original request and the potentials
234+
// for the successfully matched pattern.
235+
for (const { hasStar, prefix, suffix } of potentials) {
236+
let replacement = prefix;
272237

273-
if (hasStar) {
274-
replacement += partial;
275-
if (suffix) {
276-
replacement += suffix;
238+
if (hasStar) {
239+
replacement += partial;
240+
if (suffix) {
241+
replacement += suffix;
242+
}
277243
}
244+
245+
yield replacement;
278246
}
247+
}
248+
}
279249

280-
yield replacement;
250+
*createReplacementRequests(
251+
request: PathPluginResolverRequest,
252+
originalRequest: string,
253+
): IterableIterator<PathPluginResolverRequest> {
254+
for (const replacement of this.findReplacements(originalRequest)) {
255+
const targetPath = path.resolve(this.baseUrl ?? '', replacement);
256+
// Resolution in the original callee location, but with the updated request
257+
// to point to the mapped target location.
258+
yield {
259+
...request,
260+
request: targetPath,
261+
typescriptPathMapped: true,
262+
};
263+
264+
// If there is no extension. i.e. the target does not refer to an explicit
265+
// file, then this is a candidate for module/package resolution.
266+
const canBeModule = path.extname(targetPath) === '';
267+
if (canBeModule) {
268+
// Resolution in the target location, preserving the original request.
269+
// This will work with the `resolve-in-package` resolution hook, supporting
270+
// package exports for e.g. locally-built APF libraries.
271+
yield {
272+
...request,
273+
path: targetPath,
274+
typescriptPathMapped: true,
275+
};
276+
}
281277
}
282278
}
283279
}

0 commit comments

Comments
 (0)