Skip to content

Commit

Permalink
feat(@schematics/angular): update compiler options target and module …
Browse files Browse the repository at this point in the history
…settings

With this change we update the target and module settings of various compilation units.

- We replace ES5 target in protractor. Protractor runs on Node.Js which support ES2018
- For applications we now use `ES2020` instead of `ESNext` as a module to avoid unexpected changes in behaviour

This changes also adds a migration to update existing projects and also removes `module` from the Universal tsconfig as per #17352 to enable lazy loading on the server.
  • Loading branch information
alan-agius4 authored and dgp1130 committed May 6, 2020
1 parent 13b0763 commit 0fd3c55
Show file tree
Hide file tree
Showing 15 changed files with 283 additions and 12 deletions.
2 changes: 1 addition & 1 deletion integration/angular_cli/e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
Expand Down
2 changes: 1 addition & 1 deletion integration/angular_cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"module": "es2020",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (profilingEnabled) {
extraPlugins.push(
new debug.ProfilingPlugin({
outputPath: path.resolve(root, `chrome-profiler-events${targetInFileName}.json`),
outputPath: path.resolve(root, 'chrome-profiler-events.json'),
}),
);
}
Expand Down Expand Up @@ -303,7 +303,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
apply(compiler: Compiler) {
compiler.hooks.emit.tap('angular-cli-stats', compilation => {
const data = JSON.stringify(compilation.getStats().toJson('verbose'));
compilation.assets[`stats${targetInFileName}.json`] = new RawSource(data);
compilation.assets['stats.json'] = new RawSource(data);
});
}
})(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum ThresholdSeverity {
}

enum DifferentialBuildType {
// FIXME: this should match the actual file suffix and not hardcoded.
ORIGINAL = 'es2015',
DOWNLEVEL = 'es5',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2015",
"module": "esnext",
"module": "es2020",
"typeRoots": [
"../node_modules/@types"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"moduleResolution": "node",
"experimentalDecorators": true,
"target": "es2015",
"module": "esnext",
"module": "es2020",
"typeRoots": [
"node_modules/@types"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"module": "es2020",
"types": []
},
"exclude": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@
"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."
},
"update-module-and-target-compiler-options": {
"version": "10.0.0-beta.3",
"factory": "./update-10/update-module-and-target-compiler-options",
"description": "Update 'module' and 'target' TypeScript compiler options."
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @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 { dirname, join, normalize } from '@angular-devkit/core';
import { Rule, Tree } from '@angular-devkit/schematics';
import { findPropertyInAstObject, removePropertyInAstObject } from '../../utility/json-utils';
import { getWorkspace } from '../../utility/workspace';
import { Builders } from '../../utility/workspace-models';
import { readJsonFileAsAstObject } from '../update-9/utils';


interface ModuleAndTargetReplamenent {
oldModule?: string;
newModule?: string | false;
oldTarget?: string;
newTarget?: string;
}

export default function (): Rule {
return async host => {
// Workspace level tsconfig
updateModuleAndTarget(host, 'tsconfig.json', {
oldModule: 'esnext',
newModule: 'es2020',
});

const workspace = await getWorkspace(host);
// Find all tsconfig which are refereces used by builders
for (const [, project] of workspace.projects) {
for (const [, target] of project.targets) {
// E2E builder doesn't reference a tsconfig but it uses one found in the root folder.
if (target.builder === Builders.Protractor && typeof target.options?.protractorConfig === 'string') {
const tsConfigPath = join(dirname(normalize(target.options.protractorConfig)), 'tsconfig.json');

updateModuleAndTarget(host, tsConfigPath, {
oldTarget: 'es5',
newTarget: 'es2018',
});

continue;
}

// Update all other known CLI builders that use a tsconfig
const tsConfigs = [
target.options || {},
...Object.values(target.configurations || {}),
]
.filter(opt => typeof opt?.tsConfig === 'string')
.map(opt => (opt as { tsConfig: string }).tsConfig);

const uniqueTsConfigs = [...new Set(tsConfigs)];

if (uniqueTsConfigs.length < 1) {
continue;
}

switch (target.builder as Builders) {
case Builders.Server:
uniqueTsConfigs.forEach(p => {
updateModuleAndTarget(host, p, {
oldModule: 'commonjs',
// False will remove the module
// NB: For server we no longer use commonjs because it is bundled using webpack which has it's own module system.
// This ensures that lazy-loaded works on the server.
newModule: false,
});
});
break;
case Builders.Karma:
case Builders.Browser:
case Builders.NgPackagr:
uniqueTsConfigs.forEach(p => {
updateModuleAndTarget(host, p, {
oldModule: 'esnext',
newModule: 'es2020',
});
});
break;
}
}
}
};
}

function updateModuleAndTarget(host: Tree, tsConfigPath: string, replacements: ModuleAndTargetReplamenent) {
const jsonAst = readJsonFileAsAstObject(host, tsConfigPath);
if (!jsonAst) {
return;
}

const compilerOptionsAst = findPropertyInAstObject(jsonAst, 'compilerOptions');
if (compilerOptionsAst?.kind !== 'object') {
return;
}

const { oldTarget, newTarget, newModule, oldModule } = replacements;

const recorder = host.beginUpdate(tsConfigPath);
if (newTarget) {
const targetAst = findPropertyInAstObject(compilerOptionsAst, 'target');
if (targetAst?.kind === 'string' && oldTarget === targetAst.value.toLowerCase()) {
const offset = targetAst.start.offset + 1;
recorder.remove(offset, targetAst.value.length);
recorder.insertLeft(offset, newTarget);
}
}

if (newModule === false) {
removePropertyInAstObject(recorder, compilerOptionsAst, 'module');
} else if (newModule) {
const moduleAst = findPropertyInAstObject(compilerOptionsAst, 'module');
if (moduleAst?.kind === 'string' && oldModule === moduleAst.value.toLowerCase()) {
const offset = moduleAst.start.offset + 1;
recorder.remove(offset, moduleAst.value.length);
recorder.insertLeft(offset, newModule);
}
}

host.commitUpdate(recorder);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @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, parseJson } from '@angular-devkit/core';
import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';

describe('Migration to update target and module compiler options', () => {
const schematicName = 'update-module-and-target-compiler-options';

const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);

function createJsonFile(tree: UnitTestTree, filePath: string, content: {}) {
tree.create(filePath, JSON.stringify(content, undefined, 2));
}

// tslint:disable-next-line: no-any
function readJsonFile(tree: UnitTestTree, filePath: string): any {
// tslint:disable-next-line: no-any
return parseJson(tree.readContent(filePath).toString(), JsonParseMode.Loose) as any;
}

function createWorkSpaceConfig(tree: UnitTestTree) {
const angularConfig: WorkspaceSchema = {
version: 1,
projects: {
app: {
root: '',
sourceRoot: 'src',
projectType: ProjectType.Application,
prefix: 'app',
architect: {
build: {
builder: Builders.Browser,
options: {
tsConfig: 'src/tsconfig.app.json',
main: '',
polyfills: '',
},
configurations: {
production: {
tsConfig: 'src/tsconfig.app.prod.json',
},
},
},
test: {
builder: Builders.Karma,
options: {
karmaConfig: '',
tsConfig: 'src/tsconfig.spec.json',
},
},
e2e: {
builder: Builders.Protractor,
options: {
protractorConfig: 'src/e2e/protractor.conf.js',
devServerTarget: '',
},
},
server: {
builder: Builders.Server,
options: {
tsConfig: 'src/tsconfig.server.json',
outputPath: '',
main: '',
},
},
},
},
},
};

createJsonFile(tree, 'angular.json', angularConfig);
}


let tree: UnitTestTree;
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
createWorkSpaceConfig(tree);

// Create tsconfigs
const compilerOptions = { target: 'es2015', module: 'esnext' };

// Workspace
createJsonFile(tree, 'tsconfig.json', { compilerOptions });

// Application
createJsonFile(tree, 'src/tsconfig.app.json', { compilerOptions });
createJsonFile(tree, 'src/tsconfig.app.prod.json', { compilerOptions });
createJsonFile(tree, 'src/tsconfig.spec.json', { compilerOptions });

// E2E
createJsonFile(tree, 'src/e2e/protractor.conf.js', '');
createJsonFile(tree, 'src/e2e/tsconfig.json', { compilerOptions: { module: 'commonjs', target: 'es5' } });

// Universal
createJsonFile(tree, 'src/tsconfig.server.json', { compilerOptions: { module: 'commonjs' } });
});

it(`should update module and target in workspace 'tsconfig.json'`, async () => {
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
const { module } = readJsonFile(newTree, 'tsconfig.json').compilerOptions;
expect(module).toBe('es2020');
});

it(`should update module and target in 'tsconfig.json' which is referenced in option`, async () => {
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
const { module } = readJsonFile(newTree, 'src/tsconfig.spec.json').compilerOptions;
expect(module).toBe('es2020');
});

it(`should update module and target in 'tsconfig.json' which is referenced in a configuration`, async () => {
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
const { module } = readJsonFile(newTree, 'src/tsconfig.app.prod.json').compilerOptions;
expect(module).toBe('es2020');
});

it(`should update target to es2018 in E2E 'tsconfig.json'`, async () => {
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
const { module, target } = readJsonFile(newTree, 'src/e2e/tsconfig.json').compilerOptions;
expect(module).toBe('commonjs');
expect(target).toBe('es2018');
});


it(`should remove module in 'tsconfig.server.json'`, async () => {
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
const { module, target } = readJsonFile(newTree, 'src/tsconfig.server.json').compilerOptions;
expect(module).toBeUndefined();
expect(target).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"module": "es2020",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
Expand Down

0 comments on commit 0fd3c55

Please sign in to comment.