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.
This commit is contained in:
Alan Agius 2022-05-12 12:21:55 +00:00 committed by Charles
parent f4fed58a05
commit a867aa4536

View File

@ -8,17 +8,20 @@
import * as path from 'path'; import * as path from 'path';
import { CompilerOptions } from 'typescript'; import { CompilerOptions } from 'typescript';
import type { Resolver } from 'webpack';
import type { Configuration } from 'webpack';
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'paths' | 'baseUrl'> {} export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'paths' | 'baseUrl'> {}
// Extract Resolver type from Webpack types since it is not directly exported // Extract ResolverRequest type from Webpack types since it is not directly exported
type Resolver = Exclude<Exclude<Configuration['resolve'], undefined>['resolver'], undefined>; type ResolverRequest = NonNullable<Parameters<Parameters<Resolver['resolve']>[4]>[2]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any interface PathPluginResolverRequest extends ResolverRequest {
type DoResolveValue = any; context?: {
issuer?: string;
};
typescriptPathMapped?: boolean;
}
interface PathPattern { interface PathPattern {
starIndex: number; starIndex: number;
@ -106,10 +109,11 @@ export class TypeScriptPathsPlugin {
// To support synchronous resolvers this hook cannot be promise based. // To support synchronous resolvers this hook cannot be promise based.
// Webpack supports synchronous resolution with `tap` and `tapAsync` hooks. // Webpack supports synchronous resolution with `tap` and `tapAsync` hooks.
resolver.getHook('described-resolve').tapAsync( resolver
.getHook('described-resolve')
.tapAsync(
'TypeScriptPathsPlugin', 'TypeScriptPathsPlugin',
// eslint-disable-next-line @typescript-eslint/no-explicit-any (request: PathPluginResolverRequest, resolveContext, callback) => {
(request: any, resolveContext, callback) => {
// Preprocessing of the options will ensure that `patterns` is either undefined or has elements to check // Preprocessing of the options will ensure that `patterns` is either undefined or has elements to check
if (!this.patterns) { if (!this.patterns) {
callback(); callback();
@ -131,7 +135,7 @@ export class TypeScriptPathsPlugin {
} }
// Only work on Javascript/TypeScript issuers. // Only work on Javascript/TypeScript issuers.
if (!request.context.issuer || !request.context.issuer.match(/\.[cm]?[jt]sx?$/)) { if (!request?.context?.issuer?.match(/\.[cm]?[jt]sx?$/)) {
callback(); callback();
return; return;
@ -154,90 +158,48 @@ export class TypeScriptPathsPlugin {
break; break;
} }
// A generator is used to limit the amount of replacements that need to be created. // A generator is used to limit the amount of replacements requests that need to be created.
// For example, if the first one resolves, any others are not needed and do not need // For example, if the first one resolves, any others are not needed and do not need
// to be created. // to be created.
const replacements = findReplacements(originalRequest, this.patterns); const requests = this.createReplacementRequests(request, originalRequest);
const basePath = this.baseUrl ?? '';
const attemptResolveRequest = (request: DoResolveValue): Promise<DoResolveValue | null> => { const tryResolve = () => {
return new Promise((resolve, reject) => { const next = requests.next();
resolver.doResolve(
target,
request,
'',
resolveContext,
(error: Error | null, result: DoResolveValue) => {
if (error) {
reject(error);
} else if (result) {
resolve(result);
} else {
resolve(null);
}
},
);
});
};
const tryNextReplacement = () => {
const next = replacements.next();
if (next.done) { if (next.done) {
callback(); callback();
return; return;
} }
const targetPath = path.resolve(basePath, next.value); resolver.doResolve(
// If there is no extension. i.e. the target does not refer to an explicit target,
// file, then this is a candidate for module/package resolution. next.value,
const canBeModule = path.extname(targetPath) === ''; '',
resolveContext,
// Resolution in the target location, preserving the original request. (error: Error | null | undefined, result: ResolverRequest | null | undefined) => {
// This will work with the `resolve-in-package` resolution hook, supporting if (error) {
// package exports for e.g. locally-built APF libraries. callback(error);
const potentialRequestAsPackage = { } else if (result) {
...request, callback(undefined, result);
path: targetPath, } else {
typescriptPathMapped: true, tryResolve();
};
// Resolution in the original callee location, but with the updated request
// to point to the mapped target location.
const potentialRequestAsFile = {
...request,
request: targetPath,
typescriptPathMapped: true,
};
let resultPromise = attemptResolveRequest(potentialRequestAsFile);
// If the request can be a module, we configure the resolution to try package/module
// resolution if the file resolution did not have a result.
if (canBeModule) {
resultPromise = resultPromise.then(
(result) => result ?? attemptResolveRequest(potentialRequestAsPackage),
);
} }
},
// If we have a result, complete. If not, and no error, try the next replacement. );
resultPromise
.then((res) => (res === null ? tryNextReplacement() : callback(undefined, res)))
.catch((error) => callback(error));
}; };
tryNextReplacement(); tryResolve();
}, },
); );
} }
*findReplacements(originalRequest: string): IterableIterator<string> {
if (!this.patterns) {
return;
} }
function* findReplacements(
originalRequest: string,
patterns: PathPattern[],
): IterableIterator<string> {
// check if any path mapping rules are relevant // check if any path mapping rules are relevant
for (const { starIndex, prefix, suffix, potentials } of patterns) { for (const { starIndex, prefix, suffix, potentials } of this.patterns) {
let partial; let partial;
if (starIndex === -1) { if (starIndex === -1) {
@ -256,7 +218,10 @@ function* findReplacements(
} else { } else {
// Star was in the middle of the pattern // Star was in the middle of the pattern
if (originalRequest.startsWith(prefix) && originalRequest.endsWith(suffix)) { if (originalRequest.startsWith(prefix) && originalRequest.endsWith(suffix)) {
partial = originalRequest.substring(prefix.length, originalRequest.length - suffix.length); partial = originalRequest.substring(
prefix.length,
originalRequest.length - suffix.length,
);
} }
} }
@ -281,3 +246,34 @@ function* findReplacements(
} }
} }
} }
*createReplacementRequests(
request: PathPluginResolverRequest,
originalRequest: string,
): IterableIterator<PathPluginResolverRequest> {
for (const replacement of this.findReplacements(originalRequest)) {
const targetPath = path.resolve(this.baseUrl ?? '', replacement);
// Resolution in the original callee location, but with the updated request
// to point to the mapped target location.
yield {
...request,
request: targetPath,
typescriptPathMapped: true,
};
// If there is no extension. i.e. the target does not refer to an explicit
// file, then this is a candidate for module/package resolution.
const canBeModule = path.extname(targetPath) === '';
if (canBeModule) {
// Resolution in the target location, preserving the original request.
// This will work with the `resolve-in-package` resolution hook, supporting
// package exports for e.g. locally-built APF libraries.
yield {
...request,
path: targetPath,
typescriptPathMapped: true,
};
}
}
}
}