diff --git a/packages/@angular/cli/commands/test.ts b/packages/@angular/cli/commands/test.ts index a585b20b7dd6..709360991e9e 100644 --- a/packages/@angular/cli/commands/test.ts +++ b/packages/@angular/cli/commands/test.ts @@ -12,6 +12,7 @@ export interface TestOptions { watch?: boolean; codeCoverage?: boolean; singleRun?: boolean; + aot?: boolean; browsers?: string; colors?: boolean; log?: string; @@ -54,6 +55,11 @@ const TestCommand = Command.extend({ description: oneLine`Use a specific config file. Defaults to the karma config file in .angular-cli.json.` }, + { + name: 'aot', + type: Boolean, + description: 'Build using Ahead of Time compilation.' + }, { name: 'single-run', type: Boolean, diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 51278a38b1d7..8de1b29e9948 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -146,7 +146,8 @@ export function getAotConfig(wco: WebpackConfigOptions) { } export function getNonAotTestConfig(wco: WebpackConfigOptions) { - const { projectRoot, appConfig } = wco; + const { projectRoot, appConfig, buildOptions } = wco; + const { aot } = buildOptions; const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.testTsconfig); const appTsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); @@ -155,7 +156,8 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions) { // since TS compilation there is stricter and tsconfig.spec.ts doesn't include them. const include = [appConfig.main, appConfig.polyfills]; - let pluginOptions: any = { tsConfigPath, skipCodeGeneration: true, include }; + let pluginOptions: any = { tsConfigPath, skipCodeGeneration: !aot, + enableSummariesForJit: aot, include }; // Fallback to correct module format on projects using a shared tsconfig. if (tsConfigPath === appTsConfigPath) { diff --git a/packages/@angular/cli/plugins/karma.ts b/packages/@angular/cli/plugins/karma.ts index ad893a5fadb5..cf1d2f747f05 100644 --- a/packages/@angular/cli/plugins/karma.ts +++ b/packages/@angular/cli/plugins/karma.ts @@ -55,6 +55,7 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { sourcemaps: true, progress: true, preserveSymlinks: false, + aot: config.aot, }, config.angularCli); if (testConfig.sourcemaps) { diff --git a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts index 1e812dd5ec7c..70c3d5951433 100644 --- a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts @@ -18,6 +18,7 @@ import { TransformOperation, makeTransform, replaceBootstrap, + replaceInitTestEnv, exportNgFactory, exportLazyModuleMap, registerLocaleData, @@ -69,6 +70,7 @@ export interface AngularCompilerPluginOptions { exclude?: string | string[]; include?: string[]; compilerOptions?: ts.CompilerOptions; + enableSummariesForJit?: boolean; } export enum PLATFORM { @@ -108,7 +110,7 @@ export class AngularCompilerPlugin implements Tapable { constructor(options: AngularCompilerPluginOptions) { CompilerCliIsSupported(); - this._options = Object.assign({}, options); + this._options = Object.assign({ enableSummariesForJit: true }, options); this._setupOptions(this._options); } @@ -242,6 +244,14 @@ export class AngularCompilerPlugin implements Tapable { this._JitMode = options.skipCodeGeneration; } + // Set ngsummaries option if AOT mode. + if (!this._JitMode || options.enableSummariesForJit !== undefined) { + this._angularCompilerOptions.enableSummariesForJit = options.enableSummariesForJit; + // If module option is 'commonjs', `ReferenceError: AppModuleNgSummary is not defined` + // is thrown at runtime. So overwrite it with 'ES2015' to suppres this error. + this._angularCompilerOptions.module = ts.ModuleKind.ES2015; + } + // Process i18n options. if (options.hasOwnProperty('i18nInFile')) { this._angularCompilerOptions.i18nInFile = options.i18nInFile; @@ -685,6 +695,10 @@ export class AngularCompilerPlugin implements Tapable { transformOps.push(...replaceBootstrap(sf, this.entryModule)); } + if (this.options.enableSummariesForJit) { + transformOps.push(...replaceInitTestEnv(sf, [this.entryModule])); + } + // If we have a locale, auto import the locale data file. if (this._angularCompilerOptions.i18nInLocale) { transformOps.push(...registerLocaleData( diff --git a/packages/@ngtools/webpack/src/ngtools_api.ts b/packages/@ngtools/webpack/src/ngtools_api.ts index ee9df8db3594..738387e89fea 100644 --- a/packages/@ngtools/webpack/src/ngtools_api.ts +++ b/packages/@ngtools/webpack/src/ngtools_api.ts @@ -38,6 +38,7 @@ export interface CompilerOptions extends ts.CompilerOptions { i18nInFile?: string; i18nInMissingTranslations?: 'error' | 'warning' | 'ignore'; preserveWhitespaces?: boolean; + enableSummariesForJit?: boolean; } export interface CompilerHost extends ts.CompilerHost { moduleNameToFileName(moduleName: string, containingFile?: string): string | null; diff --git a/packages/@ngtools/webpack/src/transformers/index.ts b/packages/@ngtools/webpack/src/transformers/index.ts index de09607bb67e..5bdfafc1b133 100644 --- a/packages/@ngtools/webpack/src/transformers/index.ts +++ b/packages/@ngtools/webpack/src/transformers/index.ts @@ -3,6 +3,7 @@ export * from './make_transform'; export * from './insert_import'; export * from './remove_import'; export * from './replace_bootstrap'; +export * from './replace_init_test_env'; export * from './export_ngfactory'; export * from './export_lazy_module_map'; export * from './register_locale_data'; diff --git a/packages/@ngtools/webpack/src/transformers/replace_init_test_env.spec.ts b/packages/@ngtools/webpack/src/transformers/replace_init_test_env.spec.ts new file mode 100644 index 000000000000..e8aca91d0439 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/replace_init_test_env.spec.ts @@ -0,0 +1,38 @@ +import * as ts from 'typescript'; +import { oneLine, stripIndent } from 'common-tags'; +import { transformTypescript } from './ast_helpers'; +import { replaceInitTestEnv } from './replace_init_test_env'; + +describe('@ngtools/webpack transformers', () => { + describe('replace_init_test_env', () => { + it('should replace initTestEnvironment', () => { + const input = stripIndent` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + + getTestBed().initTestEnvironment(BrowserDynamicTestingModule, + platformBrowserDynamicTesting()); + `; + const output = stripIndent` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + import { AppModuleNgSummary } from "./app/app.module.ngsummary"; + + getTestBed().initTestEnvironment(BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), () => [AppModuleNgSummary]); + `; + + const transformOpsCb = (sourceFile: ts.SourceFile) => + replaceInitTestEnv(sourceFile, [{ className: 'AppModule', path: './app/app.module' }]); + const result = transformTypescript(input, transformOpsCb); + + expect(oneLine`${result}`).toEqual(oneLine`${output}`); + }); + }); +}); diff --git a/packages/@ngtools/webpack/src/transformers/replace_init_test_env.ts b/packages/@ngtools/webpack/src/transformers/replace_init_test_env.ts new file mode 100644 index 000000000000..bd609f6b7cf8 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/replace_init_test_env.ts @@ -0,0 +1,53 @@ +import * as ts from 'typescript'; + +import { findAstNodes } from './ast_helpers'; +import { TransformOperation, AddNodeOperation } from './make_transform'; +import { insertImport } from './insert_import'; + +export function replaceInitTestEnv( + sourceFile: ts.SourceFile, + modules: { path: string, className: string }[] +): TransformOperation[] { + if (modules.length === 0) { + return; + } + + const ops: TransformOperation[] = []; + + // Find the initTestEnvironment calls. + const initTestEnvIdentifiers = findAstNodes(null, sourceFile, + ts.SyntaxKind.Identifier, true) + .filter(identifier => identifier.getText() === 'initTestEnvironment'); + + if (initTestEnvIdentifiers.length === 0) { + return []; + } + + const moduleNgSummaries = modules.map((mod) => { + return { + className: mod.className + 'NgSummary', + path: mod.path + '.ngsummary', + }; + }); + + moduleNgSummaries.forEach((moduleNgSummary) => + ops.push(...insertImport(sourceFile, moduleNgSummary.className, moduleNgSummary.path))); + + initTestEnvIdentifiers.forEach((initTestEnvIdentifier) => { + if (initTestEnvIdentifier.parent.parent.kind !== ts.SyntaxKind.CallExpression) { + return; + } + const initTestEnvCall = initTestEnvIdentifier.parent.parent as ts.CallExpression; + if (initTestEnvCall.arguments.length !== 2) { + return; + } + const lastArgument = initTestEnvCall.arguments[initTestEnvCall.arguments.length - 1]; + const ngSummariesArray = ts.createArrayLiteral(moduleNgSummaries.map((summary) => + ts.createIdentifier(summary.className), true)); + const aotSummariesFn = ts.createArrowFunction([], [], [], undefined, + ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ngSummariesArray); + ops.push(new AddNodeOperation(sourceFile, lastArgument, null, aotSummariesFn)); + }); + + return ops; +}