diff --git a/.drone.jsonnet b/.drone.jsonnet index 8b33506..2a13019 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -19,9 +19,20 @@ local lint_step = { commands: ["bun lint:tsc"], }; -local generate_docker_tags_step = { - name: "generate-docker-tags", - image: "git.jvalver.de/thilawyn/drone-better-docker-autotag", +local build_step = { + name: "build", + image: bun_image, + commands: ["bun run build"], +}; + +local publish_step = { + name: "publish", + image: "plugins/npm", + + settings: { + registry: "https://git.jvalver.de/api/packages/jvalverde/npm", + token: { from_secret: "npm_token" }, + }, }; @@ -48,46 +59,45 @@ local generate_docker_tags_step = { ], }, - // Build the server and legacy API docker images without publishing them for pull requests - // { - // kind: "pipeline", - // type: "docker", - // name: "build-docker", + // Build the package without publishing for pull requests + { + kind: "pipeline", + type: "docker", + name: "build", - // trigger: { - // ref: { - // include: ["refs/pull/**"] - // } - // }, + trigger: { + ref: { + include: ["refs/pull/**"] + } + }, - // steps: [ - // fetch_step, - // generate_docker_tags_step, - // build_website_docker_step(false), - // build_legacy_api_docker_step(false), - // ], - // }, + steps: [ + install_step, + lint_step, + build_step, + ], + }, - // Build the server and legacy API docker images and publish them for master and tags - // { - // kind: "pipeline", - // type: "docker", - // name: "build-publish-docker", + // Build and publish the package for master and tags + { + kind: "pipeline", + type: "docker", + name: "build-publish", - // trigger: { - // ref: { - // include: [ - // "refs/heads/master", - // "refs/tags/**", - // ] - // } - // }, + trigger: { + ref: { + include: [ + "refs/heads/master", + "refs/tags/**", + ] + } + }, - // steps: [ - // fetch_step, - // generate_docker_tags_step, - // build_website_docker_step(true), - // build_legacy_api_docker_step(true), - // ], - // }, + steps: [ + install_step, + lint_step, + build_step, + publish_step, + ], + }, ] diff --git a/bun.lockb b/bun.lockb index ada479e..6908c42 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 54a9941..f4bf5e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thilatrait", - "version": "20231208.0.0", + "version": "20231229.0.0", "type": "module", "exports": { ".": { @@ -22,12 +22,16 @@ "clean:node": "rm -rf node_modules" }, "dependencies": { - "type-fest": "^4.8.3" + "type-fest": "^4.9.0" }, "devDependencies": { "bun-types": "latest", - "rollup": "^4.7.0", + "npm-check-updates": "^16.14.12", + "npm-sort": "^0.0.4", + "rollup": "^4.9.1", + "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-ts": "^3.4.5", + "tsx": "^4.7.0", "typescript": "^5.3.3" } } diff --git a/rollup.config.js b/rollup.config.js index 87e9671..da6b231 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,5 @@ import { defineConfig } from "rollup" +import cleanup from "rollup-plugin-cleanup" import ts from "rollup-plugin-ts" import pkg from "./package.json" assert { type: "json" } @@ -6,8 +7,6 @@ import pkg from "./package.json" assert { type: "json" } export default defineConfig({ input: "src/index.ts", - plugins: [ ts() ], - output: [ { file: pkg.exports["."].import.default, @@ -19,4 +18,13 @@ export default defineConfig({ format: "cjs", }, ], + + plugins: [ + ts(), + + cleanup({ + comments: "jsdoc", + extensions: ["ts"], + }), + ], }) diff --git a/src/class.ts b/src/class.ts deleted file mode 100644 index 8cff9e9..0000000 --- a/src/class.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AbstractClass } from "type-fest" - - -/** - * Copies all own properties and methods (excluding "length" and "prototype") of one class to another class. - * - * @param from The class whose properties and methods are to be copied. - * @param to The class to which the properties and methods are to be copied. - */ -export function copyClassProperties( - from: AbstractClass, - to: AbstractClass, -) { - Object.getOwnPropertyNames(from).forEach(name => { - if (name === "length" - || name === "prototype" - ) - return - - Object.defineProperty( - to, - name, - Object.getOwnPropertyDescriptor(from, name) || Object.create(null), - ) - }) - - Object.getOwnPropertyNames(from.prototype).forEach(name => { - Object.defineProperty( - to.prototype, - name, - Object.getOwnPropertyDescriptor(from.prototype, name) || Object.create(null), - ) - }) -} - -export const flattenClass = < - C extends AbstractClass ->(class_: C) => - getInheritanceHierarchy(class_) - .reduce((flattened, current) => { - copyClassProperties(current, flattened) - return flattened - }, class {}) as C - -/** - * Returns an array of classes representing the inheritance hierarchy of a given class constructor. The array includes the given class constructor itself and all its parent classes in the order of inheritance. - * - * @param class_ The class constructor for which to generate the inheritance hierarchy. - * @returns An array of class constructors representing the inheritance hierarchy of `Class`. - */ -export function getInheritanceHierarchy( - class_: AbstractClass -): AbstractClass[] { - const parent = Object.getPrototypeOf(class_) - - return isClass(parent) - ? [...getInheritanceHierarchy(parent), class_] - : [class_] -} - -/** - * Determines whether a given value is a class constructor or not by checking if its `toString` method returns a string matching the pattern of a class definition. - * - * @param el The value to check for class constructor status. - * @returns A boolean indicating whether `el` is a class constructor or not. - */ -export const isClass = (el: { toString: () => string }) => - Boolean(el.toString().match(/^class(?: [.\S]+)?(?: extends [.\S]+)? {[\s\S]*}$/)) diff --git a/src/index.ts b/src/index.ts index 46c8360..fc70958 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,170 @@ -export * from "./class" -export * from "./trait" -export * from "./utils" +import { AbstractClass, AbstractConstructor, Opaque, UnionToIntersection } from "type-fest" + + +/** + * Represents the static members of a class. + * @template C - The class type. + */ +export type StaticMembers = Pick + + +/** + * Represents a trait that can be applied to a class. + * @template C - The abstract class type. + */ +export type Trait< + C extends AbstractClass +> = Opaque< + TraitApplier, + "thilatrait/Trait" +> + +/** + * Represents the function signature for applying a trait to a parent class. + * @template C - The abstract class type. + */ +export type TraitApplier< + C extends AbstractClass +> = + (Parent: AbstractConstructor) => C + +/** + * Unwraps the type of the class from a given trait. + * @template T - The trait type. + */ +export type UnwrapTraitC = + T extends Trait + ? C + : never + + +/** + * Creates a trait using the provided trait applier function. + * @template C - The abstract class type. + * @param applier - The trait applier function. + * @returns A trait. + * @example + * Creates a trait: + * ```ts + * const Permissible = trait(Parent => { + * abstract class Permissible extends Parent { + * static readonly defaultPermissions: string[] = [] + * permissions: string[] = [] + * + * constructor(...args: any[]) { + * super(...args) + * } + * } + * + * return Permissible + * }) + * ``` + * Creates a generic trait: + * ```ts + * const Identifiable = () => + * trait(Parent => { + * abstract class Identifiable extends Parent { + * abstract readonly id: ID + * + * equals(el: Identifiable) { + * return this.id === el.id + * } + * + * constructor(...args: any[]) { + * super(...args) + * } + * } + * + * return Identifiable + * }) + * ``` + */ +export function trait< + C extends AbstractClass +>( + applier: TraitApplier +) { + return applier as Trait +} + + +/** + * Extends a class with the given traits and expresses their combined functionality. + * @template C - The abstract class type. + * @template Traits - An array of traits. + * @param extend - The class to extend. + * @param traits - An array of traits to apply. + * @returns A new class type expressing the combined functionality of the base class and traits. + * @example + * Extends a superclass and applies traits: + * ```ts + * class User extends extendsAndExpresses(Entity, [Identifiable(), Permissible]) { + * readonly id: bigint + * + * constructor(id: bigint) { + * super() + * this.id = id + * } + * } + * ``` + */ +export function extendsAndExpresses< + C extends AbstractClass, + Traits extends readonly Trait[], +>( + extend: C, + traits: Traits, +) { + return traits.reduce( + (previous, trait) => trait(previous), + extend, + ) as ( + AbstractClass< + InstanceType & + UnionToIntersection< + InstanceType< + UnwrapTraitC< + Traits[number] + > + > + >, + + ConstructorParameters + > & + + StaticMembers & + StaticMembers< + UnionToIntersection< + UnwrapTraitC< + Traits[number] + > + > + > + ) +} + +/** + * Expresses the combined functionality of multiple traits. + * @template Traits - An array of trait. + * @param traits - An array of trait to apply. + * @returns A new class type expressing the combined functionality of the traits. + * @example + * Applies traits to a class: + * ```ts + * class User extends expresses(Identifiable(), Permissible) { + * readonly id: bigint + * + * constructor(id: bigint) { + * super() + * this.id = id + * } + * } + * ``` + */ +export function expresses< + Traits extends readonly Trait[], +>( + ...traits: Traits +) { + return extendsAndExpresses(Object, traits) +} diff --git a/src/legacy/tests.ts b/src/legacy/tests.ts new file mode 100644 index 0000000..e86528d --- /dev/null +++ b/src/legacy/tests.ts @@ -0,0 +1,74 @@ +import { AbstractClass } from "type-fest" +import { expresses } from "./trait" + + +function inspectClass(class_: AbstractClass) { + Object.getOwnPropertyNames(class_).forEach(name => { + console.log( + "[static]", + name, + Object.getOwnPropertyDescriptor(class_, name) + ) + }) + + Object.getOwnPropertyNames(class_.prototype).forEach(name => { + console.log( + "[prototype]", + name, + Object.getOwnPropertyDescriptor(class_.prototype, name) + ) + }) +} + + +abstract class Identified { + abstract id: ID + + equals(el: Identified) { + return this.id === el.id + } + + // initializer() { + // console.log("Identified initializer") + // } +} + +class ImplementsIdentifiable extends Identified { + id!: ID +} + + +abstract class Permissible { + static readonly defaultPermissions: string[] = [] + permissions: string[] = [] + // permissions!: string[] + + constructor() { + console.log("Permissible constructor") + } + + initializer() { + console.log("Permissible initializer") + this.permissions = [] + } +} + + +class User extends expresses( + Identified as typeof Identified, + // Identified, + Permissible, +) { + readonly id: bigint + + constructor(id: bigint) { + super() + this.id = id + } +} + +const user1 = new User(BigInt(1)) +const user2 = new User(BigInt(2)) + +console.log(user1) +console.log(user1.equals(user2)) diff --git a/src/legacy/trait.ts b/src/legacy/trait.ts new file mode 100644 index 0000000..10cc87b --- /dev/null +++ b/src/legacy/trait.ts @@ -0,0 +1,126 @@ +import { AbstractClass, Class, UnionToIntersection } from "type-fest" +import { StaticMembers, copyProperties, getInheritanceHierarchy } from "./util/class" + + +/** + * Represents a trait that can be used to define common behavior + * for classes and abstract classes. + * @typeParam T - The type of the trait. + */ +export type Trait = + AbstractClass + + +/** + * Creates a link class that expresses the given traits. + * @param traits - An array of traits to be expressed by the link class. + * @returns A dynamically created class that expresses the given traits. + * @typeParam Traits - An array of traits that the link class expresses. + */ +export function expresses< + Traits extends readonly Trait[] +>( + ...traits: Traits +) { + return makeLinkClass(traits) +} + + +/** + * Creates a link class that extends a base class and expresses the given traits. + * @param extend - The base class or abstract class to extend. + * @param traits - An array of traits to be expressed by the link class. + * @returns A dynamically created class that extends the given base class and expresses the given traits. + * @typeParam C - The type of the base class to extend. + * @typeParam Traits - An array of traits that the link class expresses. + */ +export function extendsAndExpresses< + C extends Class + | AbstractClass, + Traits extends readonly Trait[], +>( + extend: C, + ...traits: Traits +) { + return makeLinkClass(traits, extend) +} + + +/** + * Creates a link class that expresses the given traits and optionally extends a base class. + * @param traits - An array of traits to be expressed by the link class. + * @param extend - The base class or abstract class to extend (optional). + * @returns A dynamically created class that expresses the given traits and extends the base class. + * @typeParam Traits - An array of traits that the link class expresses. + * @typeParam C - The type of the base class to extend (optional). + */ +export function makeLinkClass< + Traits extends readonly Trait[], + C extends Class + | AbstractClass + | undefined = undefined, +>( + traits: Traits, + extend?: C, +) { + const class_ = extend + ? class extends extend { + constructor(...args: any[]) { + super(...args) + + traits.forEach(trait => { + trait.prototype.initializer?.call(this) + }) + } + } + : class { + constructor() { + traits.forEach(trait => { + trait.prototype.initializer?.call(this) + }) + } + } + + traits.forEach(trait => { + getInheritanceHierarchy(trait).forEach(current => { + copyProperties( + current, + class_, + ["name", "length"], + ["constructor"], + ) + }) + }) + + return class_ as unknown as ( + (C extends Class | AbstractClass + ? ( + AbstractClass< + InstanceType & + UnionToIntersection< + InstanceType< + Traits[number] + > + >, + + ConstructorParameters + > & + + StaticMembers + ) + : Trait< + UnionToIntersection< + InstanceType< + Traits[number] + > + > + > + ) & + + StaticMembers< + UnionToIntersection< + Traits[number] + > + > + ) +} diff --git a/src/legacy/util/class.ts b/src/legacy/util/class.ts new file mode 100644 index 0000000..518c960 --- /dev/null +++ b/src/legacy/util/class.ts @@ -0,0 +1,109 @@ +import { AbstractClass } from "type-fest" + + +/** + * Represents the static members of a class. + * + * @template C - The type of the class for which static members are extracted. + * @typeparam The static members of the class. + */ +export type StaticMembers = Pick + + +/** + * Flattens the inheritance hierarchy of a given class by copying all properties + * from its superclass chain into a single object. + * + * @template C - The type of the class to be flattened, extending AbstractClass. + * @param C - The class to be flattened. + * @returns A new class with properties flattened from the entire inheritance hierarchy. + */ +export function flattenClass< + C extends AbstractClass +>(class_: C) { + const flattenedClass = class {} as unknown as C + + getInheritanceHierarchy(class_).forEach(current => { + copyProperties(current, flattenedClass) + }) + + copyProperty(class_, flattenedClass, "name") + copyProperty(class_.prototype, flattenedClass.prototype, "constructor") + + return flattenedClass +} + + +/** + * Retrieves the inheritance hierarchy of a given class, including itself. + * + * @param class_ - The class for which the inheritance hierarchy is retrieved. + * @returns An array representing the inheritance hierarchy, ordered from the furthest in the hierarchy to `class_` itself. + */ +export function getInheritanceHierarchy( + class_: AbstractClass +): AbstractClass[] { + const parent = Object.getPrototypeOf(class_) + + return isClass(parent) + ? [...getInheritanceHierarchy(parent), class_] + : [class_] +} + + +/** + * Checks if a given element appears to be a class based on its string representation. + * + * @param el - The element to check for being a class. + * @returns `true` if the element is likely a class; otherwise, `false`. + */ +export function isClass(el: { toString: () => string }) { + return Boolean(el.toString().match(/^class(?: [.\S]+)?(?: extends [.\S]+)? {[\s\S]*}$/)) +} + + +/** + * Copies properties from one class to another, including static and prototype properties. + * + * @param from - The source class to copy properties from. + * @param to - The destination class to copy properties to. + */ +export function copyProperties( + from: AbstractClass, + to: AbstractClass, + ignoreKeys: string[] = [], + ignorePrototypeKeys: string[] = [], +) { + Object.getOwnPropertyNames(from).forEach(name => { + if (name === "prototype" + || ignoreKeys.find(v => v === name) + ) + return + + // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) + + copyProperty(from, to, name) + }) + + Object.getOwnPropertyNames(from.prototype).forEach(name => { + if (ignorePrototypeKeys.find(v => v === name)) + return + + // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) + + copyProperty(from.prototype, to.prototype, name) + }) +} + + +export function copyProperty( + from: unknown, + to: unknown, + name: string, +) { + Object.defineProperty( + to, + name, + Object.getOwnPropertyDescriptor(from, name) || Object.create(null), + ) +} diff --git a/src/tests.ts b/src/tests.ts index 4520fd6..37e9412 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,29 +1,62 @@ -import { mixTraits } from "./trait" +import { expresses, trait } from "." -abstract class Identified { - abstract id: ID +const Identifiable = () => + trait(Parent => { + abstract class Identifiable extends Parent { + abstract readonly id: ID - equals(el: Identified) { - return this.id === el.id + equals(el: Identifiable) { + return this.id === el.id + } + + constructor(...args: any[]) { + super(...args) + console.log("Identified constructor") + } + } + + return Identifiable + }) + +const ImplementsIdentifiable = (defaultID: ID) => + trait(Parent => { + abstract class ImplementsIdentifiable extends Identifiable()(Parent) { + id: ID = defaultID + } + + return ImplementsIdentifiable + }) + + +const Permissible = trait(Parent => { + abstract class Permissible extends Parent { + static readonly defaultPermissions: string[] = [] + permissions: string[] = [] + + constructor(...args: any[]) { + super(...args) + console.log("Permissible constructor") + } + } + + return Permissible +}) + + +const UserProto = expresses( + // Identifiable(), + ImplementsIdentifiable(0n), + Permissible, +) + +class User extends UserProto { + constructor(id: bigint) { + super() + this.id = id } } -abstract class ProvideIdentified extends Identified { - id!: ID -} - -abstract class Permissible { - protected permissions: string[] = [] -} - - -class User extends mixTraits( - Identified, - // Identified, - Permissible, -) { - id: bigint = BigInt(-1) -} - -const user = new User() +const user1 = new User(1n) +console.log(user1) +console.log(user1.equals(user1)) diff --git a/src/trait.ts b/src/trait.ts deleted file mode 100644 index 021d69a..0000000 --- a/src/trait.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { AbstractClass, Class, UnionToIntersection } from "type-fest" -import { copyClassProperties, flattenClass } from "./class" -import { StaticMembers } from "./utils" - - -export type Trait = - AbstractClass - - -// export function applyTrait< -// C extends Class | AbstractClass, -// TraitC extends Trait, -// >( -// class_: C, -// trait: TraitC, -// ) { -// copyClassProperties(trait, class_) - -// return class_ as ( -// (C extends Class -// ? Class< -// InstanceType & InstanceType, -// ConstructorParameters -// > -// : AbstractClass< -// InstanceType & InstanceType, -// ConstructorParameters -// > -// ) & -// StaticMembers & -// StaticMembers -// ) -// } - - -export const extendAndApplyTraits = < - C extends Class | AbstractClass, - Traits extends readonly Trait[], ->( - classToExtend: C, - traits: Traits, -) => - traits.reduce((class_, trait) => { - copyClassProperties(flattenClass(trait), class_) - return class_ - }, class extends classToExtend {}) as ( - AbstractClass< - InstanceType & - UnionToIntersection< - InstanceType - >, - - ConstructorParameters - > & - - StaticMembers< - C & - UnionToIntersection< - Traits[number] - > - > - ) - - -export const mixTraits = < - Traits extends readonly Trait[] ->( - ...traits: Traits -) => - traits.reduce((class_, trait) => { - copyClassProperties(flattenClass(trait), class_) - return class_ - }, class {}) as ( - Trait< - UnionToIntersection< - InstanceType< - Traits[number] - > - > - > & - - StaticMembers< - UnionToIntersection< - Traits[number] - > - > - ) diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 87f5f52..0000000 --- a/src/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export type StaticMembers = Pick