Skip to content

Commit

Permalink
fix(@ngtools/webpack): rebuild only changed ngfactories
Browse files Browse the repository at this point in the history
This should improve AOT compilation times.
  • Loading branch information
filipesilva committed Oct 18, 2017
1 parent a66a74a commit 559c26c
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 68 deletions.
6 changes: 5 additions & 1 deletion packages/@angular/cli/models/webpack-configs/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ export function getAotConfig(wco: WebpackConfigOptions) {
}];
}

const test = AngularCompilerPlugin.isSupported()
? /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/
: /\.ts$/;

return {
module: { rules: [{ test: /\.ts$/, use: [...boLoader, webpackLoader] }] },
module: { rules: [{ test, use: [...boLoader, webpackLoader] }] },
plugins: [ _createAotPlugin(wco, pluginOptions) ]
};
}
Expand Down
35 changes: 31 additions & 4 deletions packages/@ngtools/webpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,35 @@
Webpack plugin that AoT compiles your Angular components and modules.

## Usage
In your webpack config, add the following plugin and loader:

In your webpack config, add the following plugin and loader.

Angular version 5 and up, use `AngularCompilerPlugin`:

```typescript
import {AotPlugin} from '@ngtools/webpack'

exports = { /* ... */
module: {
rules: [
{
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
loader: '@ngtools/webpack',
sourcemap: true
}
]
},

plugins: [
new AngularCompilerPlugin({
tsConfigPath: 'path/to/tsconfig.json',
entryModule: 'path/to/app.module#AppModule'
})
]
}
```

Angular version 2 and 4, use `AotPlugin`:

```typescript
import {AotPlugin} from '@ngtools/webpack'
Expand All @@ -14,6 +42,7 @@ exports = { /* ... */
{
test: /\.ts$/,
loader: '@ngtools/webpack',
sourcemap: true
}
]
},
Expand All @@ -27,9 +56,7 @@ exports = { /* ... */
}
```

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.

For Angular version 5 and up, import `AngularCompilerPlugin` instead of `AotPlugin`.
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.

## Options

Expand Down
30 changes: 24 additions & 6 deletions packages/@ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import * as path from 'path';
import * as ts from 'typescript';

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

import { WebpackResourceLoader } from './resource_loader';
import { WebpackCompilerHost } from './compiler_host';
import { Tapable } from './webpack';
import { PathsPlugin } from './paths-plugin';
import { findLazyRoutes, LazyRouteMap } from './lazy_routes';
import { VirtualFileSystemDecorator } from './virtual_file_system_decorator';
import {
VirtualFileSystemDecorator,
VirtualWatchFileSystemDecorator
} from './virtual_file_system_decorator';
import { resolveEntryModuleFromMain } from './entry_resolver';
import {
TransformOperation,
Expand All @@ -21,6 +23,7 @@ import {
exportNgFactory,
exportLazyModuleMap,
registerLocaleData,
findResources,
replaceResources,
} from './transformers';
import { time, timeEnd } from './benchmark';
Expand Down Expand Up @@ -483,7 +486,7 @@ export class AngularCompilerPlugin implements Tapable {
compiler.plugin('environment', () => {
compiler.inputFileSystem = new VirtualFileSystemDecorator(
compiler.inputFileSystem, this._compilerHost);
compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem);
compiler.watchFileSystem = new VirtualWatchFileSystemDecorator(compiler.inputFileSystem);
});

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

getDependencies(fileName: string): string[] {
const sourceFile = this._compilerHost.getSourceFile(fileName, ts.ScriptTarget.Latest);
const resolvedFileName = this._compilerHost.resolve(fileName);
const sourceFile = this._compilerHost.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest);
if (!sourceFile) {
return [];
}

const options = this._compilerOptions;
const host = this._compilerHost;
const cache = this._moduleResolutionCache;

return findAstNodes<ts.ImportDeclaration>(null, sourceFile, ts.SyntaxKind.ImportDeclaration)
const esImports = findAstNodes<ts.ImportDeclaration>(null, sourceFile,
ts.SyntaxKind.ImportDeclaration)
.map(decl => {
const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text;
const resolved = ts.resolveModuleName(moduleName, fileName, options, host, cache);
const resolved = ts.resolveModuleName(moduleName, resolvedFileName, options, host, cache);

if (resolved.resolvedModule) {
return resolved.resolvedModule.resolvedFileName;
Expand All @@ -798,6 +807,15 @@ export class AngularCompilerPlugin implements Tapable {
}
})
.filter(x => x);

const resourceImports = findResources(sourceFile)
.map((resourceReplacement) => resourceReplacement.resourcePaths)
.reduce((prev, curr) => prev.concat(curr), [])
.map((resourcePath) => path.resolve(path.dirname(resolvedFileName), resourcePath))
.reduce((prev, curr) =>
prev.concat(...this._resourceLoader.getResourceDependencies(curr)), []);

return [...esImports, ...resourceImports];
}

// This code mostly comes from `performCompilation` in `@angular/compiler-cli`.
Expand Down
28 changes: 24 additions & 4 deletions packages/@ngtools/webpack/src/compiler_host.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as ts from 'typescript';
import {basename, dirname, join} from 'path';
import {basename, dirname, join, sep} from 'path';
import * as fs from 'fs';
import {WebpackResourceLoader} from './resource_loader';

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

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

getNgFactoryPaths(): string[] {
return Object.keys(this._files)
.filter(fileName => fileName.endsWith('.ngfactory.js') || fileName.endsWith('.ngstyle.js'));
}

invalidate(fileName: string): void {
fileName = this.resolve(fileName);
if (fileName in this._files) {
Expand Down Expand Up @@ -284,9 +290,23 @@ export class WebpackCompilerHost implements ts.CompilerHost {

readResource(fileName: string) {
if (this._resourceLoader) {
// We still read it to add it to the compiler host file list.
this.readFile(fileName);
return this._resourceLoader.get(fileName);
const denormalizedFileName = fileName.replace(/\//g, sep);
const resourceDeps = this._resourceLoader.getResourceDependencies(denormalizedFileName);

if (this._cachedResources[fileName] === undefined
|| resourceDeps.some((dep) => this._changedFiles[this.resolve(dep)])) {
return this._resourceLoader.get(denormalizedFileName)
.then((resource) => {
// Add resource dependencies to the compiler host file list.
// This way we can check the changed files list to determine whether to use cache.
this._resourceLoader.getResourceDependencies(denormalizedFileName)
.forEach((dep) => this.readFile(dep));
this._cachedResources[fileName] = resource;
return resource;
});
} else {
return this._cachedResources[fileName];
}
} else {
return this.readFile(fileName);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/@ngtools/webpack/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
const dependencies = plugin.getDependencies(sourceFileName);
dependencies.forEach(dep => this.addDependency(dep.replace(/\//g, path.sep)));

// Also add the original file dependencies to virtual files.
const virtualFilesRe = /\.(?:ngfactory|css\.shim\.ngstyle)\.js$/;
if (virtualFilesRe.test(sourceFileName)) {
const originalFile = sourceFileName.replace(virtualFilesRe, '.ts');
const origDependencies = plugin.getDependencies(originalFile);
origDependencies.forEach(dep => this.addDependency(dep.replace(/\//g, path.sep)));
}

cb(null, result.outputText, result.sourceMap);
})
.catch(err => {
Expand Down
37 changes: 12 additions & 25 deletions packages/@ngtools/webpack/src/resource_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,15 @@ const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');


interface CompilationOutput {
rendered: boolean;
outputName: string;
source: string;
}

interface CachedCompilation {
outputName: string;
evaluatedSource?: string;
}

export class WebpackResourceLoader {
private _parentCompilation: any;
private _context: string;
private _uniqueId = 0;
private _cache = new Map<string, CachedCompilation>();
private _resourceDependencies = new Map<string, string[]>();

constructor() {}

Expand All @@ -32,6 +26,10 @@ export class WebpackResourceLoader {
this._uniqueId = 0;
}

getResourceDependencies(filePath: string) {
return this._resourceDependencies.get(filePath) || [];
}

private _compile(filePath: string): Promise<CompilationOutput> {

if (!this._parentCompilation) {
Expand Down Expand Up @@ -98,9 +96,10 @@ export class WebpackResourceLoader {
}
});

// Save the dependencies for this resource.
this._resourceDependencies.set(outputName, childCompilation.fileDependencies);

resolve({
// Boolean showing if this entry was changed since the last compilation.
rendered: entries[0].rendered,
// Output name.
outputName,
// Compiled code.
Expand All @@ -113,38 +112,26 @@ export class WebpackResourceLoader {

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

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

if (typeof evaluatedSource == 'string') {
this._cache.set(output.outputName, { outputName: output.outputName, evaluatedSource });
return Promise.resolve(evaluatedSource);
}

return Promise.reject('The loader "' + output.outputName + '" didn\'t return a string.');
return Promise.reject('The loader "' + outputName + '" didn\'t return a string.');
} catch (e) {
return Promise.reject(e);
}
}

get(filePath: string): Promise<string> {
return this._compile(filePath)
.then((result: CompilationOutput) => {
if (!result.rendered) {
// Check cache.
const outputName = result.outputName;
const cachedOutput = this._cache.get(outputName);
if (cachedOutput) {
// Return cached evaluatedSource.
return Promise.resolve(cachedOutput.evaluatedSource);
}
}

return this._evaluate(result);
});
.then((result: CompilationOutput) => this._evaluate(result));
}
}
Loading

0 comments on commit 559c26c

Please sign in to comment.