Skip to content

Commit

Permalink
fix #104: initial implementation of typescript decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 8, 2020
1 parent 08c0733 commit 2cf35ea
Show file tree
Hide file tree
Showing 9 changed files with 921 additions and 221 deletions.
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

## Unreleased

* Initial implementation of TypeScript decorators ([#104](https://github.com/evanw/esbuild/issues/104))

This release contains an initial implementation of the non-standard TypeScript-specific decorator syntax. This syntax transformation is enabled by default in esbuild, so no extra configuration is needed. The TypeScript compiler will need `"experimentalDecorators": true` configured in `tsconfig.json` for type checking to work with TypeScript decorators.

Here's an example of a method decorator:

```ts
function logged(target, key, descriptor) {
let method = descriptor.value
descriptor.value = function(...args) {
let result = method.apply(this, args)
let joined = args.map(x => JSON.stringify(x)).join(', ')
console.log(`${key}(${joined}) => ${JSON.stringify(result)}`)
return result
}
}

class Example {
@logged
method(text: string) {
return text + '!'
}
}

const x = new Example
x.method('text')
```

There are four kinds of TypeScript decorators: class, method, parameter, and field decorators. See [the TypeScript decorator documentation](https://www.typescriptlang.org/docs/handbook/decorators.html) for more information. Note that esbuild only implements TypeScript's `experimentalDecorators` setting. It does not implement the `emitDecoratorMetadata` setting because that requires type information.

* Fix order of side effects for computed fields

When transforming computed class fields, esbuild had a bug where the side effects of the field property names were not evaluated in source code order. The order of side effects now matches the order in the source code.

* Fix private fields in TypeScript

This fixes a bug with private instance fields in TypeScript where the private field declaration was incorrectly removed during the TypeScript class field transform, which inlines the initializers into the constructor. Now the initializers are still moved to the constructor but the private field declaration is preserved without the initializer.
Expand Down
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ These TypeScript-only syntax features are supported, and are always converted to
| Type casts | `a as B` and `<B>a` | |
| Type imports | `import {Type} from 'foo'` | Handled by removing all unused imports |
| Type exports | `export {Type} from 'foo'` | Handled by ignoring missing exports in TypeScript files |
| Experimental decorators | `@sealed class Foo {}` | |

These TypeScript-only syntax features are parsed and ignored (a non-exhaustive list):

Expand All @@ -156,12 +157,6 @@ These TypeScript-only syntax features are parsed and ignored (a non-exhaustive l
| Type-only imports | `import type {Type} from 'foo'` |
| Type-only exports | `export type {Type} from 'foo'` |

These TypeScript-only syntax features are not yet supported, and currently cannot be parsed (an exhaustive list):

| Syntax feature | Example | Notes |
|-------------------------|------------------------|-------|
| Experimental decorators | `@sealed class Foo {}` | This is tracked [here](https://github.com/evanw/esbuild/issues/104) |

#### Disclaimers:

* As far as I know, this hasn't yet been used in production by anyone yet. It's still pretty new code and you may run into some bugs.
Expand Down
44 changes: 27 additions & 17 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,8 @@ const (
)

type Property struct {
Kind PropertyKind
IsComputed bool
IsMethod bool
IsStatic bool
Key Expr
TSDecorators []Expr
Key Expr

// This is omitted for class fields
Value *Expr
Expand All @@ -275,6 +272,11 @@ type Property struct {
// class Foo { a = 1 }
//
Initializer *Expr

Kind PropertyKind
IsComputed bool
IsMethod bool
IsStatic bool
}

type PropertyBinding struct {
Expand All @@ -286,20 +288,22 @@ type PropertyBinding struct {
}

type Arg struct {
TSDecorators []Expr
Binding Binding
Default *Expr

// "constructor(public x: boolean) {}"
IsTypeScriptCtorField bool

Binding Binding
Default *Expr
}

type Fn struct {
Name *LocRef
Args []Arg
Name *LocRef
Args []Arg
Body FnBody

IsAsync bool
IsGenerator bool
HasRestArg bool
Body FnBody
}

type FnBody struct {
Expand All @@ -308,10 +312,11 @@ type FnBody struct {
}

type Class struct {
Name *LocRef
Extends *Expr
BodyLoc Loc
Properties []Property
TSDecorators []Expr
Name *LocRef
Extends *Expr
BodyLoc Loc
Properties []Property
}

type ArrayBinding struct {
Expand Down Expand Up @@ -1257,11 +1262,16 @@ func GenerateNonUniqueNameFromPath(text string) string {

// Convert it to an ASCII identifier
bytes := []byte{}
needsGap := false
for _, c := range base {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (len(bytes) > 0 && c >= '0' && c <= '9') {
if needsGap {
bytes = append(bytes, '_')
needsGap = false
}
bytes = append(bytes, byte(c))
} else if len(bytes) > 0 && bytes[len(bytes)-1] != '_' {
bytes = append(bytes, '_')
} else if len(bytes) > 0 {
needsGap = true
}
}

Expand Down
22 changes: 11 additions & 11 deletions internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,13 @@ export * as fromB from "./b";
"/out/b.js": `export default function() {
}
`,
"/out/c.js": `export default function a() {
"/out/c.js": `export default function b() {
}
`,
"/out/d.js": `export default class {
}
`,
"/out/e.js": `export default class a {
"/out/e.js": `export default class b {
}
`,
},
Expand Down Expand Up @@ -643,7 +643,7 @@ import f, * as e from "foo";
import g, {a2 as h, b as i} from "foo";
const j = [
import("foo"),
function C() {
function F() {
return import("foo");
}
];
Expand Down Expand Up @@ -729,9 +729,9 @@ var require_c = __commonJS((exports) => {
// /d.js
var require_d = __commonJS((exports) => {
__export(exports, {
default: () => Foo
default: () => d_default
});
class Foo {
class d_default {
}
});
Expand All @@ -747,9 +747,9 @@ var require_e = __commonJS((exports) => {
// /f.js
var require_f = __commonJS((exports) => {
__export(exports, {
default: () => foo
default: () => f_default
});
function foo() {
function f_default() {
}
});
Expand All @@ -765,9 +765,9 @@ var require_g = __commonJS((exports) => {
// /h.js
var require_h = __commonJS((exports) => {
__export(exports, {
default: () => foo
default: () => h_default
});
async function foo() {
async function h_default() {
}
});
Expand Down Expand Up @@ -822,9 +822,9 @@ func TestReExportDefaultCommonJS(t *testing.T) {
"/out.js": `// /bar.js
var require_bar = __commonJS((exports) => {
__export(exports, {
default: () => foo2
default: () => bar_default
});
function foo2() {
function bar_default() {
return exports;
}
});
Expand Down

0 comments on commit 2cf35ea

Please sign in to comment.