From 22b4bc8fead0f17e8df0d30f0a7f9eb2c4ca076d Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Wed, 24 May 2023 15:58:24 -0700 Subject: [PATCH 1/9] Decorator Roadmap --- rfcs/NNNN-decorator-roadmap.md | 217 +++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 rfcs/NNNN-decorator-roadmap.md diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md new file mode 100644 index 0000000..22082a3 --- /dev/null +++ b/rfcs/NNNN-decorator-roadmap.md @@ -0,0 +1,217 @@ +--- +Status: Active +Champions: @justinfagnani +PR: {{ update_with_pr_number }} +--- + +# Decorator Roadmap + +This RFC describes how Lit can migrate to standard decorators with the smoothest upgrade path possible for developers. + +## Objective + +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. + +### Goals +- Make Lit decorators work without transpilation +- Make the migration as easy as possible on developers +- Have a single implementation for decorators +- Unify Lit's reactive property syntax +- Simplify ReactiveElement's property handling code +- Increase component performance due to more static class initialization +- Decrease code size with simpler decorator implementations + +### Non-Goals +- Change our decorators API drastically (ie, split `@property()` into `@input()`, `@output()`, `@attribute`, etc) + +## Motivation + +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 + +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 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. + +## Detailed Design + +Detailed design of the new decorators should be covered in another RFC. This RFC focuses on: +* That standard decorators become the only way to declare reactive properties +* That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators +* A multi-stage plan for making the changes so that developers can incrementally migrate + +### Audiences + +If we group our developers by how they relate to decorators, we have four distinct audiences: + +- JavaScript developers not yet using decorators +- TypeScript developers using legacy experimental decorators +- TypeScript developers not yet using decorators because of forward compatibility concerns +- TypeScript developers stuck using legacy experimental decorators because of backward compatibility concerns + +This plan needs to address all audiences during the migration. + +### End State + +After all stages of the implementation plan are complete: + +- Standard decorators are the only decorator implementation +- Decorators are the one way to define reactive properties +- Remove `static properties`, `static createProperty()` and associated APIs +- Remove `static addInitializer()` since the standard decorator API provides this +- Core decorators (`@property`, `@state`) are exported from the main `reactive-element`, `lit-element` and `lit` modules. + +### Standard Decorators + +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. + +This makes standard decorator usage must more static than experimental decorators. They can't add new class members, and can't change the type of member they decorate, and ReactiveElement's `static createProperty()` API is simply not callable. + +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. + +For example, a simplified decorator that only replaces an accessor might look like: + +```ts +export const property = + (options?: PropertyDeclaration) => + ( + _target: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext + ): ClassAccessorDecoratorResult => { + const {access, name} = context; + return { + get(this: C) { + return access.get(this); + }, + set(this: C, v: V) { + const oldValue = access.get(this); + access.set(this, v); + this.requestUpdate(name, oldValue); + }, + }; + }; +``` + +This decorator would require the new "auto accessor" feature and `accessor` keyword to use: + +```ts +class MyElement extends LitElement { + @property() + accessor foo = 42; +} +``` + +## Implementation Considerations + +### Implementation Plan + +The plan is divided into stages which must be shipped as releases. + +#### I. Add standard decorators (non-breaking) + +This stage adds support for the new decorators standard in a backward-compatible way for both TypeScript and JavaScript developers. + +##### Requirements +* TypeScript ships decorator metadata + +##### Changes +* Add new module with standard decorators + * Place them in a new module: `'lit/std-decorators.js'` +* Re-export legacy decorators from `'lit/legacy-decorators.js'` +* Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: + * A _third_ set of decorators: `'lit/experimental-decorators.js'` + * They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. + * This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. + * This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. + * There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. +* Non-core packages with decorators (@lit/labs) must follow a similar plan + * We can skip the legacy-decorators step with more aggressive breaking changes for labs packages. +* Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. + * 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. +* Deprecate the `noAccessor` property option. +* Update `static elementProperties` to read from `[Symbol.metadata]` + * 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. +* Vend codemods to upgrade decorator usage: + * One to migrate to the new standard decorators + * One to migrate to the new experimental decorators + +#### II. Prefer standard decorators (breaking) + +This stage is still usable in non-decorator environments via `static properties`, but we move decorator modules around so that legacy use must use the non-default module names (`'lit/legacy-decorators.js'`) \ + +##### Requirements + +* Stage I has landed. +* Preferred: At least one browser has shipped decorators + +##### Changes + +* Move standard decorators implementation to 'lit/decorators.js' +* Re-export standard decorators from 'lit/std-decorators.js' +* Deprecate static properties +* Deprecate static addInitializer() +* Vend codemod to migrate standard decorator imports + +#### III. Remove legacy decorators (breaking) + +This stage requires decorators for creating properties. It is no longer usable in non-decorator-supporting environments without transpilation. + +##### Requirements + +* Native decorators are shipping in all major browsers. +* Stage II has landed + +##### Changes + +* Remove previously deprecated APIs +* Deprecate `'lit/std-decorators.js'` module +* Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules. + * This increases the core module size, which is paid for by removing the deprecated APIs. + * Other decorators are more optional and remain in their own modules. + + +#### IV. Cleanup (breaking) + +When all developers can use the standard decorators API we can remove the experimental decorators fully. + +##### Requirements + +* Native decorators are shipping in all major browsers. +* Stage II has landed + +##### Changes + +* Remove 'lit/std-decorators.js' +* Remove 'lit/experimental-decorators.js' +* Vend codemod to move experimental decorator imports to standard decorators + +### Backward Compatibility + +See Implementation Plan + +### Testing Plan + +N/A + +### Performance and Code Size Impact + +Code size will ultimately be reduced due to simpler decorators and smaller API in ReactiveElement. + +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. + +### Interoperability + +No interoperability concerns. + +### Security Impact + +N/A + +### Documentation Plan + +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. + +## Downsides + +This is a bit of churn on the ecosystem, but it's unavoidable and we accepted this when we shipped experimental decorators. + +## Alternatives + +None From e69a5be098e7520c90deb6c10f8851073cc11c58 Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Tue, 22 Aug 2023 18:41:25 -0700 Subject: [PATCH 2/9] Address feedback --- rfcs/NNNN-decorator-roadmap.md | 72 ++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index 22082a3..3873411 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -1,7 +1,7 @@ --- Status: Active -Champions: @justinfagnani -PR: {{ update_with_pr_number }} +Champions: "@justinfagnani" +PR: https://github.com/lit/rfcs/pull/20 --- # Decorator Roadmap @@ -60,9 +60,9 @@ After all stages of the implementation plan are complete: ### Standard Decorators -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. +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. -This makes standard decorator usage must more static than experimental decorators. They can't add new class members, and can't change the type of member they decorate, and ReactiveElement's `static createProperty()` API is simply not callable. +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. 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. @@ -98,6 +98,24 @@ class MyElement extends LitElement { } ``` +### Changes in behavior with standard decorators + +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. + +#### `accessor` keyword is required + +As mention already, the `accessor` keyword will be required for all formerly field-decorators like `@property()`, `@query()`, etc. + +#### Initial values don't reflect for `@property()` + +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. + +#### Restoring default property values when attributes are removed + +We could also remember the initial property value in order to restore it when an associated attribute is removed. + +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. + ## Implementation Considerations ### Implementation Plan @@ -115,31 +133,43 @@ This stage adds support for the new decorators standard in a backward-compatible * Add new module with standard decorators * Place them in a new module: `'lit/std-decorators.js'` * Re-export legacy decorators from `'lit/legacy-decorators.js'` -* Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: - * A _third_ set of decorators: `'lit/experimental-decorators.js'` - * They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. - * This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. - * This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. - * There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. + * Non-core packages with decorators (@lit/labs) must follow a similar plan * We can skip the legacy-decorators step with more aggressive breaking changes for labs packages. -* Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. - * 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. -* Deprecate the `noAccessor` property option. * Update `static elementProperties` to read from `[Symbol.metadata]` * 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. + + +#### II. Deprecate legacy decorators + +##### Requirements +* Stage II has landed. +* Preferred: At least one browser has shipped decorators +* Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. + * 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. * Vend codemods to upgrade decorator usage: * One to migrate to the new standard decorators - * One to migrate to the new experimental decorators + * One to migrate imports to the explicitly legacy decorators -#### II. Prefer standard decorators (breaking) +* (Optional, for google3) Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: + * A _third_ set of decorators: `'lit/experimental-decorators.js'` + * They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. + * This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. + * This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. + * There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. + * Add a codemode to migrate to the new experimental decorators + +##### Changes +* Deprecate the `noAccessor` property option. + +#### III. Prefer standard decorators (breaking) This stage is still usable in non-decorator environments via `static properties`, but we move decorator modules around so that legacy use must use the non-default module names (`'lit/legacy-decorators.js'`) \ ##### Requirements -* Stage I has landed. -* Preferred: At least one browser has shipped decorators +* Stage II has landed. +* At least one browser has shipped decorators ##### Changes @@ -149,14 +179,14 @@ This stage is still usable in non-decorator environments via `static properties` * Deprecate static addInitializer() * Vend codemod to migrate standard decorator imports -#### III. Remove legacy decorators (breaking) +#### IV. Remove legacy decorators (breaking) This stage requires decorators for creating properties. It is no longer usable in non-decorator-supporting environments without transpilation. ##### Requirements * Native decorators are shipping in all major browsers. -* Stage II has landed +* Stage III has landed ##### Changes @@ -167,14 +197,14 @@ This stage requires decorators for creating properties. It is no longer usable i * Other decorators are more optional and remain in their own modules. -#### IV. Cleanup (breaking) +#### V. Cleanup (breaking) When all developers can use the standard decorators API we can remove the experimental decorators fully. ##### Requirements +* Stage IV has landed * Native decorators are shipping in all major browsers. -* Stage II has landed ##### Changes From 76d5332b79c9416b6e7370226d253ab6cc3bf9ea Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Tue, 22 Aug 2023 18:57:06 -0700 Subject: [PATCH 3/9] Address feedback --- rfcs/NNNN-decorator-roadmap.md | 107 +++++++++++++++++---------------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index 3873411..03a5769 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -13,6 +13,7 @@ This RFC describes how Lit can migrate to standard decorators with the smoothest 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. ### Goals + - Make Lit decorators work without transpilation - Make the migration as easy as possible on developers - Have a single implementation for decorators @@ -22,6 +23,7 @@ Migrate to using standard decorators as the only decorator implementation and un - Decrease code size with simpler decorator implementations ### Non-Goals + - Change our decorators API drastically (ie, split `@property()` into `@input()`, `@output()`, `@attribute`, etc) ## Motivation @@ -33,9 +35,10 @@ Standard decorators will allow us to unify our surface syntax which is currently ## Detailed Design Detailed design of the new decorators should be covered in another RFC. This RFC focuses on: -* That standard decorators become the only way to declare reactive properties -* That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators -* A multi-stage plan for making the changes so that developers can incrementally migrate + +- That standard decorators become the only way to declare reactive properties +- That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators +- A multi-stage plan for making the changes so that developers can incrementally migrate ### Audiences @@ -75,7 +78,7 @@ export const property = _target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext ): ClassAccessorDecoratorResult => { - const {access, name} = context; + const { access, name } = context; return { get(this: C) { return access.get(this); @@ -114,7 +117,7 @@ The new decorator spec passes field and accessor initial values through a separa We could also remember the initial property value in order to restore it when an associated attribute is removed. -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. +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. ## Implementation Considerations @@ -127,57 +130,60 @@ The plan is divided into stages which must be shipped as releases. This stage adds support for the new decorators standard in a backward-compatible way for both TypeScript and JavaScript developers. ##### Requirements -* TypeScript ships decorator metadata -##### Changes -* Add new module with standard decorators - * Place them in a new module: `'lit/std-decorators.js'` -* Re-export legacy decorators from `'lit/legacy-decorators.js'` +- TypeScript ships decorator metadata (TypeScript 5.2, scheduled for 2023-08-22) -* Non-core packages with decorators (@lit/labs) must follow a similar plan - * We can skip the legacy-decorators step with more aggressive breaking changes for labs packages. -* Update `static elementProperties` to read from `[Symbol.metadata]` - * 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. +##### Changes +- Add new module with standard decorators + - Place them in a new module: `'lit/std-decorators.js'` +- Re-export legacy decorators from `'lit/legacy-decorators.js'` +- Non-core packages with decorators (@lit/labs) must follow a similar plan +- Update `static elementProperties` to read from `[Symbol.metadata]` + - 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. #### II. Deprecate legacy decorators ##### Requirements -* Stage II has landed. -* Preferred: At least one browser has shipped decorators -* Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. - * 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. -* Vend codemods to upgrade decorator usage: - * One to migrate to the new standard decorators - * One to migrate imports to the explicitly legacy decorators - -* (Optional, for google3) Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: - * A _third_ set of decorators: `'lit/experimental-decorators.js'` - * They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. - * This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. - * This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. - * There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. - * Add a codemode to migrate to the new experimental decorators + +- Stage II has landed. +- Preferred: At least one browser has shipped decorators +- Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. + - 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. +- Vend codemods to upgrade decorator usage: + + - One to migrate to the new standard decorators + - One to migrate imports to the explicitly legacy decorators + +- (Optional, for google3) Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: + - A _third_ set of decorators: `'lit/experimental-decorators.js'` + - They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. + - This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. + - It's also useful for larger projects that want to incrementally migrate to the new decorators without multiple tsconfigs with per-file carve outs as they migrate. + - This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. + - There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. + - Add a codemode to migrate to the new experimental decorators ##### Changes -* Deprecate the `noAccessor` property option. + +- Deprecate the `noAccessor` property option. #### III. Prefer standard decorators (breaking) -This stage is still usable in non-decorator environments via `static properties`, but we move decorator modules around so that legacy use must use the non-default module names (`'lit/legacy-decorators.js'`) \ +This stage is still usable in non-decorator environments via `static properties`, but we move decorator modules around so that legacy use must use the non-default module names (`'lit/legacy-decorators.js'`). ##### Requirements -* Stage II has landed. -* At least one browser has shipped decorators +- Stage II has landed. +- At least one browser has shipped decorators ##### Changes -* Move standard decorators implementation to 'lit/decorators.js' -* Re-export standard decorators from 'lit/std-decorators.js' -* Deprecate static properties -* Deprecate static addInitializer() -* Vend codemod to migrate standard decorator imports +- Move standard decorators implementation to 'lit/decorators.js' +- Re-export standard decorators from 'lit/std-decorators.js' +- Deprecate static properties +- Deprecate static addInitializer() +- Vend codemod to migrate standard decorator imports #### IV. Remove legacy decorators (breaking) @@ -185,17 +191,16 @@ This stage requires decorators for creating properties. It is no longer usable i ##### Requirements -* Native decorators are shipping in all major browsers. -* Stage III has landed +- Native decorators are shipping in all major browsers. +- Stage III has landed ##### Changes -* Remove previously deprecated APIs -* Deprecate `'lit/std-decorators.js'` module -* Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules. - * This increases the core module size, which is paid for by removing the deprecated APIs. - * Other decorators are more optional and remain in their own modules. - +- Remove previously deprecated APIs +- Deprecate `'lit/std-decorators.js'` module +- Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules. + - This increases the core module size, which is paid for by removing the deprecated APIs. + - Other decorators are more optional and remain in their own modules. #### V. Cleanup (breaking) @@ -203,14 +208,14 @@ When all developers can use the standard decorators API we can remove the experi ##### Requirements -* Stage IV has landed -* Native decorators are shipping in all major browsers. +- Stage IV has landed +- Native decorators are shipping in all major browsers. ##### Changes -* Remove 'lit/std-decorators.js' -* Remove 'lit/experimental-decorators.js' -* Vend codemod to move experimental decorator imports to standard decorators +- Remove 'lit/std-decorators.js' +- Remove 'lit/experimental-decorators.js' +- Vend codemod to move experimental decorator imports to standard decorators ### Backward Compatibility From 6084f0009f46d04cf2201f81f694597709e3d710 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Mon, 23 Oct 2023 11:25:36 -0700 Subject: [PATCH 4/9] Update decorator RFC with hybrid decorators. --- rfcs/NNNN-decorator-roadmap.md | 132 ++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 53 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index 03a5769..0e706d0 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -1,5 +1,5 @@ --- -Status: Active +Status: Accepted Champions: "@justinfagnani" PR: https://github.com/lit/rfcs/pull/20 --- @@ -28,7 +28,7 @@ Migrate to using standard decorators as the only decorator implementation and un ## Motivation -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 +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. 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 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. @@ -38,7 +38,7 @@ Detailed design of the new decorators should be covered in another RFC. This RFC - That standard decorators become the only way to declare reactive properties - That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators -- A multi-stage plan for making the changes so that developers can incrementally migrate +- A plan so that developers can incrementally migrate to standard decorators ### Audiences @@ -63,7 +63,7 @@ After all stages of the implementation plan are complete: ### Standard Decorators -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. +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. 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. @@ -101,7 +101,7 @@ class MyElement extends LitElement { } ``` -### Changes in behavior with standard decorators +### Changes in behavior possible with standard decorators 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. @@ -113,21 +113,74 @@ As mention already, the `accessor` keyword will be required for all formerly fie 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. +We will _not_ do this, as it will make migrating more challenging. + #### Restoring default property values when attributes are removed We could also remember the initial property value in order to restore it when an associated attribute is removed. 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. +### Hybrid Decorators + +For an incremental migration path, Lit experimental decorators should be _use site_ compatible +with standard decorators. We're calling this the "hybrid" decorator approach, and +these decorators "hybrid decorators". + +To enable hybrid decorators, the existing Lit experimental decorators need some minor +breaking changes. These should most likely not impact many users. + + - `requestUpdate()` will be called automatically for `@property` and `@state` decorated accessors. + - The value of an accessor is read on first render and used as the initial value for `changedProperties` and attribute reflection. + - No longer support version `"2018-09"` of `@babel/plugin-proposal-decorators`. + +Additionally, the existing experimental decorators must also work with auto-accessors and the `accessor` keyword, so they can be syntactically identical to standard decorators. + +Hybrid decorators make migrating from experimental decorators to standard decorators incremental for existing codebases by enabling +each decorator to be independently updated to match standard decorator syntax. Once all decorators are syntactically identical to +standard decorator usage, the `experimentalDecorators` TypeScript flag can be turned off, and semantics will remain unchanged. + +#### Example migration + +1. Existing codebase using experimental decorators + +```ts +// tsconfig uses `experimentalDecorators: true`. +class El extends LitElement { + @property() + reactiveProp = "initial value"; + + @state() + secondProperty = 0; +} +``` + +2. Incrementally make decorator usage _use site_ compatible by adding `accessor` + +```ts +// tsconfig uses `experimentalDecorators: true`. +class El extends LitElement { + @property() + accessor reactiveProp = "initial value"; + + @state() + accessor secondProperty = 0; +} +``` + +3. Finally, change `experimentalDecorators` to `false` to complete the migration. + ## Implementation Considerations ### Implementation Plan -The plan is divided into stages which must be shipped as releases. +The plan is to make the Lit decorators work in both experimental decorator +environments and standard decorator environments. -#### I. Add standard decorators (non-breaking) +#### I. Make Lit decorators hybrid (breaking) -This stage adds support for the new decorators standard in a backward-compatible way for both TypeScript and JavaScript developers. +This stage adds support for the new decorators standard for both TypeScript and JavaScript developers, +and ensures experimental decorators are semantically identical to standard decorators. ##### Requirements @@ -135,76 +188,45 @@ This stage adds support for the new decorators standard in a backward-compatible ##### Changes -- Add new module with standard decorators - - Place them in a new module: `'lit/std-decorators.js'` -- Re-export legacy decorators from `'lit/legacy-decorators.js'` +- Make Lit decorators "hybrid", working in either experimental or standard environments. - Non-core packages with decorators (@lit/labs) must follow a similar plan - Update `static elementProperties` to read from `[Symbol.metadata]` - 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. -#### II. Deprecate legacy decorators +#### II. Deprecate experimental decorators ##### Requirements -- Stage II has landed. +- Stage I has landed. - Preferred: At least one browser has shipped decorators -- Deprecate legacy experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. - - 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. -- Vend codemods to upgrade decorator usage: - - - One to migrate to the new standard decorators - - One to migrate imports to the explicitly legacy decorators - -- (Optional, for google3) Add new module with a new implementation of _experimental_ decorators that work the same way standard decorators do: - - A _third_ set of decorators: `'lit/experimental-decorators.js'` - - They require `accessor`, replace the accessors on the prototype, do not call `static createProperty()`, store metadata in `[Symbol.metadata]`. - - This is so that users stuck on the experimental decorators setting (ie, google3) have a path forward when we remove the infrastructure that supports them. - - It's also useful for larger projects that want to incrementally migrate to the new decorators without multiple tsconfigs with per-file carve outs as they migrate. - - This decorator set will also be _use site_ compatible with standard decorators. Only the import will need to change to upgrade to standard decorators. - - There is no great way to emulate the standard `addInitializer()`, so we keep our `static addInitializer()` un-deprecated. - - Add a codemode to migrate to the new experimental decorators ##### Changes - Deprecate the `noAccessor` property option. +- Deprecate experimental decorators, `static createProperty()`, `static getPropertyDescriptor()`, etc. but _not_ `static properties`. + - 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. -#### III. Prefer standard decorators (breaking) - -This stage is still usable in non-decorator environments via `static properties`, but we move decorator modules around so that legacy use must use the non-default module names (`'lit/legacy-decorators.js'`). - -##### Requirements - -- Stage II has landed. -- At least one browser has shipped decorators - -##### Changes - -- Move standard decorators implementation to 'lit/decorators.js' -- Re-export standard decorators from 'lit/std-decorators.js' -- Deprecate static properties -- Deprecate static addInitializer() -- Vend codemod to migrate standard decorator imports -#### IV. Remove legacy decorators (breaking) +#### III. Remove experimental decorators (breaking) This stage requires decorators for creating properties. It is no longer usable in non-decorator-supporting environments without transpilation. ##### Requirements +- Stage II has landed - Native decorators are shipping in all major browsers. -- Stage III has landed ##### Changes - Remove previously deprecated APIs -- Deprecate `'lit/std-decorators.js'` module +- Deprecate `static properties` - Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules. - This increases the core module size, which is paid for by removing the deprecated APIs. - Other decorators are more optional and remain in their own modules. #### V. Cleanup (breaking) -When all developers can use the standard decorators API we can remove the experimental decorators fully. +When all developers can use the standard decorators API we can remove the experimental decorators fully as well as `static properties`. ##### Requirements @@ -213,17 +235,21 @@ When all developers can use the standard decorators API we can remove the experi ##### Changes -- Remove 'lit/std-decorators.js' -- Remove 'lit/experimental-decorators.js' -- Vend codemod to move experimental decorator imports to standard decorators +- Remove all non standard decorator APIs, including `static properties` ### Backward Compatibility -See Implementation Plan +See Implementation Plan. ### Testing Plan -N/A +Hybrid decorators requires testing in three configurations: + +1. Experimental decorator syntax (no `accessor` keyword), with `experimentalDecorators: true`. This reflects the existing tests. +2. Standard decorator syntax, with `experimentalDecorators: true`. Tests that experimental decorators can be incrementally migrated. +3. Standard decorator syntax, with `experimentalDecorators: false`. Test that standard decorators match experimental decorator behavior exactly. + +In the future, as we cleanup experimental decorators, we'll be able to remove tests in configurations `1.`, and `2.`. ### Performance and Code Size Impact From 63d68e2c7d786aa069f612bc7e56262cb6c18bfd Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Wed, 25 Oct 2023 18:03:35 -0700 Subject: [PATCH 5/9] Apply suggestions from code review --- rfcs/NNNN-decorator-roadmap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index 0e706d0..fbcbefa 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -224,13 +224,13 @@ This stage requires decorators for creating properties. It is no longer usable i - This increases the core module size, which is paid for by removing the deprecated APIs. - Other decorators are more optional and remain in their own modules. -#### V. Cleanup (breaking) +#### IV. Cleanup (breaking) When all developers can use the standard decorators API we can remove the experimental decorators fully as well as `static properties`. ##### Requirements -- Stage IV has landed +- Stage III has landed - Native decorators are shipping in all major browsers. ##### Changes From ac5398c8612557dbfc399d2135b3c0c2dfc12e52 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Thu, 26 Oct 2023 16:55:36 -0700 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Augustine Kim --- rfcs/NNNN-decorator-roadmap.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index fbcbefa..739b157 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -185,11 +185,12 @@ and ensures experimental decorators are semantically identical to standard decor ##### Requirements - TypeScript ships decorator metadata (TypeScript 5.2, scheduled for 2023-08-22) +- Babel ships decorator proposal plugin with metadata support (Babel 7.23.0, 2023-09-25) ##### Changes - Make Lit decorators "hybrid", working in either experimental or standard environments. -- Non-core packages with decorators (@lit/labs) must follow a similar plan +- Non-core packages with decorators (`@lit/localize`, `@lit-labs/context`) must follow a similar plan - Update `static elementProperties` to read from `[Symbol.metadata]` - 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. From 8a7fc701374c766a94904194fc4ea4ada9338579 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Thu, 26 Oct 2023 17:42:46 -0700 Subject: [PATCH 7/9] Apply suggestions from code review --- rfcs/NNNN-decorator-roadmap.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index 739b157..a7ccfa1 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -44,10 +44,10 @@ Detailed design of the new decorators should be covered in another RFC. This RFC If we group our developers by how they relate to decorators, we have four distinct audiences: -- JavaScript developers not yet using decorators -- TypeScript developers using legacy experimental decorators -- TypeScript developers not yet using decorators because of forward compatibility concerns -- TypeScript developers stuck using legacy experimental decorators because of backward compatibility concerns +- JavaScript/TypeScript developers not yet using decorators +- JavaScript developers using decorators with Babel +- TypeScript developers sticking to using legacy experimental decorators +- TypeScript developers using standard decorators This plan needs to address all audiences during the migration. @@ -232,7 +232,7 @@ When all developers can use the standard decorators API we can remove the experi ##### Requirements - Stage III has landed -- Native decorators are shipping in all major browsers. +- Native decorators are have been shipping in all major browsers for last 2 major versions. ##### Changes From dd5d4e63302f95ca0c548f7d5d38b06a5ee62667 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Fri, 27 Oct 2023 14:48:01 -0700 Subject: [PATCH 8/9] Apply Justin's suggestions from code review Co-authored-by: Justin Fagnani --- rfcs/NNNN-decorator-roadmap.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index a7ccfa1..fb213e4 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -30,11 +30,11 @@ Migrate to using standard decorators as the only decorator implementation and un 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. -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 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. +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. ## Detailed Design -Detailed design of the new decorators should be covered in another RFC. This RFC focuses on: +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: - That standard decorators become the only way to declare reactive properties - That breaking changes are made to remove support for dynamically adding reactive properties, since that will not be used by the new decorators @@ -101,7 +101,7 @@ class MyElement extends LitElement { } ``` -### Changes in behavior possible with standard decorators +### Potential and required changes in behavior with standard decorators 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. @@ -113,7 +113,7 @@ As mention already, the `accessor` keyword will be required for all formerly fie 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. -We will _not_ do this, as it will make migrating more challenging. +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. #### Restoring default property values when attributes are removed From 30ee537a3c07175fe59daad6e3a33fc9bbee60f6 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 30 Oct 2023 18:37:13 -0700 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Justin Fagnani --- rfcs/NNNN-decorator-roadmap.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rfcs/NNNN-decorator-roadmap.md b/rfcs/NNNN-decorator-roadmap.md index fb213e4..a5eb719 100644 --- a/rfcs/NNNN-decorator-roadmap.md +++ b/rfcs/NNNN-decorator-roadmap.md @@ -56,7 +56,8 @@ This plan needs to address all audiences during the migration. After all stages of the implementation plan are complete: - Standard decorators are the only decorator implementation -- Decorators are the one way to define reactive properties +- Decorators are the one _core_ way to define reactive properties +- Add a non-core utility library that can dynamically create reactive properties - Remove `static properties`, `static createProperty()` and associated APIs - Remove `static addInitializer()` since the standard decorator API provides this - Core decorators (`@property`, `@state`) are exported from the main `reactive-element`, `lit-element` and `lit` modules. @@ -220,7 +221,8 @@ This stage requires decorators for creating properties. It is no longer usable i ##### Changes - Remove previously deprecated APIs -- Deprecate `static properties` +- Deprecate `static properties` in ReactiveElement and LitElement +- Add a new mixin that supports `static properties` for migration. - Re-export `@customElement()`, `@property()` and `@state()` from the main reactive-element, lit-element, and lit modules. - This increases the core module size, which is paid for by removing the deprecated APIs. - Other decorators are more optional and remain in their own modules.