diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 0000000..8b33506 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,93 @@ +local bun_image = "oven/bun:1"; + + +local fetch_step = { + name: "fetch", + image: "alpine/git", + commands: ["git fetch --tags"], +}; + +local install_step = { + name: "install", + image: bun_image, + commands: ["bun install --frozen-lockfile"], +}; + +local lint_step = { + name: "lint", + image: bun_image, + commands: ["bun lint:tsc"], +}; + +local generate_docker_tags_step = { + name: "generate-docker-tags", + image: "git.jvalver.de/thilawyn/drone-better-docker-autotag", +}; + + +[ + // Lint the whole project when not in master, not in a PR nor on a tag + { + kind: "pipeline", + type: "docker", + name: "lint", + + trigger: { + ref: { + exclude: [ + "refs/heads/master", + "refs/pull/**", + "refs/tags/**", + ] + } + }, + + steps: [ + install_step, + lint_step, + ], + }, + + // Build the server and legacy API docker images without publishing them for pull requests + // { + // kind: "pipeline", + // type: "docker", + // name: "build-docker", + + // trigger: { + // ref: { + // include: ["refs/pull/**"] + // } + // }, + + // steps: [ + // fetch_step, + // generate_docker_tags_step, + // build_website_docker_step(false), + // build_legacy_api_docker_step(false), + // ], + // }, + + // Build the server and legacy API docker images and publish them for master and tags + // { + // kind: "pipeline", + // type: "docker", + // name: "build-publish-docker", + + // 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), + // ], + // }, +] diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ada479e Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..54a9941 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "thilatrait", + "version": "20231208.0.0", + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/lib.d.mts", + "default": "./dist/lib.mjs" + }, + "require": { + "types": "./dist/lib.d.cts", + "default": "./dist/lib.cjs" + } + } + }, + "scripts": { + "build": "rollup -c", + "lint:tsc": "tsc --noEmit", + "clean:cache": "rm -f tsconfig.tsbuildinfo", + "clean:dist": "rm -rf dist", + "clean:node": "rm -rf node_modules" + }, + "dependencies": { + "type-fest": "^4.8.3" + }, + "devDependencies": { + "bun-types": "latest", + "rollup": "^4.7.0", + "rollup-plugin-ts": "^3.4.5", + "typescript": "^5.3.3" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..87e9671 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,22 @@ +import { defineConfig } from "rollup" +import ts from "rollup-plugin-ts" +import pkg from "./package.json" assert { type: "json" } + + +export default defineConfig({ + input: "src/index.ts", + + plugins: [ ts() ], + + output: [ + { + file: pkg.exports["."].import.default, + format: "esm", + }, + + { + file: pkg.exports["."].require.default, + format: "cjs", + }, + ], +}) diff --git a/src/class.ts b/src/class.ts new file mode 100644 index 0000000..8cff9e9 --- /dev/null +++ b/src/class.ts @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..46c8360 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from "./class" +export * from "./trait" +export * from "./utils" diff --git a/src/tests.ts b/src/tests.ts new file mode 100644 index 0000000..4520fd6 --- /dev/null +++ b/src/tests.ts @@ -0,0 +1,29 @@ +import { mixTraits } from "./trait" + + +abstract class Identified { + abstract id: ID + + equals(el: Identified) { + return this.id === el.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() diff --git a/src/trait.ts b/src/trait.ts new file mode 100644 index 0000000..021d69a --- /dev/null +++ b/src/trait.ts @@ -0,0 +1,87 @@ +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 new file mode 100644 index 0000000..87f5f52 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1 @@ +export type StaticMembers = Pick diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5470793 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + // "allowImportingTsExtensions": true, + // "noEmit": true, + "declaration": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" + ] + }, + "include": ["src"] +}