Skip to content

Commit 1824394

Browse files
committed
feat(@schematics/angular): add an update to 7.0.0-rc.0 to move polyfills
Reflect metadata Polyfills are not needed in prod if the user is using Angular AOT. This update does nothing if the project is not what we expect.
1 parent fe2cdac commit 1824394

File tree

4 files changed

+314
-1
lines changed

4 files changed

+314
-1
lines changed

packages/schematics/angular/migrations/migration-collection.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"version": "7.0.0-beta.0",
1010
"factory": "./update-7",
1111
"description": "Update an Angular CLI project to version 7."
12+
},
13+
"migration-03": {
14+
"version": "7.0.0-rc.0",
15+
"factory": "./update-7/index#polyfillMetadataRule",
16+
"description": "Update an Angular CLI project to version 7."
1217
}
1318
}
14-
}
19+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../../utility/dependencies';
1414
import { latestVersions } from '../../utility/latest-versions';
1515

16+
export { polyfillMetadataRule } from './polyfill-metadata';
1617

1718
export default function(): Rule {
1819
return (tree, context) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 { json } from '@angular-devkit/core';
9+
import { Rule, Tree, chain, noop } from '@angular-devkit/schematics';
10+
import * as ts from 'typescript';
11+
12+
13+
/**
14+
* Content of the import to add.
15+
*/
16+
const content = `
17+
// Used for reflect-metadata in JIT. For size and performance reasons, this should not be imported
18+
// in AOT unless absolutely needed. All Angular metadata is removed in AOT so this is only needed,
19+
// by default, in development.
20+
21+
`;
22+
const es6Import = `import 'core-js/es6/reflect';
23+
`;
24+
const es7Import = `import 'core-js/es7/reflect';
25+
`;
26+
27+
28+
/**
29+
* Remove the Reflect import from a polyfill file.
30+
* @param tree The tree to use.
31+
* @param path Path of the polyfill file found.
32+
* @private
33+
*/
34+
function _removeReflectFromPolyfills(tree: Tree, path: string): { es6: boolean, es7: boolean } {
35+
const source = tree.read(path);
36+
if (!source) {
37+
return { es6: false, es7: false };
38+
}
39+
40+
// Start the update of the file.
41+
const recorder = tree.beginUpdate(path);
42+
43+
const sourceFile = ts.createSourceFile(path, source.toString(), ts.ScriptTarget.Latest);
44+
const imports = (
45+
sourceFile.statements
46+
.filter(s => s.kind === ts.SyntaxKind.ImportDeclaration) as ts.ImportDeclaration[]
47+
);
48+
49+
const result = { es6: false, es7: false };
50+
for (const i of imports) {
51+
const module = i.moduleSpecifier.kind == ts.SyntaxKind.StringLiteral
52+
&& (i.moduleSpecifier as ts.StringLiteral).text;
53+
54+
switch (module) {
55+
case 'core-js/es6/reflect':
56+
recorder.remove(i.getStart(sourceFile), i.getWidth(sourceFile));
57+
result.es6 = true;
58+
break;
59+
60+
case 'core-js/es7/reflect':
61+
recorder.remove(i.getStart(sourceFile), i.getWidth(sourceFile));
62+
result.es7 = true;
63+
break;
64+
}
65+
}
66+
67+
tree.commitUpdate(recorder);
68+
69+
return result;
70+
}
71+
72+
/**
73+
* Add the reflect import to an environment file.
74+
* @param tree The tree to use.
75+
* @param path The path of the environment file.
76+
* @param es6 Whether to import the es6 reflect polyfill.
77+
* @param es7 Whether to import the es7 reflect polyfill.
78+
* @private
79+
*/
80+
function _addReflectToEnvironment(tree: Tree, path: string, es6: boolean, es7: boolean) {
81+
if (!es6 && !es7) {
82+
return;
83+
}
84+
85+
const source = tree.read(path);
86+
if (!source) {
87+
return;
88+
}
89+
90+
const recorder = tree.beginUpdate(path);
91+
92+
// After whatever imports are present as the header, or as the first line of the file.
93+
const sourceFile = ts.createSourceFile(path, source.toString(), ts.ScriptTarget.Latest);
94+
const imports = (
95+
sourceFile.statements
96+
.filter(s => s.kind === ts.SyntaxKind.ImportDeclaration) as ts.ImportDeclaration[]
97+
);
98+
99+
let index = 0;
100+
if (imports.length > 0) {
101+
index = imports[imports.length - 1].getEnd();
102+
}
103+
104+
recorder.insertLeft(index, content);
105+
if (es6) {
106+
recorder.insertLeft(index, es6Import);
107+
}
108+
if (es7) {
109+
recorder.insertLeft(index, es7Import);
110+
}
111+
tree.commitUpdate(recorder);
112+
}
113+
114+
/**
115+
* Update a project's target, maybe. Only if it's a builder supported and the options look right.
116+
* This is a rule factory so we return the new rule (or noop if we don't support doing the change).
117+
* @param root The root of the project source.
118+
* @param targetObject The target information.
119+
* @private
120+
*/
121+
function _updateProjectTarget(root: string, targetObject: json.JsonObject): Rule {
122+
// Make sure we're using the correct builder.
123+
if (targetObject.builder !== '@angular-devkit/build-angular:browser'
124+
|| !json.isJsonObject(targetObject.options)) {
125+
return noop();
126+
}
127+
const options = targetObject.options;
128+
if (typeof options.polyfills != 'string') {
129+
return noop();
130+
}
131+
132+
return tree => {
133+
const polyfillPath = `${root}/${options.polyfills}`;
134+
135+
// Try to find the environment file to update. If we can find it we abort the process.
136+
let envPath: string | null;
137+
let envContent: string | null;
138+
139+
// Default path?
140+
envPath = `${root}/src/environments/environment.ts`;
141+
envContent = (tree.read(envPath) || '').toString();
142+
143+
if (!envPath || !envContent) {
144+
// Don't remove it from the polyfill.
145+
return;
146+
}
147+
148+
const result = _removeReflectFromPolyfills(tree, polyfillPath);
149+
_addReflectToEnvironment(tree, envPath, result.es6, result.es7);
150+
};
151+
}
152+
153+
/**
154+
* Move the import reflect metadata polyfill from the polyfill file to the dev environment. This is
155+
* not guaranteed to work, but if it doesn't it will result in no changes made.
156+
*/
157+
export function polyfillMetadataRule(): Rule {
158+
return (tree) => {
159+
debugger;
160+
// Simple. Take the ast of polyfills (if it exists) and find the import metadata. Remove it.
161+
const angularConfigContent = tree.read('angular.json') || tree.read('.angular.json');
162+
const rules: Rule[] = [];
163+
164+
if (!angularConfigContent) {
165+
// Is this even an angular project?
166+
return;
167+
}
168+
169+
const angularJson = json.parseJson(angularConfigContent.toString(), json.JsonParseMode.Loose);
170+
171+
if (!json.isJsonObject(angularJson) || !json.isJsonObject(angularJson.projects)) {
172+
// If that field isn't there, no use...
173+
return;
174+
}
175+
176+
// For all projects, for all targets, read the polyfill field, and read the environment.
177+
for (const projectName of Object.keys(angularJson.projects)) {
178+
const project = angularJson.projects[projectName];
179+
if (!json.isJsonObject(project)) {
180+
continue;
181+
}
182+
if (typeof project.root != 'string') {
183+
continue;
184+
}
185+
186+
const targets = project.targets || project.architect;
187+
if (!json.isJsonObject(targets)) {
188+
continue;
189+
}
190+
191+
for (const targetName of Object.keys(targets)) {
192+
const target = targets[targetName];
193+
if (json.isJsonObject(target)) {
194+
rules.push(_updateProjectTarget(project.root, target));
195+
}
196+
}
197+
}
198+
199+
// Remove null or undefined rules.
200+
return chain(rules);
201+
};
202+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 { Action, EmptyTree } from '@angular-devkit/schematics';
9+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
11+
// Randomly import stuff (including es6 and es7 reflects).
12+
const oldPolyfills = `
13+
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
14+
// import 'core-js/es6/symbol';
15+
// import 'core-js/es6/object';
16+
import 'core-js/es6/function';
17+
import 'core-js/es6/parse-int';
18+
// import 'core-js/es6/parse-float';
19+
import 'core-js/es6/number';
20+
// import 'core-js/es6/math';
21+
// import 'core-js/es6/string';
22+
import 'core-js/es6/date';
23+
// import 'core-js/es6/array';
24+
// import 'core-js/es6/regexp';
25+
26+
/** IE10 and IE11 requires the following for the Reflect API. */
27+
import 'core-js/es6/reflect';
28+
29+
30+
/** Evergreen browsers require these. **/
31+
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
32+
import 'core-js/es7/reflect';
33+
34+
import 'web-animations-js'; // Run \`npm install --save web-animations-js\`.
35+
36+
(window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
37+
(window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
38+
39+
/***************************************************************************************************
40+
* Zone JS is required by default for Angular itself.
41+
*/
42+
import 'zone.js/dist/zone'; // Included with Angular CLI.
43+
`;
44+
45+
const oldEnvironment = `
46+
// This file can be replaced during build by using the \`fileReplacements\` array.
47+
// \`ng build --prod\` replaces \`environment.ts\` with \`environment.prod.ts\`.
48+
// The list of file replacements can be found in \`angular.json\`.
49+
50+
export const environment = {
51+
production: false
52+
};
53+
54+
import 'zone.js/dist/zone-error'; // Included with Angular CLI.
55+
`;
56+
57+
describe('polyfillMetadataRule', () => {
58+
const schematicRunner = new SchematicTestRunner(
59+
'migrations',
60+
require.resolve('../migration-collection.json'),
61+
);
62+
63+
let tree: UnitTestTree;
64+
65+
beforeEach(async () => {
66+
tree = new UnitTestTree(new EmptyTree());
67+
tree = await schematicRunner.runExternalSchematicAsync(
68+
'@schematics/angular', 'ng-new',
69+
{
70+
name: 'migration-test',
71+
version: '1.2.3',
72+
directory: '.',
73+
},
74+
tree,
75+
).toPromise();
76+
});
77+
78+
it('is noop for new projects', async () => {
79+
const mapToIdem = (x: Action) => {
80+
const content = (x.kind == 'o' || x.kind == 'c') ? x.content.toString() : null;
81+
82+
return { ...x, content, id: -1 };
83+
};
84+
85+
const expected = [...tree.actions.map(mapToIdem)];
86+
const tree2 = await schematicRunner.runSchematicAsync('migration-03', {}, tree.branch())
87+
.toPromise();
88+
89+
expect(tree2.actions.map(mapToIdem)).toEqual(expected);
90+
});
91+
92+
it('should work as expected', async () => {
93+
const polyfillPath = '/src/polyfills.ts';
94+
const envPath = '/src/environments/environment.ts';
95+
tree.overwrite(polyfillPath, oldPolyfills);
96+
tree.overwrite(envPath, oldEnvironment);
97+
const tree2 = await schematicRunner.runSchematicAsync('migration-03', {}, tree.branch())
98+
.toPromise();
99+
100+
expect(tree2.readContent(polyfillPath)).not.toMatch(/import .*es6.*reflect.*;/);
101+
expect(tree2.readContent(polyfillPath)).not.toMatch(/import .*es7.*reflect.*;/);
102+
expect(tree2.readContent(envPath)).toMatch(/import .*es7.*reflect.*;/);
103+
expect(tree2.readContent(envPath)).toMatch(/import .*es6.*reflect.*;/);
104+
});
105+
});

0 commit comments

Comments
 (0)