Skip to content

Commit 4bc834c

Browse files
authored
initial implementation of the decorators proposal (#3754)
1 parent 07cdbe0 commit 4bc834c

File tree

11 files changed

+9397
-35
lines changed

11 files changed

+9397
-35
lines changed

.github/workflows/ci.yml

+4
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ jobs:
160160
if: matrix.os == 'ubuntu-latest'
161161
run: make lib-typecheck
162162

163+
- name: Decorator Tests
164+
if: matrix.os == 'ubuntu-latest'
165+
run: make decorator-tests
166+
163167
- name: WebAssembly API Tests (browser)
164168
if: matrix.os == 'ubuntu-latest'
165169
run: make test-wasm-browser

CHANGELOG.md

+30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
## Unreleased
44

5+
* Implement the JavaScript decorators proposal ([#104](https://github.com/evanw/esbuild/issues/104))
6+
7+
With this release, esbuild now contains an implementation of the upcoming [JavaScript decorators proposal](https://github.com/tc39/proposal-decorators). This is the same feature that shipped in [TypeScript 5.0](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators). You can read more about them in that blog post and in this other (now slightly outdated) extensive blog post here: https://2ality.com/2022/10/javascript-decorators.html. Here's a quick example:
8+
9+
```js
10+
const log = (fn, context) => function() {
11+
console.log(`before ${context.name}`)
12+
const it = fn.apply(this, arguments)
13+
console.log(`after ${context.name}`)
14+
return it
15+
}
16+
17+
class Foo {
18+
@log static foo() {
19+
console.log('in foo')
20+
}
21+
}
22+
23+
// Logs "before foo", "in foo", "after foo"
24+
Foo.foo()
25+
```
26+
27+
Note that this feature is different than the existing "TypeScript experimental decorators" feature that esbuild already implements. It uses similar syntax but behaves very differently, and the two are not compatible (although it's sometimes possible to write decorators that work with both). TypeScript experimental decorators will still be supported by esbuild going forward as they have been around for a long time, are very widely used, and let you do certain things that are not possible with JavaScript decorators (such as decorating function parameters). By default esbuild will parse and transform JavaScript decorators, but you can tell esbuild to parse and transform TypeScript experimental decorators instead by setting `"experimentalDecorators": true` in your `tsconfig.json` file.
28+
29+
Probably at least half of the work for this feature went into creating a test suite that exercises many of the proposal's edge cases: https://github.com/evanw/decorator-tests. It has given me a reasonable level of confidence that esbuild's initial implementation is acceptable. However, I don't have access to a significant sample of real code that uses JavaScript decorators. If you're currently using JavaScript decorators in a real code base, please try out esbuild's implementation and let me know if anything seems off.
30+
31+
**⚠️ WARNING ⚠️**
32+
33+
This proposal has been in the works for a very long time (work began around 10 years ago in 2014) and it is finally getting close to becoming part of the JavaScript language. However, it's still a work in progress and isn't a part of JavaScript yet, so keep in mind that any code that uses JavaScript decorators may need to be updated as the feature continues to evolve. The decorators proposal is pretty close to its final form but it can and likely will undergo some small behavioral adjustments before it ends up becoming a part of the standard. If/when that happens, I will update esbuild's implementation to match the specification. I will not be supporting old versions of the specification.
34+
535
* Optimize the generated code for private methods
636
737
Previously when lowering private methods for old browsers, esbuild would generate one `WeakSet` for each private method. This mirrors similar logic for generating one `WeakSet` for each private field. Using a separate `WeakMap` for private fields is necessary as their assignment can be observable:

Makefile

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test:
1313
@$(MAKE) --no-print-directory -j6 test-common
1414

1515
# These tests are for development
16-
test-common: test-go vet-go no-filepath verify-source-map end-to-end-tests js-api-tests plugin-tests register-test node-unref-tests
16+
test-common: test-go vet-go no-filepath verify-source-map end-to-end-tests js-api-tests plugin-tests register-test node-unref-tests decorator-tests
1717

1818
# These tests are for release (the extra tests are not included in "test" because they are pretty slow)
1919
test-all:
@@ -85,6 +85,16 @@ end-to-end-tests: version-go
8585
node scripts/esbuild.js npm/esbuild/package.json --version
8686
node scripts/end-to-end-tests.js
8787

88+
# Note: The TypeScript source code for these tests was copied from the repo
89+
# https://github.com/evanw/decorator-tests, which is the official location of
90+
# the source code for these tests. Any changes to these tests should be made
91+
# there first and then copied here afterward.
92+
decorator-tests: esbuild
93+
./esbuild scripts/decorator-tests.ts --target=es2022 --outfile=scripts/decorator-tests.js
94+
node scripts/decorator-tests.js
95+
node scripts/decorator-tests.js | grep -q 'All checks passed'
96+
git diff --exit-code scripts/decorator-tests.js
97+
8898
js-api-tests: version-go
8999
node scripts/esbuild.js npm/esbuild/package.json --version
90100
node scripts/js-api-tests.js

internal/js_parser/js_parser.go

-4
Original file line numberDiff line numberDiff line change
@@ -6641,15 +6641,11 @@ func (p *parser) parseDecorators(decoratorScope *js_ast.Scope, classKeyword logg
66416641
p.log.AddErrorWithNotes(&p.tracker, p.lexer.Range(), "Parameter decorators only work when experimental decorators are enabled", []logger.MsgData{{
66426642
Text: "You can enable experimental decorators by adding \"experimentalDecorators\": true to your \"tsconfig.json\" file.",
66436643
}})
6644-
} else {
6645-
p.markSyntaxFeature(compat.Decorators, p.lexer.Range())
66466644
}
66476645
}
66486646
} else {
66496647
if (context & decoratorInFnArgs) != 0 {
66506648
p.log.AddError(&p.tracker, p.lexer.Range(), "Parameter decorators are not allowed in JavaScript")
6651-
} else {
6652-
p.markSyntaxFeature(compat.Decorators, p.lexer.Range())
66536649
}
66546650
}
66556651
}

internal/js_parser/js_parser_lower.go

-3
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@ func (p *parser) markSyntaxFeature(feature compat.JSFeature, r logger.Range) (di
7878
case compat.NestedRestBinding:
7979
name = "non-identifier array rest patterns"
8080

81-
case compat.Decorators:
82-
name = "JavaScript decorators"
83-
8481
case compat.ImportAttributes:
8582
p.log.AddError(&p.tracker, r, fmt.Sprintf(
8683
"Using an arbitrary value as the second argument to \"import()\" is not possible in %s", where))

0 commit comments

Comments
 (0)