Skip to content

Adding stage3-compatible decorator implementations #20507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions packages/@ember/-internals/metal/lib/decorator-util.ts
Original file line number Diff line number Diff line change
@@ -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<Decorator> {
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<Decorator>):
| {
kind: 'method';
value: Parameters<ClassMethodDecorator>[0];
context: Parameters<ClassMethodDecorator>[1];
}
| {
kind: 'getter';
value: Parameters<ClassGetterDecorator>[0];
context: Parameters<ClassGetterDecorator>[1];
}
| {
kind: 'setter';
value: Parameters<ClassSetterDecorator>[0];
context: Parameters<ClassSetterDecorator>[1];
}
| {
kind: 'field';
value: Parameters<ClassFieldDecorator>[0];
context: Parameters<ClassFieldDecorator>[1];
}
| {
kind: 'class';
value: Parameters<ClassDecorator>[0];
context: Parameters<ClassDecorator>[1];
}
| {
kind: 'accessor';
value: Parameters<ClassAutoAccessorDecorator>[0];
context: Parameters<ClassAutoAccessorDecorator>[1];
} {
return {
kind: args[1].kind,
value: args[0],
context: args[1],
} as ReturnType<typeof identifyModernDecoratorArgs>;
}
44 changes: 43 additions & 1 deletion packages/@ember/-internals/metal/lib/injected_property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function, any>;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<typeof decorator>));
}
};
}
}

function inject2023(
type: string,
customName: string | undefined,
args: Parameters<Decorator>
): ReturnType<ClassFieldDecorator> {
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`
);
}
}

Expand Down
49 changes: 48 additions & 1 deletion packages/@ember/-internals/metal/lib/tracked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,7 +87,17 @@ export function tracked(
key: string,
desc: DecoratorPropertyDescriptor
): DecoratorPropertyDescriptor;
export function tracked(...args: any[]): ExtendedMethodDecorator | DecoratorPropertyDescriptor {
export function tracked(
value: Parameters<ClassAutoAccessorDecorator>[0],
context: Parameters<ClassAutoAccessorDecorator>[1]
): ReturnType<ClassAutoAccessorDecorator>;
export function tracked(
...args: any[]
): ExtendedMethodDecorator | DecoratorPropertyDescriptor | ReturnType<ClassAutoAccessorDecorator> {
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)
Expand Down Expand Up @@ -200,3 +216,34 @@ export class TrackedDescriptor {
this._set.call(obj, value);
}
}

function tracked2023(...args: Parameters<Decorator>): ReturnType<ClassAutoAccessorDecorator> {
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<any, any>(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. `
);
}
}