Skip to content

Commit d378f6f

Browse files
committed
refactor(@ngtools/webpack): initial ivy-only Webpack plugin/loader
This change introduces a new Ivy-only Webpack plugin. The plugin is based on the design introduced by `tsc_wrapped` within Bazel’s `rules_typescript` and leverages the newly added ngtsc plugin from within the `@angular/compiler-cli` package. By leveraging the same plugin interface that it used by Bazel, a common interface can be used to access the Angular compiler. This has benefits to both major build systems and dramatically reduces the necessary code infrastructure to integrate the Angular compiler. The plugin also simplifies and reduces the amount of code within the plugin by leveraging newer TypeScript features and capabilities. The need for the virtual filesystem has also been removed. The file replacements capability was the primary driver for the previous need for the virtual filesystem. File replacements are now implemented using a two-pronged approach. The first, for TypeScript, is to hook TypeScript module resolution and adjust the resolved modules based on the configured file replacements. This is similar in behavior to TypeScript path mapping. The second, for Webpack, is the use of the `NormalModuleReplacementPlugin` to facilitate bundling of the configured file replacements. An advantage to this approach is that the build system (both TypeScript and Webpack) are now aware of the replacements and can operate without augmenting multiple aspects of system as was needed previously. The plugin also introduces the use of TypeScript’s builder programs. The current primary benefit is more accurate and simplified dependency discovery. Further, they also provide for the potential future introduction of incremental build support and incremental type checking. NOTE: The deprecated string format for lazy routes is not supported by this plugin. Dynamic imports are recommended for use with Ivy and are required when using the new plugin.
1 parent 621f15a commit d378f6f

File tree

10 files changed

+1137
-2
lines changed

10 files changed

+1137
-2
lines changed

packages/ngtools/webpack/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export const NgToolsLoader = __filename;
1414

1515
// We shouldn't need to export this, but webpack-rollup-loader uses it.
1616
export type { VirtualFileSystemDecorator } from './virtual_file_system_decorator';
17+
18+
export * as ivy from './ivy';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { Diagnostics, formatDiagnostics } from '@angular/compiler-cli';
9+
import { DiagnosticCategory } from 'typescript';
10+
import { addError, addWarning } from '../webpack-diagnostics';
11+
12+
export type DiagnosticsReporter = (diagnostics: Diagnostics) => void;
13+
14+
export function createDiagnosticsReporter(
15+
compilation: import('webpack').compilation.Compilation,
16+
): DiagnosticsReporter {
17+
return (diagnostics) => {
18+
for (const diagnostic of diagnostics) {
19+
const text = formatDiagnostics([diagnostic]);
20+
if (diagnostic.category === DiagnosticCategory.Error) {
21+
addError(compilation, text);
22+
} else {
23+
addWarning(compilation, text);
24+
}
25+
}
26+
};
27+
}
+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { CompilerHost } from '@angular/compiler-cli';
9+
import { createHash } from 'crypto';
10+
import * as path from 'path';
11+
import * as ts from 'typescript';
12+
import { NgccProcessor } from '../ngcc_processor';
13+
import { WebpackResourceLoader } from '../resource_loader';
14+
import { forwardSlashPath } from '../utils';
15+
16+
export function augmentHostWithResources(
17+
host: ts.CompilerHost,
18+
resourceLoader: WebpackResourceLoader,
19+
options: { directTemplateLoading?: boolean } = {},
20+
) {
21+
const resourceHost = host as CompilerHost;
22+
23+
resourceHost.readResource = function (fileName: string) {
24+
const filePath = forwardSlashPath(fileName);
25+
26+
if (
27+
options.directTemplateLoading &&
28+
(filePath.endsWith('.html') || filePath.endsWith('.svg'))
29+
) {
30+
const content = this.readFile(filePath);
31+
if (content === undefined) {
32+
throw new Error('Unable to locate component resource: ' + fileName);
33+
}
34+
35+
resourceLoader.setAffectedResources(filePath, [filePath]);
36+
37+
return content;
38+
} else {
39+
return resourceLoader.get(filePath);
40+
}
41+
};
42+
43+
resourceHost.resourceNameToFileName = function (resourceName: string, containingFile: string) {
44+
return forwardSlashPath(path.join(path.dirname(containingFile), resourceName));
45+
};
46+
47+
resourceHost.getModifiedResourceFiles = function () {
48+
return resourceLoader.getModifiedResourceFiles();
49+
};
50+
}
51+
52+
function augmentResolveModuleNames(
53+
host: ts.CompilerHost,
54+
resolvedModuleModifier: (
55+
resolvedModule: ts.ResolvedModule | undefined,
56+
moduleName: string,
57+
) => ts.ResolvedModule | undefined,
58+
moduleResolutionCache?: ts.ModuleResolutionCache,
59+
): void {
60+
if (host.resolveModuleNames) {
61+
const baseResolveModuleNames = host.resolveModuleNames;
62+
host.resolveModuleNames = function (moduleNames: string[], ...parameters) {
63+
return moduleNames.map((name) => {
64+
const result = baseResolveModuleNames.call(host, [name], ...parameters);
65+
66+
return resolvedModuleModifier(result[0], name);
67+
});
68+
};
69+
} else {
70+
host.resolveModuleNames = function (
71+
moduleNames: string[],
72+
containingFile: string,
73+
_reusedNames: string[] | undefined,
74+
redirectedReference: ts.ResolvedProjectReference | undefined,
75+
options: ts.CompilerOptions,
76+
) {
77+
return moduleNames.map((name) => {
78+
const result = ts.resolveModuleName(
79+
name,
80+
containingFile,
81+
options,
82+
host,
83+
moduleResolutionCache,
84+
redirectedReference,
85+
).resolvedModule;
86+
87+
return resolvedModuleModifier(result, name);
88+
});
89+
};
90+
}
91+
}
92+
93+
export function augmentHostWithNgcc(
94+
host: ts.CompilerHost,
95+
ngcc: NgccProcessor,
96+
moduleResolutionCache?: ts.ModuleResolutionCache,
97+
): void {
98+
augmentResolveModuleNames(
99+
host,
100+
(resolvedModule, moduleName) => {
101+
if (resolvedModule && ngcc) {
102+
ngcc.processModule(moduleName, resolvedModule);
103+
}
104+
105+
return resolvedModule;
106+
},
107+
moduleResolutionCache,
108+
);
109+
110+
if (host.resolveTypeReferenceDirectives) {
111+
const baseResolveTypeReferenceDirectives = host.resolveTypeReferenceDirectives;
112+
host.resolveTypeReferenceDirectives = function (names: string[], ...parameters) {
113+
return names.map((name) => {
114+
const result = baseResolveTypeReferenceDirectives.call(host, [name], ...parameters);
115+
116+
if (result[0] && ngcc) {
117+
ngcc.processModule(name, result[0]);
118+
}
119+
120+
return result[0];
121+
});
122+
};
123+
} else {
124+
host.resolveTypeReferenceDirectives = function (
125+
moduleNames: string[],
126+
containingFile: string,
127+
redirectedReference: ts.ResolvedProjectReference | undefined,
128+
options: ts.CompilerOptions,
129+
) {
130+
return moduleNames.map((name) => {
131+
const result = ts.resolveTypeReferenceDirective(
132+
name,
133+
containingFile,
134+
options,
135+
host,
136+
redirectedReference,
137+
).resolvedTypeReferenceDirective;
138+
139+
if (result && ngcc) {
140+
ngcc.processModule(name, result);
141+
}
142+
143+
return result;
144+
});
145+
};
146+
}
147+
}
148+
149+
export function augmentHostWithReplacements(
150+
host: ts.CompilerHost,
151+
replacements: Record<string, string>,
152+
moduleResolutionCache?: ts.ModuleResolutionCache,
153+
): void {
154+
if (Object.keys(replacements).length === 0) {
155+
return;
156+
}
157+
158+
const tryReplace = (resolvedModule: ts.ResolvedModule | undefined) => {
159+
const replacement = resolvedModule && replacements[resolvedModule.resolvedFileName];
160+
if (replacement) {
161+
return {
162+
resolvedFileName: replacement,
163+
isExternalLibraryImport: /[\/\\]node_modules[\/\\]/.test(replacement),
164+
};
165+
} else {
166+
return resolvedModule;
167+
}
168+
};
169+
170+
augmentResolveModuleNames(host, tryReplace, moduleResolutionCache);
171+
}
172+
173+
export function augmentHostWithSubstitutions(
174+
host: ts.CompilerHost,
175+
substitutions: Record<string, string>,
176+
): void {
177+
const regexSubstitutions: [RegExp, string][] = [];
178+
for (const [key, value] of Object.entries(substitutions)) {
179+
regexSubstitutions.push([new RegExp(`\\b${key}\\b`, 'g'), value]);
180+
}
181+
182+
if (regexSubstitutions.length === 0) {
183+
return;
184+
}
185+
186+
const baseReadFile = host.readFile;
187+
host.readFile = function (...parameters) {
188+
let file: string | undefined = baseReadFile.call(host, ...parameters);
189+
if (file) {
190+
for (const entry of regexSubstitutions) {
191+
file = file.replace(entry[0], entry[1]);
192+
}
193+
}
194+
195+
return file;
196+
};
197+
}
198+
199+
export function augmentHostWithVersioning(host: ts.CompilerHost): void {
200+
const baseGetSourceFile = host.getSourceFile;
201+
host.getSourceFile = function (...parameters) {
202+
const file: (ts.SourceFile & { version?: string }) | undefined = baseGetSourceFile.call(
203+
host,
204+
...parameters,
205+
);
206+
if (file && file.version === undefined) {
207+
file.version = createHash('sha256').update(file.text).digest('hex');
208+
}
209+
210+
return file;
211+
};
212+
}
213+
214+
export function augmentHostWithCaching(
215+
host: ts.CompilerHost,
216+
cache: Map<string, ts.SourceFile>,
217+
): void {
218+
const baseGetSourceFile = host.getSourceFile;
219+
host.getSourceFile = function (
220+
fileName,
221+
languageVersion,
222+
onError,
223+
shouldCreateNewSourceFile,
224+
// tslint:disable-next-line: trailing-comma
225+
...parameters
226+
) {
227+
if (!shouldCreateNewSourceFile && cache.has(fileName)) {
228+
return cache.get(fileName);
229+
}
230+
231+
const file = baseGetSourceFile.call(
232+
host,
233+
fileName,
234+
languageVersion,
235+
onError,
236+
true,
237+
...parameters,
238+
);
239+
240+
if (file) {
241+
cache.set(fileName, file);
242+
}
243+
244+
return file;
245+
};
246+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
export { angularWebpackLoader as default } from './loader';
9+
export { AngularPluginOptions, AngularWebpackPlugin } from './plugin';
10+
11+
export const AngularWebpackLoaderPath = __filename;
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as path from 'path';
9+
import { AngularPluginSymbol, FileEmitter } from './symbol';
10+
11+
export function angularWebpackLoader(
12+
this: import('webpack').loader.LoaderContext,
13+
content: string,
14+
// Source map types are broken in the webpack type definitions
15+
// tslint:disable-next-line: no-any
16+
map: any,
17+
) {
18+
if (this.loaderIndex !== this.loaders.length - 1) {
19+
this.emitWarning('The Angular Webpack loader does not support chaining prior to the loader.');
20+
}
21+
22+
const callback = this.async();
23+
if (!callback) {
24+
throw new Error('Invalid webpack version');
25+
}
26+
27+
const emitFile = this._compilation[AngularPluginSymbol] as FileEmitter;
28+
if (typeof emitFile !== 'function') {
29+
if (this.resourcePath.endsWith('.js')) {
30+
// Passthrough for JS files when no plugin is used
31+
this.callback(undefined, content, map);
32+
33+
return;
34+
}
35+
36+
callback(new Error('The Angular Webpack loader requires the AngularWebpackPlugin.'));
37+
38+
return;
39+
}
40+
41+
emitFile(this.resourcePath)
42+
.then((result) => {
43+
if (!result) {
44+
if (this.resourcePath.endsWith('.js')) {
45+
// Return original content for JS files if not compiled by TypeScript ("allowJs")
46+
this.callback(undefined, content, map);
47+
} else {
48+
// File is not part of the compilation
49+
const message =
50+
`${this.resourcePath} is missing from the TypeScript compilation. ` +
51+
`Please make sure it is in your tsconfig via the 'files' or 'include' property.`;
52+
callback(new Error(message));
53+
}
54+
55+
return;
56+
}
57+
58+
result.dependencies.forEach((dependency) => this.addDependency(dependency));
59+
60+
let resultContent = result.content || '';
61+
let resultMap;
62+
if (result.map) {
63+
resultContent = resultContent.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
64+
resultMap = JSON.parse(result.map);
65+
resultMap.sources = resultMap.sources.map((source: string) =>
66+
path.join(path.dirname(this.resourcePath), source),
67+
);
68+
}
69+
70+
callback(undefined, resultContent, resultMap);
71+
})
72+
.catch((err) => {
73+
callback(err);
74+
});
75+
}
76+
77+
export { angularWebpackLoader as default };

0 commit comments

Comments
 (0)