Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 654f162

Browse files
committedOct 5, 2016
feat(aot): adding README and type checking.
1 parent b5771df commit 654f162

File tree

6 files changed

+240
-123
lines changed

6 files changed

+240
-123
lines changed
 

‎packages/angular-cli/models/webpack-build-typescript.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'path';
22
import * as webpack from 'webpack';
33
import {findLazyModules} from './find-lazy-modules';
4-
import {NgcWebpackPlugin} from '@ngtools/webpack';
4+
import {AotPlugin} from '@ngtools/webpack';
55

66
const atl = require('awesome-typescript-loader');
77

@@ -59,11 +59,9 @@ export const getWebpackAotConfigPartial = function(projectRoot: string, appConfi
5959
]
6060
},
6161
plugins: [
62-
new NgcWebpackPlugin({
63-
project: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
64-
baseDir: path.resolve(projectRoot, appConfig.root),
65-
main: path.join(projectRoot, appConfig.root, appConfig.main),
66-
genDir: path.resolve(projectRoot, appConfig.root)
62+
new AotPlugin({
63+
tsConfigPath: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig),
64+
mainPath: path.join(projectRoot, appConfig.root, appConfig.main)
6765
}),
6866
]
6967
};

‎packages/webpack/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Angular Ahead-of-Time Webpack Plugin
2+
3+
Webpack plugin that compiles templates and styles ahead of time.
4+
5+
## Usage
6+
In your webpack config, add the following plugin and loader:
7+
8+
```typescript
9+
import {AotPlugin} from '@ngtools/webpack'
10+
11+
exports = { /* ... */
12+
module: {
13+
rules: [
14+
{
15+
test: /\.ts$/,
16+
loader: '@ngtools/webpack',
17+
}
18+
]
19+
},
20+
21+
plugins: [
22+
new AotPlugin({
23+
tsConfigPath: 'path/to/tsconfig.json',
24+
entryModule: 'path/to/app.module#AppModule'
25+
})
26+
]
27+
}
28+
```
29+
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+
## Options
33+
34+
* `tsConfigPath`. The path to the `tsconfig.json` file. This is required. In your `tsconfig.json`, you can pass options to the Angular Compiler with `angularCompilerOptions`.
35+
* `basePath`. Optional. The root to use by the compiler to resolve file paths. By default, use the `tsConfigPath` root.
36+
* `entryModule`. Optional if specified in `angularCompilerOptions`. The path and classname of the main application module. This follows the format `path/to/file#ClassName`.
37+
* `mainPath`. Optional if `entryModule` is specified. The `main.ts` file containing the bootstrap code. The plugin will use AST to determine the `entryModule`.
38+
* `genDir`. Optional. The output directory of the offline compiler. The files created by the offline compiler will be in a virtual file system, but the import paths might change. This can also be specified in `angularCompilerOptions`, and by default will be the same as `basePath`.
39+
* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack.

‎packages/webpack/src/loader.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path';
22
import * as ts from 'typescript';
3-
import {NgcWebpackPlugin} from './plugin';
3+
import {AotPlugin} from './plugin';
44
import {MultiChange, ReplaceChange, insertImport} from '@angular-cli/ast-tools';
55

66
// TODO: move all this to ast-tools.
@@ -31,7 +31,7 @@ function _removeDecorators(fileName: string, source: string): string {
3131

3232
function _replaceBootstrap(fileName: string,
3333
source: string,
34-
plugin: NgcWebpackPlugin): Promise<string> {
34+
plugin: AotPlugin): Promise<string> {
3535
// If bootstrapModule can't be found, bail out early.
3636
if (!source.match(/\bbootstrapModule\b/)) {
3737
return Promise.resolve(source);
@@ -40,11 +40,11 @@ function _replaceBootstrap(fileName: string,
4040
let changes = new MultiChange();
4141

4242
// Calculate the base path.
43-
const basePath = path.normalize(plugin.angularCompilerOptions.basePath);
43+
const basePath = path.normalize(plugin.basePath);
4444
const genDir = path.normalize(plugin.genDir);
4545
const dirName = path.normalize(path.dirname(fileName));
46-
const [entryModulePath, entryModuleName] = plugin.entryModule.split('#');
47-
const entryModuleFileName = path.normalize(entryModulePath + '.ngfactory');
46+
const entryModule = plugin.entryModule;
47+
const entryModuleFileName = path.normalize(entryModule.path + '.ngfactory');
4848
const relativeEntryModulePath = path.relative(basePath, entryModuleFileName);
4949
const fullEntryModulePath = path.resolve(genDir, relativeEntryModulePath);
5050
const relativeNgFactoryPath = path.relative(dirName, fullEntryModulePath);
@@ -82,7 +82,7 @@ function _replaceBootstrap(fileName: string,
8282
.filter(call => bootstraps.some(bs => bs == call.expression))
8383
.forEach((call: ts.CallExpression) => {
8484
changes.appendChange(new ReplaceChange(fileName, call.arguments[0].getStart(sourceFile),
85-
entryModuleName, entryModuleName + 'NgFactory'));
85+
entryModule.className, entryModule.className + 'NgFactory'));
8686
});
8787

8888
calls
@@ -98,7 +98,7 @@ function _replaceBootstrap(fileName: string,
9898
'bootstrapModule', 'bootstrapModuleFactory'));
9999
});
100100
changes.appendChange(insertImport(fileName, 'platformBrowser', '@angular/platform-browser'));
101-
changes.appendChange(insertImport(fileName, entryModuleName + 'NgFactory', ngFactoryPath));
101+
changes.appendChange(insertImport(fileName, entryModule.className + 'NgFactory', ngFactoryPath));
102102

103103
let sourceText = source;
104104
return changes.apply({
@@ -107,35 +107,52 @@ function _replaceBootstrap(fileName: string,
107107
}).then(() => sourceText);
108108
}
109109

110+
function _transpile(plugin: AotPlugin, filePath: string, sourceText: string) {
111+
const program = plugin.program;
112+
if (plugin.typeCheck) {
113+
const sourceFile = program.getSourceFile(filePath);
114+
const diagnostics = program.getSyntacticDiagnostics(sourceFile)
115+
.concat(program.getSemanticDiagnostics(sourceFile))
116+
.concat(program.getDeclarationDiagnostics(sourceFile));
117+
118+
if (diagnostics.length > 0) {
119+
const message = diagnostics
120+
.map(diagnostic => {
121+
const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
122+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
123+
return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`;
124+
})
125+
.join('\n');
126+
throw new Error(message);
127+
}
128+
}
129+
130+
const result = ts.transpileModule(sourceText, {
131+
compilerOptions: plugin.compilerOptions,
132+
fileName: filePath
133+
});
134+
135+
return {
136+
outputText: result.outputText,
137+
sourceMap: JSON.parse(result.sourceMapText)
138+
};
139+
}
110140

111141
// Super simple TS transpiler loader for testing / isolated usage. does not type check!
112142
export function ngcLoader(source: string) {
113143
this.cacheable();
114144

115-
const plugin = this._compilation._ngToolsWebpackPluginInstance as NgcWebpackPlugin;
116-
if (plugin && plugin instanceof NgcWebpackPlugin) {
145+
const plugin = this._compilation._ngToolsWebpackPluginInstance as AotPlugin;
146+
// We must verify that AotPlugin is an instance of the right class.
147+
if (plugin && plugin instanceof AotPlugin) {
117148
const cb: any = this.async();
118149

119150
plugin.done
120151
.then(() => _removeDecorators(this.resource, source))
121152
.then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin))
122153
.then(sourceText => {
123-
const result = ts.transpileModule(sourceText, {
124-
compilerOptions: {
125-
target: ts.ScriptTarget.ES5,
126-
module: ts.ModuleKind.ES2015,
127-
}
128-
});
129-
130-
if (result.diagnostics && result.diagnostics.length) {
131-
let message = '';
132-
result.diagnostics.forEach(d => {
133-
message += d.messageText + '\n';
134-
});
135-
cb(new Error(message));
136-
}
137-
138-
cb(null, result.outputText, result.sourceMapText ? JSON.parse(result.sourceMapText) : null);
154+
const result = _transpile(plugin, this.resource, sourceText);
155+
cb(null, result.outputText, result.sourceMap);
139156
})
140157
.catch(err => cb(err));
141158
} else {

‎packages/webpack/src/plugin.ts

Lines changed: 151 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import * as ts from 'typescript';
1+
import * as fs from 'fs';
22
import * as path from 'path';
3+
import * as ts from 'typescript';
34

45
import {NgModule} from '@angular/core';
56
import * as ngCompiler from '@angular/compiler-cli';
@@ -8,81 +9,129 @@ import {tsc} from '@angular/tsc-wrapped/src/tsc';
89
import {patchReflectorHost} from './reflector_host';
910
import {WebpackResourceLoader} from './resource_loader';
1011
import {createResolveDependenciesFromContextMap} from './utils';
11-
import { AngularCompilerOptions } from '@angular/tsc-wrapped';
1212
import {WebpackCompilerHost} from './compiler_host';
1313
import {resolveEntryModuleFromMain} from './entry_resolver';
1414

1515

1616
/**
1717
* Option Constants
1818
*/
19-
export interface AngularWebpackPluginOptions {
20-
tsconfigPath?: string;
21-
providers?: any[];
22-
entryModule?: string;
23-
project: string;
24-
baseDir: string;
19+
export interface AotPluginOptions {
20+
tsConfigPath: string;
2521
basePath?: string;
22+
entryModule?: string;
2623
genDir?: string;
27-
main?: string;
24+
mainPath?: string;
25+
typeChecking?: boolean;
2826
}
2927

3028

31-
export class NgcWebpackPlugin {
32-
projectPath: string;
33-
rootModule: string;
34-
rootModuleName: string;
35-
reflector: ngCompiler.StaticReflector;
36-
reflectorHost: ngCompiler.ReflectorHost;
37-
program: ts.Program;
38-
compilerHost: WebpackCompilerHost;
39-
compilerOptions: ts.CompilerOptions;
40-
angularCompilerOptions: AngularCompilerOptions;
41-
files: any[];
42-
lazyRoutes: any;
43-
loader: any;
44-
genDir: string;
45-
entryModule: string;
46-
47-
done: Promise<void>;
48-
49-
nmf: any = null;
50-
cmf: any = null;
51-
compiler: any = null;
52-
compilation: any = null;
53-
54-
constructor(public options: AngularWebpackPluginOptions) {
55-
const tsConfig = tsc.readConfiguration(options.project, options.baseDir);
56-
this.compilerOptions = tsConfig.parsed.options;
57-
this.files = tsConfig.parsed.fileNames;
58-
this.angularCompilerOptions = Object.assign({}, tsConfig.ngOptions, options);
59-
60-
this.angularCompilerOptions.basePath = options.baseDir || process.cwd();
61-
this.genDir = this.options.genDir
62-
|| path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app');
63-
this.entryModule = options.entryModule || (this.angularCompilerOptions as any).entryModule;
64-
if (!options.entryModule && options.main) {
65-
this.entryModule = resolveEntryModuleFromMain(options.main);
29+
export class ModuleRoute {
30+
constructor(public readonly path: string, public readonly className: string | null) {}
31+
32+
toString() {
33+
return `${this.path}#${this.className}`;
34+
}
35+
36+
static fromString(entry: string): ModuleRoute {
37+
const split = entry.split('#');
38+
return new ModuleRoute(split[0], split[1]);
39+
}
40+
}
41+
42+
43+
export class AotPlugin {
44+
private _entryModule: ModuleRoute;
45+
private _compilerOptions: ts.CompilerOptions;
46+
private _angularCompilerOptions: ngCompiler.AngularCompilerOptions;
47+
private _program: ts.Program;
48+
private _reflector: ngCompiler.StaticReflector;
49+
private _reflectorHost: ngCompiler.ReflectorHost;
50+
private _compilerHost: WebpackCompilerHost;
51+
private _resourceLoader: WebpackResourceLoader;
52+
private _lazyRoutes: { [route: string]: string };
53+
54+
private _donePromise: Promise<void>;
55+
private _compiler: any = null;
56+
private _compilation: any = null;
57+
58+
private _typeCheck: boolean = true;
59+
60+
61+
constructor(options: AotPluginOptions) {
62+
this._setupOptions(options);
63+
}
64+
65+
get basePath() { return this._angularCompilerOptions.basePath; }
66+
get compilation() { return this._compilation; }
67+
get compilerOptions() { return this._compilerOptions; }
68+
get done() { return this._donePromise; }
69+
get entryModule() { return this._entryModule; }
70+
get genDir() { return this._angularCompilerOptions.genDir; }
71+
get program() { return this._program; }
72+
get typeCheck() { return this._typeCheck; }
73+
74+
private _setupOptions(options: AotPluginOptions) {
75+
// Fill in the missing options.
76+
if (!options.hasOwnProperty('tsConfigPath')) {
77+
throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.');
78+
}
79+
80+
// Check the base path.
81+
let basePath = path.resolve(process.cwd(), path.dirname(options.tsConfigPath));
82+
if (fs.statSync(options.tsConfigPath).isDirectory()) {
83+
basePath = options.tsConfigPath;
84+
}
85+
if (options.hasOwnProperty('basePath')) {
86+
basePath = options.basePath;
87+
}
88+
89+
const tsConfig = tsc.readConfiguration(options.tsConfigPath, basePath);
90+
91+
// Check the genDir.
92+
let genDir = basePath;
93+
if (options.hasOwnProperty('genDir')) {
94+
genDir = options.genDir;
95+
} else if (tsConfig.ngOptions.hasOwnProperty('genDir')) {
96+
genDir = tsConfig.ngOptions.genDir;
97+
}
98+
99+
this._compilerOptions = tsConfig.parsed.options;
100+
101+
if (options.entryModule) {
102+
this._entryModule = ModuleRoute.fromString(options.entryModule);
103+
} else {
104+
if (options.mainPath) {
105+
this._entryModule = ModuleRoute.fromString(resolveEntryModuleFromMain(options.mainPath));
106+
} else {
107+
this._entryModule = ModuleRoute.fromString((tsConfig.ngOptions as any).entryModule);
108+
}
109+
}
110+
this._angularCompilerOptions = Object.assign({}, tsConfig.ngOptions, {
111+
basePath,
112+
entryModule: this._entryModule.toString(),
113+
genDir
114+
});
115+
116+
if (options.hasOwnProperty('typeChecking')) {
117+
this._typeCheck = options.typeChecking;
66118
}
67119

68-
const entryModule = this.entryModule;
69-
const [rootModule, rootNgModule] = entryModule.split('#');
70-
this.projectPath = options.project;
71-
this.rootModule = rootModule;
72-
this.rootModuleName = rootNgModule;
73-
this.compilerHost = new WebpackCompilerHost(this.compilerOptions);
74-
this.program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost);
75-
this.reflectorHost = new ngCompiler.ReflectorHost(
76-
this.program, this.compilerHost, this.angularCompilerOptions);
77-
this.reflector = new ngCompiler.StaticReflector(this.reflectorHost);
120+
this._compilerHost = new WebpackCompilerHost(this._compilerOptions);
121+
this._program = ts.createProgram(
122+
tsConfig.parsed.fileNames, this._compilerOptions, this._compilerHost);
123+
this._reflectorHost = new ngCompiler.ReflectorHost(
124+
this._program, this._compilerHost, this._angularCompilerOptions);
125+
this._reflector = new ngCompiler.StaticReflector(this._reflectorHost);
126+
127+
// this._writeMain();
78128
}
79129

80130
// registration hook for webpack plugin
81131
apply(compiler: any) {
82-
this.compiler = compiler;
83-
compiler.plugin('normal-module-factory', (nmf: any) => this.nmf = nmf);
132+
this._compiler = compiler;
133+
84134
compiler.plugin('context-module-factory', (cmf: any) => {
85-
this.cmf = cmf;
86135
cmf.plugin('before-resolve', (request: any, callback: (err?: any, request?: any) => void) => {
87136
if (!request) {
88137
return callback();
@@ -112,8 +161,8 @@ export class NgcWebpackPlugin {
112161

113162
compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb));
114163
compiler.plugin('after-emit', (compilation: any, cb: any) => {
115-
this.done = null;
116-
this.compilation = null;
164+
this._donePromise = null;
165+
this._compilation = null;
117166
compilation._ngToolsWebpackPluginInstance = null;
118167
cb();
119168
});
@@ -129,68 +178,82 @@ export class NgcWebpackPlugin {
129178
}
130179

131180
private _make(compilation: any, cb: (err?: any, request?: any) => void) {
132-
const rootModulePath = path.normalize(this.rootModule + '.ts');
133-
const rootModuleName = this.rootModuleName;
134-
this.compilation = compilation;
181+
const rootModulePath = path.normalize(this._entryModule.path + '.ts');
182+
const rootModuleName = this._entryModule.className;
183+
this._compilation = compilation;
135184

136-
if (this.compilation._ngToolsWebpackPluginInstance) {
137-
cb(new Error('A ngtools/webpack plugin already exist for this compilation.'));
185+
if (this._compilation._ngToolsWebpackPluginInstance) {
186+
cb(new Error('An @ngtools/webpack plugin already exist for this compilation.'));
138187
}
139-
this.compilation._ngToolsWebpackPluginInstance = this;
188+
this._compilation._ngToolsWebpackPluginInstance = this;
140189

141-
this.loader = new WebpackResourceLoader(compilation);
190+
this._resourceLoader = new WebpackResourceLoader(compilation);
142191

143-
const i18nOptions: any = {
192+
const i18nOptions: ngCompiler.NgcCliOptions = {
144193
i18nFile: undefined,
145194
i18nFormat: undefined,
146195
locale: undefined,
147-
basePath: this.options.baseDir
196+
basePath: this.basePath
148197
};
149198

150199
// Create the Code Generator.
151200
const codeGenerator = ngCompiler.CodeGenerator.create(
152-
this.angularCompilerOptions,
201+
this._angularCompilerOptions,
153202
i18nOptions,
154-
this.program,
155-
this.compilerHost,
156-
new ngCompiler.NodeReflectorHostContext(this.compilerHost),
157-
this.loader
203+
this._program,
204+
this._compilerHost,
205+
new ngCompiler.NodeReflectorHostContext(this._compilerHost),
206+
this._resourceLoader
158207
);
159208

160209
// We need to temporarily patch the CodeGenerator until either it's patched or allows us
161210
// to pass in our own ReflectorHost.
162211
patchReflectorHost(codeGenerator);
163-
this.done = codeGenerator.codegen()
212+
this._donePromise = codeGenerator.codegen()
164213
.then(() => {
165-
// process the lazy routes
166-
const lazyModules = this._processNgModule(rootModulePath, rootModuleName, rootModulePath)
167-
.map(moduleKey => moduleKey.split('#')[0]);
168-
this.lazyRoutes = lazyModules.reduce((lazyRoutes: any, lazyModule: any) => {
169-
const genDir = this.genDir;
170-
lazyRoutes[`${lazyModule}.ngfactory`] = path.join(genDir, lazyModule + '.ngfactory.ts');
214+
const diagnostics = this._program.getGlobalDiagnostics();
215+
if (diagnostics.length > 0) {
216+
const message = diagnostics
217+
.map(diagnostic => {
218+
const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
219+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
220+
return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`;
221+
})
222+
.join('\n');
223+
224+
throw new Error(message);
225+
}
226+
})
227+
.then(() => {
228+
// Process the lazy routes
229+
this._lazyRoutes =
230+
this._processNgModule(rootModulePath, rootModuleName, rootModulePath)
231+
.map(module => ModuleRoute.fromString(module))
232+
.reduce((lazyRoutes: any, module: ModuleRoute) => {
233+
lazyRoutes[`${module.path}.ngfactory`] = path.join(
234+
this.genDir, module.path + '.ngfactory.ts');
171235
return lazyRoutes;
172236
}, {});
173237
})
174238
.then(() => cb(), (err) => cb(err));
175239
}
176240

177241
private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] {
178-
const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
242+
const staticSymbol = this._reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
179243
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
180244
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);
181245

182-
return loadChildren.reduce((res, lc) => {
183-
const [childModule, childNgModule] = lc.split('#');
246+
return loadChildren.reduce((res: string[], loadChildren: string) => {
247+
const childModule = ModuleRoute.fromString(loadChildren);
184248

185249
// TODO calculate a different containingFile for relative paths
186-
187-
const children = this._processNgModule(childModule, childNgModule, containingFile);
188-
return res.concat(children);
250+
return res.concat(
251+
this._processNgModule(childModule.path, childModule.className, containingFile));
189252
}, loadChildren);
190253
}
191254

192255
private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
193-
const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
256+
const ngModules = this._reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
194257
if (ngModules.length === 0) {
195258
throw new Error(`${staticSymbol.name} is not an NgModule`);
196259
}
@@ -208,7 +271,7 @@ export class NgcWebpackPlugin {
208271
if (!providers) {
209272
return [];
210273
}
211-
const ROUTES = this.reflectorHost.findDeclaration(
274+
const ROUTES = this._reflectorHost.findDeclaration(
212275
'@angular/router/src/router_config_loader', 'ROUTES', undefined);
213276

214277
return providers.reduce((m, p) => {

‎tests/e2e/assets/webpack/test-app/webpack.config.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const NgcWebpackPlugin = require('@ngtools/webpack').NgcWebpackPlugin;
1+
const ngToolsWebpack = require('@ngtools/webpack');
22
const path = require('path');
33

44
module.exports = {
@@ -12,9 +12,8 @@ module.exports = {
1212
filename: 'app.main.js'
1313
},
1414
plugins: [
15-
new NgcWebpackPlugin({
16-
project: './tsconfig.json',
17-
baseDir: path.resolve(__dirname, '')
15+
new ngToolsWebpack.AotPlugin({
16+
tsConfigPath: './tsconfig.json'
1817
})
1918
],
2019
module: {

‎tests/e2e_runner.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ testsToRun.reduce((previous, relativeName) => {
111111
.then(
112112
() => {
113113
console.log(green('Done.'));
114+
while(1) {}
114115
process.exit(0);
115116
},
116117
(err) => {

0 commit comments

Comments
 (0)
Please sign in to comment.