From 81df665b195192bce437ce0e50d263fc88ebeb33 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Sat, 22 Jul 2023 12:26:12 -0700 Subject: [PATCH 1/3] Adding stage3-compatible decorator implementations (This should stay draft and not merge until we have some consensus on the ecosystem-spanning parts of the migration.) This is an exploration to see how easy it is to make the ember-provided decorators work under both legacy decorators and stage3 decorators. (I don't think it's hard!) The first commit here implements `@tracked`. I tested it in an application with the configuration: ```js const app = new EmberApp(defaults, { 'ember-cli-babel': { disableDecoratorTransforms: true, }, babel: { plugins: [ [ '@babel/plugin-proposal-decorators', { version: '2023-05', }, ], ], }, }); ``` With those settings, `@tracked foo` becomes a runtime error and will tell you to use `@tracked accessor foo` instead, and when you do it all works correctly. --- .../-internals/metal/lib/decorator-util.ts | 124 ++++++++++++++++++ .../@ember/-internals/metal/lib/tracked.ts | 44 ++++++- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 packages/@ember/-internals/metal/lib/decorator-util.ts diff --git a/packages/@ember/-internals/metal/lib/decorator-util.ts b/packages/@ember/-internals/metal/lib/decorator-util.ts new file mode 100644 index 00000000000..7dda1e383b6 --- /dev/null +++ b/packages/@ember/-internals/metal/lib/decorator-util.ts @@ -0,0 +1,124 @@ +/* + Types and utilities for working with 2023-05 decorators -- the ones that are + currently (as of 2023-07-22) in Stage 3. +*/ + +export type ClassMethodDecorator = ( + value: Function, + context: { + kind: 'method'; + name: string | symbol; + access: { get(): unknown }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; + } +) => Function | void; + +export type ClassGetterDecorator = ( + value: Function, + context: { + kind: 'getter'; + name: string | symbol; + access: { get(): unknown }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; + } +) => Function | void; + +export type ClassSetterDecorator = ( + value: Function, + context: { + kind: 'setter'; + name: string | symbol; + access: { set(value: unknown): void }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; + } +) => Function | void; + +export type ClassFieldDecorator = ( + value: undefined, + context: { + kind: 'field'; + name: string | symbol; + access: { get(): unknown; set(value: unknown): void }; + static: boolean; + private: boolean; + } +) => (initialValue: unknown) => unknown | void; + +export type ClassDecorator = ( + value: Function, + context: { + kind: 'class'; + name: string | undefined; + addInitializer(initializer: () => void): void; + } +) => Function | void; + +export type ClassAutoAccessorDecorator = ( + value: { + get(): unknown; + set(value: unknown): void; + }, + context: { + kind: 'accessor'; + name: string | symbol; + access: { get(): unknown; set(value: unknown): void }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; + } +) => { + get?: () => unknown; + set?: (value: unknown) => void; + init?: (initialValue: unknown) => unknown; +} | void; + +// this is designed to turn the arguments into a discriminated union so you can +// check the kind once and then have the right types for them. +export function identify2023DecoratorArgs(args: unknown[]): + | { + kind: 'method'; + value: Parameters[0]; + context: Parameters[1]; + } + | { + kind: 'getter'; + value: Parameters[0]; + context: Parameters[1]; + } + | { + kind: 'setter'; + value: Parameters[0]; + context: Parameters[1]; + } + | { + kind: 'field'; + value: Parameters[0]; + context: Parameters[1]; + } + | { + kind: 'class'; + value: Parameters[0]; + context: Parameters[1]; + } + | { + kind: 'accessor'; + value: Parameters[0]; + context: Parameters[1]; + } + | undefined { + if (args.length !== 2 || typeof args[1] !== 'object' || args[1] == null || !('kind' in args[1])) { + return undefined; + } + + return { + kind: args[1].kind, + value: args[0], + context: args[1], + } as ReturnType; +} diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index f5f696997f5..84aba3d9b48 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -8,6 +8,7 @@ import { CHAIN_PASS_THROUGH } from './chain-tags'; import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; import { COMPUTED_SETTERS, isElementDescriptor, setClassicDecorator } from './decorator'; import { SELF_TAG } from './tags'; +import { type ClassAutoAccessorDecorator, identify2023DecoratorArgs } from './decorator-util'; /** @decorator @@ -81,7 +82,35 @@ export function tracked( key: string, desc: DecoratorPropertyDescriptor ): DecoratorPropertyDescriptor; -export function tracked(...args: any[]): ExtendedMethodDecorator | DecoratorPropertyDescriptor { +export function tracked( + value: Parameters[0], + context: Parameters[1] +): ReturnType; +export function tracked( + ...args: any[] +): ExtendedMethodDecorator | DecoratorPropertyDescriptor | ReturnType { + let modernDecorator = identify2023DecoratorArgs(args); + if (modernDecorator) { + if (modernDecorator.kind === 'field') { + // this is the most comment error case we expect, given that it was legal + // under legacy decorators. + throw new Error( + `You must replace "@tracked ${String( + modernDecorator.context.name + )}" with "@tracked accessor ${String( + modernDecorator.context.name + )}". @tracked without accessor only works when using the legacy decorators transform, and you are using modern decorators.` + ); + } + if (modernDecorator.kind !== 'accessor') { + // we also need to error on other cases that don't make sense for tracked. + throw new Error( + `You used @tracked on a ${modernDecorator.context.kind}, but tracked only works on accessors. ` + ); + } + return tracked2023(modernDecorator.value, modernDecorator.context); + } + assert( `@tracked can only be used directly as a native decorator. If you're using tracked in classic classes, add parenthesis to call it like a function: tracked()`, !(isElementDescriptor(args.slice(0, 3)) && args.length === 5 && args[4] === true) @@ -200,3 +229,16 @@ export class TrackedDescriptor { this._set.call(obj, value); } } + +const tracked2023: ClassAutoAccessorDecorator = function (value, context) { + let { getter, setter } = trackedData(context.name, value.get); + + return { + get() { + return getter(this); + }, + set(value) { + return setter(this, value); + }, + }; +}; From 101fe443ca39c6252c1827f0acb30a4e5a88f754 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Sat, 22 Jul 2023 12:48:41 -0700 Subject: [PATCH 2/3] implement service injection for the zero-argument case --- .../-internals/metal/lib/injected_property.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/@ember/-internals/metal/lib/injected_property.ts b/packages/@ember/-internals/metal/lib/injected_property.ts index 1228bf97454..69e835206d1 100644 --- a/packages/@ember/-internals/metal/lib/injected_property.ts +++ b/packages/@ember/-internals/metal/lib/injected_property.ts @@ -5,6 +5,7 @@ import { computed } from './computed'; import type { DecoratorPropertyDescriptor, ElementDescriptor } from './decorator'; import { isElementDescriptor } from './decorator'; import { defineProperty } from './properties'; +import { type ClassFieldDecorator, identify2023DecoratorArgs } from './decorator-util'; export let DEBUG_INJECTION_FUNCTIONS: WeakMap; @@ -46,6 +47,19 @@ function inject( ...args: [] | [name: string] | ElementDescriptor ): PropertyDecorator | DecoratorPropertyDescriptor | void { assert('a string type must be provided to inject', typeof type === 'string'); + + let modernDecorator = identify2023DecoratorArgs(args); + if (modernDecorator) { + switch (modernDecorator.kind) { + case 'field': + return inject2023(type, modernDecorator.context); + default: + throw new Error( + `tried to use injection decorator on a ${modernDecorator.kind} but it only supports fields.` + ); + } + } + let elementDescriptor; let name: string | undefined; @@ -88,4 +102,20 @@ function inject( } } +function inject2023( + type: string, + context: Parameters[1] +): ReturnType { + return function (this: object) { + let owner = getOwner(this) || (this as any).container; // fallback to `container` for backwards compat + + assert( + `Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.`, + Boolean(owner) + ); + + return owner.lookup(`${type}:${String(context.name)}`); + }; +} + export default inject; From 7be846a1b78da7da62386b5ee2f837462736400f Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Sat, 22 Jul 2023 13:19:23 -0700 Subject: [PATCH 3/3] implement injection with custom name argument --- .../-internals/metal/lib/decorator-util.ts | 23 +++--- .../-internals/metal/lib/injected_property.ts | 56 +++++++++------ .../@ember/-internals/metal/lib/tracked.ts | 71 ++++++++++--------- 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/packages/@ember/-internals/metal/lib/decorator-util.ts b/packages/@ember/-internals/metal/lib/decorator-util.ts index 7dda1e383b6..bb21d4e3b8b 100644 --- a/packages/@ember/-internals/metal/lib/decorator-util.ts +++ b/packages/@ember/-internals/metal/lib/decorator-util.ts @@ -78,9 +78,21 @@ export type ClassAutoAccessorDecorator = ( init?: (initialValue: unknown) => unknown; } | void; +export type Decorator = + | ClassMethodDecorator + | ClassGetterDecorator + | ClassSetterDecorator + | ClassFieldDecorator + | ClassDecorator + | ClassAutoAccessorDecorator; + +export function isModernDecoratorArgs(args: unknown[]): args is Parameters { + return args.length === 2 && typeof args[1] === 'object' && args[1] != null && 'kind' in args[1]; +} + // this is designed to turn the arguments into a discriminated union so you can // check the kind once and then have the right types for them. -export function identify2023DecoratorArgs(args: unknown[]): +export function identifyModernDecoratorArgs(args: Parameters): | { kind: 'method'; value: Parameters[0]; @@ -110,15 +122,10 @@ export function identify2023DecoratorArgs(args: unknown[]): kind: 'accessor'; value: Parameters[0]; context: Parameters[1]; - } - | undefined { - if (args.length !== 2 || typeof args[1] !== 'object' || args[1] == null || !('kind' in args[1])) { - return undefined; - } - + } { return { kind: args[1].kind, value: args[0], context: args[1], - } as ReturnType; + } as ReturnType; } diff --git a/packages/@ember/-internals/metal/lib/injected_property.ts b/packages/@ember/-internals/metal/lib/injected_property.ts index 69e835206d1..25ca87499a9 100644 --- a/packages/@ember/-internals/metal/lib/injected_property.ts +++ b/packages/@ember/-internals/metal/lib/injected_property.ts @@ -5,7 +5,12 @@ import { computed } from './computed'; import type { DecoratorPropertyDescriptor, ElementDescriptor } from './decorator'; import { isElementDescriptor } from './decorator'; import { defineProperty } from './properties'; -import { type ClassFieldDecorator, identify2023DecoratorArgs } from './decorator-util'; +import { + type ClassFieldDecorator, + identifyModernDecoratorArgs, + isModernDecoratorArgs, + type Decorator, +} from './decorator-util'; export let DEBUG_INJECTION_FUNCTIONS: WeakMap; @@ -48,16 +53,8 @@ function inject( ): PropertyDecorator | DecoratorPropertyDescriptor | void { assert('a string type must be provided to inject', typeof type === 'string'); - let modernDecorator = identify2023DecoratorArgs(args); - if (modernDecorator) { - switch (modernDecorator.kind) { - case 'field': - return inject2023(type, modernDecorator.context); - default: - throw new Error( - `tried to use injection decorator on a ${modernDecorator.kind} but it only supports fields.` - ); - } + if (isModernDecoratorArgs(args)) { + return inject2023(type, undefined, args); } let elementDescriptor; @@ -98,24 +95,39 @@ function inject( if (elementDescriptor) { return decorator(elementDescriptor[0], elementDescriptor[1], elementDescriptor[2]); } else { - return decorator; + return function (...args: unknown[]) { + if (isModernDecoratorArgs(args)) { + return inject2023(type, name, args); + } else { + return decorator(...(args as Parameters)); + } + }; } } function inject2023( type: string, - context: Parameters[1] + customName: string | undefined, + args: Parameters ): ReturnType { - return function (this: object) { - let owner = getOwner(this) || (this as any).container; // fallback to `container` for backwards compat - - assert( - `Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.`, - Boolean(owner) - ); + let dec = identifyModernDecoratorArgs(args); + switch (dec.kind) { + case 'field': + return function (this: object) { + let owner = getOwner(this) || (this as any).container; // fallback to `container` for backwards compat + + assert( + `Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.`, + Boolean(owner) + ); - return owner.lookup(`${type}:${String(context.name)}`); - }; + return owner.lookup(`${type}:${customName ?? String(dec.context.name)}`); + }; + default: + throw new Error( + `tried to use injection decorator on ${dec.kind} but it only supports fields` + ); + } } export default inject; diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 84aba3d9b48..31b5b27dccd 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -8,7 +8,12 @@ import { CHAIN_PASS_THROUGH } from './chain-tags'; import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; import { COMPUTED_SETTERS, isElementDescriptor, setClassicDecorator } from './decorator'; import { SELF_TAG } from './tags'; -import { type ClassAutoAccessorDecorator, identify2023DecoratorArgs } from './decorator-util'; +import { + type ClassAutoAccessorDecorator, + type Decorator, + isModernDecoratorArgs, + identifyModernDecoratorArgs, +} from './decorator-util'; /** @decorator @@ -89,26 +94,8 @@ export function tracked( export function tracked( ...args: any[] ): ExtendedMethodDecorator | DecoratorPropertyDescriptor | ReturnType { - let modernDecorator = identify2023DecoratorArgs(args); - if (modernDecorator) { - if (modernDecorator.kind === 'field') { - // this is the most comment error case we expect, given that it was legal - // under legacy decorators. - throw new Error( - `You must replace "@tracked ${String( - modernDecorator.context.name - )}" with "@tracked accessor ${String( - modernDecorator.context.name - )}". @tracked without accessor only works when using the legacy decorators transform, and you are using modern decorators.` - ); - } - if (modernDecorator.kind !== 'accessor') { - // we also need to error on other cases that don't make sense for tracked. - throw new Error( - `You used @tracked on a ${modernDecorator.context.kind}, but tracked only works on accessors. ` - ); - } - return tracked2023(modernDecorator.value, modernDecorator.context); + if (isModernDecoratorArgs(args)) { + return tracked2023(...args); } assert( @@ -230,15 +217,33 @@ export class TrackedDescriptor { } } -const tracked2023: ClassAutoAccessorDecorator = function (value, context) { - let { getter, setter } = trackedData(context.name, value.get); - - return { - get() { - return getter(this); - }, - set(value) { - return setter(this, value); - }, - }; -}; +function tracked2023(...args: Parameters): ReturnType { + let dec = identifyModernDecoratorArgs(args); + switch (dec.kind) { + case 'field': + // this is the most comment error case we expect, given that it was legal + // under legacy decorators. + throw new Error( + `You must replace "@tracked ${String(dec.context.name)}" with "@tracked accessor ${String( + dec.context.name + )}". @tracked without accessor only works when using the legacy decorators transform, and you are using modern decorators.` + ); + case 'accessor': { + let { getter, setter } = trackedData(dec.context.name, dec.value.get); + + return { + get() { + return getter(this); + }, + set(value) { + return setter(this, value); + }, + }; + } + default: + // we also need to error on other cases that don't make sense for tracked. + throw new Error( + `You used @tracked on a ${dec.context.kind}, but tracked only works on accessors. ` + ); + } +}