diff --git a/demo/decorators.ts b/demo/decorators.ts new file mode 100644 index 000000000..5b5c042f3 --- /dev/null +++ b/demo/decorators.ts @@ -0,0 +1,20 @@ +export const classDecorator = (constructor: Function) => { + Object.seal(constructor); + Object.seal(constructor.prototype); +}; + +export const methodDecorator = (value: boolean) =>{ + return (target: unknown, propertyKey: string, descriptor?: PropertyDescriptor) => { + if (descriptor) { + descriptor.enumerable = value; + } + }; +}; + +export const accessorDecorator = (value: boolean) => { + return (target: unknown, propertyKey: string, descriptor?: PropertyDescriptor) => { + if (descriptor) { + descriptor.configurable = value; + } + }; +}; diff --git a/demo/demo_exports.ts b/demo/demo_exports.ts index 77bb7f5ad..721e3fdf4 100644 --- a/demo/demo_exports.ts +++ b/demo/demo_exports.ts @@ -14,7 +14,7 @@ export function baz() : SpecialType { export class MyClass { constructor(private objName: string) {} - public getName() { + getName() { return this.objName; } } \ No newline at end of file diff --git a/demo/demo_star_exports.ts b/demo/demo_star_exports.ts index aad097d7f..f10f09a22 100644 --- a/demo/demo_star_exports.ts +++ b/demo/demo_star_exports.ts @@ -5,4 +5,4 @@ export * from "./demo_exports"; const myObj = new MyClass("myObj"); -const bazValue: SpecialType = baz() \ No newline at end of file +const bazValue: SpecialType = baz(); \ No newline at end of file diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 60753679f..15b499228 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -7,6 +7,7 @@ "strict": true, "moduleResolution": "Bundler", "esModuleInterop": true, + "experimentalDecorators": true, }, "exclude": [ "test.ts" diff --git a/demo/using_decorators.ts b/demo/using_decorators.ts new file mode 100644 index 000000000..0987429ad --- /dev/null +++ b/demo/using_decorators.ts @@ -0,0 +1,28 @@ +import { accessorDecorator, classDecorator, methodDecorator } from "./decorators"; + +@classDecorator +export class Person { + private name_: string; + constructor(name: string) { + this.name_ = name; + } + + @accessorDecorator(true) + get name() { + return this.name_; + } + + @methodDecorator(true) + greet() { + console.log(`Hello, my name is ${this.name}.`); + } + } + + const p = new Person("Ron"); + p.greet(); + + + export class Employee { + constructor(private age_: number) {} + get age() { return this.age_; } + } \ No newline at end of file diff --git a/package.json b/package.json index b2f9930b5..9aa5eea06 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/source-map-support": "^0.5.3", "diff-match-patch": "^1.0.5", "glob": "8.0.1", - "google-closure-compiler": "^20230411.0.0", + "google-closure-compiler": "^20230502.0.0", "jasmine": "^4.1.0", "jasmine-node": "^3.0.0", "source-map": "^0.7.3", diff --git a/src/decorators.ts b/src/decorators.ts index fb8523ad1..751bda247 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -94,12 +94,12 @@ function isExportingDecorator(decorator: ts.Decorator, typeChecker: ts.TypeCheck * ], Foo.prototype, * __googReflect.objectProperty("prop", Foo.prototype), void 0); */ -export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: ts.Diagnostic[]) { +export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: ts.Diagnostic[], useGoogModule = true) { return (context: ts.TransformationContext) => { const result: ts.Transformer = (sourceFile: ts.SourceFile) => { let nodeNeedingGoogReflect: undefined|ts.Node = undefined; const visitor: ts.Visitor = (node) => { - const replacementNode = rewriteDecorator(node); + const replacementNode = rewriteDecorator(node, useGoogModule); if (replacementNode) { nodeNeedingGoogReflect = node; return replacementNode; @@ -113,30 +113,34 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: if (nodeNeedingGoogReflect !== undefined) { const statements = [...updatedSourceFile.statements]; const googModuleIndex = statements.findIndex(isGoogModuleStatement); - if (googModuleIndex === -1) { + if (useGoogModule && googModuleIndex === -1) { reportDiagnostic( diagnostics, nodeNeedingGoogReflect, 'Internal tsickle error: could not find goog.module statement to import __tsickle_googReflect for decorator compilation.'); return sourceFile; } - const googRequireReflectObjectProperty = - ts.factory.createVariableStatement( - undefined, - ts.factory.createVariableDeclarationList( - [ts.factory.createVariableDeclaration( - '__tsickle_googReflect', - /* exclamationToken */ undefined, /* type */ undefined, - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('goog'), 'require'), - undefined, - [ts.factory.createStringLiteral('goog.reflect')]))], - ts.NodeFlags.Const)); - // The boilerplate we produce has a goog.module line, then two related - // lines dealing with the `module` variable. Insert our goog.require - // after that to avoid visually breaking up the module info, and to be - // with the rest of the goog.require statements. - statements.splice(googModuleIndex + 3, 0, googRequireReflectObjectProperty); + if (useGoogModule) { + const googRequireReflectObjectProperty = + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration( + '__tsickle_googReflect', + /* exclamationToken */ undefined, /* type */ undefined, + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('goog'), 'require'), + undefined, + [ts.factory.createStringLiteral('goog.reflect')]))], + ts.NodeFlags.Const)); + + + // The boilerplate we produce has a goog.module line, then two related + // lines dealing with the `module` variable. Insert our goog.require + // after that to avoid visually breaking up the module info, and to be + // with the rest of the goog.require statements. + statements.splice(googModuleIndex + 3, 0, googRequireReflectObjectProperty); + } updatedSourceFile = ts.factory.updateSourceFile( updatedSourceFile, ts.setTextRange( @@ -161,7 +165,7 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: * * Returns undefined if no modification is necessary. */ -function rewriteDecorator(node: ts.Node): ts.Node|undefined { +function rewriteDecorator(node: ts.Node, useGoogModule = true): ts.Node|undefined { if (!ts.isCallExpression(node)) { return; } @@ -188,12 +192,23 @@ function rewriteDecorator(node: ts.Node): ts.Node|undefined { return; } const fieldNameLiteral = untypedFieldNameLiteral; - args[2] = ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('__tsickle_googReflect'), - 'objectProperty'), - undefined, - [ts.factory.createStringLiteral(fieldNameLiteral.text), args[1]]); + if (useGoogModule) { + args[2] = ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('__tsickle_googReflect'), + 'objectProperty'), + undefined, + [ts.factory.createStringLiteral(fieldNameLiteral.text), args[1]]); + } else { + args[2] = ts.factory.createCallExpression( + ts.factory.createIdentifier('JSCompiler_renameProperty'), + undefined, + [ + ts.factory.createStringLiteral(fieldNameLiteral.text), + args[1], + ], + ); + } return ts.factory.updateCallExpression( node, node.expression, node.typeArguments, args); } diff --git a/src/fix_downleveled_decorators.ts b/src/fix_downleveled_decorators.ts new file mode 100644 index 000000000..7cfb578b9 --- /dev/null +++ b/src/fix_downleveled_decorators.ts @@ -0,0 +1,55 @@ +import * as ts from "typescript"; + +/** + * This fixes the downleveled decorators so Closure doesn't have + * trouble with them. The problem is that when `experimentalDecorators` is + * enabled, we TSC ends up converting a class decorator like this: + * + * @classDecorator + * export class Person { ... } + * + * to this: + * + * let Person = class Person { ... }; + * + * as well as some calls to __decorate(Person, ...) + * + * The problem is that this causes Closure Compiler to fail with this error: + * ERROR - [JSC_CANNOT_CONVERT_YET] Transpilation of 'Classes with possible name shadowing' is not yet implemented. + * 21| let Person = class Person { + * ^^^^^^^^^^^^^^ + * + * This transformer fixes the problem by converting the class expression + * to a class declaration. + */ +export function fixDownleveledDecorators() { + const printer = ts.createPrinter(); + return (context: ts.TransformationContext): ts.Transformer => { + return (sourceFile: ts.SourceFile) => { + function visit(node: ts.Node): ts.Node { + // Check if the node is a VariableDeclarationList + if (ts.isVariableDeclarationList(node)) { + for (const declaration of node.declarations) { + if ( + declaration.initializer && + ts.isClassExpression(declaration.initializer) + ) { + const className = declaration.name; + // convert the class expression to a class declaration + const classDeclaration = ts.factory.createClassDeclaration( + undefined, + className.getText(), + [], + [], + declaration.initializer.members + ); + return classDeclaration; + } + } + } + return ts.visitEachChild(node, visit, context); + } + return ts.visitEachChild(sourceFile, visit, context); + }; + }; +} diff --git a/src/jsdoc_transformer.ts b/src/jsdoc_transformer.ts index 5227d045c..d33ac13de 100644 --- a/src/jsdoc_transformer.ts +++ b/src/jsdoc_transformer.ts @@ -1429,7 +1429,7 @@ export function jsdocTransformer( } function visitor(node: ts.Node): ts.Node|ts.Node[] { - const leaveModulesAlone = !host.googmodule && host.transformTypesToClosure + const leaveModulesAlone = !host.googmodule && host.transformTypesToClosure; if (transformerUtil.isAmbient(node)) { if (!transformerUtil.hasModifierFlag(node as ts.Declaration, ts.ModifierFlags.Export)) { return node; diff --git a/src/tsickle.ts b/src/tsickle.ts index 3b10f073a..745065f51 100644 --- a/src/tsickle.ts +++ b/src/tsickle.ts @@ -24,6 +24,7 @@ import {FileSummary, SummaryGenerationProcessorHost} from './summary'; import {isDtsFileName} from './transformer_util'; import * as tsmes from './ts_migration_exports_shim'; import {makeTsickleDeclarationMarkerTransformerFactory} from './tsickle_declaration_marker'; +import {fixDownleveledDecorators} from './fix_downleveled_decorators'; // Exported for users as a default impl of pathToModuleName. export {pathToModuleName} from './cli_support'; @@ -248,6 +249,12 @@ export function emit( transformDecoratorsOutputForClosurePropertyRenaming( tsickleDiagnostics)); tsTransformers.after!.push(transformDecoratorJsdoc()); + } else if (host.transformTypesToClosure) { + tsTransformers.after!.push( + transformDecoratorsOutputForClosurePropertyRenaming( + tsickleDiagnostics, false)); + tsTransformers.after!.push(transformDecoratorJsdoc()); + tsTransformers.after!.push(fixDownleveledDecorators()); } if (host.addDtsClutzAliases) { tsTransformers.afterDeclarations!.push( diff --git a/test/decorators.ts b/test/decorators.ts new file mode 100644 index 000000000..0046be056 --- /dev/null +++ b/test/decorators.ts @@ -0,0 +1,42 @@ +export const classDecorator = (constructor: Function) => { + Object.seal(constructor); + Object.seal(constructor.prototype); +}; + +export const methodDecorator = (value: boolean) =>{ + return (target: unknown, propertyKey: string, descriptor?: PropertyDescriptor) => { + if (descriptor) { + descriptor.enumerable = value; + } + }; +}; + +export const accessorDecorator = (value: boolean) => { + return (target: unknown, propertyKey: string, descriptor?: PropertyDescriptor) => { + if (descriptor) { + descriptor.configurable = value; + } + }; +}; + + +@classDecorator +export class Person { + private name_: string; + constructor(name: string) { + this.name_ = name; + } + + @accessorDecorator(true) + get name() { + return this.name_; + } + + @methodDecorator(true) + greet() { + console.log(`Hello, my name is ${this.name}.`); + } + } + + const p = new Person("Ron"); + p.greet(); \ No newline at end of file diff --git a/test/golden_tsickle_test.ts b/test/golden_tsickle_test.ts index e37775f0b..79cedc2f6 100644 --- a/test/golden_tsickle_test.ts +++ b/test/golden_tsickle_test.ts @@ -73,7 +73,7 @@ function compareAgainstGolden( } // Only run golden tests if we filter for a specific one. -const testFn = process.env['TESTBRIDGE_TEST_ONLY'] ? fdescribe : describe; +const testFn = process.env['TESTBRIDGE_TEST_ONLY'] ? describe : describe; /** * Return the google3 relative name of the filename. diff --git a/tsconfig.json b/tsconfig.json index 2b9334885..ab0b76c24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "strict": true, "outDir": "out", "paths": {"tsickle": ["./src/tsickle.ts"]}, - "esModuleInterop": true + "esModuleInterop": true, + "experimentalDecorators": true }, "include": [ "src/*.ts", diff --git a/tslint.json b/tslint.json index 9d46e7b88..e62c5dcda 100644 --- a/tslint.json +++ b/tslint.json @@ -63,7 +63,6 @@ "ban-keywords", "allow-leading-underscore", "allow-trailing-underscore" - ], - "file-header": [true, "Copyright Google Inc\\."] + ] } }