Skip to content

Commit 559c26c

Browse files
committed
fix(@ngtools/webpack): rebuild only changed ngfactories
This should improve AOT compilation times.
1 parent a66a74a commit 559c26c

File tree

10 files changed

+289
-68
lines changed

10 files changed

+289
-68
lines changed

packages/@angular/cli/models/webpack-configs/typescript.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,12 @@ export function getAotConfig(wco: WebpackConfigOptions) {
139139
}];
140140
}
141141

142+
const test = AngularCompilerPlugin.isSupported()
143+
? /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/
144+
: /\.ts$/;
145+
142146
return {
143-
module: { rules: [{ test: /\.ts$/, use: [...boLoader, webpackLoader] }] },
147+
module: { rules: [{ test, use: [...boLoader, webpackLoader] }] },
144148
plugins: [ _createAotPlugin(wco, pluginOptions) ]
145149
};
146150
}

packages/@ngtools/webpack/README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,35 @@
33
Webpack plugin that AoT compiles your Angular components and modules.
44

55
## Usage
6-
In your webpack config, add the following plugin and loader:
6+
7+
In your webpack config, add the following plugin and loader.
8+
9+
Angular version 5 and up, use `AngularCompilerPlugin`:
10+
11+
```typescript
12+
import {AotPlugin} from '@ngtools/webpack'
13+
14+
exports = { /* ... */
15+
module: {
16+
rules: [
17+
{
18+
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
19+
loader: '@ngtools/webpack',
20+
sourcemap: true
21+
}
22+
]
23+
},
24+
25+
plugins: [
26+
new AngularCompilerPlugin({
27+
tsConfigPath: 'path/to/tsconfig.json',
28+
entryModule: 'path/to/app.module#AppModule'
29+
})
30+
]
31+
}
32+
```
33+
34+
Angular version 2 and 4, use `AotPlugin`:
735

836
```typescript
937
import {AotPlugin} from '@ngtools/webpack'
@@ -14,6 +42,7 @@ exports = { /* ... */
1442
{
1543
test: /\.ts$/,
1644
loader: '@ngtools/webpack',
45+
sourcemap: true
1746
}
1847
]
1948
},
@@ -27,9 +56,7 @@ exports = { /* ... */
2756
}
2857
```
2958

30-
The loader works with the webpack plugin to compile your TypeScript. It's important to include both, and to not include any other TypeScript compiler loader.
31-
32-
For Angular version 5 and up, import `AngularCompilerPlugin` instead of `AotPlugin`.
59+
The loader works with webpack plugin to compile your TypeScript. It's important to include both, and to not include any other TypeScript compiler loader.
3360

3461
## Options
3562

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import * as path from 'path';
44
import * as ts from 'typescript';
55

66
const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency');
7-
const NodeWatchFileSystem = require('webpack/lib/node/NodeWatchFileSystem');
87
const treeKill = require('tree-kill');
98

109
import { WebpackResourceLoader } from './resource_loader';
1110
import { WebpackCompilerHost } from './compiler_host';
1211
import { Tapable } from './webpack';
1312
import { PathsPlugin } from './paths-plugin';
1413
import { findLazyRoutes, LazyRouteMap } from './lazy_routes';
15-
import { VirtualFileSystemDecorator } from './virtual_file_system_decorator';
14+
import {
15+
VirtualFileSystemDecorator,
16+
VirtualWatchFileSystemDecorator
17+
} from './virtual_file_system_decorator';
1618
import { resolveEntryModuleFromMain } from './entry_resolver';
1719
import {
1820
TransformOperation,
@@ -21,6 +23,7 @@ import {
2123
exportNgFactory,
2224
exportLazyModuleMap,
2325
registerLocaleData,
26+
findResources,
2427
replaceResources,
2528
} from './transformers';
2629
import { time, timeEnd } from './benchmark';
@@ -483,7 +486,7 @@ export class AngularCompilerPlugin implements Tapable {
483486
compiler.plugin('environment', () => {
484487
compiler.inputFileSystem = new VirtualFileSystemDecorator(
485488
compiler.inputFileSystem, this._compilerHost);
486-
compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem);
489+
compiler.watchFileSystem = new VirtualWatchFileSystemDecorator(compiler.inputFileSystem);
487490
});
488491

489492
// Add lazy modules to the context module for @angular/core
@@ -781,15 +784,21 @@ export class AngularCompilerPlugin implements Tapable {
781784
}
782785

783786
getDependencies(fileName: string): string[] {
784-
const sourceFile = this._compilerHost.getSourceFile(fileName, ts.ScriptTarget.Latest);
787+
const resolvedFileName = this._compilerHost.resolve(fileName);
788+
const sourceFile = this._compilerHost.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest);
789+
if (!sourceFile) {
790+
return [];
791+
}
792+
785793
const options = this._compilerOptions;
786794
const host = this._compilerHost;
787795
const cache = this._moduleResolutionCache;
788796

789-
return findAstNodes<ts.ImportDeclaration>(null, sourceFile, ts.SyntaxKind.ImportDeclaration)
797+
const esImports = findAstNodes<ts.ImportDeclaration>(null, sourceFile,
798+
ts.SyntaxKind.ImportDeclaration)
790799
.map(decl => {
791800
const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text;
792-
const resolved = ts.resolveModuleName(moduleName, fileName, options, host, cache);
801+
const resolved = ts.resolveModuleName(moduleName, resolvedFileName, options, host, cache);
793802

794803
if (resolved.resolvedModule) {
795804
return resolved.resolvedModule.resolvedFileName;
@@ -798,6 +807,15 @@ export class AngularCompilerPlugin implements Tapable {
798807
}
799808
})
800809
.filter(x => x);
810+
811+
const resourceImports = findResources(sourceFile)
812+
.map((resourceReplacement) => resourceReplacement.resourcePaths)
813+
.reduce((prev, curr) => prev.concat(curr), [])
814+
.map((resourcePath) => path.resolve(path.dirname(resolvedFileName), resourcePath))
815+
.reduce((prev, curr) =>
816+
prev.concat(...this._resourceLoader.getResourceDependencies(curr)), []);
817+
818+
return [...esImports, ...resourceImports];
801819
}
802820

803821
// This code mostly comes from `performCompilation` in `@angular/compiler-cli`.

packages/@ngtools/webpack/src/compiler_host.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as ts from 'typescript';
2-
import {basename, dirname, join} from 'path';
2+
import {basename, dirname, join, sep} from 'path';
33
import * as fs from 'fs';
44
import {WebpackResourceLoader} from './resource_loader';
55

@@ -96,6 +96,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
9696
private _delegate: ts.CompilerHost;
9797
private _files: {[path: string]: VirtualFileStats | null} = Object.create(null);
9898
private _directories: {[path: string]: VirtualDirStats | null} = Object.create(null);
99+
private _cachedResources: {[path: string]: string | undefined} = Object.create(null);
99100

100101
private _changedFiles: {[path: string]: boolean} = Object.create(null);
101102
private _changedDirs: {[path: string]: boolean} = Object.create(null);
@@ -157,6 +158,11 @@ export class WebpackCompilerHost implements ts.CompilerHost {
157158
return Object.keys(this._changedFiles);
158159
}
159160

161+
getNgFactoryPaths(): string[] {
162+
return Object.keys(this._files)
163+
.filter(fileName => fileName.endsWith('.ngfactory.js') || fileName.endsWith('.ngstyle.js'));
164+
}
165+
160166
invalidate(fileName: string): void {
161167
fileName = this.resolve(fileName);
162168
if (fileName in this._files) {
@@ -284,9 +290,23 @@ export class WebpackCompilerHost implements ts.CompilerHost {
284290

285291
readResource(fileName: string) {
286292
if (this._resourceLoader) {
287-
// We still read it to add it to the compiler host file list.
288-
this.readFile(fileName);
289-
return this._resourceLoader.get(fileName);
293+
const denormalizedFileName = fileName.replace(/\//g, sep);
294+
const resourceDeps = this._resourceLoader.getResourceDependencies(denormalizedFileName);
295+
296+
if (this._cachedResources[fileName] === undefined
297+
|| resourceDeps.some((dep) => this._changedFiles[this.resolve(dep)])) {
298+
return this._resourceLoader.get(denormalizedFileName)
299+
.then((resource) => {
300+
// Add resource dependencies to the compiler host file list.
301+
// This way we can check the changed files list to determine whether to use cache.
302+
this._resourceLoader.getResourceDependencies(denormalizedFileName)
303+
.forEach((dep) => this.readFile(dep));
304+
this._cachedResources[fileName] = resource;
305+
return resource;
306+
});
307+
} else {
308+
return this._cachedResources[fileName];
309+
}
290310
} else {
291311
return this.readFile(fileName);
292312
}

packages/@ngtools/webpack/src/loader.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
588588
const dependencies = plugin.getDependencies(sourceFileName);
589589
dependencies.forEach(dep => this.addDependency(dep.replace(/\//g, path.sep)));
590590

591+
// Also add the original file dependencies to virtual files.
592+
const virtualFilesRe = /\.(?:ngfactory|css\.shim\.ngstyle)\.js$/;
593+
if (virtualFilesRe.test(sourceFileName)) {
594+
const originalFile = sourceFileName.replace(virtualFilesRe, '.ts');
595+
const origDependencies = plugin.getDependencies(originalFile);
596+
origDependencies.forEach(dep => this.addDependency(dep.replace(/\//g, path.sep)));
597+
}
598+
591599
cb(null, result.outputText, result.sourceMap);
592600
})
593601
.catch(err => {

packages/@ngtools/webpack/src/resource_loader.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,15 @@ const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
88

99

1010
interface CompilationOutput {
11-
rendered: boolean;
1211
outputName: string;
1312
source: string;
1413
}
1514

16-
interface CachedCompilation {
17-
outputName: string;
18-
evaluatedSource?: string;
19-
}
20-
2115
export class WebpackResourceLoader {
2216
private _parentCompilation: any;
2317
private _context: string;
2418
private _uniqueId = 0;
25-
private _cache = new Map<string, CachedCompilation>();
19+
private _resourceDependencies = new Map<string, string[]>();
2620

2721
constructor() {}
2822

@@ -32,6 +26,10 @@ export class WebpackResourceLoader {
3226
this._uniqueId = 0;
3327
}
3428

29+
getResourceDependencies(filePath: string) {
30+
return this._resourceDependencies.get(filePath) || [];
31+
}
32+
3533
private _compile(filePath: string): Promise<CompilationOutput> {
3634

3735
if (!this._parentCompilation) {
@@ -98,9 +96,10 @@ export class WebpackResourceLoader {
9896
}
9997
});
10098

99+
// Save the dependencies for this resource.
100+
this._resourceDependencies.set(outputName, childCompilation.fileDependencies);
101+
101102
resolve({
102-
// Boolean showing if this entry was changed since the last compilation.
103-
rendered: entries[0].rendered,
104103
// Output name.
105104
outputName,
106105
// Compiled code.
@@ -113,38 +112,26 @@ export class WebpackResourceLoader {
113112

114113
private _evaluate(output: CompilationOutput): Promise<string> {
115114
try {
115+
const outputName = output.outputName;
116116
const vmContext = vm.createContext(Object.assign({ require: require }, global));
117-
const vmScript = new vm.Script(output.source, { filename: output.outputName });
117+
const vmScript = new vm.Script(output.source, { filename: outputName });
118118

119119
// Evaluate code and cast to string
120120
let evaluatedSource: string;
121121
evaluatedSource = vmScript.runInContext(vmContext);
122122

123123
if (typeof evaluatedSource == 'string') {
124-
this._cache.set(output.outputName, { outputName: output.outputName, evaluatedSource });
125124
return Promise.resolve(evaluatedSource);
126125
}
127126

128-
return Promise.reject('The loader "' + output.outputName + '" didn\'t return a string.');
127+
return Promise.reject('The loader "' + outputName + '" didn\'t return a string.');
129128
} catch (e) {
130129
return Promise.reject(e);
131130
}
132131
}
133132

134133
get(filePath: string): Promise<string> {
135134
return this._compile(filePath)
136-
.then((result: CompilationOutput) => {
137-
if (!result.rendered) {
138-
// Check cache.
139-
const outputName = result.outputName;
140-
const cachedOutput = this._cache.get(outputName);
141-
if (cachedOutput) {
142-
// Return cached evaluatedSource.
143-
return Promise.resolve(cachedOutput.evaluatedSource);
144-
}
145-
}
146-
147-
return this._evaluate(result);
148-
});
135+
.then((result: CompilationOutput) => this._evaluate(result));
149136
}
150137
}

0 commit comments

Comments
 (0)