Skip to content

Commit

Permalink
Merge pull request #4 from aedart/rework-reflections
Browse files Browse the repository at this point in the history
Rework reflections
  • Loading branch information
aedart authored Feb 2, 2024
2 parents 211e3ea + 6afbfaf commit 44928b2
Show file tree
Hide file tree
Showing 72 changed files with 20,685 additions and 19,636 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
fail-fast: true
matrix:
os: [ 'ubuntu-22.04' ]
node: [ '18' ]
node: [ '20' ]

steps:

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.idea/
Thumbs.db
temp/*
.nx

yarn.lock
lerna-debug.log
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

* `targetMeta()`, `inheritTargetMeta()` and `getTargetMeta()` utils method in `@aedart/support/meta`.
* `isConstructor()` util method in `@aedart/support/reflections`.
* `isCallable()` and `isClassConstructor()` util methods in `@aedart/support/reflections` (_Unsafe / unstable!_).
* `Kind` enum which contains cases of the kind of element that is being decorated (_defined in a decorator context object_).
* `Arrayable` interface, in `@aedart/contracts/support` submodule.
* `toWeakRef()` util method in `@aedart/support/misc`.
* `mergeKets()` util method in `@aedart/support/misc`.
* `isKey()` util method in `@aedart/support/misc`.
* `isPropertyKey()` util method in `@aedart/support/misc`.
* Documentation for `uniqueId()` and `hasUniqueId()` util methods, in the `@aedart/support/objects` package.

### Changed

**Breaking**

* Node `^v20.11.0` is now required when working with the ion mono-repository.
* Decorator `Context` is now an alias for TypeScript's `DecoratorContext` (_affects `@aedart/support/meta`_).
* Decorator `MetadataRecord` is now an alias for TypeScript's `DecoratorMetadata` (_affects `@aedart/support/meta`_).

**Non-Breaking**

* Bumped license year.
* Dependencies updated (_service update_).
* Refactored internal `save()` method to no longer attempt to overwrite `context.metadata` because it has been defined as read-only property by TypeScript.
* JSDoc now clearly states that `meta()` is intended to be used as a decorator.

### Removed

* Private `@aedart/reflections` package. Desired features added as a submodule in `@aedart/support` package.
* Experimental reflection components in `@aedart/support/reflections` submodule (_was never published_).
* `PropertyKey` from `@aedart/contacts/support` (_Replaced by TypeScript's own definition hereof_).

### Deprecated

* `ClassContext`, `MethodContext`, `GetterContext`, `SetterContext`, `FieldContext`, `AccessorContext` and `MetadataContext`, in `@aedart/contracts/support/meta` - replaced by corresponding TypeScript declarations and will be removed in next version.
* `MemberContext` in `@aedart/contracts/support/meta` (_no longer needed_).

### Fixed

* Docs broken due to out-of-date vuepress dependencies (_switched to `@vuepress` `v2.0.0-rc.2`_).
* `@vuepress/utils` not resolved by rollup during tests (_missing dependency, in the `@aedart/vuepress-utils` package_).

## [0.6.1] - 2023-04-28

### Security
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2023-2023 Alin Eugen Deac <[email protected]> . All rights reserved.
Copyright (c) 2023-2024 Alin Eugen Deac <[email protected]> . All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Expand Down
1 change: 1 addition & 0 deletions aliases.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
alias: {
// contracts
'@aedart/contracts/support/meta': path.resolve(__dirname, './packages/contracts/support/meta'),
'@aedart/contracts/support/reflections': path.resolve(__dirname, './packages/contracts/support/reflections'),
'@aedart/contracts/support': path.resolve(__dirname, './packages/contracts/support'),
'@aedart/contracts': path.resolve(__dirname, './packages/contracts/src'),

Expand Down
32 changes: 19 additions & 13 deletions babel.config.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
]
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"version": "2023-01"
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods",
"@babel/plugin-transform-class-static-block"
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-01" }],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
]
}
1 change: 1 addition & 0 deletions docs/.vuepress/archive/Version0x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default PagesCollection.make('v0.x', '/v0x', [
'packages/support/install',
'packages/support/meta',
'packages/support/objects',
'packages/support/reflections',
'packages/support/misc',
]
},
Expand Down
10 changes: 8 additions & 2 deletions docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {defaultTheme, defineUserConfig, Page} from 'vuepress';
import {defineUserConfig, Page} from 'vuepress';
import defaultTheme from "@vuepress/theme-default"
import { webpackBundler } from "@vuepress/bundler-webpack"
import {backToTopPlugin} from "@vuepress/plugin-back-to-top";
import {searchPlugin} from "@vuepress/plugin-search";
import {baseURL, prefixPath} from "@aedart/vuepress-utils";
Expand All @@ -16,6 +18,10 @@ const BASE_URL = baseURL('ion');
* Vuepress configuration for docs...
*/
export default defineUserConfig({
bundler: webpackBundler({
// N/A
}),

base: BASE_URL,
dest: './.build',
lang: 'en-GB',
Expand Down Expand Up @@ -71,7 +77,7 @@ export default defineUserConfig({
},

getExtraFields: (page: Page) => {
return [page.frontmatter.description] ?? [];
return [page.frontmatter.description];
},
}),

Expand Down
22 changes: 22 additions & 0 deletions docs/archive/current/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ _TBD: "To be decided"._

## `v0.x` Highlights

### "Target" Meta Decorator <Badge type="tip" text="Available since v0.7" />

Associate arbitrary metadata directly with the target element that is being decorated.
_See [target meta decorator](./packages/support/meta.md) fro additional details._

```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

class Service {

@targetMeta('desc', 'Seaches for cities')
search() {
// ...not shown...
}
}

const instance = new Service();

// ...later in your application...
getTargetMeta(instance.search, 'desc'); // Seaches for cities
```

### Meta Decorator <Badge type="tip" text="Available since v0.6" />

The [meta decorator](./packages/support/meta.md) is able to associate arbitrary metadata with a class and its elements.
Expand Down
183 changes: 180 additions & 3 deletions docs/archive/current/packages/support/meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ import {meta, getMeta} from '@aedart/support/meta';
@meta('service_alias', 'locationSearcher')
class Service {}

getMeta(Service, 'service_alias');
getMeta(Service, 'service_alias'); // locationSearcher
```

**Roughly "desugars" to the following:**
Expand All @@ -240,10 +240,187 @@ function meta(key, value) {
@meta('service_alias', 'locationSearcher')
class Service {}

Service[Symbol.metadata].service_alias;
Service[Symbol.metadata].service_alias; // locationSearcher
```
(_Above shown example is very simplified. Actual implementation is a bit more complex..._)

At present, the internal mechanisms of the `meta` decorator must rely on a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) to associate metadata with the intended class.
When the [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata) becomes more mature and transpilers offer the `context.metadata` object (_or when browsers support it_),
then this decorator will be updated respectfully to use the available metadata object.
then this decorator will be updated respectfully to use the available metadata object.

## Target Meta <Badge type="tip" text="Available since v0.7" vertical="middle" />

The `targetMeta()` decorator offers the ability to associate metadata directly with a class instance or class method reference.
This can be useful in situations when you do not know the class that owns the metadata.

Behind the scene, `targetMeta()` uses the `meta()` decorator and stores a reference to the target that is decorated inside a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap).

::: tip Supported Elements

Unlike the [`meta()` decorator](#supported-elements), `targetMeta()` only supports the following elements:

* `class`
* `method`

:::

**Example: class instance**

```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

@targetMeta('description', { type: 'Search Service', alias: 'Location Sercher' })
class LocationSearcherService {}

const instance = new LocationSearcherService();

// ...later in your application...
getTargetMeta(instance, 'description')?.type; // Search Service
```
**Example: method reference**
```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

class LocationSearcherService {

@targetMeta('dependencies', [ 'httpClient' ])
search(apiClient) {}
}

const instance = new LocationSearcherService();

// ...later in your application...
getTargetMeta(instance.search, 'dependencies'); // [ 'httpClient' ]
```
### Inheritance
Target meta is automatically inherited by subclasses and can also be overwritten, similar to that of the [`meta()` decorator](#inheritance).
**Example: classes**
```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

@meta('service_alias', 'locationSearcher')
class Service {}

class CitySearcher extends Service {}

const instance = new CitySearcher();

// ...later in your application...
getTargetMeta(instance, 'service_alias'); // locationSearcher
```
**Example: methods**
```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

class Service {

@targetMeta('dependencies', [ 'countrySearchApiClient' ])
search(apiClient) {
// ...not shown...
}
}

class CountrySearcher extends Service {
// ... not method overwrite here...
}

class CitySearcher extends Service {

@targetMeta('dependencies', [ 'citySearchApiClient' ])
search(apiClient) {
// ...not shown...
}
}

const instanceA = new Service();
const instanceB = new CountrySearcher();
const instanceC = new CitySearcher();

// ...later in your application...
getTargetMeta(instanceA.search, 'dependencies'); // [ 'countrySearchApiClient' ]
getTargetMeta(instanceB.search, 'dependencies'); // [ 'countrySearchApiClient' ]
getTargetMeta(instanceC.search, 'dependencies'); // [ 'citySearchApiClient' ]
```
#### Static Methods
Inheritance for static methods works a bit differently. By default, any subclass will automatically inherit target metadata, even for static methods.
However, if you overwrite the given static method, the metadata is lost.
::: tip Limitation
_When a static method is overwritten, the parent's "target" metadata cannot be obtained due to a general limitation of the `meta()` decorator.
The decorator has no late `this` binding available to the overwritten static method.
This makes it impossible to associate the overwritten static method with metadata from the parent._
:::
**Example: inheritance for static methods**
```js
import {targetMeta, getTargetMeta} from '@aedart/support/meta';

class Service {

@targetMeta('dependencies', [ 'xmlClient' ])
static search(client) {
// ...not shown...
}
}

class CountrySearcher extends Service {
// ... not method overwrite here...
}

class CitySearcher extends Service {

// Overwite of static method - target meta is lost
static search(client) {}
}

// ...later in your application...
getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ]
getTargetMeta(CitySearcher.search, 'dependencies'); // undefined
```
To overcome the above shown issue, you can use the `inheritTargetMeta()` decorator. It forces the static method to "copy" metadata from its parent, if available.
**Example: force inheritance for static methods**
```js
import {
targetMeta,
getTargetMeta,
inheritTargetMeta
} from '@aedart/support/meta';

class Service {

@targetMeta('dependencies', [ 'xmlClient' ])
static search(client) {
// ...not shown...
}
}

class CountrySearcher extends Service {
// ... not method overwrite here...
}

class CitySearcher extends Service {

@inheritTargetMeta()
static search(client) {}
}

// ...later in your application...
getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ]
getTargetMeta(CitySearcher.search, 'dependencies'); // [ 'xmlClient' ]
```
Loading

0 comments on commit 44928b2

Please sign in to comment.