Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/schematics/angular/migrations/update-9/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
*/

import { Rule, chain } from '@angular-devkit/schematics';
import { UpdateLibraries } from './ivy-libraries';
import { UpdateWorkspaceConfig } from './update-workspace-config';

export default function(): Rule {
return () => {
return chain([
UpdateWorkspaceConfig(),
UpdateLibraries(),
]);
};
}
118 changes: 118 additions & 0 deletions packages/schematics/angular/migrations/update-9/ivy-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @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 { JsonParseMode, parseJsonAst } from '@angular-devkit/core';
import { Rule, Tree } from '@angular-devkit/schematics';
import { getWorkspacePath } from '../../utility/config';
import {
appendPropertyInAstObject,
findPropertyInAstObject,
insertPropertyInAstObjectInOrder,
} from '../../utility/json-utils';
import { Builders } from '../../utility/workspace-models';
import { getTargets, getWorkspace } from './utils';

/**
* Updates a pre version 9 library to version 9 Ivy library.
*
* The main things that this migrations does are:
* - Creates a production configuration for VE compilations.
* - Create a prod tsconfig for which disables Ivy and enables VE compilations.
*/
export function UpdateLibraries(): Rule {
return (tree: Tree) => {
const workspacePath = getWorkspacePath(tree);
const workspace = getWorkspace(tree);

const recorder = tree.beginUpdate(workspacePath);
for (const { target, project } of getTargets(workspace, 'build', Builders.NgPackagr)) {
const projectRoot = findPropertyInAstObject(project, 'root');
if (!projectRoot || projectRoot.kind !== 'string') {
break;
}

const configurations = findPropertyInAstObject(target, 'configurations');
const tsConfig = `${projectRoot.value}/tsconfig.lib.prod.json`;

if (!configurations || configurations.kind !== 'object') {
// Configurations doesn't exist.
appendPropertyInAstObject(recorder, target, 'configurations', { production: { tsConfig } }, 10);
createTsConfig(tree, tsConfig);
continue;
}

const prodConfig = findPropertyInAstObject(configurations, 'production');
if (!prodConfig || prodConfig.kind !== 'object') {
// Production configuration doesn't exist.
insertPropertyInAstObjectInOrder(recorder, configurations, 'production', { tsConfig }, 12);
createTsConfig(tree, tsConfig);
continue;
}

const tsConfigOption = findPropertyInAstObject(prodConfig, 'tsConfig');
if (!tsConfigOption || tsConfigOption.kind !== 'string') {
// No tsconfig for production has been defined.
insertPropertyInAstObjectInOrder(recorder, prodConfig, 'tsConfig', tsConfig, 14);
createTsConfig(tree, tsConfig);
continue;
}

// tsConfig for production already exists.
const tsConfigContent = tree.read(tsConfigOption.value);
if (!tsConfigContent) {
continue;
}

const tsConfigAst = parseJsonAst(tsConfigContent.toString(), JsonParseMode.Loose);
if (!tsConfigAst || tsConfigAst.kind !== 'object') {
// Invalid tsConfig
continue;
}

const tsConfigRecorder = tree.beginUpdate(tsConfigOption.value);
const ngCompilerOptions = findPropertyInAstObject(tsConfigAst, 'angularCompilerOptions');
if (!ngCompilerOptions) {
// Add angularCompilerOptions to the production tsConfig
appendPropertyInAstObject(tsConfigRecorder, tsConfigAst, 'angularCompilerOptions', { enableIvy: false }, 2);
tree.commitUpdate(tsConfigRecorder);
continue;
}

if (ngCompilerOptions.kind === 'object') {
const enableIvy = findPropertyInAstObject(ngCompilerOptions, 'enableIvy');
// Add enableIvy false
if (!enableIvy) {
appendPropertyInAstObject(tsConfigRecorder, ngCompilerOptions, 'enableIvy', false, 4);
tree.commitUpdate(tsConfigRecorder);
continue;
}

if (enableIvy.kind !== 'false') {
const { start, end } = enableIvy;
tsConfigRecorder.remove(start.offset, end.offset - start.offset);
tsConfigRecorder.insertLeft(start.offset, 'false');
tree.commitUpdate(tsConfigRecorder);
}
}
}

tree.commitUpdate(recorder);

return tree;
};
}

function createTsConfig(tree: Tree, tsConfigPath: string) {
const tsConfigContent = {
extends: './tsconfig.lib.json',
angularCompilerOptions: {
enableIvy: false,
},
};

tree.create(tsConfigPath, JSON.stringify(tsConfigContent, undefined, 2));
}
145 changes: 145 additions & 0 deletions packages/schematics/angular/migrations/update-9/ivy-libraries_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* @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 { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { WorkspaceTargets } from '../../utility/workspace-models';

// tslint:disable-next-line: no-any
function getWorkspaceTargets(tree: UnitTestTree): any {
return JSON.parse(tree.readContent(workspacePath))
.projects['migration-lib'].architect;
}

function updateWorkspaceTargets(tree: UnitTestTree, workspaceTargets: WorkspaceTargets) {
const config = JSON.parse(tree.readContent(workspacePath));
config.projects['migration-lib'].architect = workspaceTargets;
tree.overwrite(workspacePath, JSON.stringify(config, undefined, 2));
}

const workspacePath = '/angular.json';
const libProdTsConfig = 'migration-lib/tsconfig.lib.prod.json';

// tslint:disable:no-big-function
describe('Migration to version 9', () => {
describe('Migrate Ivy Libraries', () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);

let tree: UnitTestTree;

beforeEach(async () => {
tree = new UnitTestTree(new EmptyTree());
tree = await schematicRunner
.runExternalSchematicAsync(
require.resolve('../../collection.json'),
'workspace',
{
name: 'migration-test',
version: '1.2.3',
directory: '.',
},
tree,
)
.toPromise();
tree = await schematicRunner
.runExternalSchematicAsync(
require.resolve('../../collection.json'),
'library',
{
name: 'migration-lib',
},
tree,
)
.toPromise();
});

it(`should add 'tsConfig' option in production when configurations doesn't exists`, async () => {
let config = getWorkspaceTargets(tree);
config.build.configurations = undefined;

updateWorkspaceTargets(tree, config);

const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
});

it(`should add 'tsConfig' option in production when configurations exists`, async () => {
let config = getWorkspaceTargets(tree);
config.build.configurations = {};

updateWorkspaceTargets(tree, config);

const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
});

it(`should add 'tsConfig' option in production when production configurations exists`, async () => {
let config = getWorkspaceTargets(tree);
config.build.configurations = { production: {} };

updateWorkspaceTargets(tree, config);

const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
});

it(`should add enableIvy false in prod tsconfig if already exists`, async () => {
let config = getWorkspaceTargets(tree);
const prodLibTsConfig = 'migration-lib/tsconfig.library.prod.json';
config.build.configurations = { production: { tsConfig: prodLibTsConfig } };

updateWorkspaceTargets(tree, config);

const tsconfig = {
compilerOptions: {},
};

tree.create(prodLibTsConfig, JSON.stringify(tsconfig, undefined, 2));

const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.tsConfig).toEqual(prodLibTsConfig);

const tsConfigContent = tree2.readContent(prodLibTsConfig);
expect(JSON.parse(tsConfigContent).angularCompilerOptions).toEqual({ enableIvy: false });
});

it('should set enableIvy to false in prod tsconfig if true', async () => {
let config = getWorkspaceTargets(tree);
const prodLibTsConfig = 'migration-lib/tsconfig.library.prod.json';
config.build.configurations = { production: { tsConfig: prodLibTsConfig } };

updateWorkspaceTargets(tree, config);

const tsconfig = {
compilerOptions: {},
angularCompilerOptions: {
enableIvy: true,
},
};

tree.create(prodLibTsConfig, JSON.stringify(tsconfig, undefined, 2));

const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.tsConfig).toEqual(prodLibTsConfig);

const tsConfigContent = tree2.readContent(prodLibTsConfig);
expect(JSON.parse(tsConfigContent).angularCompilerOptions).toEqual({ enableIvy: false });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
* 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 {
JsonAstObject,
JsonParseMode,
parseJsonAst,
} from '@angular-devkit/core';
import { JsonAstObject } from '@angular-devkit/core';
import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics';
import { getWorkspacePath } from '../../utility/config';
import {
appendValueInAstArray,
findPropertyInAstObject,
insertPropertyInAstObjectInOrder,
removePropertyInAstObject,
} from '../../utility/json-utils';
import { Builders } from '../../utility/workspace-models';
import { getAllOptions, getTargets, getWorkspace } from './utils';

export const ANY_COMPONENT_STYLE_BUDGET = {
type: 'anyComponentStyle',
Expand All @@ -25,61 +24,19 @@ export const ANY_COMPONENT_STYLE_BUDGET = {

export function UpdateWorkspaceConfig(): Rule {
return (tree: Tree) => {
let workspaceConfigPath = 'angular.json';
let angularConfigContent = tree.read(workspaceConfigPath);

if (!angularConfigContent) {
workspaceConfigPath = '.angular.json';
angularConfigContent = tree.read(workspaceConfigPath);

if (!angularConfigContent) {
return;
}
}

const angularJson = parseJsonAst(angularConfigContent.toString(), JsonParseMode.Loose);
if (angularJson.kind !== 'object') {
return;
const workspacePath = getWorkspacePath(tree);
const workspace = getWorkspace(tree);
const recorder = tree.beginUpdate(workspacePath);

for (const { target } of getTargets(workspace, 'build', Builders.Browser)) {
updateStyleOrScriptOption('styles', recorder, target);
updateStyleOrScriptOption('scripts', recorder, target);
addAnyComponentStyleBudget(recorder, target);
}

const projects = findPropertyInAstObject(angularJson, 'projects');
if (!projects || projects.kind !== 'object') {
return;
}

// For all projects
const recorder = tree.beginUpdate(workspaceConfigPath);
for (const project of projects.properties) {
const projectConfig = project.value;
if (projectConfig.kind !== 'object') {
break;
}

const architect = findPropertyInAstObject(projectConfig, 'architect');
if (!architect || architect.kind !== 'object') {
break;
}

const buildTarget = findPropertyInAstObject(architect, 'build');
if (buildTarget && buildTarget.kind === 'object') {
const builder = findPropertyInAstObject(buildTarget, 'builder');
// Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:browser') {
updateStyleOrScriptOption('styles', recorder, buildTarget);
updateStyleOrScriptOption('scripts', recorder, buildTarget);
addAnyComponentStyleBudget(recorder, buildTarget);
}
}

const testTarget = findPropertyInAstObject(architect, 'test');
if (testTarget && testTarget.kind === 'object') {
const builder = findPropertyInAstObject(testTarget, 'builder');
// Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:karma') {
updateStyleOrScriptOption('styles', recorder, testTarget);
updateStyleOrScriptOption('scripts', recorder, testTarget);
}
}
for (const { target } of getTargets(workspace, 'test', Builders.Karma)) {
updateStyleOrScriptOption('styles', recorder, target);
updateStyleOrScriptOption('scripts', recorder, target);
}

tree.commitUpdate(recorder);
Expand All @@ -88,23 +45,6 @@ export function UpdateWorkspaceConfig(): Rule {
};
}

/**
* Helper to retreive all the options in various configurations
*/
function getAllOptions(builderConfig: JsonAstObject, configurationsOnly = false): JsonAstObject[] {
const options = [];
const configurations = findPropertyInAstObject(builderConfig, 'configurations');
if (configurations && configurations.kind === 'object') {
options.push(...configurations.properties.map(x => x.value));
}

if (!configurationsOnly) {
options.push(findPropertyInAstObject(builderConfig, 'options'));
}

return options.filter(o => o && o.kind === 'object') as JsonAstObject[];
}

function updateStyleOrScriptOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) {
const options = getAllOptions(builderConfig);

Expand Down
Loading