@praxisjs/decorators 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,23 @@ export function createGetterDecorator(behavior) {
5
5
  };
6
6
  };
7
7
  }
8
+ export function createWritableGetterDecorator(behavior) {
9
+ return function (target, context) {
10
+ context.addInitializer(function () {
11
+ const instance = this;
12
+ const binding = behavior.bind(instance, context.name, target);
13
+ const descriptor = {
14
+ get: binding.get,
15
+ configurable: true,
16
+ enumerable: false,
17
+ };
18
+ if (binding.set !== undefined)
19
+ descriptor.set = binding.set;
20
+ Object.defineProperty(instance, context.name, descriptor);
21
+ });
22
+ return target;
23
+ };
24
+ }
8
25
  export function createGetterObserverDecorator(behavior) {
9
26
  return function (target, context) {
10
27
  context.addInitializer(function () {
@@ -1 +1 @@
1
- {"version":3,"file":"create-getter-decorator.js","sourceRoot":"","sources":["../src/create-getter-decorator.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,qBAAqB,CAAC,QAAwB;IAC5D,OAAO,UACL,MAAyB,EACzB,QAA8C;QAE9C,OAAO;YACL,OAAO,QAAQ,CAAC,IAAI,CAClB,MAAmC,EACnC,IAAc,CACf,EAAO,CAAC;QACX,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAeD,MAAM,UAAU,6BAA6B,CAAC,QAAgC;IAC5E,OAAO,UACL,MAAyB,EACzB,OAA6C;QAE7C,OAAO,CAAC,cAAc,CAAC;YACrB,QAAQ,CAAC,OAAO,CACd,MAAmC,EACnC,IAAc,EACd,OAAO,CAAC,IAAc,CACvB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"create-getter-decorator.js","sourceRoot":"","sources":["../src/create-getter-decorator.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,qBAAqB,CAAC,QAAwB;IAC5D,OAAO,UACL,MAAyB,EACzB,QAA8C;QAE9C,OAAO;YACL,OAAO,QAAQ,CAAC,IAAI,CAClB,MAAmC,EACnC,IAAc,CACf,EAAO,CAAC;QACX,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAqBD,MAAM,UAAU,6BAA6B,CAAC,QAAgC;IAC5E,OAAO,UACL,MAAyB,EACzB,OAA6C;QAE7C,OAAO,CAAC,cAAc,CAAC;YACrB,MAAM,QAAQ,GAAG,IAAc,CAAC;YAChC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,MAAmC,CAAC,CAAC;YAC3F,MAAM,UAAU,GAAuB;gBACrC,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,YAAY,EAAE,IAAI;gBAClB,UAAU,EAAE,KAAK;aAClB,CAAC;YACF,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS;gBAAE,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;YAC5D,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC;AAeD,MAAM,UAAU,6BAA6B,CAAC,QAAgC;IAC5E,OAAO,UACL,MAAyB,EACzB,OAA6C;QAE7C,OAAO,CAAC,cAAc,CAAC;YACrB,QAAQ,CAAC,OAAO,CACd,MAAmC,EACnC,IAAc,EACd,OAAO,CAAC,IAAc,CACvB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@ export { type ReactiveHost } from "./reactive-host";
7
7
  export { createClassDecorator, ClassBehavior, type ClassEnhancement, } from "./create-class-decorator";
8
8
  export { createMethodDecorator, type MethodBehavior, } from "./create-method-decorator";
9
9
  export { createLifecycleMethodDecorator, type LifecycleMethodBehavior, } from "./create-lifecycle-method-decorator";
10
- export { createGetterDecorator, type GetterBehavior, createGetterObserverDecorator, type GetterObserverBehavior, } from "./create-getter-decorator";
10
+ export { createGetterDecorator, type GetterBehavior, createWritableGetterDecorator, type WritableGetterBehavior, type WritableGetterBinding, createGetterObserverDecorator, type GetterObserverBehavior, } from "./create-getter-decorator";
11
+ export { createAccessorDecorator, type AccessorBehavior, type AccessorBinding, } from "./create-accessor-decorator";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,QAAQ,EACb,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,SAAS,EACd,OAAO,EACP,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,MAAM,EACN,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,8BAA8B,EAC9B,KAAK,uBAAuB,GAC7B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,6BAA6B,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,QAAQ,EACb,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,SAAS,EACd,OAAO,EACP,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,MAAM,EACN,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,8BAA8B,EAC9B,KAAK,uBAAuB,GAC7B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,6BAA6B,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,6BAA6B,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,uBAAuB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -6,5 +6,6 @@ export { createFieldDecorator, } from "./create-field-decorator";
6
6
  export { createClassDecorator, ClassBehavior, } from "./create-class-decorator";
7
7
  export { createMethodDecorator, } from "./create-method-decorator";
8
8
  export { createLifecycleMethodDecorator, } from "./create-lifecycle-method-decorator";
9
- export { createGetterDecorator, createGetterObserverDecorator, } from "./create-getter-decorator";
9
+ export { createGetterDecorator, createWritableGetterDecorator, createGetterObserverDecorator, } from "./create-getter-decorator";
10
+ export { createAccessorDecorator, } from "./create-accessor-decorator";
10
11
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,GAGN,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,SAAS,EACT,OAAO,EAEP,OAAO,EACP,MAAM,EACN,QAAQ,EACR,kBAAkB,EAGlB,MAAM,EACN,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAgB,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EACL,oBAAoB,GAGrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,oBAAoB,EACpB,aAAa,GAEd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,GAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,8BAA8B,GAE/B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,qBAAqB,EAErB,6BAA6B,GAE9B,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,GAGN,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,SAAS,EACT,OAAO,EAEP,OAAO,EACP,MAAM,EACN,QAAQ,EACR,kBAAkB,EAGlB,MAAM,EACN,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAgB,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EACL,oBAAoB,GAGrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,oBAAoB,EACpB,aAAa,GAEd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,qBAAqB,GAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,8BAA8B,GAE/B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,qBAAqB,EAErB,6BAA6B,EAG7B,6BAA6B,GAE9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,uBAAuB,GAGxB,MAAM,6BAA6B,CAAC"}
@@ -1,2 +1,12 @@
1
- export declare function Computed(): <This, T>(target: (this: This) => T, _context: ClassGetterDecoratorContext<This, T>) => (this: This) => T;
1
+ export interface ComputedOptions<T = unknown> {
2
+ get?: (this: object) => T;
3
+ set?: (this: object, value: T) => void;
4
+ }
5
+ export declare function Computed<T, Owner extends object = object>(options: {
6
+ get: (this: Owner) => T;
7
+ set: (this: Owner, value: T) => void;
8
+ }): <This extends object>(target: ClassAccessorDecoratorTarget<This, T>, context: ClassAccessorDecoratorContext<This, T>) => ClassAccessorDecoratorResult<This, T>;
9
+ export declare function Computed(options?: {
10
+ set?: (this: object, value: unknown) => void;
11
+ }): <This, R>(target: (this: This) => R, context: ClassGetterDecoratorContext<This, R>) => (this: This) => R;
2
12
  //# sourceMappingURL=computed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/properties/computed.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,8GAavB"}
1
+ {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/properties/computed.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;IAC1B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACxC;AAKD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE;IAClE,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,CAAC;IACxB,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACtC,GAAG,CAAC,IAAI,SAAS,MAAM,EACtB,MAAM,EAAE,4BAA4B,CAAC,IAAI,EAAE,CAAC,CAAC,EAC7C,OAAO,EAAE,6BAA6B,CAAC,IAAI,EAAE,CAAC,CAAC,KAC5C,4BAA4B,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAG3C,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE;IACjC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9C,GAAG,CAAC,IAAI,EAAE,CAAC,EACV,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EACzB,OAAO,EAAE,2BAA2B,CAAC,IAAI,EAAE,CAAC,CAAC,KAC1C,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,CAAC"}
@@ -1,15 +1,27 @@
1
1
  import { computed } from "@praxisjs/core/internal";
2
- import { createGetterDecorator } from "../create-getter-decorator";
3
- export function Computed() {
4
- const cache = new WeakMap();
5
- return createGetterDecorator({
6
- wrap(original, instance) {
7
- let c = cache.get(instance);
8
- if (!c) {
9
- c = computed(() => original.call(instance));
10
- cache.set(instance, c);
11
- }
12
- return c;
2
+ import { createAccessorDecorator } from "../create-accessor-decorator";
3
+ import { createWritableGetterDecorator } from "../create-getter-decorator";
4
+ export function Computed(options) {
5
+ const optionsGet = options?.get;
6
+ const optionsSet = options?.set;
7
+ if (optionsGet) {
8
+ return createAccessorDecorator({
9
+ bind(instance, _name, _initialValue) {
10
+ const c = computed(() => optionsGet.call(instance));
11
+ return {
12
+ get: c,
13
+ set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
14
+ };
15
+ },
16
+ });
17
+ }
18
+ return createWritableGetterDecorator({
19
+ bind(instance, _name, original) {
20
+ const c = computed(() => original.call(instance));
21
+ return {
22
+ get: c,
23
+ set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
24
+ };
13
25
  },
14
26
  });
15
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/properties/computed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,MAAM,UAAU,QAAQ;IACtB,MAAM,KAAK,GAAG,IAAI,OAAO,EAAyB,CAAC;IAEnD,OAAO,qBAAqB,CAAC;QAC3B,IAAI,CAAC,QAAQ,EAAE,QAAQ;YACrB,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC5B,IAAI,CAAC,CAAC,EAAE,CAAC;gBACP,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC5C,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,CAAC,CAAC;QACX,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/properties/computed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AA0B3E,MAAM,UAAU,QAAQ,CAAc,OAA4B;IAChE,MAAM,UAAU,GAAG,OAAO,EAAE,GAA8C,CAAC;IAC3E,MAAM,UAAU,GAAG,OAAO,EAAE,GAA2D,CAAC;IAExF,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,uBAAuB,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,aAAa;gBACjC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACpD,OAAO;oBACL,GAAG,EAAE,CAAC;oBACN,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;iBAC/E,CAAC;YACJ,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,6BAA6B,CAAC;QACnC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ;YAC5B,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YAClD,OAAO;gBACL,GAAG,EAAE,CAAC;gBACN,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;aAC/E,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/decorators",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,8 +15,8 @@
15
15
  "typescript": "^5.9.3"
16
16
  },
17
17
  "dependencies": {
18
- "@praxisjs/core": "1.7.0",
19
- "@praxisjs/shared": "0.2.0"
18
+ "@praxisjs/core": "1.8.0",
19
+ "@praxisjs/shared": "0.3.0"
20
20
  },
21
21
  "scripts": {
22
22
  "build": "tsc",
@@ -4,6 +4,8 @@ import { StatefulComponent } from "@praxisjs/core";
4
4
  import { signal, computed } from "@praxisjs/core/internal";
5
5
 
6
6
  import { createFieldDecorator } from "../create-field-decorator";
7
+ import { createGetterDecorator } from "../create-getter-decorator";
8
+ import { createAccessorDecorator } from "../create-accessor-decorator";
7
9
  import { Watch } from "../functions/watch";
8
10
  import { When } from "../functions/when";
9
11
  import { Compose } from "../properties/compose";
@@ -152,39 +154,243 @@ describe("Prop decorator", () => {
152
154
 
153
155
  describe("Computed decorator", () => {
154
156
  it("wraps a getter in a computed — result is memoized", () => {
155
- const fn = vi.fn(function (this: { base: number }) {
156
- return this.base * 2;
157
- });
157
+ const s = signal(5);
158
+ const fn = vi.fn(function (this: object) { return s() * 2; });
158
159
 
159
- const wrappedGetter = Computed()(fn, {} as ClassGetterDecoratorContext);
160
- const instance = { base: 5 } as unknown as StatefulComponent;
160
+ const { ctx, run } = getterCtx("doubled");
161
+ Computed()(fn, ctx);
162
+ const instance = {} as unknown as StatefulComponent;
163
+ run(instance);
161
164
 
162
- const result1 = wrappedGetter.call(instance);
163
- const result2 = wrappedGetter.call(instance);
165
+ const result1 = (instance as unknown as Record<string, unknown>).doubled;
166
+ const result2 = (instance as unknown as Record<string, unknown>).doubled;
164
167
  expect(result1).toBe(10);
165
168
  expect(result2).toBe(10);
166
- expect(fn).toHaveBeenCalledTimes(1); // lazy caching
169
+ expect(fn).toHaveBeenCalledTimes(1);
167
170
  });
168
171
 
169
172
  it("recomputes when underlying signal changes", () => {
170
173
  const s = signal(3);
171
174
  const getter = function (this: object) { return s() * 2; };
172
- const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
175
+ const { ctx, run } = getterCtx("val");
176
+ Computed()(getter, ctx);
173
177
  const obj = {} as unknown as StatefulComponent;
174
- expect(wrapped.call(obj)).toBe(6);
178
+ run(obj);
179
+
180
+ expect((obj as unknown as Record<string, unknown>).val).toBe(6);
175
181
  s.set(5);
176
- expect(wrapped.call(obj)).toBe(10);
182
+ expect((obj as unknown as Record<string, unknown>).val).toBe(10);
177
183
  });
178
184
 
179
185
  it("is per-instance — separate computed per object", () => {
180
186
  const s = signal(1);
181
187
  const getter = function (this: object) { return s(); };
182
- const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
188
+ const { ctx, run } = getterCtx("val");
189
+ Computed()(getter, ctx);
183
190
  const a = {} as unknown as StatefulComponent;
184
191
  const b = {} as unknown as StatefulComponent;
185
- expect(wrapped.call(a)).toBe(1);
186
- expect(wrapped.call(b)).toBe(1);
187
- // Both share the same signal but have independent computed instances
192
+ run(a);
193
+ run(b);
194
+
195
+ expect((a as unknown as Record<string, unknown>).val).toBe(1);
196
+ expect((b as unknown as Record<string, unknown>).val).toBe(1);
197
+ });
198
+ });
199
+
200
+ // ── Computed (writable) ───────────────────────────────────────────────────────
201
+
202
+ function getterCtx(name: string) {
203
+ const initializers: Array<(this: unknown) => void> = [];
204
+ return {
205
+ ctx: {
206
+ name,
207
+ kind: "getter" as const,
208
+ addInitializer(fn: (this: unknown) => void) {
209
+ initializers.push(fn);
210
+ },
211
+ } as unknown as ClassGetterDecoratorContext,
212
+ run(instance: unknown) {
213
+ initializers.forEach((fn) => { fn.call(instance); });
214
+ },
215
+ };
216
+ }
217
+
218
+ describe("Computed decorator — writable (set option)", () => {
219
+ it("setter is called when property is assigned", () => {
220
+ const written: number[] = [];
221
+ const { ctx, run } = getterCtx("double");
222
+
223
+ const s = signal(3);
224
+ const getter = function (this: object) { return s() * 2; };
225
+ Computed({ set: (v: unknown) => { written.push(v as number); } })(getter, ctx);
226
+
227
+ const instance = {} as unknown as StatefulComponent;
228
+ run(instance);
229
+
230
+ (instance as unknown as Record<string, unknown>).double = 42;
231
+ expect(written).toEqual([42]);
232
+ });
233
+
234
+ it("setter receives the component instance as this", () => {
235
+ let capturedThis: unknown;
236
+ const { ctx, run } = getterCtx("val");
237
+
238
+ const getter = function (this: object) { return 0; };
239
+ Computed({ set(this: unknown, _v: unknown) { capturedThis = this; } })(getter, ctx);
240
+
241
+ const instance = { id: "target" } as unknown as StatefulComponent;
242
+ run(instance);
243
+
244
+ (instance as unknown as Record<string, unknown>).val = 99;
245
+ expect(capturedThis).toBe(instance);
246
+ });
247
+
248
+ it("getter still returns the computed value after set option is added", () => {
249
+ const s = signal(5);
250
+ const { ctx, run } = getterCtx("doubled");
251
+
252
+ const getter = function (this: object) { return s() * 2; };
253
+ Computed({ set: (_v: unknown) => { /* no-op */ } })(getter, ctx);
254
+
255
+ const instance = {} as unknown as StatefulComponent;
256
+ run(instance);
257
+
258
+ expect((instance as unknown as Record<string, unknown>).doubled).toBe(10);
259
+ s.set(7);
260
+ expect((instance as unknown as Record<string, unknown>).doubled).toBe(14);
261
+ });
262
+
263
+ it("setter writes through to underlying signals, updating the getter", () => {
264
+ const s = signal(0);
265
+ const { ctx, run } = getterCtx("celsius");
266
+
267
+ const getter = function (this: object) { return s(); };
268
+ Computed({ set: (v: unknown) => { s.set(v as number); } })(getter, ctx);
269
+
270
+ const instance = {} as unknown as StatefulComponent;
271
+ run(instance);
272
+
273
+ (instance as unknown as Record<string, unknown>).celsius = 100;
274
+ expect((instance as unknown as Record<string, unknown>).celsius).toBe(100);
275
+ });
276
+ });
277
+
278
+ // ── Computed (accessor form) ──────────────────────────────────────────────────
279
+
280
+ describe("Computed decorator — accessor form ({ get, set })", () => {
281
+ function applyAccessorDecorator<T>(
282
+ options: { get: (this: object) => T; set: (this: object, value: T) => void },
283
+ instance: object,
284
+ ): { get: () => T; set: (v: T) => void } {
285
+ const target = {
286
+ get(this: object) { return undefined as T; },
287
+ set(_v: T) { /* original */ },
288
+ };
289
+ const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, T>;
290
+ const result = Computed(options)(target, ctx);
291
+ result.init!.call(instance, undefined as unknown as T);
292
+ return {
293
+ get: () => result.get!.call(instance),
294
+ set: (v: T) => result.set!.call(instance, v),
295
+ };
296
+ }
297
+
298
+ it("get function is wrapped in a computed — result is memoized", () => {
299
+ const s = signal(3);
300
+ const getFn = vi.fn(function (this: object) { return s() * 2; });
301
+ const instance = {};
302
+ const { get } = applyAccessorDecorator({ get: getFn, set: () => { /* no-op */ } }, instance);
303
+
304
+ expect(get()).toBe(6);
305
+ expect(get()).toBe(6);
306
+ expect(getFn).toHaveBeenCalledTimes(1);
307
+ });
308
+
309
+ it("recomputes when underlying signal changes", () => {
310
+ const s = signal(10);
311
+ const instance = {};
312
+ const { get } = applyAccessorDecorator({
313
+ get(this: object) { return s() + 1; },
314
+ set() { /* no-op */ },
315
+ }, instance);
316
+
317
+ expect(get()).toBe(11);
318
+ s.set(20);
319
+ expect(get()).toBe(21);
320
+ });
321
+
322
+ it("set function is called when the setter is invoked", () => {
323
+ const written: number[] = [];
324
+ const instance = {};
325
+ const { set } = applyAccessorDecorator({
326
+ get(this: object) { return 0; },
327
+ set(_v: number) { written.push(_v); },
328
+ }, instance);
329
+
330
+ set(42);
331
+ expect(written).toEqual([42]);
332
+ });
333
+
334
+ it("set function receives the component instance as this", () => {
335
+ let capturedThis: unknown;
336
+ const instance = { id: "target" };
337
+ const { set } = applyAccessorDecorator({
338
+ get(this: object) { return 0; },
339
+ set(this: unknown) { capturedThis = this; },
340
+ }, instance);
341
+
342
+ set(1);
343
+ expect(capturedThis).toBe(instance);
344
+ });
345
+
346
+ it("set writes through to underlying signals, updating the getter", () => {
347
+ const s = signal(0);
348
+ const instance = {};
349
+ const { get, set } = applyAccessorDecorator({
350
+ get(this: object) { return s(); },
351
+ set(this: object, v: number) { s.set(v); },
352
+ }, instance);
353
+
354
+ set(100);
355
+ expect(get()).toBe(100);
356
+ });
357
+
358
+ it("is per-instance — separate computed per object", () => {
359
+ const s = signal(5);
360
+ const options = {
361
+ get(this: object) { return s() * 2; },
362
+ set() { /* no-op */ },
363
+ };
364
+ const a = {};
365
+ const b = {};
366
+ const decA = applyAccessorDecorator(options, a);
367
+ const decB = applyAccessorDecorator(options, b);
368
+
369
+ expect(decA.get()).toBe(10);
370
+ expect(decB.get()).toBe(10);
371
+ s.set(7);
372
+ expect(decA.get()).toBe(14);
373
+ expect(decB.get()).toBe(14);
374
+ });
375
+
376
+ it("accessor form without set — setter is a no-op", () => {
377
+ const s = signal(5);
378
+ const instance = {};
379
+ const target = { get() { return undefined as unknown as number; }, set() { /* original */ } };
380
+ const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, number>;
381
+ // Bypass overload constraint — optionsSet is undefined at runtime
382
+ type AccessorDecoFactory = (opts: { get: (this: object) => number }) => (
383
+ t: typeof target,
384
+ c: typeof ctx,
385
+ ) => ClassAccessorDecoratorResult<object, number>;
386
+ const result = (Computed as unknown as AccessorDecoFactory)(
387
+ { get(this: object) { return s() * 2; } },
388
+ )(target, ctx);
389
+ result.init!.call(instance, 0);
390
+
391
+ expect(result.get!.call(instance)).toBe(10);
392
+ expect(() => { result.set!.call(instance, 99); }).not.toThrow();
393
+ expect(result.get!.call(instance)).toBe(10); // unchanged — no setter
188
394
  });
189
395
  });
190
396
 
@@ -1157,3 +1363,83 @@ describe("createFieldDecorator — additional property", () => {
1157
1363
  expect((instance as unknown as Record<string, unknown>).extraProp).toBe("extra");
1158
1364
  });
1159
1365
  });
1366
+
1367
+ // ── createGetterDecorator ─────────────────────────────────────────────────────
1368
+
1369
+ describe("createGetterDecorator", () => {
1370
+ it("calls behavior.wrap on access and returns the wrapped value", () => {
1371
+ const wrap = vi.fn((original: (this: object) => unknown, instance: object) => {
1372
+ return () => (original.call(instance) as number) * 2;
1373
+ });
1374
+ const decorator = createGetterDecorator({ wrap });
1375
+ const originalGetter = function (this: object) { return 5 as unknown; };
1376
+ const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
1377
+ const wrapped = decorator(originalGetter as (this: object) => number, ctx);
1378
+
1379
+ const instance = {};
1380
+ expect(wrapped.call(instance)).toBe(10);
1381
+ expect(wrap).toHaveBeenCalledWith(originalGetter, instance);
1382
+ });
1383
+
1384
+ it("passes the correct instance as this to behavior.wrap", () => {
1385
+ let capturedInstance: unknown;
1386
+ const decorator = createGetterDecorator({
1387
+ wrap(_original, instance) {
1388
+ capturedInstance = instance;
1389
+ return () => 0;
1390
+ },
1391
+ });
1392
+ const getter = function (this: object) { return 0 as unknown; };
1393
+ const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
1394
+ const wrapped = decorator(getter as (this: object) => number, ctx);
1395
+
1396
+ const instance = { id: "test-instance" };
1397
+ wrapped.call(instance);
1398
+ expect(capturedInstance).toBe(instance);
1399
+ });
1400
+ });
1401
+
1402
+ // ── createAccessorDecorator — init ────────────────────────────────────────────
1403
+
1404
+ describe("createAccessorDecorator — init", () => {
1405
+ it("init stores the initial value that bind receives on first get", () => {
1406
+ let capturedInitialValue: unknown = "NOT_SET";
1407
+ const behavior = {
1408
+ bind(_inst: object, _name: string | symbol, initialValue: unknown) {
1409
+ capturedInitialValue = initialValue;
1410
+ return { get: () => initialValue };
1411
+ },
1412
+ };
1413
+ const decorator = createAccessorDecorator(behavior);
1414
+ const target = {
1415
+ get() { return 0; },
1416
+ set() { /* original */ },
1417
+ } as ClassAccessorDecoratorTarget<object, number>;
1418
+ const ctx = { kind: "accessor", name: "val" } as unknown as ClassAccessorDecoratorContext<object, number>;
1419
+ const result = decorator(target, ctx);
1420
+
1421
+ const instance = {};
1422
+ const returned = result.init!.call(instance, 42);
1423
+ result.get!.call(instance); // triggers bind
1424
+ expect(returned).toBe(42);
1425
+ expect(capturedInitialValue).toBe(42);
1426
+ });
1427
+
1428
+ it("init returns the initial value unchanged", () => {
1429
+ const behavior = {
1430
+ bind(_inst: object, _name: string | symbol, initialValue: unknown) {
1431
+ return { get: () => initialValue };
1432
+ },
1433
+ };
1434
+ const decorator = createAccessorDecorator(behavior);
1435
+ const target = {
1436
+ get() { return ""; },
1437
+ set() { /* original */ },
1438
+ } as ClassAccessorDecoratorTarget<object, string>;
1439
+ const ctx = { kind: "accessor", name: "s" } as unknown as ClassAccessorDecoratorContext<object, string>;
1440
+ const result = decorator(target, ctx);
1441
+
1442
+ const instance = {};
1443
+ expect(result.init!.call(instance, "hello")).toBe("hello");
1444
+ });
1445
+ });
@@ -0,0 +1,45 @@
1
+ export interface AccessorBinding {
2
+ get: () => unknown;
3
+ set?: (value: unknown) => void;
4
+ }
5
+
6
+ export interface AccessorBehavior {
7
+ /**
8
+ * Called once per instance on first access. `initialValue` is the value
9
+ * provided by the `accessor` field initializer (e.g. `accessor count = 0`).
10
+ * Returns the get/set pair used by the accessor.
11
+ */
12
+ bind(instance: object, name: string | symbol, initialValue: unknown): AccessorBinding;
13
+ }
14
+
15
+ export function createAccessorDecorator(behavior: AccessorBehavior) {
16
+ return function <This extends object, V>(
17
+ _target: ClassAccessorDecoratorTarget<This, V>,
18
+ context: ClassAccessorDecoratorContext<This, V>,
19
+ ): ClassAccessorDecoratorResult<This, V> {
20
+ const bindings = new WeakMap<object, AccessorBinding>();
21
+ const initValues = new WeakMap<object, unknown>();
22
+
23
+ function getBinding(instance: object): AccessorBinding {
24
+ let b = bindings.get(instance);
25
+ if (!b) {
26
+ b = behavior.bind(instance, context.name, initValues.get(instance));
27
+ bindings.set(instance, b);
28
+ }
29
+ return b;
30
+ }
31
+
32
+ return {
33
+ init(this: This, value: V): V {
34
+ initValues.set(this as object, value);
35
+ return value;
36
+ },
37
+ get(this: This): V {
38
+ return getBinding(this as object).get() as V;
39
+ },
40
+ set(this: This, value: V): void {
41
+ getBinding(this as object).set?.(value as unknown);
42
+ },
43
+ };
44
+ };
45
+ }
@@ -23,6 +23,45 @@ export function createGetterDecorator(behavior: GetterBehavior) {
23
23
  };
24
24
  }
25
25
 
26
+ export interface WritableGetterBinding {
27
+ get: () => unknown;
28
+ set?: (value: unknown) => void;
29
+ }
30
+
31
+ export interface WritableGetterBehavior {
32
+ /**
33
+ * Called once per instance during construction. Returns the get/set pair
34
+ * that will be installed on the instance via `Object.defineProperty`.
35
+ * If `set` is omitted, the property is read-only at runtime (though TypeScript
36
+ * may still see a setter if one is declared in the class body).
37
+ */
38
+ bind(
39
+ instance: object,
40
+ name: string | symbol,
41
+ original: (this: object) => unknown,
42
+ ): WritableGetterBinding;
43
+ }
44
+
45
+ export function createWritableGetterDecorator(behavior: WritableGetterBehavior) {
46
+ return function <This, T>(
47
+ target: (this: This) => T,
48
+ context: ClassGetterDecoratorContext<This, T>,
49
+ ): (this: This) => T {
50
+ context.addInitializer(function (this: unknown) {
51
+ const instance = this as object;
52
+ const binding = behavior.bind(instance, context.name, target as (this: object) => unknown);
53
+ const descriptor: PropertyDescriptor = {
54
+ get: binding.get,
55
+ configurable: true,
56
+ enumerable: false,
57
+ };
58
+ if (binding.set !== undefined) descriptor.set = binding.set;
59
+ Object.defineProperty(instance, context.name, descriptor);
60
+ });
61
+ return target;
62
+ };
63
+ }
64
+
26
65
  export interface GetterObserverBehavior {
27
66
  /**
28
67
  * Called once per instance when the class is initialized.
package/src/index.ts CHANGED
@@ -54,6 +54,14 @@ export {
54
54
  export {
55
55
  createGetterDecorator,
56
56
  type GetterBehavior,
57
+ createWritableGetterDecorator,
58
+ type WritableGetterBehavior,
59
+ type WritableGetterBinding,
57
60
  createGetterObserverDecorator,
58
61
  type GetterObserverBehavior,
59
62
  } from "./create-getter-decorator";
63
+ export {
64
+ createAccessorDecorator,
65
+ type AccessorBehavior,
66
+ type AccessorBinding,
67
+ } from "./create-accessor-decorator";