diff --git a/packages/ngtools/webpack/src/transformers/ast_helpers.ts b/packages/ngtools/webpack/src/transformers/ast_helpers.ts index 1e850d938d84..319e473cea5d 100644 --- a/packages/ngtools/webpack/src/transformers/ast_helpers.ts +++ b/packages/ngtools/webpack/src/transformers/ast_helpers.ts @@ -50,13 +50,14 @@ export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null { // Test transform helpers. const basePath = '/project/src/'; const fileName = basePath + 'test-file.ts'; +const typeScriptLibFiles = loadTypeScriptLibFiles(); const tsLibFiles = loadTsLibFiles(); export function createTypescriptContext( content: string, additionalFiles?: Record, useLibs = false, - importHelpers = true, + extraCompilerOptions: ts.CompilerOptions = {}, ) { // Set compiler options. const compilerOptions: ts.CompilerOptions = { @@ -68,7 +69,9 @@ export function createTypescriptContext( target: ts.ScriptTarget.ESNext, skipLibCheck: true, sourceMap: false, - importHelpers, + importHelpers: true, + experimentalDecorators: true, + ...extraCompilerOptions, }; // Create compiler host. @@ -86,11 +89,17 @@ export function createTypescriptContext( // Write the default libs. // These are needed for tests that use import(), because it relies on a Promise being there. const compilerLibFolder = dirname(compilerHost.getDefaultLibFileName(compilerOptions)); - for (const [k, v] of Object.entries(tsLibFiles)) { + for (const [k, v] of Object.entries(typeScriptLibFiles)) { compilerHost.writeFile(join(compilerLibFolder, k), v, false); } } + if (compilerOptions.importHelpers) { + for (const [k, v] of Object.entries(tsLibFiles)) { + compilerHost.writeFile(k, v, false); + } + } + if (additionalFiles) { for (const key in additionalFiles) { compilerHost.writeFile(basePath + key, additionalFiles[key], false); @@ -108,8 +117,7 @@ export function transformTypescript( transformers: ts.TransformerFactory[], program?: ts.Program, compilerHost?: WebpackCompilerHost, -) { - +): string | undefined { // Use given context or create a new one. if (content !== undefined) { const typescriptContext = createTypescriptContext(content); @@ -137,18 +145,29 @@ export function transformTypescript( return compilerHost.readFile(fileName.replace(/\.tsx?$/, '.js')); } -function loadTsLibFiles() { +function loadTypeScriptLibFiles(): Record { const libFolderPath = dirname(require.resolve('typescript/lib/lib.d.ts')); const libFolderFiles = readdirSync(libFolderPath); const libFileNames = libFolderFiles.filter(f => f.startsWith('lib.') && f.endsWith('.d.ts')); // Return a map of the lib names to their content. - return libFileNames.reduce( - (map, f) => { - map[f] = readFileSync(join(libFolderPath, f), 'utf-8'); + const libs: Record = {}; + for (const f of libFileNames) { + libs[f] = readFileSync(join(libFolderPath, f), 'utf-8'); + } + + return libs; +} + +function loadTsLibFiles(): Record { + const libFolderPath = dirname(require.resolve('tslib/package.json')); + const libFolderFiles = readdirSync(libFolderPath); + + // Return a map of the lib names to their content. + const libs: Record = {}; + for (const f of libFolderFiles) { + libs[join('node_modules/tslib', f)] = readFileSync(join(libFolderPath, f), 'utf-8'); + } - return map; - }, - {} as { [k: string]: string }, - ); + return libs; } diff --git a/packages/ngtools/webpack/src/transformers/elide_imports.ts b/packages/ngtools/webpack/src/transformers/elide_imports.ts index bc61bc58b762..21303f45b847 100644 --- a/packages/ngtools/webpack/src/transformers/elide_imports.ts +++ b/packages/ngtools/webpack/src/transformers/elide_imports.ts @@ -19,6 +19,7 @@ export function elideImports( sourceFile: ts.SourceFile, removedNodes: ts.Node[], getTypeChecker: () => ts.TypeChecker, + compilerOptions: ts.CompilerOptions, ): TransformOperation[] { const ops: TransformOperation[] = []; @@ -33,8 +34,8 @@ export function elideImports( const imports: ts.ImportDeclaration[] = []; ts.forEachChild(sourceFile, function visit(node) { - // Skip type references and removed nodes. We consider both unused. - if (node.kind == ts.SyntaxKind.TypeReference || removedNodes.includes(node)) { + // Skip removed nodes. + if (removedNodes.includes(node)) { return; } @@ -46,17 +47,48 @@ export function elideImports( } let symbol: ts.Symbol | undefined; + if (ts.isTypeReferenceNode(node)) { + if (!compilerOptions.emitDecoratorMetadata) { + // Skip and mark as unused if emitDecoratorMetadata is disabled. + return; + } - switch (node.kind) { - case ts.SyntaxKind.Identifier: + const parent = node.parent; + let isTypeReferenceForDecoratoredNode = false; + + switch (parent.kind) { + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + isTypeReferenceForDecoratoredNode = !!parent.decorators?.length; + break; + case ts.SyntaxKind.Parameter: + // - A constructor parameter can be decorated or the class itself is decorated. + // - The parent of the parameter is decorated example a method declaration or a set accessor. + // In all cases we need the type reference not to be elided. + isTypeReferenceForDecoratoredNode = !!(parent.decorators?.length || + (ts.isSetAccessor(parent.parent) && !!parent.parent.decorators?.length) || + (ts.isConstructorDeclaration(parent.parent) && !!parent.parent.parent.decorators?.length)); + break; + } + if (isTypeReferenceForDecoratoredNode) { symbol = typeChecker.getSymbolAtLocation(node); - break; - case ts.SyntaxKind.ExportSpecifier: - symbol = typeChecker.getExportSpecifierLocalTargetSymbol(node as ts.ExportSpecifier); - break; - case ts.SyntaxKind.ShorthandPropertyAssignment: - symbol = typeChecker.getShorthandAssignmentValueSymbol(node); - break; + } else { + // If type reference is not for Decorator skip and marked as unused. + return; + } + } else { + switch (node.kind) { + case ts.SyntaxKind.Identifier: + symbol = typeChecker.getSymbolAtLocation(node); + break; + case ts.SyntaxKind.ExportSpecifier: + symbol = typeChecker.getExportSpecifierLocalTargetSymbol(node as ts.ExportSpecifier); + break; + case ts.SyntaxKind.ShorthandPropertyAssignment: + symbol = typeChecker.getShorthandAssignmentValueSymbol(node); + break; + } } if (symbol) { diff --git a/packages/ngtools/webpack/src/transformers/elide_imports_spec.ts b/packages/ngtools/webpack/src/transformers/elide_imports_spec.ts index c7ed3ece60f9..93da5aec7061 100644 --- a/packages/ngtools/webpack/src/transformers/elide_imports_spec.ts +++ b/packages/ngtools/webpack/src/transformers/elide_imports_spec.ts @@ -31,6 +31,14 @@ describe('@ngtools/webpack transformers', () => { export const take = () => null; export default promise; `, + 'decorator.ts': ` + export function Decorator(value?: any): any { + return function (): any { }; + } + `, + 'service.ts': ` + export class Service { } + `, }; it('should remove unused imports', () => { @@ -239,7 +247,7 @@ describe('@ngtools/webpack transformers', () => { it('should only drop default imports when having named and default (3)', () => { const input = tags.stripIndent` - import promise, { fromPromise } from './const'; + import promise, { promise as fromPromise } from './const'; const used = promise; const unused = fromPromise; `; @@ -272,5 +280,230 @@ describe('@ngtools/webpack transformers', () => { expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); }); + describe('should elide imports decorator type references when emitDecoratorMetadata is false', () => { + const extraCompilerOptions: ts.CompilerOptions = { + emitDecoratorMetadata: false, + experimentalDecorators: true, + }; + + it('should remove ctor parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + @Decorator() + export class Foo { + constructor(param: Service) { + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate } from "tslib"; + import { Decorator } from './decorator'; + + let Foo = class Foo { constructor(param) { } }; + Foo = __decorate([ Decorator() ], Foo); + export { Foo }; + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); + + describe('should not elide imports decorator type references when emitDecoratorMetadata is true', () => { + const extraCompilerOptions: ts.CompilerOptions = { + emitDecoratorMetadata: true, + experimentalDecorators: true, + }; + + it('should not remove ctor parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + @Decorator() + export class Foo { + constructor(param: Service) { + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + import { Decorator } from './decorator'; + import { Service } from './service'; + + let Foo = class Foo { constructor(param) { } }; + Foo = __decorate([ Decorator(), __metadata("design:paramtypes", [Service]) ], Foo); + export { Foo }; + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove property declaration parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { + @Decorator() foo: Service; + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + import { Decorator } from './decorator'; + + import { Service } from './service'; + + export class Foo { } + __decorate([ Decorator(), __metadata("design:type", Service) ], Foo.prototype, "foo", void 0); + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove set accessor parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { + _foo: Service; + + @Decorator() + set name(f: Service) { + this._foo = f; + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { set name(f) { this._foo = f; } } + __decorate([ Decorator(), __metadata("design:type", Service), __metadata("design:paramtypes", [Service]) ], Foo.prototype, "name", null); + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove get accessor parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { + _foo: Service; + + @Decorator() + get name(): Service { + return this._foo; + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { get name() { return this._foo; } } + __decorate([ Decorator(), __metadata("design:type", Service), __metadata("design:paramtypes", []) ], Foo.prototype, "name", null); + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove decorated method return type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { + @Decorator() + name(): Service { + return undefined; + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { name() { return undefined; } } + __decorate([ Decorator(), __metadata("design:type", Function), + __metadata("design:paramtypes", []), __metadata("design:returntype", Service) ], Foo.prototype, "name", null); + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove decorated method parameter type reference', () => { + const input = tags.stripIndent` + import { Decorator } from './decorator'; + import { Service } from './service'; + + export class Foo { + @Decorator() + name(f: Service) { + } + } + + ${dummyNode} + `; + + const output = tags.stripIndent` + import { __decorate, __metadata } from "tslib"; + + import { Decorator } from './decorator'; + export class Foo { name(f) { } } + + __decorate([ Decorator(), __metadata("design:type", Function), __metadata("design:paramtypes", [Service]), + __metadata("design:returntype", void 0) ], Foo.prototype, "name", null); + `; + + const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true, extraCompilerOptions); + const result = transformTypescript(undefined, [transformer(program)], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); }); }); diff --git a/packages/ngtools/webpack/src/transformers/make_transform.ts b/packages/ngtools/webpack/src/transformers/make_transform.ts index 5d998db5f26d..01f6205d3a04 100644 --- a/packages/ngtools/webpack/src/transformers/make_transform.ts +++ b/packages/ngtools/webpack/src/transformers/make_transform.ts @@ -38,7 +38,7 @@ export function makeTransform( // replace_resources), but may not be true for new transforms. if (getTypeChecker && removeOps.length + replaceOps.length > 0) { const removedNodes = removeOps.concat(replaceOps).map(op => op.target); - removeOps.push(...elideImports(sf, removedNodes, getTypeChecker)); + removeOps.push(...elideImports(sf, removedNodes, getTypeChecker, context.getCompilerOptions())); } const visitor: ts.Visitor = node => { diff --git a/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts b/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts index 962f4fe9d434..aaa10bb82727 100644 --- a/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts +++ b/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts @@ -11,7 +11,7 @@ import { createTypescriptContext, transformTypescript } from './ast_helpers'; import { removeDecorators } from './remove_decorators'; describe('@ngtools/webpack transformers', () => { - describe('decorator_remover', () => { + describe('remove_decorators', () => { it('should remove Angular decorators', () => { const input = tags.stripIndent` import { Component } from '@angular/core'; diff --git a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts index 6094ea284433..bc0da19b8eba 100644 --- a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts +++ b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts @@ -15,8 +15,7 @@ function transform( directTemplateLoading = true, importHelpers = true, ) { - const { program, compilerHost } = - createTypescriptContext(input, undefined, undefined, importHelpers); + const { program, compilerHost } = createTypescriptContext(input, undefined, undefined, { importHelpers }); const getTypeChecker = () => program.getTypeChecker(); const transformer = replaceResources( () => shouldTransform, getTypeChecker, directTemplateLoading);