Skip to content

Commit cbf0feb

Browse files
alan-agius4dgp1130
authored andcommittedMay 5, 2020
feat(@schematics/angular): enable stricter type checking and optimization effective coding rules
With this change we enable stricter type checking and optimization effective coding rules when using the `--strict` option. Changes in schematics - `ng-new`: A prompt for the `--strict` option was added. This option is a proxy and will be passed to the application and workspace schematics. - `application`: A `package.json` was added in the `app` folder, to tell the bundlers whether the application is free from side-effect code. When `strict` is `true`. the `sideEffects` will be set `false`. - `workspace` When `strict` is true, we add stricter TypeScript and Angular type-checking options. Note: AIO is already using these strict TypeScript compiler settings. PR to enable `strictTemplates` angular/angular#36391 Reference: TOOL-1366
1 parent 2d48ab3 commit cbf0feb

22 files changed

+192
-27
lines changed
 

‎packages/schematics/angular/application/index_spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,24 @@ describe('Application Schematic', () => {
280280
});
281281
});
282282

283+
it('sideEffects property should be true when strict mode', async () => {
284+
const options = { ...defaultOptions, projectRoot: '', strict: true };
285+
286+
const tree = await schematicRunner.runSchematicAsync('application', options, workspaceTree)
287+
.toPromise();
288+
const content = JSON.parse(tree.readContent('/src/app/package.json'));
289+
expect(content.sideEffects).toBe(false);
290+
});
291+
292+
it('sideEffects property should be false when not in strict mode', async () => {
293+
const options = { ...defaultOptions, projectRoot: '', strict: false };
294+
295+
const tree = await schematicRunner.runSchematicAsync('application', options, workspaceTree)
296+
.toPromise();
297+
const content = JSON.parse(tree.readContent('/src/app/package.json'));
298+
expect(content.sideEffects).toBe(true);
299+
});
300+
283301
describe('custom projectRoot', () => {
284302
it('should put app files in the right spot', async () => {
285303
const options = { ...defaultOptions, projectRoot: '' };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "<%= utils.dasherize(name) %>",
3+
"private": true,
4+
"description": "This is a special package.json file that is not used by package managers. It is however used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size. It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.",
5+
"sideEffects": <%= !strict %>
6+
}

‎packages/schematics/angular/application/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
"description": "When true, applies lint fixes after generating the application.",
107107
"x-user-analytics": 15
108108
},
109+
"strict": {
110+
"description": "Creates an application with stricter build optimization options.",
111+
"type": "boolean",
112+
"default": false
113+
},
109114
"legacyBrowsers": {
110115
"type": "boolean",
111116
"description": "Add support for legacy browsers like Internet Explorer using differential loading.",

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

+5
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@
8484
"version": "10.0.0-beta.3",
8585
"factory": "./update-10/update-angular-config",
8686
"description": "Remove various deprecated builders options from 'angular.json'."
87+
},
88+
"side-effects-package-json": {
89+
"version": "10.0.0-beta.3",
90+
"factory": "./update-10/side-effects-package-json",
91+
"description": "Create a special 'package.json' file that is used to tell the tools and bundlers whether the code under the app directory is free of code with non-local side-effect."
8792
}
8893
}
8994
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 { join, normalize, strings } from '@angular-devkit/core';
10+
import { Rule } from '@angular-devkit/schematics';
11+
import { getWorkspace } from '../../utility/workspace';
12+
import { ProjectType } from '../../utility/workspace-models';
13+
14+
export default function (): Rule {
15+
return async (host, context) => {
16+
const workspace = await getWorkspace(host);
17+
const logger = context.logger;
18+
19+
for (const [projectName, project] of workspace.projects) {
20+
if (project.extensions.projectType !== ProjectType.Application) {
21+
// Only interested in application projects
22+
continue;
23+
}
24+
25+
const appDir = join(normalize(project.sourceRoot || ''), 'app');
26+
const { subdirs, subfiles } = host.getDir(appDir);
27+
if (!subdirs.length && !subfiles.length) {
28+
logger.error(`Application directory '${appDir}' for project '${projectName}' doesn't exist.`);
29+
continue;
30+
}
31+
32+
const pkgJson = join(appDir, 'package.json');
33+
if (!host.exists(pkgJson)) {
34+
const pkgJsonContent = {
35+
name: strings.dasherize(projectName),
36+
private: true,
37+
description: `This is a special package.json file that is not used by package managers. It is however used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size. It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.`,
38+
sideEffects: true,
39+
};
40+
41+
host.create(pkgJson, JSON.stringify(pkgJsonContent, undefined, 2));
42+
}
43+
}
44+
};
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 { EmptyTree } from '@angular-devkit/schematics';
9+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
11+
12+
function createWorkSpaceConfig(tree: UnitTestTree) {
13+
const angularConfig: WorkspaceSchema = {
14+
version: 1,
15+
projects: {
16+
demo: {
17+
root: '',
18+
sourceRoot: 'src',
19+
projectType: ProjectType.Application,
20+
prefix: 'app',
21+
architect: {
22+
build: {
23+
builder: Builders.Browser,
24+
options: {
25+
tsConfig: '',
26+
main: '',
27+
polyfills: '',
28+
},
29+
},
30+
},
31+
},
32+
},
33+
};
34+
35+
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
36+
}
37+
38+
describe(`Migration to add sideEffects package.json`, () => {
39+
const schematicName = 'side-effects-package-json';
40+
41+
const schematicRunner = new SchematicTestRunner(
42+
'migrations',
43+
require.resolve('../migration-collection.json'),
44+
);
45+
46+
let tree: UnitTestTree;
47+
beforeEach(() => {
48+
tree = new UnitTestTree(new EmptyTree());
49+
createWorkSpaceConfig(tree);
50+
tree.create('src/app/main.ts', '');
51+
});
52+
53+
it(`should create a package.json with sideEffects true under app folder.`, async () => {
54+
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
55+
const { name, sideEffects } = JSON.parse(newTree.readContent('src/app/package.json'));
56+
expect(name).toBe('demo');
57+
expect(sideEffects).toBeTrue();
58+
});
59+
});

‎packages/schematics/angular/ng-new/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export default function(options: NgNewOptions): Rule {
5858
skipPackageJson: false,
5959
// always 'skipInstall' here, so that we do it after the move
6060
skipInstall: true,
61+
strict: options.strict,
6162
minimal: options.minimal,
6263
legacyBrowsers: options.legacyBrowsers,
6364
};

‎packages/schematics/angular/ng-new/schema.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,21 @@
118118
"x-user-analytics": 12
119119
},
120120
"createApplication": {
121-
"description": "When true (the default), creates a new initial app project in the src folder of the new workspace. When false, creates an empty workspace with no initial app. You can then use the generate application command so that all apps are created in the projects folder.",
121+
"description": "When true (the default), creates a new initial application project in the src folder of the new workspace. When false, creates an empty workspace with no initial app. You can then use the generate application command so that all apps are created in the projects folder.",
122122
"type": "boolean",
123123
"default": true
124124
},
125125
"minimal": {
126-
"description": "When true, creates a project without any testing frameworks. (Use for learning purposes only.)",
126+
"description": "When true, creates a workspace without any testing frameworks. (Use for learning purposes only.)",
127127
"type": "boolean",
128128
"default": false,
129129
"x-user-analytics": 14
130130
},
131131
"strict": {
132-
"description": "Creates a workspace with stricter TypeScript compiler options.",
132+
"description": "Creates a workspace with stricter type checking and build optimization options.",
133133
"type": "boolean",
134-
"default": false
134+
"default": false,
135+
"x-prompt": "Create a workspace with stricter type checking and more efficient production optimizations?"
135136
},
136137
"legacyBrowsers": {
137138
"type": "boolean",

‎packages/schematics/angular/workspace/files/tsconfig.json.template

+7-8
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
"compilerOptions": {
44
"baseUrl": "./",
55
"outDir": "./dist/out-tsc",<% if (strict) { %>
6-
"noImplicitAny": true,
6+
"forceConsistentCasingInFileNames": true,
7+
"strict": true,
78
"noImplicitReturns": true,
8-
"noImplicitThis": true,
9-
"noFallthroughCasesInSwitch": true,
10-
"strictNullChecks": true,<% } %>
9+
"noFallthroughCasesInSwitch": true,<% } %>
1110
"sourceMap": true,
1211
"declaration": false,
1312
"downlevelIteration": true,
@@ -20,9 +19,9 @@
2019
"es2018",
2120
"dom"
2221
]
23-
},
22+
}<% if (strict) { %>,
2423
"angularCompilerOptions": {
25-
"fullTemplateTypeCheck": true,
26-
"strictInjectionParameters": true
27-
}
24+
"strictInjectionParameters": true,
25+
"strictTemplates": true
26+
}<% } %>
2827
}

‎packages/schematics/angular/workspace/index_spec.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ describe('Workspace Schematic', () => {
7474

7575
it('should not add strict compiler options when false', async () => {
7676
const tree = await schematicRunner.runSchematicAsync('workspace', { ...defaultOptions, strict: false }).toPromise();
77-
const { compilerOptions } = JSON.parse(tree.readContent('/tsconfig.json'));
78-
expect(compilerOptions.strictNullChecks).not.toBeDefined();
77+
const { compilerOptions, angularCompilerOptions } = JSON.parse(tree.readContent('/tsconfig.json'));
78+
expect(compilerOptions.strict).toBeUndefined();
79+
expect(angularCompilerOptions).toBeUndefined();
7980
});
8081

8182
it('should not add strict compiler options when true', async () => {
8283
const tree = await schematicRunner.runSchematicAsync('workspace', { ...defaultOptions, strict: true }).toPromise();
83-
const { compilerOptions } = JSON.parse(tree.readContent('/tsconfig.json'));
84-
expect(compilerOptions.strictNullChecks).toBe(true);
84+
const { compilerOptions, angularCompilerOptions } = JSON.parse(tree.readContent('/tsconfig.json'));
85+
expect(compilerOptions.strict).toBe(true);
86+
expect(angularCompilerOptions.strictTemplates).toBe(true);
8587
});
8688
});

‎packages/schematics/angular/workspace/schema.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"x-user-analytics": 14
3434
},
3535
"strict": {
36-
"description": "Creates a workspace with stricter TypeScript compiler options.",
36+
"description": "Creates a workspace with stricter type checking options.",
3737
"type": "boolean",
3838
"default": false
3939
},

‎tests/legacy-cli/e2e/setup/500-create-project.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export default async function() {
2424

2525
if (argv['ve']) {
2626
await updateJsonFile('tsconfig.json', config => {
27-
config.angularCompilerOptions.enableIvy = false;
27+
const { angularCompilerOptions = {} } = config;
28+
angularCompilerOptions.enableIvy = false;
29+
config.angularCompilerOptions = angularCompilerOptions;
2830
});
2931

3032
// In VE non prod builds are non AOT by default

‎tests/legacy-cli/e2e/tests/basic/ivy-opt-out.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export default async function() {
2222

2323
// View Engine (NGC) compilation should work after running NGCC from Webpack
2424
await updateJsonFile('tsconfig.json', config => {
25-
config.angularCompilerOptions.enableIvy = false;
25+
const { angularCompilerOptions = {} } = config;
26+
angularCompilerOptions.enableIvy = false;
27+
config.angularCompilerOptions = angularCompilerOptions;
2628
});
2729

2830
// verify that VE compilation works during runtime
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ng } from '../../utils/process';
2+
import { createProject } from '../../utils/project';
3+
4+
export default async function() {
5+
await createProject('strict-test-project', '--strict');
6+
await ng('e2e', '--prod');
7+
}

‎tests/legacy-cli/e2e/tests/build/strict-workspace.ts

-7
This file was deleted.

‎tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export async function executeTest() {
2222

2323
await updateJsonFile('tsconfig.json', config => {
2424
config.compilerOptions.target = 'es2015';
25+
if (!config.angularCompilerOptions) {
26+
config.angularCompilerOptions = {};
27+
}
2528
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
2629
});
2730

‎tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export default async function() {
1212
await writeFile('.browserslistrc', 'Chrome 65');
1313
await updateJsonFile('tsconfig.json', config => {
1414
config.compilerOptions.target = 'es2015';
15+
if (!config.angularCompilerOptions) {
16+
config.angularCompilerOptions = {};
17+
}
1518
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
1619
});
1720

‎tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export default async function() {
1111
// Ensure a es5 build is used.
1212
await updateJsonFile('tsconfig.json', config => {
1313
config.compilerOptions.target = 'es5';
14+
if (!config.angularCompilerOptions) {
15+
config.angularCompilerOptions = {};
16+
}
1417
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
1518
});
1619

‎tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export default async function() {
3434

3535
await updateJsonFile('tsconfig.json', config => {
3636
config.compilerOptions.target = 'es2015';
37+
if (!config.angularCompilerOptions) {
38+
config.angularCompilerOptions = {};
39+
}
3740
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
3841
});
3942

‎tests/legacy-cli/e2e/tests/i18n/ve-localize-es2015.ts

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default async function() {
2525
await writeFile('.browserslistrc', 'Chrome 65');
2626
await updateJsonFile('tsconfig.json', config => {
2727
config.compilerOptions.target = 'es2015';
28+
if (!config.angularCompilerOptions) {
29+
config.angularCompilerOptions = {};
30+
}
2831
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
2932
});
3033

‎tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export default async function (skipCleaning: () => void) {
1313
await createProjectFromAsset('webpack/test-app');
1414
if (isVe) {
1515
await updateJsonFile('tsconfig.json', config => {
16-
config.angularCompilerOptions.enableIvy = false;
16+
const { angularCompilerOptions = {} } = config;
17+
angularCompilerOptions.enableIvy = false;
18+
config.angularCompilerOptions = angularCompilerOptions;
1719
});
1820
}
1921

‎tests/legacy-cli/e2e/utils/project.ts

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export async function createProject(name: string, ...args: string[]) {
4343
// Disable the TS version check to make TS updates easier.
4444
// Only VE does it, but on Ivy the i18n extraction uses VE.
4545
await updateJsonFile('tsconfig.json', config => {
46+
if (!config.angularCompilerOptions) {
47+
config.angularCompilerOptions = {};
48+
}
4649
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
4750
});
4851
}

0 commit comments

Comments
 (0)
Please sign in to comment.