Skip to content

feat(@schematics/angular): update compiler options target and module settings #17637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -193,7 +193,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 @@ -302,7 +302,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