From c646e170e6a4218b1aec142e6c4c69940a7ea12c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 7 Nov 2019 12:51:32 +0100 Subject: [PATCH] fix(@schematics/angular): migrate module compiler option This migration coverts the TypeScript `module` compiler option to `esnext` or `commonjs` which is required when using `import()`. Fixes: #16094 --- .../update-9/update-app-tsconfigs.ts | 54 +++++++++++-- .../update-9/update-app-tsconfigs_spec.ts | 76 +++++++++++++++++++ 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/packages/schematics/angular/migrations/update-9/update-app-tsconfigs.ts b/packages/schematics/angular/migrations/update-9/update-app-tsconfigs.ts index bac0071376c0..05f6d47e40e6 100644 --- a/packages/schematics/angular/migrations/update-9/update-app-tsconfigs.ts +++ b/packages/schematics/angular/migrations/update-9/update-app-tsconfigs.ts @@ -20,21 +20,26 @@ import { getAllOptions, getTargets, getWorkspace, readJsonFileAsAstObject } from * Update the tsconfig files for applications * - Removes enableIvy: true * - Sets stricter file inclusions + * - Sets module compiler option to esnext or commonjs */ export function updateApplicationTsConfigs(): Rule { return (tree, context) => { const workspace = getWorkspace(tree); + const logger = context.logger; + + // Add `module` option in the workspace tsconfig + updateModuleCompilerOption(tree, '/tsconfig.json'); for (const { target } of getTargets(workspace, 'build', Builders.Browser)) { - updateTsConfig(tree, target, Builders.Browser, context.logger); + updateTsConfig(tree, target, Builders.Browser, logger); } for (const { target } of getTargets(workspace, 'server', Builders.Server)) { - updateTsConfig(tree, target, Builders.Server, context.logger); + updateTsConfig(tree, target, Builders.Server, logger); } for (const { target } of getTargets(workspace, 'test', Builders.Karma)) { - updateTsConfig(tree, target, Builders.Karma, context.logger); + updateTsConfig(tree, target, Builders.Karma, logger); } return tree; @@ -74,6 +79,9 @@ function updateTsConfig(tree: Tree, builderConfig: JsonAstObject, builderName: B } } + // Update 'module' compilerOption + updateModuleCompilerOption(tree, tsConfigPath, builderName); + // Add stricter file inclusions to avoid unused file warning during compilation if (builderName !== Builders.Karma) { // Note: we need to re-read the tsconfig after very commit because @@ -81,7 +89,7 @@ function updateTsConfig(tree: Tree, builderConfig: JsonAstObject, builderName: B // we are already checking that tsconfig exists above! // tslint:disable-next-line: no-non-null-assertion - tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath) !; + tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath)!; const include = findPropertyInAstObject(tsConfigAst, 'include'); if (include && include.kind === 'array') { @@ -113,17 +121,51 @@ function updateTsConfig(tree: Tree, builderConfig: JsonAstObject, builderName: B if (newFiles.length) { recorder = tree.beginUpdate(tsConfigPath); // tslint:disable-next-line: no-non-null-assertion - tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath) !; + tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath)!; insertPropertyInAstObjectInOrder(recorder, tsConfigAst, 'files', newFiles, 2); tree.commitUpdate(recorder); } recorder = tree.beginUpdate(tsConfigPath); // tslint:disable-next-line: no-non-null-assertion - tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath) !; + tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath)!; removePropertyInAstObject(recorder, tsConfigAst, 'exclude'); tree.commitUpdate(recorder); } } } } + +function updateModuleCompilerOption(tree: Tree, tsConfigPath: string, builderName?: Builders) { + const tsConfigAst = readJsonFileAsAstObject(tree, tsConfigPath); + + if (!tsConfigAst) { + return; + } + + const compilerOptions = findPropertyInAstObject(tsConfigAst, 'compilerOptions'); + if (!compilerOptions || compilerOptions.kind !== 'object') { + return; + } + + const configExtends = findPropertyInAstObject(tsConfigAst, 'extends'); + const isExtendedConfig = configExtends && configExtends.kind === 'string'; + const recorder = tree.beginUpdate(tsConfigPath); + + // Server tsconfig should have a module of commonjs + const moduleType = builderName === Builders.Server ? 'commonjs' : 'esnext'; + if (isExtendedConfig && builderName !== Builders.Server) { + removePropertyInAstObject(recorder, compilerOptions, 'module'); + } else { + const scriptModule = findPropertyInAstObject(compilerOptions, 'module'); + if (!scriptModule) { + insertPropertyInAstObjectInOrder(recorder, compilerOptions, 'module', moduleType, 4); + } else if (scriptModule.value !== moduleType) { + const { start, end } = scriptModule; + recorder.remove(start.offset, end.offset - start.offset); + recorder.insertLeft(start.offset, `"${moduleType}"`); + } + } + + tree.commitUpdate(recorder); +} diff --git a/packages/schematics/angular/migrations/update-9/update-app-tsconfigs_spec.ts b/packages/schematics/angular/migrations/update-9/update-app-tsconfigs_spec.ts index 83359f2778ca..8baa6d31fc28 100644 --- a/packages/schematics/angular/migrations/update-9/update-app-tsconfigs_spec.ts +++ b/packages/schematics/angular/migrations/update-9/update-app-tsconfigs_spec.ts @@ -17,6 +17,7 @@ function overrideJsonFile(tree: UnitTestTree, path: string, newContent: object) const defaultTsConfigOptions = { extends: './tsconfig.json', compilerOptions: { + module: 'es2015', outDir: './out-tsc/app', types: [], }, @@ -146,5 +147,80 @@ describe('Migration to version 9', () => { expect(angularCompilerOptions.enableIvy).toBe(false); expect(angularCompilerOptions.fullTemplateTypeCheck).toBe(true); }); + + it(`should remove 'module' if in an extended configuration`, async () => { + const tsConfigContent = { + ...defaultTsConfigOptions, + angularCompilerOptions: { + enableIvy: true, + fullTemplateTypeCheck: true, + }, + }; + + overrideJsonFile(tree, 'tsconfig.app.json', tsConfigContent); + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const { compilerOptions } = JSON.parse(tree2.readContent('tsconfig.app.json')); + expect(compilerOptions.module).toBeUndefined(); + + const { compilerOptions: workspaceCompilerOptions } = JSON.parse(tree2.readContent('tsconfig.json')); + expect(workspaceCompilerOptions.module).toBe('esnext'); + }); + + it(`should set 'module' to 'esnext' if app tsconfig is not extended`, async () => { + const tsConfigContent = { + ...defaultTsConfigOptions, + extends: undefined, + angularCompilerOptions: { + enableIvy: true, + fullTemplateTypeCheck: true, + }, + }; + + overrideJsonFile(tree, 'tsconfig.app.json', tsConfigContent); + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const { compilerOptions } = JSON.parse(tree2.readContent('tsconfig.app.json')); + expect(compilerOptions.module).toBe('esnext'); + }); + + it(`should set 'module' to 'commonjs' in server tsconfig`, async () => { + const tsConfigContent = { + ...defaultTsConfigOptions, + compilerOptions: { + module: 'es2015', + outDir: './out-tsc/server', + }, + angularCompilerOptions: { + enableIvy: true, + }, + }; + + tree = await schematicRunner + .runExternalSchematicAsync( + require.resolve('../../collection.json'), + 'universal', + { + clientProject: 'migration-test', + }, + tree, + ) + .toPromise(); + + overrideJsonFile(tree, 'tsconfig.server.json', tsConfigContent); + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const { compilerOptions } = JSON.parse(tree2.readContent('tsconfig.server.json')); + expect(compilerOptions.module).toBe('commonjs'); + }); + + it(`should set 'module' to 'esnext' in workspace tsconfig`, async () => { + const tsConfigContent = { + ...defaultTsConfigOptions, + extends: undefined, + }; + + overrideJsonFile(tree, 'tsconfig.json', tsConfigContent); + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const { compilerOptions } = JSON.parse(tree2.readContent('tsconfig.json')); + expect(compilerOptions.module).toBe('esnext'); + }); }); });