Skip to content

Commit 24a4984

Browse files
Decorator Roadmap (#20)
1 parent e5d7967 commit 24a4984

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

rfcs/0006-decorator-roadmap.md

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
---
2+
Status: Accepted
3+
Champions: "@justinfagnani"
4+
PR: https://github.com/lit/rfcs/pull/20
5+
---
6+
7+
# Decorator Roadmap
8+
9+
This RFC describes how Lit can migrate to standard decorators with the smoothest upgrade path possible for developers.
10+
11+
## Objective
12+
13+
Migrate to using standard decorators as the only decorator implementation and unify Lit's syntax around decorators as the only way to declare reactive properties. When the migration is complete, Lit will require a decorator-supporting environment or toolchain.
14+
15+
### Goals
16+
17+
- Make Lit decorators work without transpilation
18+
- Make the migration as easy as possible on developers
19+
- Have a single implementation for decorators
20+
- Unify Lit's reactive property syntax
21+
- Simplify ReactiveElement's property handling code
22+
- Increase component performance due to more static class initialization
23+
- Decrease code size with simpler decorator implementations
24+
25+
### Non-Goals
26+
27+
- Change our decorators API drastically (ie, split `@property()` into `@input()`, `@output()`, `@attribute`, etc)
28+
29+
## Motivation
30+
31+
As [standard decorators](https://github.com/tc39/proposal-decorators) are starting to ship in [compilers](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators) and soon in VMs, we need to prepare for migrating Lit to use them.
32+
33+
Standard decorators will allow us to unify our surface syntax which is currently different in plain JS and compiled sources using TypeScript or Babel. This will let us eventually use decorators as the one way of declaring reactive properties, instead of also offering the `static properties` feature. Removing `static properties` in turn lets us remove the infrastructure for dynamically creating reactive properties, simplifying ReactiveElement.
34+
35+
## Detailed Design
36+
37+
Detailed design of the new decorators should be covered in pull requests and reviews, given that they follow the constraints in this RFC. This RFC focuses on:
38+
39+
- That standard decorators become the only way to declare reactive properties
40+
- That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators
41+
- A plan so that developers can incrementally migrate to standard decorators
42+
43+
### Audiences
44+
45+
If we group our developers by how they relate to decorators, we have four distinct audiences:
46+
47+
- JavaScript/TypeScript developers not yet using decorators
48+
- JavaScript developers using decorators with Babel
49+
- TypeScript developers sticking to using legacy experimental decorators
50+
- TypeScript developers using standard decorators
51+
52+
This plan needs to address all audiences during the migration.
53+
54+
### End State
55+
56+
After all stages of the implementation plan are complete:
57+
58+
- Standard decorators are the only decorator implementation
59+
- Decorators are the one _core_ way to define reactive properties
60+
- Add a non-core utility library that can dynamically create reactive properties
61+
- Remove `static properties`, `static createProperty()` and associated APIs
62+
- Remove `static addInitializer()` since the standard decorator API provides this
63+
- Core decorators (`@property`, `@state`) are exported from the main `reactive-element`, `lit-element` and `lit` modules.
64+
65+
### Standard Decorators
66+
67+
Standard decorators have a much different design than TypeScript's experimental decorators. Rather than getting access to the class and making dynamic, imperative changes to the class, standard decorators have no direct access to the class or the class member they're decorating and must instead return an object describing a replacement for the class member. For instance, class accessor decorators return an object with `get` and `set` methods.
68+
69+
This makes standard decorator usage much more static than experimental decorators. They can't add new class members, and can't change the kind of member they decorate, and ReactiveElement's `static createProperty()` API is simply not callable.
70+
71+
The good news is that our decorator implementations are likely to be smaller and simpler. For example, the two main functions of our `@property()` decorator are to replace a field with a getter/setter pair that calls `requestUpdate()` in the setter, and to store metadata about the field for use in attribute reflection.
72+
73+
For example, a simplified decorator that only replaces an accessor might look like:
74+
75+
```ts
76+
export const property =
77+
(options?: PropertyDeclaration) =>
78+
<C extends ReactiveElement, V>(
79+
_target: ClassAccessorDecoratorTarget<C, V>,
80+
context: ClassAccessorDecoratorContext<C, V>
81+
): ClassAccessorDecoratorResult<C, V> => {
82+
const { access, name } = context;
83+
return {
84+
get(this: C) {
85+
return access.get(this);
86+
},
87+
set(this: C, v: V) {
88+
const oldValue = access.get(this);
89+
access.set(this, v);
90+
this.requestUpdate(name, oldValue);
91+
},
92+
};
93+
};
94+
```
95+
96+
This decorator would require the new "auto accessor" feature and `accessor` keyword to use:
97+
98+
```ts
99+
class MyElement extends LitElement {
100+
@property()
101+
accessor foo = 42;
102+
}
103+
```
104+
105+
### Potential and required changes in behavior with standard decorators
106+
107+
We intend to keep the new standard decorators implementations mostly compatible with the existing legacy decorators, but the new decorator standard does force us or allow us to make some breaking changes in behavior.
108+
109+
#### `accessor` keyword is required
110+
111+
As mention already, the `accessor` keyword will be required for all formerly field-decorators like `@property()`, `@query()`, etc.
112+
113+
#### Initial values don't reflect for `@property()`
114+
115+
The new decorator spec passes field and accessor initial values through a separate callback from `set()`. This allows us to know when we are receiving an initial value and not reflect it to an attribute. This is part of a [long-standing issue](https://github.com/lit/lit/issues/1476) where we would like to not create any attributes spontaneously on an element.
116+
117+
We will _not_ do this initially, as it will make migrating more challenging. We will instead open another RFC on how to opt out of initial value reflection for both experimental and standard decorators.
118+
119+
#### Restoring default property values when attributes are removed
120+
121+
We could also remember the initial property value in order to restore it when an associated attribute is removed.
122+
123+
We will _not_ do this, however, since there is a decent chance of harmful memory leaks from retaining initial values. We will instead investigate allowing a default value to be specified in property options. This will act as an opt-in for the behavior and retain only one object per class instead of per instance, at the cost of duplicating a default and initializer.
124+
125+
### Hybrid Decorators
126+
127+
For an incremental migration path, Lit experimental decorators should be _use site_ compatible
128+
with standard decorators. We're calling this the "hybrid" decorator approach, and
129+
these decorators "hybrid decorators".
130+
131+
To enable hybrid decorators, the existing Lit experimental decorators need some minor
132+
breaking changes. These should most likely not impact many users.
133+
134+
- `requestUpdate()` will be called automatically for `@property` and `@state` decorated accessors.
135+
- The value of an accessor is read on first render and used as the initial value for `changedProperties` and attribute reflection.
136+
- No longer support version `"2018-09"` of `@babel/plugin-proposal-decorators`.
137+
138+
Additionally, the existing experimental decorators must also work with auto-accessors and the `accessor` keyword, so they can be syntactically identical to standard decorators.
139+
140+
Hybrid decorators make migrating from experimental decorators to standard decorators incremental for existing codebases by enabling
141+
each decorator to be independently updated to match standard decorator syntax. Once all decorators are syntactically identical to
142+
standard decorator usage, the `experimentalDecorators` TypeScript flag can be turned off, and semantics will remain unchanged.
143+
144+
#### Example migration
145+
146+
1. Existing codebase using experimental decorators
147+
148+
```ts
149+
// tsconfig uses `experimentalDecorators: true`.
150+
class El extends LitElement {
151+
@property()
152+
reactiveProp = "initial value";
153+
154+
@state()
155+
secondProperty = 0;
156+
}
157+
```
158+
159+
2. Incrementally make decorator usage _use site_ compatible by adding `accessor`
160+
161+
```ts
162+
// tsconfig uses `experimentalDecorators: true`.
163+
class El extends LitElement {
164+
@property()
165+
accessor reactiveProp = "initial value";
166+
167+
@state()
168+
accessor secondProperty = 0;
169+
}
170+
```
171+
172+
3. Finally, change `experimentalDecorators` to `false` to complete the migration.
173+
174+
## Implementation Considerations
175+
176+
### Implementation Plan
177+
178+
The plan is to make the Lit decorators work in both experimental decorator
179+
environments and standard decorator environments.
180+
181+
#### I. Make Lit decorators hybrid (breaking)
182+
183+
This stage adds support for the new decorators standard for both TypeScript and JavaScript developers,
184+
and ensures experimental decorators are semantically identical to standard decorators.
185+
186+
##### Requirements
187+
188+
- TypeScript ships decorator metadata (TypeScript 5.2, scheduled for 2023-08-22)
189+
- Babel ships decorator proposal plugin with metadata support (Babel 7.23.0, 2023-09-25)
190+
191+
##### Changes
192+
193+
- Make Lit decorators "hybrid", working in either experimental or standard environments.
194+
- Non-core packages with decorators (`@lit/localize`, `@lit-labs/context`) must follow a similar plan
195+
- Update `static elementProperties` to read from `[Symbol.metadata]`
196+
- Standard decorators cannot call into the `static createProperty()` API, so they must place property options into `[Symbol.metadata]`. We should use that as the source-of-truth going forward.
197+
198+
#### II. Deprecate experimental decorators
199+
200+
##### Requirements
201+
202+
- Stage I has landed.
203+
- Preferred: At least one browser has shipped decorators
204+
205+
##### Changes
206+
207+
- Deprecate the `noAccessor` property option.
208+
- Deprecate experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`.
209+
- Since there are no native implementations of decorators yet, we don't quite yet want to deprecate the main developer-facing API for property declaration.
210+
211+
212+
#### III. Remove experimental decorators (breaking)
213+
214+
This stage requires decorators for creating properties. It is no longer usable in non-decorator-supporting environments without transpilation.
215+
216+
##### Requirements
217+
218+
- Stage II has landed
219+
- Native decorators are shipping in all major browsers.
220+
221+
##### Changes
222+
223+
- Remove previously deprecated APIs
224+
- Deprecate `static properties` in ReactiveElement and LitElement
225+
- Add a new mixin that supports `static properties` for migration.
226+
- Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules.
227+
- This increases the core module size, which is paid for by removing the deprecated APIs.
228+
- Other decorators are more optional and remain in their own modules.
229+
230+
#### IV. Cleanup (breaking)
231+
232+
When all developers can use the standard decorators API we can remove the experimental decorators fully as well as `static properties`.
233+
234+
##### Requirements
235+
236+
- Stage III has landed
237+
- Native decorators are have been shipping in all major browsers for last 2 major versions.
238+
239+
##### Changes
240+
241+
- Remove all non standard decorator APIs, including `static properties`
242+
243+
### Backward Compatibility
244+
245+
See Implementation Plan.
246+
247+
### Testing Plan
248+
249+
Hybrid decorators requires testing in three configurations:
250+
251+
1. Experimental decorator syntax (no `accessor` keyword), with `experimentalDecorators: true`. This reflects the existing tests.
252+
2. Standard decorator syntax, with `experimentalDecorators: true`. Tests that experimental decorators can be incrementally migrated.
253+
3. Standard decorator syntax, with `experimentalDecorators: false`. Test that standard decorators match experimental decorator behavior exactly.
254+
255+
In the future, as we cleanup experimental decorators, we'll be able to remove tests in configurations `1.`, and `2.`.
256+
257+
### Performance and Code Size Impact
258+
259+
Code size will ultimately be reduced due to simpler decorators and smaller API in ReactiveElement.
260+
261+
The code size of components needs to be investigated since the TypeScript emit for standard decorators appears to be larger than experimental decorators. We might want to point this out and not recommend them as the default until browsers ship natively.
262+
263+
### Interoperability
264+
265+
No interoperability concerns.
266+
267+
### Security Impact
268+
269+
N/A
270+
271+
### Documentation Plan
272+
273+
We will need to document both standard and legacy decorators for a while, but since the set of decorators are the same, hopefully there is just common decorator overview and config documentation that covers each, and individual decorators are documented once.
274+
275+
## Downsides
276+
277+
This is a bit of churn on the ecosystem, but it's unavoidable and we accepted this when we shipped experimental decorators.
278+
279+
## Alternatives
280+
281+
None

0 commit comments

Comments
 (0)