mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-25 08:41:39 +08:00
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:
parent
f4fed58a05
commit
a867aa4536
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user