Skip to content

Commit 12a8244

Browse files
committed
refactor(@ngtools/webpack): use webpack resolver plugin for path mapping
1 parent 9295217 commit 12a8244

File tree

5 files changed

+111
-114
lines changed

5 files changed

+111
-114
lines changed

packages/ngtools/webpack/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@angular-devkit/core": "0.0.0",
29+
"enhanced-resolve": "^4.1.0",
2930
"rxjs": "^6.0.0",
3031
"tree-kill": "^1.0.0",
3132
"webpack-sources": "^1.1.0"

packages/ngtools/webpack/src/angular_compiler_plugin.ts

+9-16
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
formatDiagnostics,
3434
readConfiguration,
3535
} from './ngtools_api';
36-
import { resolveWithPaths } from './paths-plugin';
36+
import { TypeScriptPathsPlugin } from './paths-plugin';
3737
import { WebpackResourceLoader } from './resource_loader';
3838
import {
3939
exportLazyModuleMap,
@@ -716,6 +716,14 @@ export class AngularCompilerPlugin {
716716
});
717717

718718
compiler.hooks.afterResolvers.tap('angular-compiler', compiler => {
719+
// tslint:disable-next-line:no-any
720+
(compiler as any).resolverFactory.hooks.resolver
721+
.for('normal')
722+
// tslint:disable-next-line:no-any
723+
.tap('angular-compiler', (resolver: any) => {
724+
new TypeScriptPathsPlugin(this._compilerOptions).apply(resolver);
725+
});
726+
719727
compiler.hooks.normalModuleFactory.tap('angular-compiler', nmf => {
720728
// Virtual file system.
721729
// TODO: consider if it's better to remove this plugin and instead make it wait on the
@@ -741,21 +749,6 @@ export class AngularCompilerPlugin {
741749
);
742750
});
743751
});
744-
745-
compiler.hooks.normalModuleFactory.tap('angular-compiler', nmf => {
746-
nmf.hooks.beforeResolve.tapAsync(
747-
'angular-compiler',
748-
(request: NormalModuleFactoryRequest, callback: Callback<NormalModuleFactoryRequest>) => {
749-
resolveWithPaths(
750-
request,
751-
callback,
752-
this._compilerOptions,
753-
this._compilerHost,
754-
this._moduleResolutionCache,
755-
);
756-
},
757-
);
758-
});
759752
}
760753

761754
private async _make(compilation: compilation.Compilation) {

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

+97-96
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,97 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import * as path from 'path';
9-
import * as ts from 'typescript';
10-
import {
11-
Callback,
12-
NormalModuleFactoryRequest,
13-
} from './webpack';
14-
15-
16-
export function resolveWithPaths(
17-
request: NormalModuleFactoryRequest,
18-
callback: Callback<NormalModuleFactoryRequest>,
19-
compilerOptions: ts.CompilerOptions,
20-
host: ts.CompilerHost,
21-
cache?: ts.ModuleResolutionCache,
22-
) {
23-
if (!request || !request.request || !compilerOptions.paths) {
24-
callback(null, request);
25-
26-
return;
27-
}
9+
import { CompilerOptions, MapLike } from 'typescript';
10+
import { NormalModuleFactoryRequest } from './webpack';
2811

29-
// Only work on Javascript/TypeScript issuers.
30-
if (!request.contextInfo.issuer || !request.contextInfo.issuer.match(/\.[jt]s$/)) {
31-
callback(null, request);
12+
const getInnerRequest = require('enhanced-resolve/lib/getInnerRequest');
3213

33-
return;
34-
}
14+
export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'paths' | 'baseUrl'> {
3515

36-
const originalRequest = request.request.trim();
16+
}
3717

38-
// Relative requests are not mapped
39-
if (originalRequest.startsWith('.') || originalRequest.startsWith('/')) {
40-
callback(null, request);
18+
export class TypeScriptPathsPlugin {
19+
constructor(private _options: TypeScriptPathsPluginOptions) { }
4120

42-
return;
43-
}
44-
45-
// Amd requests are not mapped
46-
if (originalRequest.startsWith('!!webpack amd')) {
47-
callback(null, request);
21+
// tslint:disable-next-line:no-any
22+
apply(resolver: any) {
23+
if (!this._options.paths || Object.keys(this._options.paths).length === 0) {
24+
return;
25+
}
4826

49-
return;
27+
const target = resolver.ensureHook('resolve');
28+
const resolveAsync = (request: NormalModuleFactoryRequest, requestContext: {}) => {
29+
return new Promise<NormalModuleFactoryRequest | undefined>((resolve, reject) => {
30+
resolver.doResolve(
31+
target,
32+
request,
33+
'',
34+
requestContext,
35+
(error: Error | null, result: NormalModuleFactoryRequest | undefined) => {
36+
if (error) {
37+
reject(error);
38+
} else {
39+
resolve(result);
40+
}
41+
},
42+
);
43+
});
44+
};
45+
46+
resolver.getHook('described-resolve').tapPromise(
47+
'TypeScriptPathsPlugin',
48+
async (request: NormalModuleFactoryRequest, resolveContext: {}) => {
49+
if (!request || request.typescriptPathMapped) {
50+
return;
51+
}
52+
53+
const originalRequest = getInnerRequest(resolver, request);
54+
if (!originalRequest) {
55+
return;
56+
}
57+
58+
// Only work on Javascript/TypeScript issuers.
59+
if (!request.context.issuer || !request.context.issuer.match(/\.[jt]sx?$/)) {
60+
return;
61+
}
62+
63+
// Relative or absolute requests are not mapped
64+
if (originalRequest.startsWith('.') || originalRequest.startsWith('/')) {
65+
return;
66+
}
67+
68+
// Amd requests are not mapped
69+
if (originalRequest.startsWith('!!webpack amd')) {
70+
return;
71+
}
72+
73+
const replacements = findReplacements(originalRequest, this._options.paths || {});
74+
for (const potential of replacements) {
75+
const potentialRequest = {
76+
...request,
77+
request: path.resolve(this._options.baseUrl || '', potential),
78+
typescriptPathMapped: true,
79+
};
80+
const result = await resolveAsync(potentialRequest, resolveContext);
81+
82+
if (result) {
83+
return result;
84+
}
85+
}
86+
},
87+
);
5088
}
89+
}
5190

91+
function findReplacements(
92+
originalRequest: string,
93+
paths: MapLike<string[]>,
94+
): Iterable<string> {
5295
// check if any path mapping rules are relevant
5396
const pathMapOptions = [];
54-
for (const pattern in compilerOptions.paths) {
97+
for (const pattern in paths) {
5598
// get potentials and remove duplicates; JS Set maintains insertion order
56-
const potentials = Array.from(new Set(compilerOptions.paths[pattern]));
99+
const potentials = Array.from(new Set(paths[pattern]));
57100
if (potentials.length === 0) {
58101
// no potential replacements so skip
59102
continue;
@@ -96,9 +139,7 @@ export function resolveWithPaths(
96139
}
97140

98141
if (pathMapOptions.length === 0) {
99-
callback(null, request);
100-
101-
return;
142+
return [];
102143
}
103144

104145
// exact matches take priority then largest prefix match
@@ -112,63 +153,23 @@ export function resolveWithPaths(
112153
}
113154
});
114155

115-
if (pathMapOptions[0].potentials.length === 1) {
116-
const onlyPotential = pathMapOptions[0].potentials[0];
117-
let replacement;
118-
const starIndex = onlyPotential.indexOf('*');
119-
if (starIndex === -1) {
120-
replacement = onlyPotential;
121-
} else if (starIndex === onlyPotential.length - 1) {
122-
replacement = onlyPotential.slice(0, -1) + pathMapOptions[0].partial;
123-
} else {
124-
const [prefix, suffix] = onlyPotential.split('*');
125-
replacement = prefix + pathMapOptions[0].partial + suffix;
126-
}
127-
128-
request.request = path.resolve(compilerOptions.baseUrl || '', replacement);
129-
callback(null, request);
130-
131-
return;
132-
}
133-
134-
// TODO: The following is used when there is more than one potential and will not be
135-
// needed once this is turned into a full webpack resolver plugin
136-
137-
const moduleResolver = ts.resolveModuleName(
138-
originalRequest,
139-
request.contextInfo.issuer,
140-
compilerOptions,
141-
host,
142-
cache,
143-
);
144-
145-
const moduleFilePath = moduleResolver.resolvedModule
146-
&& moduleResolver.resolvedModule.resolvedFileName;
147-
148-
// If there is no result, let webpack try to resolve
149-
if (!moduleFilePath) {
150-
callback(null, request);
151-
152-
return;
153-
}
154-
155-
// If TypeScript gives us a `.d.ts`, it is probably a node module
156-
if (moduleFilePath.endsWith('.d.ts')) {
157-
// If in a package, let webpack resolve the package
158-
const packageRootPath = path.join(path.dirname(moduleFilePath), 'package.json');
159-
if (!host.fileExists(packageRootPath)) {
160-
// Otherwise, if there is a file with a .js extension use that
161-
const jsFilePath = moduleFilePath.slice(0, -5) + '.js';
162-
if (host.fileExists(jsFilePath)) {
163-
request.request = jsFilePath;
156+
const replacements: string[] = [];
157+
pathMapOptions.forEach(option => {
158+
for (const potential of option.potentials) {
159+
let replacement;
160+
const starIndex = potential.indexOf('*');
161+
if (starIndex === -1) {
162+
replacement = potential;
163+
} else if (starIndex === potential.length - 1) {
164+
replacement = potential.slice(0, -1) + option.partial;
165+
} else {
166+
const [prefix, suffix] = potential.split('*');
167+
replacement = prefix + option.partial + suffix;
164168
}
165-
}
166-
167-
callback(null, request);
168169

169-
return;
170-
}
170+
replacements.push(replacement);
171+
}
172+
});
171173

172-
request.request = moduleFilePath;
173-
callback(null, request);
174+
return replacements;
174175
}

packages/ngtools/webpack/src/webpack.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export interface Callback<T = any> {
1616

1717
export interface NormalModuleFactoryRequest {
1818
request: string;
19+
context: { issuer: string };
1920
contextInfo: { issuer: string };
21+
typescriptPathMapped?: boolean;
2022
}
2123

2224
export interface InputFileSystem {

tests/legacy-cli/e2e/tests/misc/module-resolution.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ export default async function () {
7373

7474
await updateJsonFile('tsconfig.json', tsconfig => {
7575
tsconfig.compilerOptions.paths = {
76-
'@firebase/polyfill': ['@firebase/polyfill/index.ts'],
76+
'@firebase/polyfill': ['./node_modules/@firebase/polyfill/index.ts'],
7777
};
7878
});
7979
await expectToFail(() => ng('build'));
8080

8181
await updateJsonFile('tsconfig.json', tsconfig => {
8282
tsconfig.compilerOptions.paths = {
83-
'@firebase/polyfill*': ['@firebase/polyfill/index.ts'],
83+
'@firebase/polyfill*': ['./node_modules/@firebase/polyfill/index.ts'],
8484
};
8585
});
8686
await expectToFail(() => ng('build'));

0 commit comments

Comments
 (0)