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..bb21d4e3b8b --- /dev/null +++ b/packages/@ember/-internals/metal/lib/decorator-util.ts @@ -0,0 +1,131 @@ +/* + 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; + +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 identifyModernDecoratorArgs(args: Parameters): + | { + 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]; + } { + return { + kind: args[1].kind, + value: args[0], + context: args[1], + } as ReturnType; +} diff --git a/packages/@ember/-internals/metal/lib/injected_property.ts b/packages/@ember/-internals/metal/lib/injected_property.ts index 1228bf97454..25ca87499a9 100644 --- a/packages/@ember/-internals/metal/lib/injected_property.ts +++ b/packages/@ember/-internals/metal/lib/injected_property.ts @@ -5,6 +5,12 @@ import { computed } from './computed'; import type { DecoratorPropertyDescriptor, ElementDescriptor } from './decorator'; import { isElementDescriptor } from './decorator'; import { defineProperty } from './properties'; +import { + type ClassFieldDecorator, + identifyModernDecoratorArgs, + isModernDecoratorArgs, + type Decorator, +} from './decorator-util'; export let DEBUG_INJECTION_FUNCTIONS: WeakMap; @@ -46,6 +52,11 @@ function inject( ...args: [] | [name: string] | ElementDescriptor ): PropertyDecorator | DecoratorPropertyDescriptor | void { assert('a string type must be provided to inject', typeof type === 'string'); + + if (isModernDecoratorArgs(args)) { + return inject2023(type, undefined, args); + } + let elementDescriptor; let name: string | undefined; @@ -84,7 +95,38 @@ 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, + customName: string | undefined, + args: Parameters +): ReturnType { + 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}:${customName ?? String(dec.context.name)}`); + }; + default: + throw new Error( + `tried to use injection decorator on ${dec.kind} but it only supports fields` + ); } } diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index f5f696997f5..31b5b27dccd 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -8,6 +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, + type Decorator, + isModernDecoratorArgs, + identifyModernDecoratorArgs, +} from './decorator-util'; /** @decorator @@ -81,7 +87,17 @@ 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 { + if (isModernDecoratorArgs(args)) { + return tracked2023(...args); + } + 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 +216,34 @@ export class TrackedDescriptor { this._set.call(obj, 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. ` + ); + } +}