diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts index e674ae4c281a..9b9750fa2d96 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts @@ -15,7 +15,7 @@ import { removePropertyInAstObject, } from '../../utility/json-utils'; import { Builders } from '../../utility/workspace-models'; -import { getAllOptions, getTargets, getWorkspace } from './utils'; +import { getAllOptions, getTargets, getWorkspace, isIvyEnabled } from './utils'; export const ANY_COMPONENT_STYLE_BUDGET = { type: 'anyComponentStyle', @@ -32,6 +32,7 @@ export function UpdateWorkspaceConfig(): Rule { updateStyleOrScriptOption('styles', recorder, target); updateStyleOrScriptOption('scripts', recorder, target); addAnyComponentStyleBudget(recorder, target); + updateAotOption(tree, recorder, target); } for (const { target } of getTargets(workspace, 'test', Builders.Karma)) { @@ -45,6 +46,41 @@ export function UpdateWorkspaceConfig(): Rule { }; } +function updateAotOption(tree: Tree, recorder: UpdateRecorder, builderConfig: JsonAstObject) { + const options = findPropertyInAstObject(builderConfig, 'options'); + if (!options || options.kind !== 'object') { + return; + } + + + const tsConfig = findPropertyInAstObject(options, 'tsConfig'); + // Do not add aot option if the users already opted out from Ivy. + if (tsConfig && tsConfig.kind === 'string' && !isIvyEnabled(tree, tsConfig.value)) { + return; + } + + // Add aot to options. + const aotOption = findPropertyInAstObject(options, 'aot'); + + if (!aotOption) { + insertPropertyInAstObjectInOrder(recorder, options, 'aot', true, 12); + + return; + } + + if (aotOption.kind !== 'true') { + const { start, end } = aotOption; + recorder.remove(start.offset, end.offset - start.offset); + recorder.insertLeft(start.offset, 'true'); + } + + // Remove aot properties from other configurations as they are no redundant + const configOptions = getAllOptions(builderConfig, true); + for (const options of configOptions) { + removePropertyInAstObject(recorder, options, 'aot'); + } +} + function updateStyleOrScriptOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) { const options = getAllOptions(builderConfig); @@ -75,12 +111,6 @@ function addAnyComponentStyleBudget(recorder: UpdateRecorder, builderConfig: Jso const options = getAllOptions(builderConfig, true); for (const option of options) { - const aotOption = findPropertyInAstObject(option, 'aot'); - if (!aotOption || aotOption.kind !== 'true') { - // AnyComponentStyle only works for AOT - continue; - } - const budgetOption = findPropertyInAstObject(option, 'budgets'); if (!budgetOption) { // add diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts index 5bf68031575f..00d59ab12683 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts @@ -169,5 +169,84 @@ describe('Migration to version 9', () => { expect(config.configurations.production.budgets).toEqual([ANY_COMPONENT_STYLE_BUDGET]); }); }); + + describe('aot option', () => { + it('should update aot option when false', async () => { + let config = getWorkspaceTargets(tree); + config.build.options.aot = false; + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.aot).toBe(true); + }); + + it('should add aot option when not defined', async () => { + let config = getWorkspaceTargets(tree); + config.build.options.aot = undefined; + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.aot).toBe(true); + }); + + it('should not aot option when opted-out of Ivy', async () => { + const tsConfig = JSON.stringify( + { + extends: './tsconfig.json', + angularCompilerOptions: { + enableIvy: false, + }, + }, + null, + 2, + ); + + tree.overwrite('/tsconfig.app.json', tsConfig); + + let config = getWorkspaceTargets(tree); + config.build.options.aot = false; + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.aot).toBe(false); + }); + + it('should not aot option when opted-out of Ivy in workspace', async () => { + const tsConfig = JSON.stringify( + { + angularCompilerOptions: { + enableIvy: false, + }, + }, + null, + 2, + ); + + tree.overwrite('/tsconfig.json', tsConfig); + + let config = getWorkspaceTargets(tree); + config.build.options.aot = false; + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.aot).toBe(false); + }); + + it('should remove aot option from production configuration', async () => { + let config = getWorkspaceTargets(tree); + config.build.options.aot = false; + config.build.configurations.production.aot = true; + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.aot).toBe(true); + expect(config.configurations.production.aot).toBeUndefined(); + }); + }); }); }); diff --git a/packages/schematics/angular/migrations/update-9/utils.ts b/packages/schematics/angular/migrations/update-9/utils.ts index 04542a708f37..b4c6def533e1 100644 --- a/packages/schematics/angular/migrations/update-9/utils.ts +++ b/packages/schematics/angular/migrations/update-9/utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { JsonAstObject, JsonParseMode, parseJsonAst } from '@angular-devkit/core'; +import { JsonAstObject, JsonParseMode, dirname, normalize, parseJsonAst, resolve } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; import { getWorkspacePath } from '../../utility/config'; import { findPropertyInAstObject } from '../../utility/json-utils'; @@ -81,3 +81,40 @@ export function getWorkspace(host: Tree): JsonAstObject { return parseJsonAst(content, JsonParseMode.Loose) as JsonAstObject; } + +export function isIvyEnabled(tree: Tree, tsConfigPath: string): boolean { + // In version 9, Ivy is turned on by default + // Ivy is opted out only when 'enableIvy' is set to false. + + const buffer = tree.read(tsConfigPath); + if (!buffer) { + return true; + } + + const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose); + + if (tsCfgAst.kind !== 'object') { + return true; + } + + const ngCompilerOptions = findPropertyInAstObject(tsCfgAst, 'angularCompilerOptions'); + if (ngCompilerOptions && ngCompilerOptions.kind === 'object') { + const enableIvy = findPropertyInAstObject(ngCompilerOptions, 'enableIvy'); + + if (enableIvy) { + return !!enableIvy.value; + } + } + + const configExtends = findPropertyInAstObject(tsCfgAst, 'extends'); + if (configExtends && configExtends.kind === 'string') { + const extendedTsConfigPath = resolve( + dirname(normalize(tsConfigPath)), + normalize(configExtends.value), + ); + + return isIvyEnabled(tree, extendedTsConfigPath); + } + + return true; +} diff --git a/packages/schematics/angular/migrations/update-9/utils_spec.ts b/packages/schematics/angular/migrations/update-9/utils_spec.ts new file mode 100644 index 000000000000..0726bb04bc8a --- /dev/null +++ b/packages/schematics/angular/migrations/update-9/utils_spec.ts @@ -0,0 +1,107 @@ + +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { HostTree } from '@angular-devkit/schematics'; +import { isIvyEnabled } from './utils'; + +describe('migrations update-9 utils', () => { + describe('isIvyEnabled', () => { + let tree: HostTree; + + beforeEach(() => { + tree = new HostTree(); + }); + + it('should return false when disabled in base tsconfig', () => { + tree.create('tsconfig.json', JSON.stringify({ + angularCompilerOptions: { + enableIvy: false, + }, + })); + + tree.create('foo/tsconfig.app.json', JSON.stringify({ + extends: '../tsconfig.json', + })); + + expect(isIvyEnabled(tree, 'foo/tsconfig.app.json')).toBe(false); + }); + + it('should return true when enable in child tsconfig but disabled in base tsconfig', () => { + tree.create('tsconfig.json', JSON.stringify({ + angularCompilerOptions: { + enableIvy: false, + }, + })); + + tree.create('foo/tsconfig.app.json', JSON.stringify({ + extends: '../tsconfig.json', + angularCompilerOptions: { + enableIvy: true, + }, + })); + + expect(isIvyEnabled(tree, 'foo/tsconfig.app.json')).toBe(true); + }); + + it('should return false when disabled in child tsconfig but enabled in base tsconfig', () => { + tree.create('tsconfig.json', JSON.stringify({ + angularCompilerOptions: { + enableIvy: true, + }, + })); + + tree.create('foo/tsconfig.app.json', JSON.stringify({ + extends: '../tsconfig.json', + angularCompilerOptions: { + enableIvy: false, + }, + })); + + expect(isIvyEnabled(tree, 'foo/tsconfig.app.json')).toBe(false); + }); + + it('should return false when disabled in base with multiple extends', () => { + tree.create('tsconfig.json', JSON.stringify({ + angularCompilerOptions: { + enableIvy: false, + }, + })); + + tree.create('foo/tsconfig.project.json', JSON.stringify({ + extends: '../tsconfig.json', + })); + + tree.create('foo/tsconfig.app.json', JSON.stringify({ + extends: './tsconfig.project.json', + })); + + expect(isIvyEnabled(tree, 'foo/tsconfig.app.json')).toBe(false); + }); + + it('should return true when enable in intermediate tsconfig with multiple extends', () => { + tree.create('tsconfig.json', JSON.stringify({ + angularCompilerOptions: { + enableIvy: false, + }, + })); + + tree.create('foo/tsconfig.project.json', JSON.stringify({ + extends: '../tsconfig.json', + angularCompilerOptions: { + enableIvy: true, + }, + })); + + tree.create('foo/tsconfig.app.json', JSON.stringify({ + extends: './tsconfig.project.json', + })); + + expect(isIvyEnabled(tree, 'foo/tsconfig.app.json')).toBe(true); + }); + }); +});