From 94db4af1c1e0fd4e2cd413fa2cf3dee37e8d86cf Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 13:21:25 +0200 Subject: [PATCH 01/59] Rename "registry" file to "registries" --- packages/support/src/meta/discoverAndFlush.ts | 2 +- packages/support/src/meta/findRepository.ts | 2 +- packages/support/src/meta/flush.ts | 2 +- packages/support/src/meta/getOrCreateBaseRepository.ts | 2 +- packages/support/src/meta/index.ts | 2 +- packages/support/src/meta/meta.ts | 2 +- packages/support/src/meta/{registry.ts => registries.ts} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename packages/support/src/meta/{registry.ts => registries.ts} (100%) diff --git a/packages/support/src/meta/discoverAndFlush.ts b/packages/support/src/meta/discoverAndFlush.ts index 205f4e63..b4fabd48 100644 --- a/packages/support/src/meta/discoverAndFlush.ts +++ b/packages/support/src/meta/discoverAndFlush.ts @@ -1,7 +1,7 @@ import { ConstructorLike } from '@aedart/contracts'; import { isConstructor } from '../reflections/isConstructor.js'; import { flush } from './flush.js'; -import { FLUSHED_METADATA, MEMBER_TO_METADATA } from './registry.js'; +import { FLUSHED_METADATA, MEMBER_TO_METADATA } from './registries'; /** * Discovers and flushes staged metadata for the given target. diff --git a/packages/support/src/meta/findRepository.ts b/packages/support/src/meta/findRepository.ts index 8dd3494c..fb9e280b 100644 --- a/packages/support/src/meta/findRepository.ts +++ b/packages/support/src/meta/findRepository.ts @@ -1,6 +1,6 @@ // @aedart/support/meta/findRepository.js import { Repository } from '@aedart/contracts/support/meta'; -import { registry } from './registry.js'; +import { registry } from './registries.js'; /** * Find the nearest parent repository in the prototype chain. diff --git a/packages/support/src/meta/flush.ts b/packages/support/src/meta/flush.ts index f29945ec..4f297922 100644 --- a/packages/support/src/meta/flush.ts +++ b/packages/support/src/meta/flush.ts @@ -1,6 +1,6 @@ import { ConstructorLike } from '@aedart/contracts'; import { getOrCreateBaseRepository } from './getOrCreateBaseRepository.js'; -import { FLUSHED_METADATA } from './registry.js'; +import { FLUSHED_METADATA } from './registries'; /** * Flush staged metadata into the repositories of the given owner. diff --git a/packages/support/src/meta/getOrCreateBaseRepository.ts b/packages/support/src/meta/getOrCreateBaseRepository.ts index afff9633..28a9ecdc 100644 --- a/packages/support/src/meta/getOrCreateBaseRepository.ts +++ b/packages/support/src/meta/getOrCreateBaseRepository.ts @@ -1,7 +1,7 @@ import { Repository } from '@aedart/contracts/support/meta/index.js'; import { findRepository } from './findRepository.js'; import MetaRepository from './MetaRepository.js'; -import { registry } from './registry.js'; +import { registry } from './registries.js'; /** * Gets the existing repository for a target, or creates a new one diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index c4cc4e3c..8c918b3b 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -9,5 +9,5 @@ export * from './findRepository.js'; export * from './getOrCreateBaseRepository.js'; export * from './getOrCreateRepository.js'; export * from './meta.js'; -export * from './registry.js'; +export * from './registries.js'; // export * from './resolveKeyValue.js'; // internal diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 4d9386b1..52e96388 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -3,7 +3,7 @@ import { MetaCallback } from '@aedart/contracts/support/meta/index.js'; import { Key } from '@aedart/contracts/support/types.js'; import { flush } from './flush.js'; import { getOrCreateRepository } from './getOrCreateRepository.js'; -import { MEMBER_TO_METADATA } from './registry.js'; +import { MEMBER_TO_METADATA } from './registries.js'; import { resolveKeyValue } from './resolveKeyValue.js'; /** diff --git a/packages/support/src/meta/registry.ts b/packages/support/src/meta/registries.ts similarity index 100% rename from packages/support/src/meta/registry.ts rename to packages/support/src/meta/registries.ts From 8e9328ac7641887b23d1408a2283a933b67db42b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 13:25:44 +0200 Subject: [PATCH 02/59] Cleanup --- packages/contracts/src/support/meta/Repository.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index a810fc37..987158f7 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -1,8 +1,7 @@ -// @aedart/contracts/support/meta/Repository.js import { Key } from '../types.js'; /** - * Metadata Repository Contract + * Metadata Repository */ export default interface Repository { /** From 294666f37960b2b8b53f1929b88b6a8a1af1fca3 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 14:10:12 +0200 Subject: [PATCH 03/59] Add interfaces for Owner Context and Member Address --- .../src/support/meta/MemberAddress.ts | 52 +++++++++++++++++++ .../src/support/meta/OwnerContext.ts | 12 +++++ packages/contracts/src/support/meta/index.ts | 9 +++- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/meta/MemberAddress.ts create mode 100644 packages/contracts/src/support/meta/OwnerContext.ts diff --git a/packages/contracts/src/support/meta/MemberAddress.ts b/packages/contracts/src/support/meta/MemberAddress.ts new file mode 100644 index 00000000..a6f5ab4f --- /dev/null +++ b/packages/contracts/src/support/meta/MemberAddress.ts @@ -0,0 +1,52 @@ +import { Key } from '../types.js'; +import OwnerContext from "./OwnerContext.js"; + +/** + * Member Address + */ +export default interface MemberAddress +{ + /** + * Owner Context + * + * @type {OwnerContext} + */ + readonly ctx: OwnerContext; + + /** + * Indicates whether the member is static (`true`) or otherwise (`false`) + * + * @type {boolean} + */ + readonly static: boolean; + + /** + * The kind of member + * + * @type {string} + */ + readonly kind: string; + + /** + * Name of the member + * + * @type {string|symbol} + */ + readonly name: string | symbol; + + /** + * The "base" path to where metadata is stored inside a Meta Repository + * + * @type {Key} + */ + readonly basePath: Key; + + /** + * Returns the full path to the given key in a Meta Repository, for the member + * + * @param {Key} [key] + * + * @returns {Key} {@link basePath} if no key provided + */ + path(key?: Key): Key; +} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/OwnerContext.ts b/packages/contracts/src/support/meta/OwnerContext.ts new file mode 100644 index 00000000..3ffff5e3 --- /dev/null +++ b/packages/contracts/src/support/meta/OwnerContext.ts @@ -0,0 +1,12 @@ +/** + * Owner Context + */ +export default interface OwnerContext +{ + /** + * Reference to the owner of a Meta Repository + * + * @type {WeakRef} + */ + readonly ownerRef: WeakRef; +} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/index.ts b/packages/contracts/src/support/meta/index.ts index fb71f8e9..5d2361c1 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -7,6 +7,13 @@ export const SUPPORT_META: unique symbol = Symbol('@aedart/contracts/support/met import MetaEntry from './MetaEntry.js'; import Repository from './Repository.js'; -export { type MetaEntry, type Repository }; +import OwnerContext from "./OwnerContext.js"; +import MemberAddress from "./MemberAddress.js"; +export { + type MetaEntry, + type Repository, + type OwnerContext, + type MemberAddress +}; export type * from './types.js'; From 7c1e534a6d958bf30e737dd11a117b55420b3bd5 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 15:12:22 +0200 Subject: [PATCH 04/59] Fix formatting and minor lint issue --- .../src/support/meta/MemberAddress.ts | 23 +++++++++---------- .../src/support/meta/OwnerContext.ts | 7 +++--- packages/contracts/src/support/meta/index.ts | 11 +++------ packages/support/src/meta/polyfill.ts | 2 +- packages/support/src/meta/registries.ts | 2 +- packages/support/src/meta/resolveKeyValue.ts | 2 +- tests/browser/xyz/decorators.test.ts | 4 ++-- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/contracts/src/support/meta/MemberAddress.ts b/packages/contracts/src/support/meta/MemberAddress.ts index a6f5ab4f..b718041b 100644 --- a/packages/contracts/src/support/meta/MemberAddress.ts +++ b/packages/contracts/src/support/meta/MemberAddress.ts @@ -1,52 +1,51 @@ import { Key } from '../types.js'; -import OwnerContext from "./OwnerContext.js"; +import OwnerContext from './OwnerContext.js'; /** * Member Address */ -export default interface MemberAddress -{ +export default interface MemberAddress { /** * Owner Context - * + * * @type {OwnerContext} */ readonly ctx: OwnerContext; /** * Indicates whether the member is static (`true`) or otherwise (`false`) - * + * * @type {boolean} */ readonly static: boolean; /** * The kind of member - * + * * @type {string} */ readonly kind: string; /** * Name of the member - * + * * @type {string|symbol} */ readonly name: string | symbol; /** * The "base" path to where metadata is stored inside a Meta Repository - * + * * @type {Key} */ readonly basePath: Key; - + /** * Returns the full path to the given key in a Meta Repository, for the member - * + * * @param {Key} [key] - * + * * @returns {Key} {@link basePath} if no key provided */ path(key?: Key): Key; -} \ No newline at end of file +} diff --git a/packages/contracts/src/support/meta/OwnerContext.ts b/packages/contracts/src/support/meta/OwnerContext.ts index 3ffff5e3..84cf9b37 100644 --- a/packages/contracts/src/support/meta/OwnerContext.ts +++ b/packages/contracts/src/support/meta/OwnerContext.ts @@ -1,12 +1,11 @@ /** * Owner Context */ -export default interface OwnerContext -{ +export default interface OwnerContext { /** * Reference to the owner of a Meta Repository - * + * * @type {WeakRef} */ readonly ownerRef: WeakRef; -} \ No newline at end of file +} diff --git a/packages/contracts/src/support/meta/index.ts b/packages/contracts/src/support/meta/index.ts index 5d2361c1..b1965c3d 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -5,15 +5,10 @@ */ export const SUPPORT_META: unique symbol = Symbol('@aedart/contracts/support/meta'); +import MemberAddress from './MemberAddress.js'; import MetaEntry from './MetaEntry.js'; +import OwnerContext from './OwnerContext.js'; import Repository from './Repository.js'; -import OwnerContext from "./OwnerContext.js"; -import MemberAddress from "./MemberAddress.js"; -export { - type MetaEntry, - type Repository, - type OwnerContext, - type MemberAddress -}; +export { type MemberAddress, type MetaEntry, type OwnerContext, type Repository }; export type * from './types.js'; diff --git a/packages/support/src/meta/polyfill.ts b/packages/support/src/meta/polyfill.ts index 8cbf95b2..2d4a8950 100644 --- a/packages/support/src/meta/polyfill.ts +++ b/packages/support/src/meta/polyfill.ts @@ -2,4 +2,4 @@ * Polyfill Symbol.metadata if it does not exist. */ // @ts-expect-error: Symbol.metadata is not yet in the TypeScript Symbol interface -Symbol.metadata ??= Symbol.for('Symbol.metadata'); \ No newline at end of file +Symbol.metadata ??= Symbol.for('Symbol.metadata'); diff --git a/packages/support/src/meta/registries.ts b/packages/support/src/meta/registries.ts index 4feffece..8d1250fc 100644 --- a/packages/support/src/meta/registries.ts +++ b/packages/support/src/meta/registries.ts @@ -1,5 +1,5 @@ import { Repository } from '@aedart/contracts/support/meta/index.js'; -import "./polyfill.js"; +import './polyfill.js'; /** * The internal registry for all metadata repositories. diff --git a/packages/support/src/meta/resolveKeyValue.ts b/packages/support/src/meta/resolveKeyValue.ts index 3cd7a517..acc8b0c7 100644 --- a/packages/support/src/meta/resolveKeyValue.ts +++ b/packages/support/src/meta/resolveKeyValue.ts @@ -6,7 +6,7 @@ import { Key } from '@aedart/contracts/support/types.js'; * * @param {Key | MetaCallback} koc Key or Callback * @param {unknown} v Value - * @param {unknown} target The target + * @param {unknown} target The target * @param {ClassDecoratorContext | ClassMemberDecoratorContext} context * * @returns {{ key: Key, val: unknown }} diff --git a/tests/browser/xyz/decorators.test.ts b/tests/browser/xyz/decorators.test.ts index 2ffc7b17..8d399830 100644 --- a/tests/browser/xyz/decorators.test.ts +++ b/tests/browser/xyz/decorators.test.ts @@ -287,13 +287,13 @@ describe('@aedart/xyz', () => { {/* empty */} @decorator('private call') + // eslint-disable-next-line no-unused-private-class-members #call() - // eslint-disable-line no-unused-private-class-members {/* empty */} @decorator('static private ping') + // eslint-disable-next-line no-unused-private-class-members static #ping() - // eslint-disable-line no-unused-private-class-members {/* empty */} } From 7efd6aa96c3b0cfb6804232aac8d45932c51f49d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 15:18:33 +0200 Subject: [PATCH 05/59] Add OwnerContext and MemberAddress --- packages/support/src/meta/MemberAddress.ts | 101 ++++++++++++++++++ packages/support/src/meta/OwnerContext.ts | 34 ++++++ packages/support/src/meta/index.ts | 6 +- .../support/meta/MemberAddress.test.ts | 41 +++++++ 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 packages/support/src/meta/MemberAddress.ts create mode 100644 packages/support/src/meta/OwnerContext.ts create mode 100644 tests/browser/support/meta/MemberAddress.test.ts diff --git a/packages/support/src/meta/MemberAddress.ts b/packages/support/src/meta/MemberAddress.ts new file mode 100644 index 00000000..e8e883e5 --- /dev/null +++ b/packages/support/src/meta/MemberAddress.ts @@ -0,0 +1,101 @@ +import { type Key } from '@aedart/contracts/support'; +import { + type MemberAddress as MemberAddressContract, + type OwnerContext, +} from '@aedart/contracts/support/meta'; +import { toParts } from '../objects/toParts.js'; + +/** + * Member Address + */ +export default class MemberAddress implements MemberAddressContract +{ + /** + * @inheritdoc + */ + readonly ctx: OwnerContext; + + /** + * @inheritdoc + */ + readonly static: boolean; + + /** + * @inheritdoc + */ + readonly kind: string; + + /** + * @inheritdoc + */ + readonly name: string | symbol; + + /** + * @inheritdoc + */ + readonly basePath: Key; + + /** + * Create a new member address instance + * + * @param {OwnerContext} ctx + * @param {boolean} isStatic + * @param {string} kind + * @param {string | symbol} name + */ + constructor(ctx: OwnerContext, isStatic: boolean, kind: string, name: string | symbol) + { + this.ctx = ctx; + this.static = isStatic; + this.kind = kind; + this.name = name; + this.basePath = this.resolveBasePath(); + } + + /** + * @inheritdoc + */ + path(key?: Key): Key + { + if (key === undefined || key === null) { + return this.basePath; + } + + const output = toParts(this.basePath); + const parts = toParts(key); + const len = parts.length; + + for (let i = 0; i < len; ++i) { + output.push(parts[i]); + } + + return output; + } + + /** + * Resolves the base path + * + * @returns {Key} + * + * @protected + */ + protected resolveBasePath(): Key + { + // Base path is resolved as an array because a member's name can + // be a symbol, and not just a string! + + const kind = this.kind === 'method' + ? 'methods' + : 'fields'; + + // E.g. [ 'methods', 'playSound' ], or [ 'fields', 'foo' ] + const path = [kind, this.name]; + + // E.g. [ 'static', 'methods', 'playSound' ], or [ 'static', 'fields', 'foo' ] + if (this.static) { + path.unshift('static'); + } + + return path; + } +} diff --git a/packages/support/src/meta/OwnerContext.ts b/packages/support/src/meta/OwnerContext.ts new file mode 100644 index 00000000..fb09df2f --- /dev/null +++ b/packages/support/src/meta/OwnerContext.ts @@ -0,0 +1,34 @@ +import { type OwnerContext as OwnerContextContract } from '@aedart/contracts/support/meta'; +import { toWeakRef } from '../misc/toWeakRef'; + +/** + * Owner Context + */ +export default class OwnerContext implements OwnerContextContract +{ + /** + * Reference to the owner of a Meta Repository + * + * @type {WeakRef} + * + * @private + */ + readonly #owner: WeakRef; + + /** + * Create a new owner context + * + * @param {object} owner + */ + constructor(owner: object) + { + this.#owner = toWeakRef(owner)!; + } + + /** + * @inheritdoc + */ + get ownerRef(): WeakRef { + return this.#owner; + } +} diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 8c918b3b..af349830 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,7 +1,9 @@ +import MemberAddress from './MemberAddress.js'; import Metadata from './Metadata.js'; import MetaRepository from './MetaRepository.js'; +import OwnerContext from './OwnerContext.js'; -export { Metadata, MetaRepository }; +export { MemberAddress, Metadata, MetaRepository, OwnerContext }; // export * from './discoverAndFlush.js'; // internal export * from './findRepository.js'; @@ -9,5 +11,5 @@ export * from './findRepository.js'; export * from './getOrCreateBaseRepository.js'; export * from './getOrCreateRepository.js'; export * from './meta.js'; -export * from './registries.js'; +// export * from './registries.js'; // internal // export * from './resolveKeyValue.js'; // internal diff --git a/tests/browser/support/meta/MemberAddress.test.ts b/tests/browser/support/meta/MemberAddress.test.ts new file mode 100644 index 00000000..8384f8aa --- /dev/null +++ b/tests/browser/support/meta/MemberAddress.test.ts @@ -0,0 +1,41 @@ +import { type MemberAddress as MemberAddressContract } from '@aedart/contracts/support/meta'; +import { MemberAddress, OwnerContext } from '@aedart/support/meta'; +import { describe, expect, test } from 'vitest'; + +describe('@meta() decorator', () => { + describe('MemberAddress', () => { + test('can obtain base path', () => { + const ctx = new OwnerContext({}); + const address: MemberAddressContract = new MemberAddress(ctx, true, 'method', 'foo'); + + const basePath = address.basePath; + + expect(basePath) + .toEqual(['static', 'methods', 'foo']); + }); + + test('returns base path when no key given', () => { + const ctx = new OwnerContext({}); + const address: MemberAddressContract = new MemberAddress(ctx, false, 'method', 'foo'); + + const result = address.path(); + + expect(result) + .toEqual(['methods', 'foo']); + }); + + test('returns full path to given key', () => { + const ctx = new OwnerContext({}); + const name = Symbol('my_foo_method'); + const key = Symbol('my_secret'); + const address: MemberAddressContract = new MemberAddress(ctx, true, 'method', name); + + const result = address.path(key); + + console.log('RESULT', result); + + expect(result) + .toEqual(['static', 'methods', name, key]); + }); + }); +}); From d9bab5617bbc15375b6810765b96c48c2021ce8a Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 15:39:35 +0200 Subject: [PATCH 06/59] Refactor, allow ctx to be set Also renamed concrete class to "Address" because IDE / eslint has issues resolving the type when implementing an aliased interface. --- .../src/support/meta/MemberAddress.ts | 4 +-- .../src/meta/{MemberAddress.ts => Address.ts} | 10 +++---- packages/support/src/meta/index.ts | 4 +-- ...{MemberAddress.test.ts => Address.test.ts} | 29 ++++++++++++++----- 4 files changed, 30 insertions(+), 17 deletions(-) rename packages/support/src/meta/{MemberAddress.ts => Address.ts} (87%) rename tests/browser/support/meta/{MemberAddress.test.ts => Address.test.ts} (54%) diff --git a/packages/contracts/src/support/meta/MemberAddress.ts b/packages/contracts/src/support/meta/MemberAddress.ts index b718041b..40f10e5f 100644 --- a/packages/contracts/src/support/meta/MemberAddress.ts +++ b/packages/contracts/src/support/meta/MemberAddress.ts @@ -8,9 +8,9 @@ export default interface MemberAddress { /** * Owner Context * - * @type {OwnerContext} + * @type {OwnerContext | undefined} */ - readonly ctx: OwnerContext; + ctx: OwnerContext | undefined; /** * Indicates whether the member is static (`true`) or otherwise (`false`) diff --git a/packages/support/src/meta/MemberAddress.ts b/packages/support/src/meta/Address.ts similarity index 87% rename from packages/support/src/meta/MemberAddress.ts rename to packages/support/src/meta/Address.ts index e8e883e5..9b0e0983 100644 --- a/packages/support/src/meta/MemberAddress.ts +++ b/packages/support/src/meta/Address.ts @@ -1,6 +1,6 @@ import { type Key } from '@aedart/contracts/support'; import { - type MemberAddress as MemberAddressContract, + type MemberAddress, type OwnerContext, } from '@aedart/contracts/support/meta'; import { toParts } from '../objects/toParts.js'; @@ -8,12 +8,12 @@ import { toParts } from '../objects/toParts.js'; /** * Member Address */ -export default class MemberAddress implements MemberAddressContract +export default class Address implements MemberAddress { /** * @inheritdoc */ - readonly ctx: OwnerContext; + ctx: OwnerContext | undefined; /** * @inheritdoc @@ -38,12 +38,12 @@ export default class MemberAddress implements MemberAddressContract /** * Create a new member address instance * - * @param {OwnerContext} ctx + * @param {OwnerContext | undefined} ctx * @param {boolean} isStatic * @param {string} kind * @param {string | symbol} name */ - constructor(ctx: OwnerContext, isStatic: boolean, kind: string, name: string | symbol) + constructor(ctx: OwnerContext | undefined, isStatic: boolean, kind: string, name: string | symbol) { this.ctx = ctx; this.static = isStatic; diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index af349830..75bf5c80 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,9 +1,9 @@ -import MemberAddress from './MemberAddress.js'; +import Address from './Address.js'; import Metadata from './Metadata.js'; import MetaRepository from './MetaRepository.js'; import OwnerContext from './OwnerContext.js'; -export { MemberAddress, Metadata, MetaRepository, OwnerContext }; +export { Address, Metadata, MetaRepository, OwnerContext }; // export * from './discoverAndFlush.js'; // internal export * from './findRepository.js'; diff --git a/tests/browser/support/meta/MemberAddress.test.ts b/tests/browser/support/meta/Address.test.ts similarity index 54% rename from tests/browser/support/meta/MemberAddress.test.ts rename to tests/browser/support/meta/Address.test.ts index 8384f8aa..99bde31f 100644 --- a/tests/browser/support/meta/MemberAddress.test.ts +++ b/tests/browser/support/meta/Address.test.ts @@ -1,12 +1,11 @@ -import { type MemberAddress as MemberAddressContract } from '@aedart/contracts/support/meta'; -import { MemberAddress, OwnerContext } from '@aedart/support/meta'; +import { Address, OwnerContext } from '@aedart/support/meta'; import { describe, expect, test } from 'vitest'; describe('@meta() decorator', () => { - describe('MemberAddress', () => { + describe('Address', () => { test('can obtain base path', () => { - const ctx = new OwnerContext({}); - const address: MemberAddressContract = new MemberAddress(ctx, true, 'method', 'foo'); + + const address = new Address(undefined, true, 'method', 'foo'); const basePath = address.basePath; @@ -15,8 +14,7 @@ describe('@meta() decorator', () => { }); test('returns base path when no key given', () => { - const ctx = new OwnerContext({}); - const address: MemberAddressContract = new MemberAddress(ctx, false, 'method', 'foo'); + const address = new Address(undefined, false, 'method', 'foo'); const result = address.path(); @@ -28,7 +26,7 @@ describe('@meta() decorator', () => { const ctx = new OwnerContext({}); const name = Symbol('my_foo_method'); const key = Symbol('my_secret'); - const address: MemberAddressContract = new MemberAddress(ctx, true, 'method', name); + const address = new Address(undefined, true, 'method', name); const result = address.path(key); @@ -37,5 +35,20 @@ describe('@meta() decorator', () => { expect(result) .toEqual(['static', 'methods', name, key]); }); + + test('can (re)set ctx', () => { + + const contextA = new OwnerContext({}); + + + const address = new Address(contextA, true, 'method', 'foo'); + expect(address.ctx) + .toEqual(contextA); + + const contextB = new OwnerContext({}); + address.ctx = contextB; + expect(address.ctx) + .toEqual(contextB); + }); }); }); From b94ee1fa94a83af5f59bf0b8ee7cf3669afff778 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 15:40:33 +0200 Subject: [PATCH 07/59] Cleanup --- tests/browser/support/meta/Address.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/browser/support/meta/Address.test.ts b/tests/browser/support/meta/Address.test.ts index 99bde31f..d007f9e7 100644 --- a/tests/browser/support/meta/Address.test.ts +++ b/tests/browser/support/meta/Address.test.ts @@ -23,15 +23,12 @@ describe('@meta() decorator', () => { }); test('returns full path to given key', () => { - const ctx = new OwnerContext({}); const name = Symbol('my_foo_method'); const key = Symbol('my_secret'); const address = new Address(undefined, true, 'method', name); const result = address.path(key); - console.log('RESULT', result); - expect(result) .toEqual(['static', 'methods', name, key]); }); From cbb96364bc11223928dd889c28f314ffa5e2425b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 16:33:21 +0200 Subject: [PATCH 08/59] Add test for using symbol on instance member / method --- tests/browser/support/meta/meta.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index 56d57d3e..42d96971 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -230,6 +230,22 @@ describe('@meta() decorator', () => { expect(Metadata.get(SecureClass, MY_KEY)).toBe('secret-value'); }); + test('can use Symbols as metadata keys, for method', () => { + const MY_KEY = Symbol('my_key'); + + class MyClass + { + @meta(MY_KEY, 'secret-value') + myMethod() + {/* empty */} + } + + // Debug + // new MyClass(); + + expect(Metadata.get(MyClass, ['methods', 'myMethod', MY_KEY])).toBe('secret-value'); + }); + describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { From a51bd5593da695e912280acbb69704494b2a217c Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 16:33:57 +0200 Subject: [PATCH 09/59] Fix formatting --- packages/support/src/meta/Address.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/support/src/meta/Address.ts b/packages/support/src/meta/Address.ts index 9b0e0983..c2d5d852 100644 --- a/packages/support/src/meta/Address.ts +++ b/packages/support/src/meta/Address.ts @@ -1,8 +1,5 @@ import { type Key } from '@aedart/contracts/support'; -import { - type MemberAddress, - type OwnerContext, -} from '@aedart/contracts/support/meta'; +import { type MemberAddress, type OwnerContext } from '@aedart/contracts/support/meta'; import { toParts } from '../objects/toParts.js'; /** @@ -43,7 +40,12 @@ export default class Address implements MemberAddress * @param {string} kind * @param {string | symbol} name */ - constructor(ctx: OwnerContext | undefined, isStatic: boolean, kind: string, name: string | symbol) + constructor( + ctx: OwnerContext | undefined, + isStatic: boolean, + kind: string, + name: string | symbol, + ) { this.ctx = ctx; this.static = isStatic; From 14fa2dab2f2b31b4b9308189f2382636126d5f88 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 16:34:13 +0200 Subject: [PATCH 10/59] Fix formatting --- tests/browser/support/meta/Address.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/browser/support/meta/Address.test.ts b/tests/browser/support/meta/Address.test.ts index d007f9e7..c2f05489 100644 --- a/tests/browser/support/meta/Address.test.ts +++ b/tests/browser/support/meta/Address.test.ts @@ -4,7 +4,6 @@ import { describe, expect, test } from 'vitest'; describe('@meta() decorator', () => { describe('Address', () => { test('can obtain base path', () => { - const address = new Address(undefined, true, 'method', 'foo'); const basePath = address.basePath; @@ -34,10 +33,8 @@ describe('@meta() decorator', () => { }); test('can (re)set ctx', () => { - const contextA = new OwnerContext({}); - - + const address = new Address(contextA, true, 'method', 'foo'); expect(address.ctx) .toEqual(contextA); From 82f1529757db0ce5dbb09d4d60e1b906598e4198 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 16:34:47 +0200 Subject: [PATCH 11/59] Fix unable to use symbol as key for instance method --- packages/support/src/meta/flush.ts | 6 +++--- packages/support/src/meta/meta.ts | 34 +++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/support/src/meta/flush.ts b/packages/support/src/meta/flush.ts index 4f297922..b7baadff 100644 --- a/packages/support/src/meta/flush.ts +++ b/packages/support/src/meta/flush.ts @@ -6,11 +6,11 @@ import { FLUSHED_METADATA } from './registries'; * Flush staged metadata into the repositories of the given owner. * * @param {ConstructorLike} owner The Class (constructor) - * @param {Record} staged The metadata object from decorator context + * @param {Record} staged The metadata object from decorator context * * @internal */ -export function flush(owner: ConstructorLike, staged: Record): void +export function flush(owner: ConstructorLike, staged: Record): void { if (FLUSHED_METADATA.has(staged)) { return; @@ -23,7 +23,7 @@ export function flush(owner: ConstructorLike, staged: Record): for (let i = 0, len = keys.length; i < len; i++) { const path = keys[i] as string; - const destination = path.startsWith('static.') + const destination = path.startsWith('static') ? owner : prototype; diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 52e96388..aaa13818 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -1,6 +1,9 @@ import { ConstructorLike } from '@aedart/contracts'; import { MetaCallback } from '@aedart/contracts/support/meta/index.js'; import { Key } from '@aedart/contracts/support/types.js'; +import { set } from '../objects/set.js'; +import { isKeyUnsafe } from '../reflections/isKeyUnsafe.js'; +import Address from './Address.js'; import { flush } from './flush.js'; import { getOrCreateRepository } from './getOrCreateRepository.js'; import { MEMBER_TO_METADATA } from './registries.js'; @@ -40,22 +43,34 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) return; } - // 4. If it's a member decorator, stage the metadata - const kind = context.kind === 'method' ? 'methods' : 'fields'; - const prefix = isStatic ? 'static.' : ''; + // 4. Create new member address, without owner context (will be set later...) + const memberAddress = new Address(undefined, isStatic, context.kind, context.name); - // E.g. 'static.methods.playSound.volumne', 'fields.id.fetch_url' - const path = `${prefix}${kind}.${String(context.name)}.${String(key)}`; + // Generate a full path (from address) so it can be stored / staged... + let pathParts = memberAddress.path(key) as PropertyKey[]; + if (!Array.isArray(pathParts)) { + pathParts = [pathParts]; + } - metadataObj[path] = val; + // Fail if any path segment is unsafe. This is needed because `set()` ignores + // any unsafe path. + const partsLen = pathParts.length; + for (let i = 0; i < partsLen; i++) { + if (isKeyUnsafe(pathParts[i])) { + throw new TypeError(`Unsafe metadata key/path detected: ${String(key)}`); + } + } - // 5. Link the member to the metadata object for discovery + // 5. If it's a member decorator, stage the metadata + set(metadataObj, pathParts, val); + + // 6. Link the member to the metadata object for discovery // For methods, target is the function. For fields, it's undefined (in 2023-11). if (target !== undefined && target !== null) { MEMBER_TO_METADATA.set(target, metadataObj); } - // 6. Use addInitializer to flush metadata. + // 7. Use addInitializer to flush metadata. // For static members, this runs during class definition. // For instance members, this runs during instantiation. context.addInitializer(function(this: unknown) @@ -69,6 +84,9 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) if (constructor) { flush(constructor, metadataObj); + + // TODO: resolve owner context for constructor... + // TODO: Save member address in registry... } }); }; From eecafc26701e36f2af23c15bb2ad3c8fcc64e596 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 May 2026 16:36:33 +0200 Subject: [PATCH 12/59] Add test for storing symbol on static method --- tests/browser/support/meta/meta.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index 42d96971..10a5be1e 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -246,6 +246,22 @@ describe('@meta() decorator', () => { expect(Metadata.get(MyClass, ['methods', 'myMethod', MY_KEY])).toBe('secret-value'); }); + test('can use Symbols as metadata keys, for static method', () => { + const MY_KEY = Symbol('my_key'); + + class MyClass + { + @meta(MY_KEY, 'secret-value') + static myMethod() + {/* empty */} + } + + // Debug + // new MyClass(); + + expect(Metadata.get(MyClass, ['static', 'methods', 'myMethod', MY_KEY])).toBe('secret-value'); + }); + describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { From ca80cd7fb130c69e7860dc8b0282adf523c6b271 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:05:09 +0200 Subject: [PATCH 13/59] Update dependencies --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ca2c98b8..a48aa59e 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,11 @@ "@babel/plugin-proposal-decorators": "^7.29.0", "@eslint/js": "^10.0.1", "@types/luxon": "^3.7.1", - "@types/node": "^25.6.2", - "@vitest/browser": "^4.1.5", - "@vitest/browser-playwright": "^4.1.5", - "@vitest/coverage-istanbul": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5", + "@types/node": "^25.7.0", + "@vitest/browser": "^4.1.6", + "@vitest/browser-playwright": "^4.1.6", + "@vitest/coverage-istanbul": "^4.1.6", + "@vitest/coverage-v8": "^4.1.6", "@vuepress/bundler-vite": "2.0.0-rc.29", "@vuepress/client": "2.0.0-rc.29", "@vuepress/plugin-back-to-top": "2.0.0-rc.128", @@ -47,14 +47,14 @@ "eslint": "^10.3.0", "globals": "^17.6.0", "luxon": "^3.7.2", - "playwright": "^1.59.1", + "playwright": "^1.60.0", "sass": "^1.99.0", "tslib": "^2.8.1", "turbo": "^2.9.12", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", - "vite": "^8.0.11", - "vitest": "^4.1.5", + "typescript-eslint": "^8.59.3", + "vite": "^8.0.12", + "vitest": "^4.1.6", "vue": "^3.5.34", "vuepress": "2.0.0-rc.29" }, @@ -104,5 +104,5 @@ "publish-check": "Simulates a workspace-wide npm publish (dry-run).", "publish": "Full release workflow: sync, build, tag, and publish." }, - "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8" + "packageManager": "pnpm@11.1.1" } From 80d3439e295e0fe976b2008df16010acfc08c771 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:07:35 +0200 Subject: [PATCH 14/59] Add context cache (registry) --- packages/support/src/meta/registries.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/support/src/meta/registries.ts b/packages/support/src/meta/registries.ts index 8d1250fc..46b5d9f5 100644 --- a/packages/support/src/meta/registries.ts +++ b/packages/support/src/meta/registries.ts @@ -1,4 +1,7 @@ -import { Repository } from '@aedart/contracts/support/meta/index.js'; +import { + type Repository, + type OwnerContext +} from '@aedart/contracts/support/meta'; import './polyfill.js'; /** @@ -27,3 +30,12 @@ export const MEMBER_TO_METADATA = new WeakMap} */ export const FLUSHED_METADATA = new WeakSet(); + +/** + * Cache of Owner Context instances + * + * @internal + * + * @type {WeakMap} + */ +export const contextCache = new WeakMap(); From 7539991171030033813c30caf0d84bc25951ee54 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:15:55 +0200 Subject: [PATCH 15/59] Add getOrCreateOwnerContext() util --- .../src/meta/getOrCreateOwnerContext.ts | 23 +++++++++++++++++++ packages/support/src/meta/index.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 packages/support/src/meta/getOrCreateOwnerContext.ts diff --git a/packages/support/src/meta/getOrCreateOwnerContext.ts b/packages/support/src/meta/getOrCreateOwnerContext.ts new file mode 100644 index 00000000..079b8f2d --- /dev/null +++ b/packages/support/src/meta/getOrCreateOwnerContext.ts @@ -0,0 +1,23 @@ +import { type OwnerContext as OwnerContextContract } from '@aedart/contracts/support/meta'; +import OwnerContext from "./OwnerContext.js"; +import { contextCache } from "./registries.js"; + +/** + * Returns the "owner context" for owner, or creates a new context if one does not exist + * + * @param {object} owner + * + * @returns {OwnerContext} + */ +export function getOrCreateOwnerContext(owner: object): OwnerContextContract +{ + let ctx = contextCache.get(owner); + + if (!ctx) { + ctx = new OwnerContext(owner); + + contextCache.set(owner, ctx); + } + + return ctx; +} \ No newline at end of file diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 75bf5c80..8c2bfe47 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -9,6 +9,7 @@ export { Address, Metadata, MetaRepository, OwnerContext }; export * from './findRepository.js'; // export * from './flush.js'; // internal export * from './getOrCreateBaseRepository.js'; +export * from './getOrCreateOwnerContext.js'; export * from './getOrCreateRepository.js'; export * from './meta.js'; // export * from './registries.js'; // internal From e2fc24bc42d7e2dba7c5b487a99e16462f89e92f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:18:13 +0200 Subject: [PATCH 16/59] Add "member address" registry --- packages/support/src/meta/registries.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/support/src/meta/registries.ts b/packages/support/src/meta/registries.ts index 46b5d9f5..f3f9751d 100644 --- a/packages/support/src/meta/registries.ts +++ b/packages/support/src/meta/registries.ts @@ -1,6 +1,7 @@ import { type Repository, - type OwnerContext + type OwnerContext, + type MemberAddress } from '@aedart/contracts/support/meta'; import './polyfill.js'; @@ -39,3 +40,12 @@ export const FLUSHED_METADATA = new WeakSet(); * @type {WeakMap} */ export const contextCache = new WeakMap(); + +/** + * Member Address registry + * + * @internal + * + * @type {WeakMap} + */ +export const addressRegistry = new WeakMap(); From b7f0d1805e1861224c7ec691ce04eec194151802 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:26:52 +0200 Subject: [PATCH 17/59] Add register address util --- packages/support/src/meta/index.ts | 1 + packages/support/src/meta/registerAddress.ts | 24 ++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 packages/support/src/meta/registerAddress.ts diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 8c2bfe47..de67499c 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -12,5 +12,6 @@ export * from './getOrCreateBaseRepository.js'; export * from './getOrCreateOwnerContext.js'; export * from './getOrCreateRepository.js'; export * from './meta.js'; +export * from './registerAddress.js'; // export * from './registries.js'; // internal // export * from './resolveKeyValue.js'; // internal diff --git a/packages/support/src/meta/registerAddress.ts b/packages/support/src/meta/registerAddress.ts new file mode 100644 index 00000000..a543bcab --- /dev/null +++ b/packages/support/src/meta/registerAddress.ts @@ -0,0 +1,24 @@ +import { + type MemberAddress +} from '@aedart/contracts/support/meta' +import { getOrCreateOwnerContext } from "./getOrCreateOwnerContext.js"; +import { addressRegistry } from './registries.js'; + +/** + * Register an address for the given member, in the given owner + * + * @param {object} owner + * @param {object} member + * @param {MemberAddress} address + */ +export function registerAddress( + owner: object, + member: object, + address: MemberAddress, +): void +{ + // "Force" set the context for the address,... + address.ctx = getOrCreateOwnerContext(owner); + + addressRegistry.set(member, address); +} \ No newline at end of file From 33cae736378ded8c4b9860900ddc3a78da5bbd3a Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 13:38:16 +0200 Subject: [PATCH 18/59] Register member address when member is a method --- packages/support/src/meta/meta.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index aaa13818..b2abf487 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -8,6 +8,7 @@ import { flush } from './flush.js'; import { getOrCreateRepository } from './getOrCreateRepository.js'; import { MEMBER_TO_METADATA } from './registries.js'; import { resolveKeyValue } from './resolveKeyValue.js'; +import { registerAddress } from "./registerAddress.js"; /** * Store metadata on a class or class member. @@ -85,8 +86,11 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) if (constructor) { flush(constructor, metadataObj); - // TODO: resolve owner context for constructor... - // TODO: Save member address in registry... + // Save the target (member) address, for the given owner. + // This will enable meta lookups, using the member directly. + if (context.kind === 'method') { + registerAddress(constructor, target as object, memberAddress); + } } }); }; From 5b2b4f64fc412228fabb6cd28fe3843618cae05c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 14:32:34 +0200 Subject: [PATCH 19/59] Optimise Owner Context, avoid using toWeakRef There was no reason for using that util, in this case. --- packages/support/src/meta/OwnerContext.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/support/src/meta/OwnerContext.ts b/packages/support/src/meta/OwnerContext.ts index fb09df2f..8d19a137 100644 --- a/packages/support/src/meta/OwnerContext.ts +++ b/packages/support/src/meta/OwnerContext.ts @@ -1,5 +1,4 @@ import { type OwnerContext as OwnerContextContract } from '@aedart/contracts/support/meta'; -import { toWeakRef } from '../misc/toWeakRef'; /** * Owner Context @@ -22,7 +21,7 @@ export default class OwnerContext implements OwnerContextContract */ constructor(owner: object) { - this.#owner = toWeakRef(owner)!; + this.#owner = new WeakRef(owner); } /** From 1408df3a7b2569fc78902ac8456ddf88a08aecaf Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 14:34:44 +0200 Subject: [PATCH 20/59] Optimise "base path" and path() method This should use slightly less memory, then before. --- packages/support/src/meta/Address.ts | 42 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/support/src/meta/Address.ts b/packages/support/src/meta/Address.ts index c2d5d852..7de1e618 100644 --- a/packages/support/src/meta/Address.ts +++ b/packages/support/src/meta/Address.ts @@ -63,12 +63,24 @@ export default class Address implements MemberAddress return this.basePath; } - const output = toParts(this.basePath); + const base = this.basePath as PropertyKey[]; const parts = toParts(key); - const len = parts.length; - for (let i = 0; i < len; ++i) { - output.push(parts[i]); + if (parts.length === 0) { + return this.basePath; + } + + // Pre-allocate the exact array size needed. + // This avoids V8 inner-loop array resizing and memory reallocation steps. + const output = new Array(base.length + parts.length); + + // Linear copy operations (highly optimized by V8 runtime JIT) + for (let i = 0; i < base.length; ++i) { + output[i] = base[i]; + } + + for (let i = 0; i < parts.length; ++i) { + output[base.length + i] = parts[i]; } return output; @@ -83,21 +95,15 @@ export default class Address implements MemberAddress */ protected resolveBasePath(): Key { - // Base path is resolved as an array because a member's name can - // be a symbol, and not just a string! - - const kind = this.kind === 'method' - ? 'methods' - : 'fields'; + const kind = this.kind === 'method' ? 'methods' : 'fields'; - // E.g. [ 'methods', 'playSound' ], or [ 'fields', 'foo' ] - const path = [kind, this.name]; - - // E.g. [ 'static', 'methods', 'playSound' ], or [ 'static', 'fields', 'foo' ] - if (this.static) { - path.unshift('static'); - } + // Direct layout construction avoids slow unshift structural re-indexes + // E.g. [ 'static', 'methods', 'playSound' ], or [ 'methods', 'playSound' ] + const path = this.static + ? ['static', kind, this.name] + : [kind, this.name]; - return path; + // Freezing stops V8 from keeping hidden "growth memory buffers" + return Object.freeze(path) as unknown as Key; } } From 832d6920a38a6691bb0153d5028d9db25636443b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 14:51:10 +0200 Subject: [PATCH 21/59] Add "find or create" member address util --- .../src/meta/findOrCreateMemberAddress.ts | 29 +++++++++++++++++++ packages/support/src/meta/index.ts | 1 + 2 files changed, 30 insertions(+) create mode 100644 packages/support/src/meta/findOrCreateMemberAddress.ts diff --git a/packages/support/src/meta/findOrCreateMemberAddress.ts b/packages/support/src/meta/findOrCreateMemberAddress.ts new file mode 100644 index 00000000..7b62da64 --- /dev/null +++ b/packages/support/src/meta/findOrCreateMemberAddress.ts @@ -0,0 +1,29 @@ +import { + type MemberAddress, + type OwnerContext +} from '@aedart/contracts/support/meta' +import Address from "./Address.js"; +import { addressRegistry } from "./registries.js"; + +/** + * Find or create "member address" for the given member + * + * @param {unknown} member + * @param {ClassMemberDecoratorContext} context + * @param {OwnerContext | undefined} [owner] + * + * @returns {MemberAddress} + */ +export function findOrCreateMemberAddress( + member: unknown, + context: ClassMemberDecoratorContext, + owner?: OwnerContext +): MemberAddress +{ + let address = addressRegistry.get(member as object); + + // Create member address (possibly without "owner context")... + address ??= new Address(owner, context.static ?? false, context.kind, context.name); + + return address; +} \ No newline at end of file diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index de67499c..6ec12c5d 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -6,6 +6,7 @@ import OwnerContext from './OwnerContext.js'; export { Address, Metadata, MetaRepository, OwnerContext }; // export * from './discoverAndFlush.js'; // internal +export * from './findOrCreateMemberAddress.js'; export * from './findRepository.js'; // export * from './flush.js'; // internal export * from './getOrCreateBaseRepository.js'; From 9efe67a1d1a811a38b1414e53b9ad1d551353cc1 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 14:55:20 +0200 Subject: [PATCH 22/59] Reuse existing member address, if one already exists This should avoid unnecessary memory allocation for member address, if one was previously already created. --- packages/support/src/meta/meta.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index b2abf487..43d65ea7 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -9,6 +9,7 @@ import { getOrCreateRepository } from './getOrCreateRepository.js'; import { MEMBER_TO_METADATA } from './registries.js'; import { resolveKeyValue } from './resolveKeyValue.js'; import { registerAddress } from "./registerAddress.js"; +import { findOrCreateMemberAddress } from "./findOrCreateMemberAddress.js"; /** * Store metadata on a class or class member. @@ -44,8 +45,8 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) return; } - // 4. Create new member address, without owner context (will be set later...) - const memberAddress = new Address(undefined, isStatic, context.kind, context.name); + // 4. Find or create member address (in this case without the "owner context", which is resolved later). + const memberAddress = findOrCreateMemberAddress(target, context); // Generate a full path (from address) so it can be stored / staged... let pathParts = memberAddress.path(key) as PropertyKey[]; From 57b720998c8b278ef96b5ee4e184492644c15f9e Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 13 May 2026 15:11:53 +0200 Subject: [PATCH 23/59] Fix lint and formatting --- packages/support/src/meta/Address.ts | 2 +- .../src/meta/findOrCreateMemberAddress.ts | 21 ++++++++----------- .../src/meta/getOrCreateOwnerContext.ts | 10 ++++----- packages/support/src/meta/meta.ts | 7 +++---- packages/support/src/meta/registerAddress.ts | 12 +++++------ packages/support/src/meta/registries.ts | 12 +++++------ tests/browser/support/meta/meta.test.ts | 6 ++++-- 7 files changed, 33 insertions(+), 37 deletions(-) diff --git a/packages/support/src/meta/Address.ts b/packages/support/src/meta/Address.ts index 7de1e618..667d58ab 100644 --- a/packages/support/src/meta/Address.ts +++ b/packages/support/src/meta/Address.ts @@ -103,7 +103,7 @@ export default class Address implements MemberAddress ? ['static', kind, this.name] : [kind, this.name]; - // Freezing stops V8 from keeping hidden "growth memory buffers" + // Freezing stops V8 from keeping hidden "growth memory buffers" return Object.freeze(path) as unknown as Key; } } diff --git a/packages/support/src/meta/findOrCreateMemberAddress.ts b/packages/support/src/meta/findOrCreateMemberAddress.ts index 7b62da64..e49881a9 100644 --- a/packages/support/src/meta/findOrCreateMemberAddress.ts +++ b/packages/support/src/meta/findOrCreateMemberAddress.ts @@ -1,29 +1,26 @@ -import { - type MemberAddress, - type OwnerContext -} from '@aedart/contracts/support/meta' -import Address from "./Address.js"; -import { addressRegistry } from "./registries.js"; +import { type MemberAddress, type OwnerContext } from '@aedart/contracts/support/meta'; +import Address from './Address.js'; +import { addressRegistry } from './registries.js'; /** * Find or create "member address" for the given member - * + * * @param {unknown} member * @param {ClassMemberDecoratorContext} context * @param {OwnerContext | undefined} [owner] - * + * * @returns {MemberAddress} */ export function findOrCreateMemberAddress( member: unknown, context: ClassMemberDecoratorContext, - owner?: OwnerContext + owner?: OwnerContext, ): MemberAddress { let address = addressRegistry.get(member as object); - - // Create member address (possibly without "owner context")... + + // Create member address (possibly without "owner context")... address ??= new Address(owner, context.static ?? false, context.kind, context.name); return address; -} \ No newline at end of file +} diff --git a/packages/support/src/meta/getOrCreateOwnerContext.ts b/packages/support/src/meta/getOrCreateOwnerContext.ts index 079b8f2d..976a83ed 100644 --- a/packages/support/src/meta/getOrCreateOwnerContext.ts +++ b/packages/support/src/meta/getOrCreateOwnerContext.ts @@ -1,12 +1,12 @@ import { type OwnerContext as OwnerContextContract } from '@aedart/contracts/support/meta'; -import OwnerContext from "./OwnerContext.js"; -import { contextCache } from "./registries.js"; +import OwnerContext from './OwnerContext.js'; +import { contextCache } from './registries.js'; /** * Returns the "owner context" for owner, or creates a new context if one does not exist * * @param {object} owner - * + * * @returns {OwnerContext} */ export function getOrCreateOwnerContext(owner: object): OwnerContextContract @@ -15,9 +15,9 @@ export function getOrCreateOwnerContext(owner: object): OwnerContextContract if (!ctx) { ctx = new OwnerContext(owner); - + contextCache.set(owner, ctx); } return ctx; -} \ No newline at end of file +} diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 43d65ea7..c0f0c275 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -3,13 +3,12 @@ import { MetaCallback } from '@aedart/contracts/support/meta/index.js'; import { Key } from '@aedart/contracts/support/types.js'; import { set } from '../objects/set.js'; import { isKeyUnsafe } from '../reflections/isKeyUnsafe.js'; -import Address from './Address.js'; +import { findOrCreateMemberAddress } from './findOrCreateMemberAddress.js'; import { flush } from './flush.js'; import { getOrCreateRepository } from './getOrCreateRepository.js'; +import { registerAddress } from './registerAddress.js'; import { MEMBER_TO_METADATA } from './registries.js'; import { resolveKeyValue } from './resolveKeyValue.js'; -import { registerAddress } from "./registerAddress.js"; -import { findOrCreateMemberAddress } from "./findOrCreateMemberAddress.js"; /** * Store metadata on a class or class member. @@ -90,7 +89,7 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) // Save the target (member) address, for the given owner. // This will enable meta lookups, using the member directly. if (context.kind === 'method') { - registerAddress(constructor, target as object, memberAddress); + registerAddress(constructor, target as object, memberAddress); } } }); diff --git a/packages/support/src/meta/registerAddress.ts b/packages/support/src/meta/registerAddress.ts index a543bcab..46a43940 100644 --- a/packages/support/src/meta/registerAddress.ts +++ b/packages/support/src/meta/registerAddress.ts @@ -1,12 +1,10 @@ -import { - type MemberAddress -} from '@aedart/contracts/support/meta' -import { getOrCreateOwnerContext } from "./getOrCreateOwnerContext.js"; +import { type MemberAddress } from '@aedart/contracts/support/meta'; +import { getOrCreateOwnerContext } from './getOrCreateOwnerContext.js'; import { addressRegistry } from './registries.js'; /** * Register an address for the given member, in the given owner - * + * * @param {object} owner * @param {object} member * @param {MemberAddress} address @@ -19,6 +17,6 @@ export function registerAddress( { // "Force" set the context for the address,... address.ctx = getOrCreateOwnerContext(owner); - + addressRegistry.set(member, address); -} \ No newline at end of file +} diff --git a/packages/support/src/meta/registries.ts b/packages/support/src/meta/registries.ts index f3f9751d..4211345c 100644 --- a/packages/support/src/meta/registries.ts +++ b/packages/support/src/meta/registries.ts @@ -1,7 +1,7 @@ import { - type Repository, + type MemberAddress, type OwnerContext, - type MemberAddress + type Repository, } from '@aedart/contracts/support/meta'; import './polyfill.js'; @@ -34,18 +34,18 @@ export const FLUSHED_METADATA = new WeakSet(); /** * Cache of Owner Context instances - * + * * @internal - * + * * @type {WeakMap} */ export const contextCache = new WeakMap(); /** * Member Address registry - * + * * @internal - * + * * @type {WeakMap} */ export const addressRegistry = new WeakMap(); diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index 10a5be1e..d91a4849 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -259,9 +259,11 @@ describe('@meta() decorator', () => { // Debug // new MyClass(); - expect(Metadata.get(MyClass, ['static', 'methods', 'myMethod', MY_KEY])).toBe('secret-value'); + expect(Metadata.get(MyClass, ['static', 'methods', 'myMethod', MY_KEY])).toBe( + 'secret-value', + ); }); - + describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { From c9afcd12a69386f4651b20a5bb18bea8367e2f0b Mon Sep 17 00:00:00 2001 From: aedart Date: Wed, 13 May 2026 20:04:02 +0200 Subject: [PATCH 24/59] Set allow builds --- pnpm-workspace.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7bde30f6..445c1ea3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1 +1,5 @@ -packages: ['packages/*'] +packages: [ 'packages/*' ] +allowBuilds: + '@parcel/watcher': true + dprint: true + esbuild: true From e80313dad9a5a69b8148a678a74c865141c00f62 Mon Sep 17 00:00:00 2001 From: aedart Date: Wed, 13 May 2026 20:04:35 +0200 Subject: [PATCH 25/59] Refactor Metadata, allow target to be a class member --- packages/support/src/meta/Metadata.ts | 41 ++++++++++++++++++++++++--- packages/support/src/meta/Resolved.ts | 18 ++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 packages/support/src/meta/Resolved.ts diff --git a/packages/support/src/meta/Metadata.ts b/packages/support/src/meta/Metadata.ts index 2a10ce27..dd83157e 100644 --- a/packages/support/src/meta/Metadata.ts +++ b/packages/support/src/meta/Metadata.ts @@ -3,6 +3,8 @@ import { Key } from '@aedart/contracts/support'; import { toParts } from '../objects/toParts.js'; import { isConstructor } from '../reflections/isConstructor.js'; import { getOrCreateRepository } from './getOrCreateRepository.js'; +import { addressRegistry } from './registries.js'; +import type Resolved from './Resolved.js'; /** * Metadata Helper @@ -23,9 +25,9 @@ export default class Metadata */ static get(target: object, key: Key, defaultValue?: T): T | undefined { - const resolved = this.resolveTarget(target, key); + const { resolvedTarget, resolvedKey } = this.resolveTargetAndKey(target, key); - return getOrCreateRepository(resolved).get(key, defaultValue); + return getOrCreateRepository(resolvedTarget).get(resolvedKey, defaultValue); } /** @@ -38,9 +40,9 @@ export default class Metadata */ static has(target: object, key: Key): boolean { - const resolved = this.resolveTarget(target, key); + const { resolvedTarget, resolvedKey } = this.resolveTargetAndKey(target, key); - return getOrCreateRepository(resolved).has(key); + return getOrCreateRepository(resolvedTarget).has(resolvedKey); } /** @@ -55,6 +57,37 @@ export default class Metadata return getOrCreateRepository(target).all(); } + /** + * Resolve the actual target and key for metadata lookup + * + * @param {object} target + * @param {Key} key + * + * @returns {Resolved} + * + * @protected + */ + protected static resolveTargetAndKey(target: object, key: Key): Resolved + { + let owner = target; + let resolvedKey = key; + + const address = addressRegistry.get(target); + if (address !== undefined) { + const resolvedOwner = address.ctx?.ownerRef?.deref(); + if (resolvedOwner !== undefined) { + owner = resolvedOwner; + } + + resolvedKey = address.path(key); + } + + return { + resolvedTarget: this.resolveTarget(owner, resolvedKey), + resolvedKey, + }; + } + /** * Resolves the actual target for metadata lookup * diff --git a/packages/support/src/meta/Resolved.ts b/packages/support/src/meta/Resolved.ts new file mode 100644 index 00000000..cda386a0 --- /dev/null +++ b/packages/support/src/meta/Resolved.ts @@ -0,0 +1,18 @@ +import { Key } from '@aedart/contracts/support'; + +/** + * Resolved target and key + * + * @internal + */ +export default interface Resolved { + /** + * The resolved target + */ + resolvedTarget: object; + + /** + * The resolved + */ + resolvedKey: Key; +} From 2e97716a231aa4bfca5175dbe15f0767401ac3b2 Mon Sep 17 00:00:00 2001 From: aedart Date: Wed, 13 May 2026 20:07:01 +0200 Subject: [PATCH 26/59] WIP --- tests/browser/support/meta/meta.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index d91a4849..daa2c3e0 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -264,6 +264,31 @@ describe('@meta() decorator', () => { ); }); + test('can get meta using instance method reference', () => { + class MyService + { + @meta('foo', 'bar') + play() + {/* empty */} + } + + // Debug + const instance = new MyService(); + const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method + + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + // TODO: Inherit instance method meta, ... obtain via member directly. + // TODO: getter / setter (field) meta, ... obtain via member directly. + // TODO: Static method meta, ... obtain via static member directly. + // TODO: Inherit static method meta, ... obtain via static member directly. + // TODO: Static getter / setter (field) meta, ... obtain via member directly. + describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { From 9ee69c31f6a3fa6d06c3b1a437b74fa179145c07 Mon Sep 17 00:00:00 2001 From: aedart Date: Wed, 13 May 2026 20:07:17 +0200 Subject: [PATCH 27/59] Cleanup --- tests/browser/support/meta/meta.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index daa2c3e0..58fbcad2 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -288,7 +288,7 @@ describe('@meta() decorator', () => { // TODO: Static method meta, ... obtain via static member directly. // TODO: Inherit static method meta, ... obtain via static member directly. // TODO: Static getter / setter (field) meta, ... obtain via member directly. - + describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { From ff040f4b1425115a3ef41e4faa709d2126583fed Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 15:37:07 +0200 Subject: [PATCH 28/59] Extract security tests into own file --- .../support/meta/meta-security.test.ts | 47 +++++++++++++++++++ tests/browser/support/meta/meta.test.ts | 45 +----------------- 2 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 tests/browser/support/meta/meta-security.test.ts diff --git a/tests/browser/support/meta/meta-security.test.ts b/tests/browser/support/meta/meta-security.test.ts new file mode 100644 index 00000000..cc73c467 --- /dev/null +++ b/tests/browser/support/meta/meta-security.test.ts @@ -0,0 +1,47 @@ +import { meta } from '@aedart/support/meta'; +import { describe, expect, test } from 'vitest'; + +describe('@meta() security: prototype pollution', () => { + test('immediately prevents pollution via class decorator', () => { + const trigger = () => { + @meta('__proto__.polluted', true) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + class Polluter + {} + }; + + // Class decorators run immediately; should throw during definition. + expect(trigger).toThrow(); + }); + + test('prevents pollution via static member decorator', () => { + const trigger = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + class StaticPolluter + { + @meta('constructor.prototype.polluted', true) + static someField = 123; + } + }; + + // Static initializers run during class definition; should throw here. + expect(trigger).toThrow(); + }); + + test('prevents pollution via instance member decorator', () => { + const trigger = () => { + class InstancePolluter + { + @meta('constructor.prototype.polluted', true) + someMethod() + {/* empty */} + } + + // CRITICAL: Must instantiate to trigger the member's addInitializer + new InstancePolluter(); + }; + + // Now that we instantiate, the initializer runs and set() throws. + expect(trigger).toThrow(); + }); +}); \ No newline at end of file diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index 58fbcad2..c6c18990 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -289,48 +289,5 @@ describe('@meta() decorator', () => { // TODO: Inherit static method meta, ... obtain via static member directly. // TODO: Static getter / setter (field) meta, ... obtain via member directly. - describe('@meta() security: prototype pollution', () => { - test('immediately prevents pollution via class decorator', () => { - const trigger = () => { - @meta('__proto__.polluted', true) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - class Polluter - {} - }; - - // Class decorators run immediately; should throw during definition. - expect(trigger).toThrow(); - }); - - test('prevents pollution via static member decorator', () => { - const trigger = () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - class StaticPolluter - { - @meta('constructor.prototype.polluted', true) - static someField = 123; - } - }; - - // Static initializers run during class definition; should throw here. - expect(trigger).toThrow(); - }); - - test('prevents pollution via instance member decorator', () => { - const trigger = () => { - class InstancePolluter - { - @meta('constructor.prototype.polluted', true) - someMethod() - {/* empty */} - } - - // CRITICAL: Must instantiate to trigger the member's addInitializer - new InstancePolluter(); - }; - - // Now that we instantiate, the initializer runs and set() throws. - expect(trigger).toThrow(); - }); - }); + }); From ed241661f269bb8682a826e29adac9e732e4eaf3 Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 15:40:34 +0200 Subject: [PATCH 29/59] Extract "member" related tests into own file --- .../browser/support/meta/meta-member.test.ts | 32 +++++++++++++++++++ tests/browser/support/meta/meta.test.ts | 32 ++----------------- 2 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 tests/browser/support/meta/meta-member.test.ts diff --git a/tests/browser/support/meta/meta-member.test.ts b/tests/browser/support/meta/meta-member.test.ts new file mode 100644 index 00000000..b3f5d165 --- /dev/null +++ b/tests/browser/support/meta/meta-member.test.ts @@ -0,0 +1,32 @@ +import { meta, Metadata } from '@aedart/support/meta'; +import { describe, expect, test } from 'vitest'; + +describe('@meta() decorator (member)', () => { + + test('can get meta using instance method reference', () => { + class MyService + { + @meta('foo', 'bar') + play() + {/* empty */} + } + + // Debug + const instance = new MyService(); + const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method + + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + // TODO: Inherit instance method meta, ... obtain via member directly. + // TODO: getter / setter (field) meta, ... obtain via member directly. + // TODO: Static method meta, ... obtain via static member directly. + // TODO: Inherit static method meta, ... obtain via static member directly. + // TODO: Static getter / setter (field) meta, ... obtain via member directly. + + +}); diff --git a/tests/browser/support/meta/meta.test.ts b/tests/browser/support/meta/meta.test.ts index c6c18990..354aebda 100644 --- a/tests/browser/support/meta/meta.test.ts +++ b/tests/browser/support/meta/meta.test.ts @@ -259,35 +259,7 @@ describe('@meta() decorator', () => { // Debug // new MyClass(); - expect(Metadata.get(MyClass, ['static', 'methods', 'myMethod', MY_KEY])).toBe( - 'secret-value', - ); + expect(Metadata.get(MyClass, ['static', 'methods', 'myMethod', MY_KEY])) + .toBe('secret-value'); }); - - test('can get meta using instance method reference', () => { - class MyService - { - @meta('foo', 'bar') - play() - {/* empty */} - } - - // Debug - const instance = new MyService(); - const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method - - expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') - .toBe(true); - - expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') - .toBe('bar'); - }); - - // TODO: Inherit instance method meta, ... obtain via member directly. - // TODO: getter / setter (field) meta, ... obtain via member directly. - // TODO: Static method meta, ... obtain via static member directly. - // TODO: Inherit static method meta, ... obtain via static member directly. - // TODO: Static getter / setter (field) meta, ... obtain via member directly. - - }); From 84bf3a1b5ddaa84ed07f12aef1b5cc94935b5a85 Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 20:47:40 +0200 Subject: [PATCH 30/59] Handle accessor --- .../support/src/meta/findOrCreateMemberAddress.ts | 10 +++++++++- packages/support/src/meta/registerAddress.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/support/src/meta/findOrCreateMemberAddress.ts b/packages/support/src/meta/findOrCreateMemberAddress.ts index e49881a9..861b722b 100644 --- a/packages/support/src/meta/findOrCreateMemberAddress.ts +++ b/packages/support/src/meta/findOrCreateMemberAddress.ts @@ -17,7 +17,15 @@ export function findOrCreateMemberAddress( owner?: OwnerContext, ): MemberAddress { - let address = addressRegistry.get(member as object); + let addressMember = member as object; + + // When accessor is given, attempt to find address via the `get` method. + // See `registerAddress()` for additional details! + if (context.kind === 'accessor') { + addressMember = (member as Record)['get']; + } + + let address = addressRegistry.get(addressMember); // Create member address (possibly without "owner context")... address ??= new Address(owner, context.static ?? false, context.kind, context.name); diff --git a/packages/support/src/meta/registerAddress.ts b/packages/support/src/meta/registerAddress.ts index 46a43940..5d285f9c 100644 --- a/packages/support/src/meta/registerAddress.ts +++ b/packages/support/src/meta/registerAddress.ts @@ -8,6 +8,8 @@ import { addressRegistry } from './registries.js'; * @param {object} owner * @param {object} member * @param {MemberAddress} address + * + * @throws {TypeError} */ export function registerAddress( owner: object, @@ -18,5 +20,18 @@ export function registerAddress( // "Force" set the context for the address,... address.ctx = getOrCreateOwnerContext(owner); + // Abort if member is of "field" kind (in which case the member is `undefined`) + if (address.kind === 'field') { + throw new TypeError(`Unable to register address for "${String(address.name)}": "field" type is not supported.`); + } + + // Store two entries using same address, when accessor given (for both the `get` and `set` methods) + if (address.kind === 'accessor') { + addressRegistry.set((member as Record)['get'], address); + addressRegistry.set((member as Record)['set'], address); + return; + } + + // Store address using the member as provided... addressRegistry.set(member, address); } From 15f779dae84958ec68485945b5c323977c85efa1 Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 20:48:11 +0200 Subject: [PATCH 31/59] Resolve meta inheritance, for address registration --- packages/support/src/meta/meta.ts | 32 +++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index c0f0c275..8541824e 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -86,10 +86,38 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) if (constructor) { flush(constructor, metadataObj); + // Skip further processing if "field" is given (target is then `undefined`) + if (context.kind === 'field') { + return; + } + // Save the target (member) address, for the given owner. // This will enable meta lookups, using the member directly. - if (context.kind === 'method') { - registerAddress(constructor, target as object, memberAddress); + // console.log('address registration for', target); + registerAddress(constructor, target as object, memberAddress); + + // To ensure that meta is still available for a member, when it is overridden in child classes, + // we register the same address again, using the member defined in the prototype, which is obtained + // via `Reflect.getOwnPropertyDescriptor`, so we can handle as many kind as possible. + // NOTE: This sadly does not work for static members! + + const descriptor = Reflect.getOwnPropertyDescriptor(constructor.prototype, context.name); + if (descriptor === undefined) { + return; + } + + const proto = (() => { + switch (context.kind) { + case 'method': return descriptor.value as object; + case 'accessor': return descriptor as object; + case 'setter': return descriptor.set as object; + case 'getter': return descriptor.get as object; + default: return undefined; + } + })(); + + if (proto !== undefined && proto !== target) { + registerAddress(constructor, proto, memberAddress); } } }); From a673c980240ceb2948c39216ee6a65f02af9ae3e Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 20:49:45 +0200 Subject: [PATCH 32/59] Add tests for "instance" members In these tests, the idea is to obtain meta using member references, rather than the class and full path. --- .../meta/meta-instance-members.test.ts | 231 ++++++++++++++++++ .../browser/support/meta/meta-member.test.ts | 32 --- 2 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 tests/browser/support/meta/meta-instance-members.test.ts delete mode 100644 tests/browser/support/meta/meta-member.test.ts diff --git a/tests/browser/support/meta/meta-instance-members.test.ts b/tests/browser/support/meta/meta-instance-members.test.ts new file mode 100644 index 00000000..a1b69b2e --- /dev/null +++ b/tests/browser/support/meta/meta-instance-members.test.ts @@ -0,0 +1,231 @@ +import { type ConstructorLike} from "@aedart/contracts"; +import { meta, Metadata } from '@aedart/support/meta'; +import { getClassPropertyDescriptors } from "@aedart/support/reflections"; +import { describe, expect, test } from 'vitest'; + +describe('@meta() decorator (instance members)', () => { + + class BaseService + { + @meta('wip', 'wap') myField = 'abc'; + + @meta('fip', 'fup') accessor value = 42; + + protected _name: string = 'my-service'; + + @meta('get_name', 'zar') + get name(): string + { + return this._name; + } + + @meta('set_name', 'fin') + set name(n: string) + { + this._name = n; + } + + @meta('foo', 'bar') + play() + {/* empty */} + + // TODO: Static field + // TODO: Static accessor + // TODO: Static getter / setter? + // TODO: Static method + } + + class AlphaService extends BaseService + {} + + class BetaService extends AlphaService + {} + + class GammaService extends BetaService + { + override myField = 'cda'; + + override accessor value = 51; + + override get name(): string + { + return this._name; + } + + override set name(n: string) + { + this._name = n; + } + + override play() + {/* empty */} + + // TODO: override Static field + // TODO: override Static accessor + // TODO: override Static getter / setter? + // TODO: override Static method + } + + test('can get meta using instance member reference', () => { + + const instance = new BaseService(); + + // --------------------------------------------------------------------------------------------------- // + // Field: In current version, we cannot obtain meta using property descriptors / reference to a field. + // The only way to do this, is via the class. + expect(Metadata.get(BaseService, 'fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Accessor: A bit tricky because we have to use property descriptor, using the instance's prototype + + const valueDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'value') as PropertyDescriptor; + expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + .toBe(true); + expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + .toBe('fup'); + expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + .toBe(true); + expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter: Similar to the accessor + + const nameDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'name') as PropertyDescriptor; + expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + .toBe('zar'); + expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + test('can get meta using instance member reference (inherited)', () => { + + const instance = new BetaService(); + + // --------------------------------------------------------------------------------------------------- // + // Field: The same as in previous test. We can only get meta for "field" kind, using the class reference. + expect(Metadata.get(BetaService, 'fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Get the class descriptors. Unlike previous test, we cannot obtain property descriptors directory from + // the instance's prototype. We need to obtain them from the "base" class, so this util comes in handy. + const descriptors = getClassPropertyDescriptors( + instance.constructor as ConstructorLike, + true + ); + + // Debug + //console.log('descriptors', descriptors); + + // --------------------------------------------------------------------------------------------------- // + // Accessor + + const valueDescriptor = descriptors['value'] as PropertyDescriptor; + expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + .toBe(true); + expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + .toBe('fup'); + expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + .toBe(true); + expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter + + const nameDescriptor = descriptors['name'] as PropertyDescriptor; + expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + .toBe('zar'); + expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + test('can get meta using instance member reference (overridden)', () => { + + const instance = new GammaService(); + + // --------------------------------------------------------------------------------------------------- // + // Field: Same as previous tests... + expect(Metadata.get(GammaService, 'fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Get the class descriptors, just like in the previous test + const overriddenDescriptors = getClassPropertyDescriptors( + instance.constructor as ConstructorLike, + true + ); + + // Debug + //console.log('descriptors (overridden)', overriddenDescriptors); + + // --------------------------------------------------------------------------------------------------- // + // Accessor + + const valueDescriptor = overriddenDescriptors['value'] as PropertyDescriptor; + expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + .toBe(true); + expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + .toBe('fup'); + expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + .toBe(true); + expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter + + const nameDescriptor = overriddenDescriptors['name'] as PropertyDescriptor; + expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + .toBe('zar'); + expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + .toBe(true); + expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + // TODO: Static method meta, ... obtain via static member directly. + // TODO: Inherit static method meta, ... obtain via static member directly. + // TODO: Static getter / setter (field) meta, ... obtain via member directly. + + +}); diff --git a/tests/browser/support/meta/meta-member.test.ts b/tests/browser/support/meta/meta-member.test.ts deleted file mode 100644 index b3f5d165..00000000 --- a/tests/browser/support/meta/meta-member.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { meta, Metadata } from '@aedart/support/meta'; -import { describe, expect, test } from 'vitest'; - -describe('@meta() decorator (member)', () => { - - test('can get meta using instance method reference', () => { - class MyService - { - @meta('foo', 'bar') - play() - {/* empty */} - } - - // Debug - const instance = new MyService(); - const { play } = instance; // eslint-disable-line @typescript-eslint/unbound-method - - expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') - .toBe(true); - - expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') - .toBe('bar'); - }); - - // TODO: Inherit instance method meta, ... obtain via member directly. - // TODO: getter / setter (field) meta, ... obtain via member directly. - // TODO: Static method meta, ... obtain via static member directly. - // TODO: Inherit static method meta, ... obtain via static member directly. - // TODO: Static getter / setter (field) meta, ... obtain via member directly. - - -}); From 382fc48732deef54f585df112a08b66c85905f86 Mon Sep 17 00:00:00 2001 From: aedart Date: Thu, 14 May 2026 21:03:31 +0200 Subject: [PATCH 33/59] Fix lint and formatting --- .../src/meta/findOrCreateMemberAddress.ts | 4 +- packages/support/src/meta/meta.ts | 24 +- packages/support/src/meta/registerAddress.ts | 18 +- .../meta/meta-instance-members.test.ts | 210 ++++++++++++------ .../support/meta/meta-security.test.ts | 4 +- 5 files changed, 174 insertions(+), 86 deletions(-) diff --git a/packages/support/src/meta/findOrCreateMemberAddress.ts b/packages/support/src/meta/findOrCreateMemberAddress.ts index 861b722b..89610ad7 100644 --- a/packages/support/src/meta/findOrCreateMemberAddress.ts +++ b/packages/support/src/meta/findOrCreateMemberAddress.ts @@ -18,11 +18,11 @@ export function findOrCreateMemberAddress( ): MemberAddress { let addressMember = member as object; - + // When accessor is given, attempt to find address via the `get` method. // See `registerAddress()` for additional details! if (context.kind === 'accessor') { - addressMember = (member as Record)['get']; + addressMember = (member as Record).get; } let address = addressRegistry.get(addressMember); diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 8541824e..93b3f5c6 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -95,27 +95,35 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) // This will enable meta lookups, using the member directly. // console.log('address registration for', target); registerAddress(constructor, target as object, memberAddress); - + // To ensure that meta is still available for a member, when it is overridden in child classes, // we register the same address again, using the member defined in the prototype, which is obtained // via `Reflect.getOwnPropertyDescriptor`, so we can handle as many kind as possible. // NOTE: This sadly does not work for static members! - const descriptor = Reflect.getOwnPropertyDescriptor(constructor.prototype, context.name); + const descriptor = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + context.name, + ); if (descriptor === undefined) { return; } const proto = (() => { switch (context.kind) { - case 'method': return descriptor.value as object; - case 'accessor': return descriptor as object; - case 'setter': return descriptor.set as object; - case 'getter': return descriptor.get as object; - default: return undefined; + case 'method': + return descriptor.value as object; + case 'accessor': + return descriptor as object; + case 'setter': + return descriptor.set as object; + case 'getter': + return descriptor.get as object; + default: + return undefined; } })(); - + if (proto !== undefined && proto !== target) { registerAddress(constructor, proto, memberAddress); } diff --git a/packages/support/src/meta/registerAddress.ts b/packages/support/src/meta/registerAddress.ts index 5d285f9c..a4c830ac 100644 --- a/packages/support/src/meta/registerAddress.ts +++ b/packages/support/src/meta/registerAddress.ts @@ -8,7 +8,7 @@ import { addressRegistry } from './registries.js'; * @param {object} owner * @param {object} member * @param {MemberAddress} address - * + * * @throws {TypeError} */ export function registerAddress( @@ -20,18 +20,22 @@ export function registerAddress( // "Force" set the context for the address,... address.ctx = getOrCreateOwnerContext(owner); - // Abort if member is of "field" kind (in which case the member is `undefined`) + // Abort if member is of "field" kind (in which case the member is `undefined`) if (address.kind === 'field') { - throw new TypeError(`Unable to register address for "${String(address.name)}": "field" type is not supported.`); + throw new TypeError( + `Unable to register address for "${ + String(address.name) + }": "field" type is not supported.`, + ); } - + // Store two entries using same address, when accessor given (for both the `get` and `set` methods) if (address.kind === 'accessor') { - addressRegistry.set((member as Record)['get'], address); - addressRegistry.set((member as Record)['set'], address); + addressRegistry.set((member as Record).get, address); + addressRegistry.set((member as Record).set, address); return; } - + // Store address using the member as provided... addressRegistry.set(member, address); } diff --git a/tests/browser/support/meta/meta-instance-members.test.ts b/tests/browser/support/meta/meta-instance-members.test.ts index a1b69b2e..eb31bf90 100644 --- a/tests/browser/support/meta/meta-instance-members.test.ts +++ b/tests/browser/support/meta/meta-instance-members.test.ts @@ -1,27 +1,26 @@ -import { type ConstructorLike} from "@aedart/contracts"; +import { type ConstructorLike } from '@aedart/contracts'; import { meta, Metadata } from '@aedart/support/meta'; -import { getClassPropertyDescriptors } from "@aedart/support/reflections"; +import { getClassPropertyDescriptors } from '@aedart/support/reflections'; import { describe, expect, test } from 'vitest'; describe('@meta() decorator (instance members)', () => { - class BaseService { - @meta('wip', 'wap') myField = 'abc'; - - @meta('fip', 'fup') accessor value = 42; - - protected _name: string = 'my-service'; + @meta('wip', 'wap') + myField = 'abc'; + + @meta('fip', 'fup') + accessor value = 42; + + protected _name = 'my-service'; @meta('get_name', 'zar') - get name(): string - { + get name(): string { return this._name; } @meta('set_name', 'fin') - set name(n: string) - { + set name(n: string) { this._name = n; } @@ -34,7 +33,7 @@ describe('@meta() decorator (instance members)', () => { // TODO: Static getter / setter? // TODO: Static method } - + class AlphaService extends BaseService {} @@ -46,17 +45,15 @@ describe('@meta() decorator (instance members)', () => { override myField = 'cda'; override accessor value = 51; - - override get name(): string - { + + override get name(): string { return this._name; } - - override set name(n: string) - { + + override set name(n: string) { this._name = n; } - + override play() {/* empty */} @@ -65,9 +62,8 @@ describe('@meta() decorator (instance members)', () => { // TODO: override Static getter / setter? // TODO: override Static method } - + test('can get meta using instance member reference', () => { - const instance = new BaseService(); // --------------------------------------------------------------------------------------------------- // @@ -75,31 +71,63 @@ describe('@meta() decorator (instance members)', () => { // The only way to do this, is via the class. expect(Metadata.get(BaseService, 'fields.myField.wip'), 'Unable to get meta for field') .toBe('wap'); - + // --------------------------------------------------------------------------------------------------- // // Accessor: A bit tricky because we have to use property descriptor, using the instance's prototype - const valueDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'value') as PropertyDescriptor; - expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + const valueDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(instance), + 'value', + )!; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) .toBe('fup'); - expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) .toBe('fup'); - + // --------------------------------------------------------------------------------------------------- // - // Getter / Setter: Similar to the accessor + // Getter / Setter: Similar to the accessor + + const nameDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(instance), + 'name', + )!; - const nameDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'name') as PropertyDescriptor; - expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) .toBe('zar'); - expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) .toBe('fin'); // --------------------------------------------------------------------------------------------------- // @@ -113,7 +141,6 @@ describe('@meta() decorator (instance members)', () => { }); test('can get meta using instance member reference (inherited)', () => { - const instance = new BetaService(); // --------------------------------------------------------------------------------------------------- // @@ -126,36 +153,62 @@ describe('@meta() decorator (instance members)', () => { // the instance's prototype. We need to obtain them from the "base" class, so this util comes in handy. const descriptors = getClassPropertyDescriptors( instance.constructor as ConstructorLike, - true + true, ); - + // Debug - //console.log('descriptors', descriptors); - + // console.log('descriptors', descriptors); + // --------------------------------------------------------------------------------------------------- // // Accessor - - const valueDescriptor = descriptors['value'] as PropertyDescriptor; - expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + + const valueDescriptor = descriptors.value; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) .toBe('fup'); - expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) .toBe('fup'); // --------------------------------------------------------------------------------------------------- // // Getter / Setter - const nameDescriptor = descriptors['name'] as PropertyDescriptor; - expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + const nameDescriptor = descriptors.name; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) .toBe('zar'); - expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) .toBe('fin'); // --------------------------------------------------------------------------------------------------- // @@ -169,7 +222,6 @@ describe('@meta() decorator (instance members)', () => { }); test('can get meta using instance member reference (overridden)', () => { - const instance = new GammaService(); // --------------------------------------------------------------------------------------------------- // @@ -181,36 +233,62 @@ describe('@meta() decorator (instance members)', () => { // Get the class descriptors, just like in the previous test const overriddenDescriptors = getClassPropertyDescriptors( instance.constructor as ConstructorLike, - true + true, ); // Debug - //console.log('descriptors (overridden)', overriddenDescriptors); + // console.log('descriptors (overridden)', overriddenDescriptors); // --------------------------------------------------------------------------------------------------- // // Accessor - const valueDescriptor = overriddenDescriptors['value'] as PropertyDescriptor; - expect(Metadata.has(valueDescriptor.get as object, 'fip'), 'Unable to determine if has meta for accessor (get)') + const valueDescriptor = overriddenDescriptors.value; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.get as object, 'fip'), 'Incorrect meta value for accessor (get)') + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) .toBe('fup'); - expect(Metadata.has(valueDescriptor.set as object, 'fip'), 'Unable to determine if has meta for accessor (set)') + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) .toBe(true); - expect(Metadata.get(valueDescriptor.set as object, 'fip'), 'Incorrect meta value for accessor (set)') + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) .toBe('fup'); // --------------------------------------------------------------------------------------------------- // // Getter / Setter - const nameDescriptor = overriddenDescriptors['name'] as PropertyDescriptor; - expect(Metadata.has(nameDescriptor.get as object, 'get_name'), 'Unable to determine if has meta for getter') + const nameDescriptor = overriddenDescriptors.name; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'get_name'), 'Incorrect meta value for getter') + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) .toBe('zar'); - expect(Metadata.has(nameDescriptor.get as object, 'set_name'), 'Unable to determine if has meta for setter') + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) .toBe(true); - expect(Metadata.get(nameDescriptor.get as object, 'set_name'), 'Incorrect meta value for setter') + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) .toBe('fin'); // --------------------------------------------------------------------------------------------------- // @@ -222,10 +300,8 @@ describe('@meta() decorator (instance members)', () => { expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') .toBe('bar'); }); - + // TODO: Static method meta, ... obtain via static member directly. // TODO: Inherit static method meta, ... obtain via static member directly. // TODO: Static getter / setter (field) meta, ... obtain via member directly. - - }); diff --git a/tests/browser/support/meta/meta-security.test.ts b/tests/browser/support/meta/meta-security.test.ts index cc73c467..b201c7e6 100644 --- a/tests/browser/support/meta/meta-security.test.ts +++ b/tests/browser/support/meta/meta-security.test.ts @@ -5,7 +5,7 @@ describe('@meta() security: prototype pollution', () => { test('immediately prevents pollution via class decorator', () => { const trigger = () => { @meta('__proto__.polluted', true) - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars class Polluter {} }; @@ -44,4 +44,4 @@ describe('@meta() security: prototype pollution', () => { // Now that we instantiate, the initializer runs and set() throws. expect(trigger).toThrow(); }); -}); \ No newline at end of file +}); From 719a40b3b535bb869a51f465aee037499091bac5 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 18:23:02 +0200 Subject: [PATCH 34/59] Rename test file --- .../meta/{meta-instance-members.test.ts => meta-members.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/browser/support/meta/{meta-instance-members.test.ts => meta-members.test.ts} (100%) diff --git a/tests/browser/support/meta/meta-instance-members.test.ts b/tests/browser/support/meta/meta-members.test.ts similarity index 100% rename from tests/browser/support/meta/meta-instance-members.test.ts rename to tests/browser/support/meta/meta-members.test.ts From 9d5bce3ae4c67866f899780c483f53a372d39595 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 18:28:25 +0200 Subject: [PATCH 35/59] Improve test names / descriptions --- tests/browser/support/meta/meta-members.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/browser/support/meta/meta-members.test.ts b/tests/browser/support/meta/meta-members.test.ts index eb31bf90..3ac4c5a8 100644 --- a/tests/browser/support/meta/meta-members.test.ts +++ b/tests/browser/support/meta/meta-members.test.ts @@ -140,7 +140,7 @@ describe('@meta() decorator (instance members)', () => { .toBe('bar'); }); - test('can get meta using instance member reference (inherited)', () => { + test('inherits meta using instance member reference', () => { const instance = new BetaService(); // --------------------------------------------------------------------------------------------------- // @@ -221,7 +221,7 @@ describe('@meta() decorator (instance members)', () => { .toBe('bar'); }); - test('can get meta using instance member reference (overridden)', () => { + test('inherits meta using overridden instance member reference', () => { const instance = new GammaService(); // --------------------------------------------------------------------------------------------------- // From 87610b4f20de81267bf308dffac023afaabeec1e Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 18:29:06 +0200 Subject: [PATCH 36/59] Cleanup --- tests/browser/support/meta/meta-members.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/browser/support/meta/meta-members.test.ts b/tests/browser/support/meta/meta-members.test.ts index 3ac4c5a8..fbf6dfa1 100644 --- a/tests/browser/support/meta/meta-members.test.ts +++ b/tests/browser/support/meta/meta-members.test.ts @@ -27,11 +27,6 @@ describe('@meta() decorator (instance members)', () => { @meta('foo', 'bar') play() {/* empty */} - - // TODO: Static field - // TODO: Static accessor - // TODO: Static getter / setter? - // TODO: Static method } class AlphaService extends BaseService @@ -56,11 +51,6 @@ describe('@meta() decorator (instance members)', () => { override play() {/* empty */} - - // TODO: override Static field - // TODO: override Static accessor - // TODO: override Static getter / setter? - // TODO: override Static method } test('can get meta using instance member reference', () => { @@ -300,8 +290,4 @@ describe('@meta() decorator (instance members)', () => { expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') .toBe('bar'); }); - - // TODO: Static method meta, ... obtain via static member directly. - // TODO: Inherit static method meta, ... obtain via static member directly. - // TODO: Static getter / setter (field) meta, ... obtain via member directly. }); From fb0b575d2c1355d590ba0ac56117b92b90fede98 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 19:28:26 +0200 Subject: [PATCH 37/59] Cleanup --- packages/support/src/reflections/getClassPropertyDescriptors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 8c9d8fd7..93b1da29 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -38,7 +38,6 @@ export function getClassPropertyDescriptors( // Since walkParents yields nearest parent first, we iterate the chain in // reverse order to ensure child descriptors win or are merged onto parent descriptors. for (let i = chain.length - 1; i >= 0; i--) { - // populateDescriptors(output, chain[i]); populateDescriptors(output, chain[i].prototype as object); } From d05e252a1f9c8200062be664accc3235e8d14ef0 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 19:39:44 +0200 Subject: [PATCH 38/59] Rename "proto" argument to "target" --- .../support/src/reflections/populateDescriptors.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/support/src/reflections/populateDescriptors.ts b/packages/support/src/reflections/populateDescriptors.ts index b5203735..c6ab086c 100644 --- a/packages/support/src/reflections/populateDescriptors.ts +++ b/packages/support/src/reflections/populateDescriptors.ts @@ -1,26 +1,26 @@ import { merge } from '../objects/merge.js'; /** - * Populate descriptors from a given prototype object into the output record. + * Populate descriptors from a given target object into the output record. * * When a key collision occurs, the existing descriptor (child) takes precedence * and is deep-merged with the incoming descriptor (parent). * * @param {Record} output - The accumulator record to populate. - * @param {object} proto - The prototype object to extract descriptors from. + * @param {object} target - The object to extract descriptors from. * * @returns {Record} The populated output record. */ export function populateDescriptors( output: Record, - proto: object, + target: object, ): Record { - const keys = Reflect.ownKeys(proto); + const keys = Reflect.ownKeys(target); const len = keys.length; for (let i = 0; i < len; i++) { const key = keys[i]; - const descriptor = Reflect.getOwnPropertyDescriptor(proto, key); + const descriptor = Reflect.getOwnPropertyDescriptor(target, key); if (descriptor === undefined) { continue; } From 73b733b4a6be195cb1771bde74cbe8f8f3a05686 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 19:54:12 +0200 Subject: [PATCH 39/59] Refactor, add support for returning descriptors of static defined props --- .../getClassPropertyDescriptors.ts | 20 ++- .../getClassPropertyDescriptors.test.ts | 120 +++++++++++++++++- 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 93b1da29..c74b3e21 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -4,10 +4,11 @@ import { populateDescriptors } from './populateDescriptors.js'; import { walkParents } from './walkParents.js'; /** - * Returns all property descriptors defined on the target's prototype chain. + * Returns all property descriptors defined in the target. * * @param {ConstructorLike} target - The target class. - * @param {boolean} [recursive=false] - If `true`, the target's parent prototypes are traversed. + * @param {boolean} [recursive=false] - If `true`, the target's parents are traversed. + * @param {boolean} [fromPrototype=true] - If `true`, property descriptors from the target's prototype is returned. * * @returns {Record} * @@ -18,14 +19,19 @@ import { walkParents } from './walkParents.js'; export function getClassPropertyDescriptors( target: ConstructorLike, recursive = false, + fromPrototype = true, ): Record { assertHasPrototypeProperty(target); const output = Object.create(null) as Record; - // If not recursive, we only care about the immediate prototype. + // If not recursive, we only care about the immediate target. if (!recursive) { - return populateDescriptors(output, target.prototype as object); + const from = fromPrototype + ? target.prototype as object + : target as object; + + return populateDescriptors(output, from); } // To respect the inheritance priority (child overrides parent), we collect @@ -38,7 +44,11 @@ export function getClassPropertyDescriptors( // Since walkParents yields nearest parent first, we iterate the chain in // reverse order to ensure child descriptors win or are merged onto parent descriptors. for (let i = chain.length - 1; i >= 0; i--) { - populateDescriptors(output, chain[i].prototype as object); + const from = fromPrototype + ? chain[i].prototype as object + : chain[i] as object; + + populateDescriptors(output, from); } return output; diff --git a/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts b/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts index f6616adb..944ee6ed 100644 --- a/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts +++ b/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts @@ -18,7 +18,7 @@ describe('@aedart/support/refelctions', () => { .toThrow(TypeError); }); - test('can get property descriptors for class', () => { + test('can get property descriptors for class\'s prototype', () => { const MY_SYMBOL = Symbol('my_symbol'); class A @@ -72,7 +72,62 @@ describe('@aedart/support/refelctions', () => { } }); - test('can get property descriptors for class recursively', () => { + test('can get static property descriptors for class', () => { + const MY_SYMBOL = Symbol('my_symbol'); + + class A + { + static set name(v) { + /* empty */ + } + static get name() { + return null; + } + static bar() + { + /* empty */ + } + static [MY_SYMBOL]() + { + /* empty */ + } + } + + // -------------------------------------------------------------------------------- // + + const expected = [ + 'prototype', + 'length', + 'name', + 'bar', + MY_SYMBOL, + ]; + + const descriptors = getClassPropertyDescriptors(A, false, false); + + // Debug + // console.log(descriptors); + + for (const key of expected) { + const k = (typeof key == 'symbol') + ? key.toString() + : key; + + expect(Reflect.has(descriptors, key), 'Key ' + k + ' not in descriptors record') + .toBeTruthy(); + + const descriptor = descriptors[key]; + + // Debug + // console.log(key, descriptor); + + expect(descriptor, 'No descriptor returned for ' + k) + .not + .toBeUndefined(); + } + }); + + test('can get property descriptors for class\'s prototype recursively', () => { const MY_SYMBOL = Symbol('my_symbol'); class A @@ -129,6 +184,67 @@ describe('@aedart/support/refelctions', () => { } }); + test('can get static property descriptors for class recursively', () => { + const MY_SYMBOL = Symbol('my_symbol'); + + class A + { + static set name(v) { + /* empty */ + } + static get name() { + return null; + } + static foo() + { + /* empty */ + } + static [MY_SYMBOL]() + { + /* empty */ + } + } + + class B extends A + { + static set bar(v) { + /* empty */ + } + static get bar() { + return null; + } + } + + // -------------------------------------------------------------------------------- // + + const expected = [ + 'prototype', + 'length', + 'name', + 'foo', + 'bar', + MY_SYMBOL, + ]; + + const descriptors = getClassPropertyDescriptors(B, true, false); + // Debug + // console.log(descriptors); + + for (const key of expected) { + const k = (typeof key == 'symbol') + ? key.toString() + : key; + + expect(Reflect.has(descriptors, key), 'Key ' + k + ' not in descriptors record') + .toBeTruthy(); + + const descriptor = descriptors[key]; + expect(descriptor, 'No descriptor returned for ' + k) + .not + .toBeUndefined(); + } + }); + test('returns top-most property descriptors', () => { const MY_SYMBOL = Symbol('my_symbol'); From f541aa5f22d564ae73160ac2738282b48dd38211 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 19:54:18 +0200 Subject: [PATCH 40/59] Change release notes --- CHANGELOG_CLEAN_SLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_CLEAN_SLATE.md b/CHANGELOG_CLEAN_SLATE.md index 3a6eb252..aa7579a1 100644 --- a/CHANGELOG_CLEAN_SLATE.md +++ b/CHANGELOG_CLEAN_SLATE.md @@ -41,6 +41,7 @@ Temporary changelog for feature branch. * `useCloneable` option has been renamed to `clone` and set to `false` by default, in `MergeOptions` (_contracts and support packages_) * Object `merge()` behaviour changed to correctly deal with concat-spreadable objects and arrays (_support package_). * `set()`, `get()`, `has()`, `forget()` and `isArrayLike()` no longer use lodash (_support package_). +* `ObjectId` properties are now set to be private. **Non-breaking Changes** @@ -49,7 +50,7 @@ Temporary changelog for feature branch. * Optimized performance of reflections utilities (_support package_). * Optimized performance of objects' `hasAll()`, `hasAny()`, `forgetAll()`, `ObjectId`, `isset()` and `populate()` (_support package_). * Improved typing of array `merge()` to work with any amount of arrays (_support package_). -* `ObjectId` properties are now set to be private. +* `getClassPropertyDescriptors()` can now also return property descriptors for static defined properties (_support package_). * The TypeScript source files are now also published, along with appropriate source maps. * dprint is now used for formatting. From a143ad3d1397d0e973c1d33306b431374b0082fa Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 20:43:21 +0200 Subject: [PATCH 41/59] Add support for inheriting meta for static members NOTE: this does NOT work for overridden static members. We still need some kind of `@inheritMeta()` decorator / helper to ensure this can become available for developers. --- packages/support/src/meta/meta.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 93b3f5c6..185d6c7d 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -93,18 +93,16 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) // Save the target (member) address, for the given owner. // This will enable meta lookups, using the member directly. - // console.log('address registration for', target); registerAddress(constructor, target as object, memberAddress); - // To ensure that meta is still available for a member, when it is overridden in child classes, - // we register the same address again, using the member defined in the prototype, which is obtained - // via `Reflect.getOwnPropertyDescriptor`, so we can handle as many kind as possible. - // NOTE: This sadly does not work for static members! + // To ensure that meta is still available vai a member reference directly, even when overridden + // in a child class, we register the address again, using member obtained from a property descriptor. + // NOTE: This sadly DOES NOT work for overridden static members (no late static binding of `this`)! + + const descriptor = !isStatic + ? Reflect.getOwnPropertyDescriptor(constructor.prototype, context.name) + : Reflect.getOwnPropertyDescriptor(constructor, context.name); - const descriptor = Reflect.getOwnPropertyDescriptor( - constructor.prototype, - context.name, - ); if (descriptor === undefined) { return; } From fb5f267aa12ac3c8dbd8c335e77233cc6eea0ae4 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 20:46:18 +0200 Subject: [PATCH 42/59] Add tests for getting meta via static members references (incomplete) --- .../support/meta/meta-static-members.test.ts | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 tests/browser/support/meta/meta-static-members.test.ts diff --git a/tests/browser/support/meta/meta-static-members.test.ts b/tests/browser/support/meta/meta-static-members.test.ts new file mode 100644 index 00000000..c544629b --- /dev/null +++ b/tests/browser/support/meta/meta-static-members.test.ts @@ -0,0 +1,297 @@ +import { meta, Metadata } from '@aedart/support/meta'; +import { getClassPropertyDescriptors } from '@aedart/support/reflections'; +import { describe, expect, test } from 'vitest'; + +describe('@meta() decorator (static members)', () => { + class BaseService + { + @meta('wip', 'wap') + static myField = 'abc'; + + @meta('fip', 'fup') + static accessor value = 42; + + protected static _name = 'my-service'; + + @meta('get_name', 'zar') + static get name(): string { + return this._name; + } + + @meta('set_name', 'fin') + static set name(n: string) { + this._name = n; + } + + @meta('foo', 'bar') + static play() + {/* empty */} + } + + class AlphaService extends BaseService + {} + + class BetaService extends AlphaService + {} + + // TODO: WIP + class GammaService extends BetaService + { + // Note: meta is sadly NOT automatically inherited for overridden static members. + // This is because decorator's context.addInitializer() do not offer a late static + // binding for `this` (this = BaseService in this case). For this reason, the only + // way to ensure inheritance, is via the custom `@inheritMeta()`! + + static override myField = 'cda'; + + static override accessor value = 51; + + static override get name(): string { + return this._name; + } + + static override set name(n: string) { + this._name = n; + } + + static override play() + {/* empty */} + } + + test('can get meta using static member reference', () => { + + // --------------------------------------------------------------------------------------------------- // + // Field: Similar as for "instance" field, we can only obtain a static field's meta using the class and + // full path. + expect(Metadata.get(BaseService, 'static.fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Accessor: Using a property descriptor of the class and the "accessor", then should be possible. + + const valueDescriptor = Object.getOwnPropertyDescriptor( + BaseService, + 'value', + )!; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) + .toBe('fup'); + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter: Similar to the accessor + + const nameDescriptor = Object.getOwnPropertyDescriptor( + BaseService, + 'name', + )!; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) + .toBe('zar'); + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = BaseService; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + test('inherits meta using static member reference', () => { + // --------------------------------------------------------------------------------------------------- // + // Field: The same as in previous test. We can only get meta for "field" kind, using the class reference. + expect(Metadata.get(BetaService, 'static.fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Get the class descriptors (static defined properties) + + const descriptors = getClassPropertyDescriptors( + BetaService, + true, + false + ); + + // Debug + // console.log('descriptors', descriptors); + + // --------------------------------------------------------------------------------------------------- // + // Accessor + + const valueDescriptor = descriptors.value; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) + .toBe('fup'); + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter + + const nameDescriptor = descriptors.name; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) + .toBe('zar'); + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = BetaService; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + // TODO: WIP + // test('inherits meta using overridden static member reference (via @inheritMeta decorator)', () => { + // + // // --------------------------------------------------------------------------------------------------- // + // // Field: Same as previous tests... + // expect(Metadata.get(GammaService, 'static.fields.myField.wip'), 'Unable to get meta for field') + // .toBe('wap'); + // + // // --------------------------------------------------------------------------------------------------- // + // // Get the class descriptors, just like in the previous test + // const overriddenDescriptors = getClassPropertyDescriptors( + // GammaService, + // true, + // false + // ); + // + // // Debug + // console.log('descriptors (overridden)', overriddenDescriptors); + // + // // --------------------------------------------------------------------------------------------------- // + // // Accessor + // + // const valueDescriptor = overriddenDescriptors.value; + // + // expect( + // Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Unable to determine if has meta for accessor (get)', + // ) + // .toBe(true); + // expect( + // Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Incorrect meta value for accessor (get)', + // ) + // .toBe('fup'); + // expect( + // Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Unable to determine if has meta for accessor (set)', + // ) + // .toBe(true); + // expect( + // Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Incorrect meta value for accessor (set)', + // ) + // .toBe('fup'); + // + // // --------------------------------------------------------------------------------------------------- // + // // Getter / Setter + // + // const nameDescriptor = overriddenDescriptors.name; + // + // expect( + // Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Unable to determine if has meta for getter', + // ) + // .toBe(true); + // expect( + // Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Incorrect meta value for getter', + // ) + // .toBe('zar'); + // expect( + // Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Unable to determine if has meta for setter', + // ) + // .toBe(true); + // expect( + // Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + // 'Incorrect meta value for setter', + // ) + // .toBe('fin'); + // + // // --------------------------------------------------------------------------------------------------- // + // // Method: This should be straight forward... + // + // const { play } = GammaService; // eslint-disable-line @typescript-eslint/unbound-method + // expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + // .toBe(true); + // expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + // .toBe('bar'); + // }); +}); From 96233cf060edee6f79bd9205cdaab41f43f69339 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 20:47:21 +0200 Subject: [PATCH 43/59] Disable dummy class WIP --- .../support/meta/meta-static-members.test.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/browser/support/meta/meta-static-members.test.ts b/tests/browser/support/meta/meta-static-members.test.ts index c544629b..91450597 100644 --- a/tests/browser/support/meta/meta-static-members.test.ts +++ b/tests/browser/support/meta/meta-static-members.test.ts @@ -35,28 +35,28 @@ describe('@meta() decorator (static members)', () => { {} // TODO: WIP - class GammaService extends BetaService - { - // Note: meta is sadly NOT automatically inherited for overridden static members. - // This is because decorator's context.addInitializer() do not offer a late static - // binding for `this` (this = BaseService in this case). For this reason, the only - // way to ensure inheritance, is via the custom `@inheritMeta()`! - - static override myField = 'cda'; - - static override accessor value = 51; - - static override get name(): string { - return this._name; - } - - static override set name(n: string) { - this._name = n; - } - - static override play() - {/* empty */} - } + // class GammaService extends BetaService + // { + // // Note: meta is sadly NOT automatically inherited for overridden static members. + // // This is because decorator's context.addInitializer() do not offer a late static + // // binding for `this` (this = BaseService in this case). For this reason, the only + // // way to ensure inheritance, is via the custom `@inheritMeta()`! + // + // static override myField = 'cda'; + // + // static override accessor value = 51; + // + // static override get name(): string { + // return this._name; + // } + // + // static override set name(n: string) { + // this._name = n; + // } + // + // static override play() + // {/* empty */} + // } test('can get meta using static member reference', () => { From b014287d9a0e5d27a02d6270549793b420fa79a2 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 20:48:11 +0200 Subject: [PATCH 44/59] Fix formatting --- .../src/reflections/getClassPropertyDescriptors.ts | 2 +- .../reflections/getClassPropertyDescriptors.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index c74b3e21..ed7fb07f 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -47,7 +47,7 @@ export function getClassPropertyDescriptors( const from = fromPrototype ? chain[i].prototype as object : chain[i] as object; - + populateDescriptors(output, from); } diff --git a/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts b/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts index 944ee6ed..fecca217 100644 --- a/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts +++ b/tests/browser/support/reflections/getClassPropertyDescriptors.test.ts @@ -18,7 +18,7 @@ describe('@aedart/support/refelctions', () => { .toThrow(TypeError); }); - test('can get property descriptors for class\'s prototype', () => { + test("can get property descriptors for class's prototype", () => { const MY_SYMBOL = Symbol('my_symbol'); class A @@ -126,8 +126,8 @@ describe('@aedart/support/refelctions', () => { .toBeUndefined(); } }); - - test('can get property descriptors for class\'s prototype recursively', () => { + + test("can get property descriptors for class's prototype recursively", () => { const MY_SYMBOL = Symbol('my_symbol'); class A @@ -229,7 +229,7 @@ describe('@aedart/support/refelctions', () => { const descriptors = getClassPropertyDescriptors(B, true, false); // Debug // console.log(descriptors); - + for (const key of expected) { const k = (typeof key == 'symbol') ? key.toString() @@ -244,7 +244,7 @@ describe('@aedart/support/refelctions', () => { .toBeUndefined(); } }); - + test('returns top-most property descriptors', () => { const MY_SYMBOL = Symbol('my_symbol'); From c93e266a03f4398be76f40402fc12c42b5b1da99 Mon Sep 17 00:00:00 2001 From: aedart Date: Fri, 15 May 2026 20:48:22 +0200 Subject: [PATCH 45/59] Fix formatting --- .../support/meta/meta-static-members.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/browser/support/meta/meta-static-members.test.ts b/tests/browser/support/meta/meta-static-members.test.ts index 91450597..f2803f79 100644 --- a/tests/browser/support/meta/meta-static-members.test.ts +++ b/tests/browser/support/meta/meta-static-members.test.ts @@ -41,7 +41,7 @@ describe('@meta() decorator (static members)', () => { // // This is because decorator's context.addInitializer() do not offer a late static // // binding for `this` (this = BaseService in this case). For this reason, the only // // way to ensure inheritance, is via the custom `@inheritMeta()`! - // + // // static override myField = 'cda'; // // static override accessor value = 51; @@ -59,11 +59,13 @@ describe('@meta() decorator (static members)', () => { // } test('can get meta using static member reference', () => { - // --------------------------------------------------------------------------------------------------- // // Field: Similar as for "instance" field, we can only obtain a static field's meta using the class and // full path. - expect(Metadata.get(BaseService, 'static.fields.myField.wip'), 'Unable to get meta for field') + expect( + Metadata.get(BaseService, 'static.fields.myField.wip'), + 'Unable to get meta for field', + ) .toBe('wap'); // --------------------------------------------------------------------------------------------------- // @@ -137,7 +139,10 @@ describe('@meta() decorator (static members)', () => { test('inherits meta using static member reference', () => { // --------------------------------------------------------------------------------------------------- // // Field: The same as in previous test. We can only get meta for "field" kind, using the class reference. - expect(Metadata.get(BetaService, 'static.fields.myField.wip'), 'Unable to get meta for field') + expect( + Metadata.get(BetaService, 'static.fields.myField.wip'), + 'Unable to get meta for field', + ) .toBe('wap'); // --------------------------------------------------------------------------------------------------- // @@ -146,7 +151,7 @@ describe('@meta() decorator (static members)', () => { const descriptors = getClassPropertyDescriptors( BetaService, true, - false + false, ); // Debug From a380fb8b0160cef9439087dee9a87c543f3dc35c Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 13:12:24 +0200 Subject: [PATCH 46/59] Refactor / cleanup --- packages/support/src/meta/meta.ts | 69 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 185d6c7d..1a5da66d 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -83,48 +83,47 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) | ConstructorLike | undefined; - if (constructor) { - flush(constructor, metadataObj); + // Skip further processing if there isn't a constructor available, or if + // the context.kind is a "field" (target is then `undefined`). + if (!constructor || context.kind === 'field') { + return; + } - // Skip further processing if "field" is given (target is then `undefined`) - if (context.kind === 'field') { - return; - } + flush(constructor, metadataObj); - // Save the target (member) address, for the given owner. - // This will enable meta lookups, using the member directly. - registerAddress(constructor, target as object, memberAddress); + // Save the target (member) address, for the given owner. + // This will enable meta lookups, using the member directly. + registerAddress(constructor, target as object, memberAddress); - // To ensure that meta is still available vai a member reference directly, even when overridden - // in a child class, we register the address again, using member obtained from a property descriptor. - // NOTE: This sadly DOES NOT work for overridden static members (no late static binding of `this`)! + // To ensure that meta is still available vai a member reference directly, even when overridden + // in a child class, we register the address again, using member obtained from a property descriptor. + // NOTE: This sadly DOES NOT work for overridden static members (no late static binding of `this`)! - const descriptor = !isStatic - ? Reflect.getOwnPropertyDescriptor(constructor.prototype, context.name) - : Reflect.getOwnPropertyDescriptor(constructor, context.name); + const descriptor = !isStatic + ? Reflect.getOwnPropertyDescriptor(constructor.prototype, context.name) + : Reflect.getOwnPropertyDescriptor(constructor, context.name); - if (descriptor === undefined) { - return; - } + if (descriptor === undefined) { + return; + } - const proto = (() => { - switch (context.kind) { - case 'method': - return descriptor.value as object; - case 'accessor': - return descriptor as object; - case 'setter': - return descriptor.set as object; - case 'getter': - return descriptor.get as object; - default: - return undefined; - } - })(); - - if (proto !== undefined && proto !== target) { - registerAddress(constructor, proto, memberAddress); + const proto = (() => { + switch (context.kind) { + case 'method': + return descriptor.value as object; + case 'accessor': + return descriptor as object; + case 'setter': + return descriptor.set as object; + case 'getter': + return descriptor.get as object; + default: + return undefined; } + })(); + + if (proto !== undefined && proto !== target) { + registerAddress(constructor, proto, memberAddress); } }); }; From 84285f83858be0db13c492e7330b533c29f4b455 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 13:28:43 +0200 Subject: [PATCH 47/59] Fix typo --- packages/support/src/meta/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 1a5da66d..394249a4 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -95,7 +95,7 @@ export function meta(keyOrCallback: Key | MetaCallback, value?: unknown) // This will enable meta lookups, using the member directly. registerAddress(constructor, target as object, memberAddress); - // To ensure that meta is still available vai a member reference directly, even when overridden + // To ensure that meta is still available via a member reference directly, even when overridden // in a child class, we register the address again, using member obtained from a property descriptor. // NOTE: This sadly DOES NOT work for overridden static members (no late static binding of `this`)! From 0b57e82dc6ce17ff721e839ca45e99e86da6b4b6 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:15:58 +0200 Subject: [PATCH 48/59] Add inheritMeta decorator --- packages/support/src/meta/index.ts | 1 + packages/support/src/meta/inheritMeta.ts | 62 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 packages/support/src/meta/inheritMeta.ts diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 6ec12c5d..bf915265 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -12,6 +12,7 @@ export * from './findRepository.js'; export * from './getOrCreateBaseRepository.js'; export * from './getOrCreateOwnerContext.js'; export * from './getOrCreateRepository.js'; +export * from './inheritMeta.js'; export * from './meta.js'; export * from './registerAddress.js'; // export * from './registries.js'; // internal diff --git a/packages/support/src/meta/inheritMeta.ts b/packages/support/src/meta/inheritMeta.ts new file mode 100644 index 00000000..9dcacb12 --- /dev/null +++ b/packages/support/src/meta/inheritMeta.ts @@ -0,0 +1,62 @@ +import { type ConstructorLike} from "@aedart/contracts"; +import { findOrCreateMemberAddress } from "./findOrCreateMemberAddress.js"; +import { registerAddress } from "./registerAddress.js"; +import Metadata from "./Metadata.js"; +import { getNameOrDesc } from "../reflections/getNameOrDesc.js"; + +/** + * Inherits metadata for a static overridden class member. + * + * If target does not have metadata, then this decorator does nothing. + * + * @returns {(target: unknown, context: (ClassDecoratorContext | ClassMemberDecoratorContext)) => void} + * + * @throws {TypeError} If target is a class, or if target is not static + */ +export function inheritMeta() +{ + return function( + target: unknown, + context: ClassDecoratorContext | ClassMemberDecoratorContext, + ): void + { + // Abort if decorator used directly on class. + if (context.kind === 'class') { + throw new TypeError(`Invalid target class "${getNameOrDesc(target as ConstructorLike)} for inheritMeta(). Only static class members can inherit meta!"`); + } + + // Abort if the member is NOT static. + if (!context.static) { + throw new TypeError(`Invalid target ${context.kind} "${String(context.name)}" for inheritMeta(). Member is not static!`); + } + + // Find or create a member address for the target (without "owner context") + const memberAddress = findOrCreateMemberAddress(target, context); + + // Using context.addInitializer, we determine the "owner" of the taget and attempt + // to find if there is metadata defined for the target, in the inheritance chain. + context.addInitializer(function(this: unknown) { + + const constructor = context.static + ? this as ConstructorLike + : ((this as object).constructor + ?? (Object.getPrototypeOf(this) as object | undefined)?.constructor) as + | ConstructorLike + | undefined; + + // Skip if there isn't a constructor available, or if of "field" kind (taget is `undefined`). + if (!constructor || context.kind === 'field') { + return; + } + + // Skip if there is no metadata defined for the member. + if (!Metadata.has(constructor, memberAddress.basePath)) { + return; + } + + // At this point, we know there is metadata for the member. Therefore, we simply register + // the address in the same way as in the `@meta` decorator. + registerAddress(constructor, target as object, memberAddress); + }); + } +} \ No newline at end of file From 5b6c652bbcc9e9bdd622ac4423f27eeb35415d45 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:16:30 +0200 Subject: [PATCH 49/59] Add "fail" tests for inheritMeta decorator Remaining tests are located in the "meta static members" tests. --- .../browser/support/meta/inheritMeta.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/browser/support/meta/inheritMeta.test.ts diff --git a/tests/browser/support/meta/inheritMeta.test.ts b/tests/browser/support/meta/inheritMeta.test.ts new file mode 100644 index 00000000..522f655f --- /dev/null +++ b/tests/browser/support/meta/inheritMeta.test.ts @@ -0,0 +1,32 @@ +import { inheritMeta } from '@aedart/support/meta'; +import { describe, expect, test } from 'vitest'; + +describe('@inheritMeta() decorator', () => { + test('fails when used on class', () => { + const trigger = () => { + + // Not intended to be used on a class... + @inheritMeta() + class A + {} + } + + expect(trigger).toThrow(); + }); + + test('fails when used on non-static members', () => { + const trigger = () => { + class A + { + // Not intended to be used on non-static member + @inheritMeta() + foo() + { /* empty */ } + } + } + + expect(trigger).toThrow(); + }); + + // NOTE: See additional tests for @inheritMeta() in `meta-static-members.test.ts`! +}); From ad962f28911bafacd795298560204e02034db709 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:18:59 +0200 Subject: [PATCH 50/59] Add tests for overridden static members and use of @inheritMeta() --- .../support/meta/meta-static-members.test.ts | 298 ++++++++++++------ 1 file changed, 194 insertions(+), 104 deletions(-) diff --git a/tests/browser/support/meta/meta-static-members.test.ts b/tests/browser/support/meta/meta-static-members.test.ts index f2803f79..cbf4b7f1 100644 --- a/tests/browser/support/meta/meta-static-members.test.ts +++ b/tests/browser/support/meta/meta-static-members.test.ts @@ -1,4 +1,4 @@ -import { meta, Metadata } from '@aedart/support/meta'; +import { meta, Metadata, inheritMeta } from '@aedart/support/meta'; import { getClassPropertyDescriptors } from '@aedart/support/reflections'; import { describe, expect, test } from 'vitest'; @@ -34,29 +34,40 @@ describe('@meta() decorator (static members)', () => { class BetaService extends AlphaService {} - // TODO: WIP - // class GammaService extends BetaService - // { - // // Note: meta is sadly NOT automatically inherited for overridden static members. - // // This is because decorator's context.addInitializer() do not offer a late static - // // binding for `this` (this = BaseService in this case). For this reason, the only - // // way to ensure inheritance, is via the custom `@inheritMeta()`! - // - // static override myField = 'cda'; - // - // static override accessor value = 51; - // - // static override get name(): string { - // return this._name; - // } - // - // static override set name(n: string) { - // this._name = n; - // } - // - // static override play() - // {/* empty */} - // } + class GammaService extends BetaService + { + // Note: meta is sadly NOT automatically inherited for overridden static members. + // This is because decorator's context.addInitializer() do not offer a late static + // binding for `this` (this = BaseService in this case). For this reason, the only + // way to obtain inherited metadata via a static member reference, is via the + // @inheritMeta() decorator. + + @inheritMeta() // NOTE: Has NO effect for field type, because "target" is undefined in decorator! + static override myField = 'cda'; + + @inheritMeta() + static override accessor value = 51; + + @inheritMeta() + static override get name(): string { + return this._name; + } + + @inheritMeta() + static override set name(n: string) { + this._name = n; + } + + @inheritMeta() + static override play() + {/* empty */} + } + + class SierraService extends GammaService + {} + + class TangoService extends SierraService + {} test('can get meta using static member reference', () => { // --------------------------------------------------------------------------------------------------- // @@ -219,84 +230,163 @@ describe('@meta() decorator (static members)', () => { .toBe('bar'); }); - // TODO: WIP - // test('inherits meta using overridden static member reference (via @inheritMeta decorator)', () => { - // - // // --------------------------------------------------------------------------------------------------- // - // // Field: Same as previous tests... - // expect(Metadata.get(GammaService, 'static.fields.myField.wip'), 'Unable to get meta for field') - // .toBe('wap'); - // - // // --------------------------------------------------------------------------------------------------- // - // // Get the class descriptors, just like in the previous test - // const overriddenDescriptors = getClassPropertyDescriptors( - // GammaService, - // true, - // false - // ); - // - // // Debug - // console.log('descriptors (overridden)', overriddenDescriptors); - // - // // --------------------------------------------------------------------------------------------------- // - // // Accessor - // - // const valueDescriptor = overriddenDescriptors.value; - // - // expect( - // Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Unable to determine if has meta for accessor (get)', - // ) - // .toBe(true); - // expect( - // Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Incorrect meta value for accessor (get)', - // ) - // .toBe('fup'); - // expect( - // Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Unable to determine if has meta for accessor (set)', - // ) - // .toBe(true); - // expect( - // Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Incorrect meta value for accessor (set)', - // ) - // .toBe('fup'); - // - // // --------------------------------------------------------------------------------------------------- // - // // Getter / Setter - // - // const nameDescriptor = overriddenDescriptors.name; - // - // expect( - // Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Unable to determine if has meta for getter', - // ) - // .toBe(true); - // expect( - // Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Incorrect meta value for getter', - // ) - // .toBe('zar'); - // expect( - // Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Unable to determine if has meta for setter', - // ) - // .toBe(true); - // expect( - // Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method - // 'Incorrect meta value for setter', - // ) - // .toBe('fin'); - // - // // --------------------------------------------------------------------------------------------------- // - // // Method: This should be straight forward... - // - // const { play } = GammaService; // eslint-disable-line @typescript-eslint/unbound-method - // expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') - // .toBe(true); - // expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') - // .toBe('bar'); - // }); + test('inherits meta using overridden static member reference (via @inheritMeta())', () => { + + // --------------------------------------------------------------------------------------------------- // + // Field: Same as previous tests... + expect(Metadata.get(GammaService, 'static.fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Get the class descriptors, just like in the previous test + const overriddenDescriptors = getClassPropertyDescriptors( + GammaService, + true, + false + ); + + // Debug + // console.log('descriptors (overridden)', overriddenDescriptors); + + // --------------------------------------------------------------------------------------------------- // + // Accessor + + const valueDescriptor = overriddenDescriptors.value; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) + .toBe('fup'); + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter + + const nameDescriptor = overriddenDescriptors.name; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) + .toBe('zar'); + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = GammaService; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); + + test('inherits meta using overridden static member reference (deep inheritance chain)', () => { + + // --------------------------------------------------------------------------------------------------- // + // Field: Same as previous tests... + expect(Metadata.get(TangoService, 'static.fields.myField.wip'), 'Unable to get meta for field') + .toBe('wap'); + + // --------------------------------------------------------------------------------------------------- // + // Get the class descriptors, just like in the previous test + const overriddenDescriptors = getClassPropertyDescriptors( + TangoService, + true, + false + ); + + // Debug + // console.log('descriptors (overridden)', overriddenDescriptors); + + // --------------------------------------------------------------------------------------------------- // + // Accessor + + const valueDescriptor = overriddenDescriptors.value; + + expect( + Metadata.has(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (get)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.get as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (get)', + ) + .toBe('fup'); + expect( + Metadata.has(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for accessor (set)', + ) + .toBe(true); + expect( + Metadata.get(valueDescriptor.set as object, 'fip'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for accessor (set)', + ) + .toBe('fup'); + + // --------------------------------------------------------------------------------------------------- // + // Getter / Setter + + const nameDescriptor = overriddenDescriptors.name; + + expect( + Metadata.has(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for getter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'get_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for getter', + ) + .toBe('zar'); + expect( + Metadata.has(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Unable to determine if has meta for setter', + ) + .toBe(true); + expect( + Metadata.get(nameDescriptor.get as object, 'set_name'), // eslint-disable-line @typescript-eslint/unbound-method + 'Incorrect meta value for setter', + ) + .toBe('fin'); + + // --------------------------------------------------------------------------------------------------- // + // Method: This should be straight forward... + + const { play } = TangoService; // eslint-disable-line @typescript-eslint/unbound-method + expect(Metadata.has(play, 'foo'), 'Unable to determine if has meta for instance method') + .toBe(true); + expect(Metadata.get(play, 'foo'), 'Incorrect meta value for instance method') + .toBe('bar'); + }); }); From 3de53f704621916d029e041782d6009e8d904a58 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:21:15 +0200 Subject: [PATCH 51/59] Fix lint and formatting --- packages/support/src/meta/inheritMeta.ts | 44 +++++++++++-------- .../browser/support/meta/inheritMeta.test.ts | 13 +++--- .../support/meta/meta-static-members.test.ts | 26 ++++++----- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/support/src/meta/inheritMeta.ts b/packages/support/src/meta/inheritMeta.ts index 9dcacb12..9bef4c8f 100644 --- a/packages/support/src/meta/inheritMeta.ts +++ b/packages/support/src/meta/inheritMeta.ts @@ -1,16 +1,16 @@ -import { type ConstructorLike} from "@aedart/contracts"; -import { findOrCreateMemberAddress } from "./findOrCreateMemberAddress.js"; -import { registerAddress } from "./registerAddress.js"; -import Metadata from "./Metadata.js"; -import { getNameOrDesc } from "../reflections/getNameOrDesc.js"; +import { type ConstructorLike } from '@aedart/contracts'; +import { getNameOrDesc } from '../reflections/getNameOrDesc.js'; +import { findOrCreateMemberAddress } from './findOrCreateMemberAddress.js'; +import Metadata from './Metadata.js'; +import { registerAddress } from './registerAddress.js'; /** * Inherits metadata for a static overridden class member. * * If target does not have metadata, then this decorator does nothing. - * + * * @returns {(target: unknown, context: (ClassDecoratorContext | ClassMemberDecoratorContext)) => void} - * + * * @throws {TypeError} If target is a class, or if target is not static */ export function inheritMeta() @@ -22,27 +22,35 @@ export function inheritMeta() { // Abort if decorator used directly on class. if (context.kind === 'class') { - throw new TypeError(`Invalid target class "${getNameOrDesc(target as ConstructorLike)} for inheritMeta(). Only static class members can inherit meta!"`); + throw new TypeError( + `Invalid target class "${ + getNameOrDesc(target as ConstructorLike) + } for inheritMeta(). Only static class members can inherit meta!"`, + ); } - + // Abort if the member is NOT static. if (!context.static) { - throw new TypeError(`Invalid target ${context.kind} "${String(context.name)}" for inheritMeta(). Member is not static!`); + throw new TypeError( + `Invalid target ${context.kind} "${ + String(context.name) + }" for inheritMeta(). Member is not static!`, + ); } - + // Find or create a member address for the target (without "owner context") const memberAddress = findOrCreateMemberAddress(target, context); - + // Using context.addInitializer, we determine the "owner" of the taget and attempt // to find if there is metadata defined for the target, in the inheritance chain. - context.addInitializer(function(this: unknown) { - + context.addInitializer(function(this: unknown) + { const constructor = context.static ? this as ConstructorLike : ((this as object).constructor ?? (Object.getPrototypeOf(this) as object | undefined)?.constructor) as - | ConstructorLike - | undefined; + | ConstructorLike + | undefined; // Skip if there isn't a constructor available, or if of "field" kind (taget is `undefined`). if (!constructor || context.kind === 'field') { @@ -58,5 +66,5 @@ export function inheritMeta() // the address in the same way as in the `@meta` decorator. registerAddress(constructor, target as object, memberAddress); }); - } -} \ No newline at end of file + }; +} diff --git a/tests/browser/support/meta/inheritMeta.test.ts b/tests/browser/support/meta/inheritMeta.test.ts index 522f655f..ed2191c3 100644 --- a/tests/browser/support/meta/inheritMeta.test.ts +++ b/tests/browser/support/meta/inheritMeta.test.ts @@ -4,29 +4,30 @@ import { describe, expect, test } from 'vitest'; describe('@inheritMeta() decorator', () => { test('fails when used on class', () => { const trigger = () => { - // Not intended to be used on a class... @inheritMeta() + // eslint-disable-next-line @typescript-eslint/no-unused-vars class A {} - } + }; expect(trigger).toThrow(); }); - + test('fails when used on non-static members', () => { const trigger = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars class A { // Not intended to be used on non-static member @inheritMeta() foo() - { /* empty */ } + {/* empty */} } - } + }; expect(trigger).toThrow(); }); - + // NOTE: See additional tests for @inheritMeta() in `meta-static-members.test.ts`! }); diff --git a/tests/browser/support/meta/meta-static-members.test.ts b/tests/browser/support/meta/meta-static-members.test.ts index cbf4b7f1..8e03a606 100644 --- a/tests/browser/support/meta/meta-static-members.test.ts +++ b/tests/browser/support/meta/meta-static-members.test.ts @@ -1,4 +1,4 @@ -import { meta, Metadata, inheritMeta } from '@aedart/support/meta'; +import { inheritMeta, meta, Metadata } from '@aedart/support/meta'; import { getClassPropertyDescriptors } from '@aedart/support/reflections'; import { describe, expect, test } from 'vitest'; @@ -39,9 +39,9 @@ describe('@meta() decorator (static members)', () => { // Note: meta is sadly NOT automatically inherited for overridden static members. // This is because decorator's context.addInitializer() do not offer a late static // binding for `this` (this = BaseService in this case). For this reason, the only - // way to obtain inherited metadata via a static member reference, is via the + // way to obtain inherited metadata via a static member reference, is via the // @inheritMeta() decorator. - + @inheritMeta() // NOTE: Has NO effect for field type, because "target" is undefined in decorator! static override myField = 'cda'; @@ -62,10 +62,10 @@ describe('@meta() decorator (static members)', () => { static override play() {/* empty */} } - + class SierraService extends GammaService {} - + class TangoService extends SierraService {} @@ -231,10 +231,12 @@ describe('@meta() decorator (static members)', () => { }); test('inherits meta using overridden static member reference (via @inheritMeta())', () => { - // --------------------------------------------------------------------------------------------------- // // Field: Same as previous tests... - expect(Metadata.get(GammaService, 'static.fields.myField.wip'), 'Unable to get meta for field') + expect( + Metadata.get(GammaService, 'static.fields.myField.wip'), + 'Unable to get meta for field', + ) .toBe('wap'); // --------------------------------------------------------------------------------------------------- // @@ -242,7 +244,7 @@ describe('@meta() decorator (static members)', () => { const overriddenDescriptors = getClassPropertyDescriptors( GammaService, true, - false + false, ); // Debug @@ -311,10 +313,12 @@ describe('@meta() decorator (static members)', () => { }); test('inherits meta using overridden static member reference (deep inheritance chain)', () => { - // --------------------------------------------------------------------------------------------------- // // Field: Same as previous tests... - expect(Metadata.get(TangoService, 'static.fields.myField.wip'), 'Unable to get meta for field') + expect( + Metadata.get(TangoService, 'static.fields.myField.wip'), + 'Unable to get meta for field', + ) .toBe('wap'); // --------------------------------------------------------------------------------------------------- // @@ -322,7 +326,7 @@ describe('@meta() decorator (static members)', () => { const overriddenDescriptors = getClassPropertyDescriptors( TangoService, true, - false + false, ); // Debug From 5d909e7debc3fc55316cb8491127bf8c04cf8cb7 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:45:33 +0200 Subject: [PATCH 52/59] Change release notes --- CHANGELOG_CLEAN_SLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG_CLEAN_SLATE.md b/CHANGELOG_CLEAN_SLATE.md index aa7579a1..4e11225b 100644 --- a/CHANGELOG_CLEAN_SLATE.md +++ b/CHANGELOG_CLEAN_SLATE.md @@ -18,6 +18,7 @@ Temporary changelog for feature branch. * `scripts/fix-esm-imports.js` util for fixing ESM imports. * `toParts()` util function (_support package_). * `isException()` util (_support package_). +* `@inheritMeta()` decorator (_support package_). ### Changed From c49045810616f5e4b8a063d894e9ed6f7503360f Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:47:48 +0200 Subject: [PATCH 53/59] Fix multiple pnpm versions specified --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 298f1fb6..ce8f3c81 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,7 +27,7 @@ jobs: - name: 'Install pnpm' uses: pnpm/action-setup@v4 with: - version: 9 + version: 11 # 2. Setup Node 24 (No pnpm cache here to ensure a fresh fetch) - name: 'Use Node ${{ matrix.node }}' From fe6b2b2528b6aad1dc48e4d090ac654f0a6c76ea Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:49:59 +0200 Subject: [PATCH 54/59] (Re)fix multiple pnpm versions specified The "version" should not be needed, when the package.json has "packageManager" specified with version specified. --- .github/workflows/tests.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ce8f3c81..6fe9bd23 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -26,8 +26,6 @@ jobs: # 1. Setup pnpm - name: 'Install pnpm' uses: pnpm/action-setup@v4 - with: - version: 11 # 2. Setup Node 24 (No pnpm cache here to ensure a fresh fetch) - name: 'Use Node ${{ matrix.node }}' From 9733cf69c0e93a79f8c14cbd78fa6f3f5549d380 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:53:37 +0200 Subject: [PATCH 55/59] Upgrade actions from version 4 to 6 --- .github/workflows/tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6fe9bd23..d19e0d8e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,15 +21,15 @@ jobs: steps: - name: 'Checkout' - uses: actions/checkout@v4 + uses: actions/checkout@v6 # 1. Setup pnpm - name: 'Install pnpm' - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 # 2. Setup Node 24 (No pnpm cache here to ensure a fresh fetch) - name: 'Use Node ${{ matrix.node }}' - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} From 8fd0a8fd8b14579acca48811583065236b27b555 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 14:58:00 +0200 Subject: [PATCH 56/59] Fix unknown option --prefer-latest --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d19e0d8e..bc2e4cf6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -36,7 +36,7 @@ jobs: # 3. Force Fresh Dependency Resolution # We ignore the lockfile and prefer the latest versions in range. - name: 'Install latest dependencies' - run: pnpm install --no-frozen-lockfile --prefer-latest + run: pnpm install --no-frozen-lockfile # 4. Install Playwright Browsers (Required for Vitest) - name: 'Install Playwright Browsers' From e116058a4dc51602a2a5e31aacd07c8096dfcea0 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 15:03:05 +0200 Subject: [PATCH 57/59] Change pipeline order Unsure if lint command fails because of missing builds. --- .github/workflows/tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index bc2e4cf6..4769d465 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -43,6 +43,9 @@ jobs: run: pnpm exec playwright install --with-deps chromium firefox # 5. Run the Pipeline + - name: 'Build' + run: pnpm build + - name: 'Check Formatting' run: pnpm format @@ -52,8 +55,5 @@ jobs: - name: 'Type Check' run: pnpm type-check - - name: 'Build' - run: pnpm build - - name: 'Test (Node & Headless Browsers)' run: pnpm test From f47c88a98e5b681648e1c077f19ddeadd5f2311d Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 15:10:12 +0200 Subject: [PATCH 58/59] Surpress type errors in mixin tests --- tests/browser/support/mixins/decorators/Cached.test.ts | 8 ++++++++ tests/browser/support/mixins/decorators/DeDupe.test.ts | 3 +++ .../browser/support/mixins/decorators/HasInstance.test.ts | 7 +++++++ tests/browser/support/mixins/decorators/Mixin.test.ts | 3 +++ 4 files changed, 21 insertions(+) diff --git a/tests/browser/support/mixins/decorators/Cached.test.ts b/tests/browser/support/mixins/decorators/Cached.test.ts index 22e99104..72e8831c 100644 --- a/tests/browser/support/mixins/decorators/Cached.test.ts +++ b/tests/browser/support/mixins/decorators/Cached.test.ts @@ -12,10 +12,14 @@ describe('@aedart/support/mixins', () => { const MyMixinA = Cached(Bare((superclass) => { aApplied++; + + // @ts-expect-error Ignore superclass type here. return class extends superclass {}; })); const MyMixinB = Cached(Bare((superclass) => { bApplied++; + + // @ts-expect-error Ignore superclass type here. return class extends superclass {}; })); @@ -23,8 +27,12 @@ describe('@aedart/support/mixins', () => { {} // Notice that the same mixins are applied on the same superclass. + + // @ts-expect-error Ignore MyMixin return type here. class B extends MyMixinA(MyMixinB(A)) {} + + // @ts-expect-error Ignore MyMixin return type here. class C extends MyMixinA(MyMixinB(A)) {} diff --git a/tests/browser/support/mixins/decorators/DeDupe.test.ts b/tests/browser/support/mixins/decorators/DeDupe.test.ts index 948e5311..cda1e3c5 100644 --- a/tests/browser/support/mixins/decorators/DeDupe.test.ts +++ b/tests/browser/support/mixins/decorators/DeDupe.test.ts @@ -8,9 +8,12 @@ describe('@aedart/support/mixins', () => { let applied = 0; const MyMixin = DeDupe(Bare((superclass) => { applied++; + + // @ts-expect-error Ignore superclas type here. return class extends superclass {}; })); + // @ts-expect-error Ignore MyMixin return type here. class A extends MyMixin( MyMixin( MyMixin(class {}), diff --git a/tests/browser/support/mixins/decorators/HasInstance.test.ts b/tests/browser/support/mixins/decorators/HasInstance.test.ts index 022913ae..888a94fa 100644 --- a/tests/browser/support/mixins/decorators/HasInstance.test.ts +++ b/tests/browser/support/mixins/decorators/HasInstance.test.ts @@ -5,10 +5,13 @@ describe('@aedart/support/mixins', () => { describe('decorators', () => { describe('HasInstance', () => { test('respects class inheritance ', () => { + // @ts-expect-error Ignore superclass type here. const MyMixin = HasInstance((superclass) => class extends superclass {}); class A {} + + // @ts-expect-error Ignore MyMixin return type here. class B extends MyMixin(A) {} @@ -25,10 +28,14 @@ describe('@aedart/support/mixins', () => { test('can determine if instance of mixin', () => { // NOTE: The Bare decorator MUST also be applied here, or instance of [mixin] will not work as intended! + + // @ts-expect-error Ignore superclass type here. const MyMixin = HasInstance(Bare((superclass) => class extends superclass {})); class A {} + + // @ts-expect-error Ignore MyMixin return type here. class B extends MyMixin(A) {} diff --git a/tests/browser/support/mixins/decorators/Mixin.test.ts b/tests/browser/support/mixins/decorators/Mixin.test.ts index c885e96f..9d5eb868 100644 --- a/tests/browser/support/mixins/decorators/Mixin.test.ts +++ b/tests/browser/support/mixins/decorators/Mixin.test.ts @@ -8,10 +8,13 @@ describe('@aedart/support/mixins', () => { // It is a bit redundant to test all of "Mixin" decorator's applied decorators. // So here we just ensure that the top-level functionality works as intended. + // @ts-expect-error Ignore superclass type here. const MyMixin = Mixin((superclass) => class extends superclass {}); class A {} + + // @ts-expect-error Ignore MyMixin type here. class B extends MyMixin(A) {} From f17007ccf2922a84838e7ca79a8f8bfa8f41c225 Mon Sep 17 00:00:00 2001 From: aedart Date: Sun, 17 May 2026 15:10:36 +0200 Subject: [PATCH 59/59] Move type-check into QC section --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a48aa59e..4777bf3c 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,13 @@ "--- [ Development & Docs ] ---": "echo", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", - "type-check": "turbo run type-check && tsc -p tsconfig.json --noEmit", "--- [ Quality Control (Lint/Format/Test) ] ---": "echo", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "dprint check", "format:fix": "dprint fmt", "fix:all": "npm run format:fix && npm run lint:fix && npm run fix:imports", + "type-check": "turbo run type-check && tsc -p tsconfig.json --noEmit", "test": "vitest run", "test:watch": "vitest", "--- [ Build & Release ] ---": "echo",