-
-
Save rbuckton/a464d1a0997bd3dab36c8b0caef0959a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function createDecoratorAdapter() { | |
type LegacyClassDecorator = <T extends abstract new (...args: any) => any>(target: T) => T | void; | |
type LegacyMemberDecorator = <T>(target: unknown, key: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; | |
type LegacyParameterDecorator = (target: unknown, key: string | symbol, index: number) => void; | |
type LegacyClassDecoratorConstraint<TDecorator extends LegacyClassDecorator> = | |
TDecorator extends <T extends infer U extends abstract new (...args: any) => any>(target: T) => T | void ? U : | |
(abstract new (...args: any) => any); | |
type LegacyMemberDecoratorConstraint<TDecorator extends LegacyMemberDecorator> = | |
TDecorator extends <T extends infer U>(target: any, key: any, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void ? U : | |
unknown; | |
interface DecoratedElement { | |
context: DecoratorContext; | |
decorators: ((...args: any) => any)[]; | |
params?: boolean; | |
} | |
let applied = false; | |
let lastElement: DecoratedElement | undefined; | |
const elements: DecoratedElement[] = []; | |
adapter.params = params; | |
return adapter; | |
function adapter<T extends abstract new (...args: any) => any>(target: T, context: ClassDecoratorContext): T | void; | |
// class decorator adapter | |
function adapter<TDecorator extends LegacyClassDecorator>(decorator: TDecorator): { | |
<T extends LegacyClassDecoratorConstraint<TDecorator>>(target: T, context: ClassDecoratorContext): void; | |
}; | |
// member decorator adapter | |
function adapter<TDecorator extends LegacyMemberDecorator>(decorator: TDecorator): { | |
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: T, context: ClassMethodDecoratorContext): void; | |
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: () => T, context: ClassGetterDecoratorContext): void; | |
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: (value: T) => void, context: ClassSetterDecoratorContext): void; | |
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: ClassAccessorDecoratorTarget<unknown, T>, context: ClassAccessorDecoratorContext): void; | |
(target: undefined, context: ClassFieldDecoratorContext): void; | |
}; | |
function adapter(decorator: any, context?: ClassDecoratorContext): any { | |
if (context?.kind === "class") { | |
applyElements(decorator); | |
return; | |
} | |
if (context) { | |
throw new TypeError("Invalid arguments"); | |
} | |
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter."); | |
return function (target: any, context: DecoratorContext) { | |
if (context.kind === "class") { | |
target = applyElements(target); | |
return decorator(target); | |
} | |
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter."); | |
if (context.private) throw new TypeError("Legacy decorators are not supported on private members"); | |
if (lastElement === undefined || !isMatchingContext(lastElement.context, context)) { | |
lastElement = { context, decorators: [] }; | |
elements.push(lastElement); | |
} | |
if (lastElement.context.kind !== context.kind) throw new TypeError("Legacy decorators cannot decorate both the getter and the setter."); | |
lastElement.decorators.push(decorator); | |
}; | |
} | |
function params(parameters: (LegacyParameterDecorator[] | undefined)[]) { | |
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter."); | |
return function (target_: unknown, context: DecoratorContext) { | |
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter."); | |
if (context.kind !== "class" && context.private) throw new TypeError("Legacy decorators are not supported on private members"); | |
if (lastElement === undefined || !isMatchingContext(lastElement.context, context)) { | |
lastElement = { context, decorators: [] }; | |
elements.push(lastElement); | |
} | |
if (lastElement.context.kind !== context.kind) throw new TypeError("Legacy decorators cannot decorate both the getter and the setter."); | |
if (lastElement.params) throw new TypeError("Parameter decorators already defined."); | |
if (lastElement.decorators.length) throw new TypeError("Parameter decorators must be the closest decorator to the decorated element."); | |
lastElement.params = true; | |
for (let i = 0; i < parameters.length; i++) { | |
const decorators = parameters[i]; | |
if (!decorators) continue; | |
for (let j = decorators.length - 1; j >= 0; j--) { | |
const decorator = decorators[j]; | |
lastElement.decorators.push((target, key) => { decorator(target, key, i); }); | |
} | |
} | |
}; | |
} | |
function applyElements(target: abstract new (...args: any[]) => any) { | |
applied = true; | |
for (const element of elements) { | |
switch (element.context.kind) { | |
case "method": | |
case "getter": | |
case "setter": | |
case "accessor": { | |
const legacyTarget = element.context.static ? target : target.prototype; | |
let descriptor = Object.getOwnPropertyDescriptor(legacyTarget, element.context.name) ?? {}; | |
for (const decorator of element.decorators) { | |
descriptor = decorator(legacyTarget, element.context.name, { ...descriptor }) ?? descriptor; | |
} | |
Object.defineProperty(legacyTarget, element.context.name, descriptor); | |
break; | |
} | |
case "field": | |
const legacyTarget = element.context.static ? target : target.prototype; | |
for (const decorator of element.decorators) { | |
decorator(legacyTarget, element.context.name); | |
} | |
break; | |
case "class": | |
// these should only exist for `.params()` elements. | |
for (const decorator of element.decorators) { | |
target = decorator(target) ?? target; | |
} | |
break; | |
} | |
} | |
elements.length = 0; | |
return target; | |
} | |
function isMatchingContext(a: DecoratorContext, b: DecoratorContext) { | |
return a.kind === "class" ? b.kind === "class" : | |
b.kind !== "class" && a.name === b.name && a.static === b.static && b.private === false; | |
} | |
} | |
// examples | |
function legacyClassDecorator<T extends abstract new (...args: any) => any>(target: T) { | |
abstract class subtype extends target { | |
constructor(...args: any) { | |
super(...args); | |
console.log("subtype"); | |
} | |
} | |
return subtype as T; | |
} | |
function legacyMemberDecorator<T>(target: unknown, key: string | symbol, descriptor: TypedPropertyDescriptor<T>) { | |
console.log("member", key); | |
} | |
function legacyParameterDecorator(target: unknown, key: string | symbol | unknown, parameterIndex: number) { | |
console.log("parameter", key, parameterIndex); | |
} | |
function legacyFieldDecorator(target: unknown, key: string | symbol) { | |
console.log("field", key); | |
} | |
let _ = createDecoratorAdapter(); | |
@_(legacyClassDecorator) | |
class C { | |
@_(legacyMemberDecorator) | |
@_.params([ [legacyParameterDecorator] ]) | |
method(x: number) {} | |
@_(legacyFieldDecorator) field = 1; | |
} | |
new C(); | |
// or, if you don't have a class decorator | |
_ = createDecoratorAdapter(); // reset the adapter for a new class | |
@_ | |
class D { | |
@_(legacyMemberDecorator) | |
@_.params([ [legacyParameterDecorator] ]) | |
method(x: number) {} | |
@_(legacyFieldDecorator) field = 1; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment