diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 595721575202..f1d2865b44e0 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -280,6 +280,24 @@ describe('Application Schematic', () => { }); }); + it('sideEffects property should be true when strict mode', async () => { + const options = { ...defaultOptions, projectRoot: '', strict: true }; + + const tree = await schematicRunner.runSchematicAsync('application', options, workspaceTree) + .toPromise(); + const content = JSON.parse(tree.readContent('/src/app/package.json')); + expect(content.sideEffects).toBe(false); + }); + + it('sideEffects property should be false when not in strict mode', async () => { + const options = { ...defaultOptions, projectRoot: '', strict: false }; + + const tree = await schematicRunner.runSchematicAsync('application', options, workspaceTree) + .toPromise(); + const content = JSON.parse(tree.readContent('/src/app/package.json')); + expect(content.sideEffects).toBe(true); + }); + describe('custom projectRoot', () => { it('should put app files in the right spot', async () => { const options = { ...defaultOptions, projectRoot: '' }; diff --git a/packages/schematics/angular/application/other-files/package.json.template b/packages/schematics/angular/application/other-files/package.json.template new file mode 100644 index 000000000000..4fa4ff5824e5 --- /dev/null +++ b/packages/schematics/angular/application/other-files/package.json.template @@ -0,0 +1,6 @@ +{ + "name": "<%= utils.dasherize(name) %>", + "private": true, + "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.", + "sideEffects": <%= !strict %> +} diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index abec92b6a0a7..0809ecc1ac43 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -106,6 +106,11 @@ "description": "When true, applies lint fixes after generating the application.", "x-user-analytics": 15 }, + "strict": { + "description": "Creates an application with stricter build optimization options.", + "type": "boolean", + "default": false + }, "legacyBrowsers": { "type": "boolean", "description": "Add support for legacy browsers like Internet Explorer using differential loading.", diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index b5d43ef0b88b..cd51f7fcca06 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -84,6 +84,11 @@ "version": "10.0.0-beta.3", "factory": "./update-10/update-angular-config", "description": "Remove various deprecated builders options from 'angular.json'." + }, + "side-effects-package-json": { + "version": "10.0.0-beta.3", + "factory": "./update-10/side-effects-package-json", + "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." } } } diff --git a/packages/schematics/angular/migrations/update-10/side-effects-package-json.ts b/packages/schematics/angular/migrations/update-10/side-effects-package-json.ts new file mode 100644 index 000000000000..6a5df4305807 --- /dev/null +++ b/packages/schematics/angular/migrations/update-10/side-effects-package-json.ts @@ -0,0 +1,45 @@ +/** + * @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 { join, normalize, strings } from '@angular-devkit/core'; +import { Rule } from '@angular-devkit/schematics'; +import { getWorkspace } from '../../utility/workspace'; +import { ProjectType } from '../../utility/workspace-models'; + +export default function (): Rule { + return async (host, context) => { + const workspace = await getWorkspace(host); + const logger = context.logger; + + for (const [projectName, project] of workspace.projects) { + if (project.extensions.projectType !== ProjectType.Application) { + // Only interested in application projects + continue; + } + + const appDir = join(normalize(project.sourceRoot || ''), 'app'); + const { subdirs, subfiles } = host.getDir(appDir); + if (!subdirs.length && !subfiles.length) { + logger.error(`Application directory '${appDir}' for project '${projectName}' doesn't exist.`); + continue; + } + + const pkgJson = join(appDir, 'package.json'); + if (!host.exists(pkgJson)) { + const pkgJsonContent = { + name: strings.dasherize(projectName), + private: true, + 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.`, + sideEffects: true, + }; + + host.create(pkgJson, JSON.stringify(pkgJsonContent, undefined, 2)); + } + } + }; +} diff --git a/packages/schematics/angular/migrations/update-10/side-effects-package-json_spec.ts b/packages/schematics/angular/migrations/update-10/side-effects-package-json_spec.ts new file mode 100644 index 000000000000..309648473540 --- /dev/null +++ b/packages/schematics/angular/migrations/update-10/side-effects-package-json_spec.ts @@ -0,0 +1,59 @@ +/** + * @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 { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +function createWorkSpaceConfig(tree: UnitTestTree) { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + demo: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + build: { + builder: Builders.Browser, + options: { + tsConfig: '', + main: '', + polyfills: '', + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); +} + +describe(`Migration to add sideEffects package.json`, () => { + const schematicName = 'side-effects-package-json'; + + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + createWorkSpaceConfig(tree); + tree.create('src/app/main.ts', ''); + }); + + it(`should create a package.json with sideEffects true under app folder.`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { name, sideEffects } = JSON.parse(newTree.readContent('src/app/package.json')); + expect(name).toBe('demo'); + expect(sideEffects).toBeTrue(); + }); +}); diff --git a/packages/schematics/angular/ng-new/index.ts b/packages/schematics/angular/ng-new/index.ts index 3aafe41867ad..bf9c0548fd54 100644 --- a/packages/schematics/angular/ng-new/index.ts +++ b/packages/schematics/angular/ng-new/index.ts @@ -58,6 +58,7 @@ export default function(options: NgNewOptions): Rule { skipPackageJson: false, // always 'skipInstall' here, so that we do it after the move skipInstall: true, + strict: options.strict, minimal: options.minimal, legacyBrowsers: options.legacyBrowsers, }; diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index cd6f4dbd65c9..d48d2fd291fd 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -118,20 +118,21 @@ "x-user-analytics": 12 }, "createApplication": { - "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.", + "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.", "type": "boolean", "default": true }, "minimal": { - "description": "When true, creates a project without any testing frameworks. (Use for learning purposes only.)", + "description": "When true, creates a workspace without any testing frameworks. (Use for learning purposes only.)", "type": "boolean", "default": false, "x-user-analytics": 14 }, "strict": { - "description": "Creates a workspace with stricter TypeScript compiler options.", + "description": "Creates a workspace with stricter type checking and build optimization options.", "type": "boolean", - "default": false + "default": false, + "x-prompt": "Create a workspace with stricter type checking and more efficient production optimizations?" }, "legacyBrowsers": { "type": "boolean", diff --git a/packages/schematics/angular/workspace/files/tsconfig.json.template b/packages/schematics/angular/workspace/files/tsconfig.json.template index 3d26cd364b2c..ea8a5c4530a1 100644 --- a/packages/schematics/angular/workspace/files/tsconfig.json.template +++ b/packages/schematics/angular/workspace/files/tsconfig.json.template @@ -3,11 +3,10 @@ "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc",<% if (strict) { %> - "noImplicitAny": true, + "forceConsistentCasingInFileNames": true, + "strict": true, "noImplicitReturns": true, - "noImplicitThis": true, - "noFallthroughCasesInSwitch": true, - "strictNullChecks": true,<% } %> + "noFallthroughCasesInSwitch": true,<% } %> "sourceMap": true, "declaration": false, "downlevelIteration": true, @@ -20,9 +19,9 @@ "es2018", "dom" ] - }, + }<% if (strict) { %>, "angularCompilerOptions": { - "fullTemplateTypeCheck": true, - "strictInjectionParameters": true - } + "strictInjectionParameters": true, + "strictTemplates": true + }<% } %> } diff --git a/packages/schematics/angular/workspace/index_spec.ts b/packages/schematics/angular/workspace/index_spec.ts index b91840fda6e5..1d722a8334b5 100644 --- a/packages/schematics/angular/workspace/index_spec.ts +++ b/packages/schematics/angular/workspace/index_spec.ts @@ -74,13 +74,15 @@ describe('Workspace Schematic', () => { it('should not add strict compiler options when false', async () => { const tree = await schematicRunner.runSchematicAsync('workspace', { ...defaultOptions, strict: false }).toPromise(); - const { compilerOptions } = JSON.parse(tree.readContent('/tsconfig.json')); - expect(compilerOptions.strictNullChecks).not.toBeDefined(); + const { compilerOptions, angularCompilerOptions } = JSON.parse(tree.readContent('/tsconfig.json')); + expect(compilerOptions.strict).toBeUndefined(); + expect(angularCompilerOptions).toBeUndefined(); }); it('should not add strict compiler options when true', async () => { const tree = await schematicRunner.runSchematicAsync('workspace', { ...defaultOptions, strict: true }).toPromise(); - const { compilerOptions } = JSON.parse(tree.readContent('/tsconfig.json')); - expect(compilerOptions.strictNullChecks).toBe(true); + const { compilerOptions, angularCompilerOptions } = JSON.parse(tree.readContent('/tsconfig.json')); + expect(compilerOptions.strict).toBe(true); + expect(angularCompilerOptions.strictTemplates).toBe(true); }); }); diff --git a/packages/schematics/angular/workspace/schema.json b/packages/schematics/angular/workspace/schema.json index c0a44d3599b9..7a4385ac6841 100644 --- a/packages/schematics/angular/workspace/schema.json +++ b/packages/schematics/angular/workspace/schema.json @@ -33,7 +33,7 @@ "x-user-analytics": 14 }, "strict": { - "description": "Creates a workspace with stricter TypeScript compiler options.", + "description": "Creates a workspace with stricter type checking options.", "type": "boolean", "default": false }, diff --git a/tests/legacy-cli/e2e/setup/500-create-project.ts b/tests/legacy-cli/e2e/setup/500-create-project.ts index 43bb0c8b9015..c166b8e122de 100644 --- a/tests/legacy-cli/e2e/setup/500-create-project.ts +++ b/tests/legacy-cli/e2e/setup/500-create-project.ts @@ -24,7 +24,9 @@ export default async function() { if (argv['ve']) { await updateJsonFile('tsconfig.json', config => { - config.angularCompilerOptions.enableIvy = false; + const { angularCompilerOptions = {} } = config; + angularCompilerOptions.enableIvy = false; + config.angularCompilerOptions = angularCompilerOptions; }); // In VE non prod builds are non AOT by default diff --git a/tests/legacy-cli/e2e/tests/basic/ivy-opt-out.ts b/tests/legacy-cli/e2e/tests/basic/ivy-opt-out.ts index aad19225ce3b..5c6e0c913dc9 100644 --- a/tests/legacy-cli/e2e/tests/basic/ivy-opt-out.ts +++ b/tests/legacy-cli/e2e/tests/basic/ivy-opt-out.ts @@ -22,7 +22,9 @@ export default async function() { // View Engine (NGC) compilation should work after running NGCC from Webpack await updateJsonFile('tsconfig.json', config => { - config.angularCompilerOptions.enableIvy = false; + const { angularCompilerOptions = {} } = config; + angularCompilerOptions.enableIvy = false; + config.angularCompilerOptions = angularCompilerOptions; }); // verify that VE compilation works during runtime diff --git a/tests/legacy-cli/e2e/tests/build/strict-mode.ts b/tests/legacy-cli/e2e/tests/build/strict-mode.ts new file mode 100644 index 000000000000..7d223001a494 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/build/strict-mode.ts @@ -0,0 +1,7 @@ +import { ng } from '../../utils/process'; +import { createProject } from '../../utils/project'; + +export default async function() { + await createProject('strict-test-project', '--strict'); + await ng('e2e', '--prod'); +} diff --git a/tests/legacy-cli/e2e/tests/build/strict-workspace.ts b/tests/legacy-cli/e2e/tests/build/strict-workspace.ts deleted file mode 100644 index 26780a6c6286..000000000000 --- a/tests/legacy-cli/e2e/tests/build/strict-workspace.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {ng} from '../../utils/process'; -import { createProject } from '../../utils/project'; - -export default async function() { - await createProject('strict-workspace-test-project', '--strict'); - await ng('build'); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts index cd7f58263e17..3a3efc1e087f 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts @@ -22,6 +22,9 @@ export async function executeTest() { await updateJsonFile('tsconfig.json', config => { config.compilerOptions.target = 'es2015'; + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts index 5ee1aa36434c..fcb41e30d8b5 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts @@ -12,6 +12,9 @@ export default async function() { await writeFile('.browserslistrc', 'Chrome 65'); await updateJsonFile('tsconfig.json', config => { config.compilerOptions.target = 'es2015'; + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts index e549ae616f55..223100b20292 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts @@ -11,6 +11,9 @@ export default async function() { // Ensure a es5 build is used. await updateJsonFile('tsconfig.json', config => { config.compilerOptions.target = 'es5'; + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts index 24536c5544c6..242b878054fe 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts @@ -34,6 +34,9 @@ export default async function() { await updateJsonFile('tsconfig.json', config => { config.compilerOptions.target = 'es2015'; + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); diff --git a/tests/legacy-cli/e2e/tests/i18n/ve-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ve-localize-es2015.ts index 9b92078b2b2b..19d5a45778ed 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ve-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ve-localize-es2015.ts @@ -25,6 +25,9 @@ export default async function() { await writeFile('.browserslistrc', 'Chrome 65'); await updateJsonFile('tsconfig.json', config => { config.compilerOptions.target = 'es2015'; + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); diff --git a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts b/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts index ab38d87e462c..33d0e6eafe25 100644 --- a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts +++ b/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts @@ -13,7 +13,9 @@ export default async function (skipCleaning: () => void) { await createProjectFromAsset('webpack/test-app'); if (isVe) { await updateJsonFile('tsconfig.json', config => { - config.angularCompilerOptions.enableIvy = false; + const { angularCompilerOptions = {} } = config; + angularCompilerOptions.enableIvy = false; + config.angularCompilerOptions = angularCompilerOptions; }); } diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index 1829ed287b9c..397ae7d5f860 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -43,6 +43,9 @@ export async function createProject(name: string, ...args: string[]) { // Disable the TS version check to make TS updates easier. // Only VE does it, but on Ivy the i18n extraction uses VE. await updateJsonFile('tsconfig.json', config => { + if (!config.angularCompilerOptions) { + config.angularCompilerOptions = {}; + } config.angularCompilerOptions.disableTypeScriptVersionCheck = true; }); }