Skip to content

Commit 23217fa

Browse files
alan-agius4Keen Yee Liau
authored andcommitted
feat(@schematics/angular): ivy library migration
Add a migration to migrate existing libraries to the new library layout considering it will be the default in version 9.
1 parent 96c457d commit 23217fa

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

packages/schematics/angular/migrations/update-9/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
*/
88

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

1213
export default function(): Rule {
1314
return () => {
1415
return chain([
1516
UpdateWorkspaceConfig(),
17+
UpdateLibraries(),
1618
]);
1719
};
1820
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { JsonParseMode, parseJsonAst } from '@angular-devkit/core';
9+
import { Rule, Tree } from '@angular-devkit/schematics';
10+
import { getWorkspace, getWorkspacePath } from '../../utility/config';
11+
import {
12+
appendPropertyInAstObject,
13+
findPropertyInAstObject,
14+
insertPropertyInAstObjectInOrder,
15+
} from '../../utility/json-utils';
16+
import { Builders } from '../../utility/workspace-models';
17+
import { getTargets } from './utils';
18+
19+
/**
20+
* Updates a pre version 9 library to version 9 Ivy library.
21+
*
22+
* The main things that this migrations does are:
23+
* - Creates a production configuration for VE compilations.
24+
* - Create a prod tsconfig for which disables Ivy and enables VE compilations.
25+
*/
26+
export function UpdateLibraries(): Rule {
27+
return (tree: Tree) => {
28+
const workspacePath = getWorkspacePath(tree);
29+
const workspace = getWorkspace(tree);
30+
31+
const recorder = tree.beginUpdate(workspacePath);
32+
for (const { target, project } of getTargets(workspace, 'build', Builders.NgPackagr)) {
33+
const projectRoot = findPropertyInAstObject(project, 'root');
34+
if (!projectRoot || projectRoot.kind !== 'string') {
35+
break;
36+
}
37+
38+
const configurations = findPropertyInAstObject(target, 'configurations');
39+
const tsConfig = `${projectRoot.value}/tsconfig.lib.prod.json`;
40+
41+
if (!configurations || configurations.kind !== 'object') {
42+
// Configurations doesn't exist.
43+
appendPropertyInAstObject(recorder, target, 'configurations', { production: { tsConfig } }, 10);
44+
createTsConfig(tree, tsConfig);
45+
continue;
46+
}
47+
48+
const prodConfig = findPropertyInAstObject(configurations, 'production');
49+
if (!prodConfig || prodConfig.kind !== 'object') {
50+
// Production configuration doesn't exist.
51+
insertPropertyInAstObjectInOrder(recorder, configurations, 'production', { tsConfig }, 12);
52+
createTsConfig(tree, tsConfig);
53+
continue;
54+
}
55+
56+
const tsConfigOption = findPropertyInAstObject(prodConfig, 'tsConfig');
57+
if (!tsConfigOption || tsConfigOption.kind !== 'string') {
58+
// No tsconfig for production has been defined.
59+
insertPropertyInAstObjectInOrder(recorder, prodConfig, 'tsConfig', tsConfig, 14);
60+
createTsConfig(tree, tsConfig);
61+
continue;
62+
}
63+
64+
// tsConfig for production already exists.
65+
const tsConfigContent = tree.read(tsConfigOption.value);
66+
if (!tsConfigContent) {
67+
continue;
68+
}
69+
70+
const tsConfigAst = parseJsonAst(tsConfigContent.toString(), JsonParseMode.Loose);
71+
if (!tsConfigAst || tsConfigAst.kind !== 'object') {
72+
// Invalid tsConfig
73+
continue;
74+
}
75+
76+
const tsConfigRecorder = tree.beginUpdate(tsConfigOption.value);
77+
const ngCompilerOptions = findPropertyInAstObject(tsConfigAst, 'angularCompilerOptions');
78+
if (!ngCompilerOptions) {
79+
// Add angularCompilerOptions to the production tsConfig
80+
appendPropertyInAstObject(tsConfigRecorder, tsConfigAst, 'angularCompilerOptions', { enableIvy: false }, 2);
81+
tree.commitUpdate(tsConfigRecorder);
82+
continue;
83+
}
84+
85+
if (ngCompilerOptions.kind === 'object') {
86+
const enableIvy = findPropertyInAstObject(ngCompilerOptions, 'enableIvy');
87+
// Add enableIvy false
88+
if (!enableIvy) {
89+
appendPropertyInAstObject(tsConfigRecorder, ngCompilerOptions, 'enableIvy', false, 4);
90+
tree.commitUpdate(tsConfigRecorder);
91+
continue;
92+
}
93+
94+
if (enableIvy.kind !== 'false') {
95+
const { start, end } = enableIvy;
96+
tsConfigRecorder.remove(start.offset, end.offset - start.offset);
97+
tsConfigRecorder.insertLeft(start.offset, 'false');
98+
tree.commitUpdate(tsConfigRecorder);
99+
}
100+
}
101+
}
102+
103+
tree.commitUpdate(recorder);
104+
105+
return tree;
106+
};
107+
}
108+
109+
function createTsConfig(tree: Tree, tsConfigPath: string) {
110+
const tsConfigContent = {
111+
extends: './tsconfig.lib.json',
112+
angularCompilerOptions: {
113+
enableIvy: false,
114+
},
115+
};
116+
117+
tree.create(tsConfigPath, JSON.stringify(tsConfigContent, undefined, 2));
118+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
import { WorkspaceTargets } from '../../utility/workspace-models';
12+
13+
// tslint:disable-next-line: no-any
14+
function getWorkspaceTargets(tree: UnitTestTree): any {
15+
return JSON.parse(tree.readContent(workspacePath))
16+
.projects['migration-lib'].architect;
17+
}
18+
19+
function updateWorkspaceTargets(tree: UnitTestTree, workspaceTargets: WorkspaceTargets) {
20+
const config = JSON.parse(tree.readContent(workspacePath));
21+
config.projects['migration-lib'].architect = workspaceTargets;
22+
tree.overwrite(workspacePath, JSON.stringify(config, undefined, 2));
23+
}
24+
25+
const workspacePath = '/angular.json';
26+
const libProdTsConfig = 'migration-lib/tsconfig.lib.prod.json';
27+
28+
// tslint:disable:no-big-function
29+
describe('Migration to version 9', () => {
30+
describe('Migrate Ivy Libraries', () => {
31+
const schematicRunner = new SchematicTestRunner(
32+
'migrations',
33+
require.resolve('../migration-collection.json'),
34+
);
35+
36+
let tree: UnitTestTree;
37+
38+
beforeEach(async () => {
39+
tree = new UnitTestTree(new EmptyTree());
40+
tree = await schematicRunner
41+
.runExternalSchematicAsync(
42+
require.resolve('../../collection.json'),
43+
'workspace',
44+
{
45+
name: 'migration-test',
46+
version: '1.2.3',
47+
directory: '.',
48+
},
49+
tree,
50+
)
51+
.toPromise();
52+
tree = await schematicRunner
53+
.runExternalSchematicAsync(
54+
require.resolve('../../collection.json'),
55+
'library',
56+
{
57+
name: 'migration-lib',
58+
},
59+
tree,
60+
)
61+
.toPromise();
62+
});
63+
64+
it(`should add 'tsConfig' option in production when configurations doesn't exists`, async () => {
65+
let config = getWorkspaceTargets(tree);
66+
config.build.configurations = undefined;
67+
68+
updateWorkspaceTargets(tree, config);
69+
70+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
71+
config = getWorkspaceTargets(tree2).build;
72+
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
73+
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
74+
});
75+
76+
it(`should add 'tsConfig' option in production when configurations exists`, async () => {
77+
let config = getWorkspaceTargets(tree);
78+
config.build.configurations = {};
79+
80+
updateWorkspaceTargets(tree, config);
81+
82+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
83+
config = getWorkspaceTargets(tree2).build;
84+
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
85+
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
86+
});
87+
88+
it(`should add 'tsConfig' option in production when production configurations exists`, async () => {
89+
let config = getWorkspaceTargets(tree);
90+
config.build.configurations = { production: {} };
91+
92+
updateWorkspaceTargets(tree, config);
93+
94+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
95+
config = getWorkspaceTargets(tree2).build;
96+
expect(config.configurations.production.tsConfig).toEqual(libProdTsConfig);
97+
expect(tree2.exists(libProdTsConfig)).toBeTruthy();
98+
});
99+
100+
it(`should add enableIvy false in prod tsconfig if already exists`, async () => {
101+
let config = getWorkspaceTargets(tree);
102+
const prodLibTsConfig = 'migration-lib/tsconfig.library.prod.json';
103+
config.build.configurations = { production: { tsConfig: prodLibTsConfig } };
104+
105+
updateWorkspaceTargets(tree, config);
106+
107+
const tsconfig = {
108+
compilerOptions: {},
109+
};
110+
111+
tree.create(prodLibTsConfig, JSON.stringify(tsconfig, undefined, 2));
112+
113+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
114+
config = getWorkspaceTargets(tree2).build;
115+
expect(config.configurations.production.tsConfig).toEqual(prodLibTsConfig);
116+
117+
const tsConfigContent = tree2.readContent(prodLibTsConfig);
118+
expect(JSON.parse(tsConfigContent).angularCompilerOptions).toEqual({ enableIvy: false });
119+
});
120+
121+
it('should set enableIvy to false in prod tsconfig if true', async () => {
122+
let config = getWorkspaceTargets(tree);
123+
const prodLibTsConfig = 'migration-lib/tsconfig.library.prod.json';
124+
config.build.configurations = { production: { tsConfig: prodLibTsConfig } };
125+
126+
updateWorkspaceTargets(tree, config);
127+
128+
const tsconfig = {
129+
compilerOptions: {},
130+
angularCompilerOptions: {
131+
enableIvy: true,
132+
},
133+
};
134+
135+
tree.create(prodLibTsConfig, JSON.stringify(tsconfig, undefined, 2));
136+
137+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
138+
config = getWorkspaceTargets(tree2).build;
139+
expect(config.configurations.production.tsConfig).toEqual(prodLibTsConfig);
140+
141+
const tsConfigContent = tree2.readContent(prodLibTsConfig);
142+
expect(JSON.parse(tsConfigContent).angularCompilerOptions).toEqual({ enableIvy: false });
143+
});
144+
});
145+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { JsonAstObject, experimental } from '@angular-devkit/core';
10+
import { findPropertyInAstObject } from '../../utility/json-utils';
11+
import { Builders, WorkspaceTargets } from '../../utility/workspace-models';
12+
13+
/** Get all workspace targets which builder and target names matches the provided. */
14+
export function getTargets(
15+
workspace: JsonAstObject | experimental.workspace.WorkspaceSchema,
16+
targetName: Exclude<keyof WorkspaceTargets, number>,
17+
builderName: Builders,
18+
): { target: JsonAstObject, project: JsonAstObject }[] {
19+
const projects = findPropertyInAstObject(workspace as JsonAstObject, 'projects');
20+
if (!projects || projects.kind !== 'object') {
21+
return [];
22+
}
23+
24+
const targets = [];
25+
for (const project of projects.properties) {
26+
const projectConfig = project.value;
27+
if (projectConfig.kind !== 'object') {
28+
continue;
29+
}
30+
31+
const projectRoot = findPropertyInAstObject(projectConfig, 'root');
32+
if (!projectRoot || projectRoot.kind !== 'string') {
33+
continue;
34+
}
35+
36+
const architect = findPropertyInAstObject(projectConfig, 'architect');
37+
if (!architect || architect.kind !== 'object') {
38+
continue;
39+
}
40+
41+
const target = findPropertyInAstObject(architect, targetName);
42+
if (!target || target.kind !== 'object') {
43+
continue;
44+
}
45+
46+
const builder = findPropertyInAstObject(target, 'builder');
47+
// Projects who's build builder is @angular-devkit/build-ng-packagr
48+
if (builder && builder.kind === 'string' && builder.value === builderName) {
49+
targets.push({ target, project: projectConfig });
50+
}
51+
}
52+
53+
return targets;
54+
}

0 commit comments

Comments
 (0)