fix: mixer, idk, runtimes sucks (#331)

* fix: declared properties

* fix: idk mixer
This commit is contained in:
Marcos Susaña 2025-02-24 03:56:43 -04:00 committed by GitHub
parent 272ddac08b
commit 749ba3e6be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 496 additions and 305 deletions

View File

@ -22,14 +22,14 @@
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@changesets/cli": "^2.27.11",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@types/node": "^22.10.7",
"@changesets/cli": "^2.28.1",
"@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.7.1",
"@types/node": "^22.13.5",
"husky": "^9.1.7",
"lint-staged": "^15.4.1",
"lint-staged": "^15.4.3",
"typescript": "^5.7.3",
"vitest": "^3.0.3"
"vitest": "^3.0.6"
},
"homepage": "https://seyfert.dev",
"repository": {

602
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,8 @@ function getNodeDescriptors(c: TypeClass) {
const result: Record<string, TypedPropertyDescriptor<unknown> | PropertyDescriptor>[] = [];
while (proto) {
const descriptors = Object.getOwnPropertyDescriptors(proto);
// @ts-expect-error this is not a function in all cases
if (descriptors.valueOf.configurable) break;
result.push(descriptors);
proto = Object.getPrototypeOf(proto);
}
@ -51,6 +53,8 @@ export function Mixin<T, C extends TypeClass[]>(...args: C): C[number] & T {
for (const mixin of args.slice(1)) {
const descriptors = getDescriptors(mixin).reverse();
for (const desc of descriptors) {
// @ts-expect-error
Object.assign(this, new desc.constructor.value(...constructorArgs));
for (const key in desc) {
if (key === 'constructor') continue;
if (key in MixedClass.prototype) continue;

183
tests/mixer.test.mts Normal file
View File

@ -0,0 +1,183 @@
// test written by claude 🩻
import { describe, expect, it } from 'vitest';
import { mix } from '../lib/deps/mixer';
describe('mix decorator', () => {
// Helper classes for testing
class BaseClass {
baseProperty = 'baseValue';
baseMethod() {
return 'base';
}
toString() {
return 'from base';
}
}
class MixinOne {
propertyOne = 1;
methodOne() {
return 'one';
}
toString() {
return 'from one';
}
}
class MixinTwo {
propertyTwo = 'two';
methodTwo() {
return 'two';
}
}
it('should correctly mix classes and preserve properties', () => {
@mix(MixinOne, MixinTwo)
class TestClass extends BaseClass {
testProperty = 'test';
testMethod() {
return 'test';
}
}
interface TestClass extends MixinOne, MixinTwo {}
const instance = new TestClass();
// Check properties
expect(instance.baseProperty).toBe('baseValue');
expect(instance.propertyOne).toBe(1);
expect(instance.propertyTwo).toBe('two');
expect(instance.testProperty).toBe('test');
// Check methods
expect(instance.baseMethod()).toBe('base');
expect(instance.methodOne()).toBe('one');
expect(instance.methodTwo()).toBe('two');
expect(instance.testMethod()).toBe('test');
expect(instance.toString()).toBe('from base');
});
it('should correctly mix classes and preserve the original class name', () => {
@mix(MixinOne, MixinTwo)
class TestClass extends BaseClass {
testMethod() {
return 'test';
}
}
interface TestClass extends MixinOne, MixinTwo {}
const instance = new TestClass();
// Check if the class name is preserved
expect(TestClass.name).toBe('TestClass');
// Check if methods from all classes are available
expect(instance.baseMethod()).toBe('base');
expect(instance.methodOne()).toBe('one');
expect(instance.methodTwo()).toBe('two');
expect(instance.testMethod()).toBe('test');
});
it('should handle mixing with no additional mixins', () => {
@mix()
class TestClass extends BaseClass {
testMethod() {
return 'test';
}
}
const instance = new TestClass();
expect(TestClass.name).toBe('TestClass');
expect(instance.baseMethod()).toBe('base');
expect(instance.testMethod()).toBe('test');
});
it('should handle mixing with getters and setters', () => {
class MixinWithAccessors {
private _value = '';
get value(): string {
return this._value;
}
set value(val: string) {
this._value = val;
}
}
interface TestClass extends MixinWithAccessors {}
@mix(MixinWithAccessors)
class TestClass extends BaseClass {}
const instance = new TestClass();
instance.value = 'test';
expect(instance.value).toBe('test');
});
it('should not override existing methods in the target class', () => {
class MixinWithConflict {
baseMethod() {
return 'mixin';
}
}
@mix(MixinWithConflict)
class TestClass extends BaseClass {
baseMethod() {
return 'override';
}
}
const instance = new TestClass();
expect(instance.baseMethod()).toBe('override');
});
it('should handle constructor arguments', () => {
class MixinWithConstructor {
constructor(public name: string) {}
getName() {
return this.name;
}
}
interface TestClass extends MixinWithConstructor {}
@mix(MixinWithConstructor)
class TestClass extends BaseClass {
constructor(name: string) {
super();
this.name = name;
}
name: string;
}
const instance = new TestClass('test');
expect(instance.getName()).toBe('test');
});
it('should handle multiple levels of inheritance', () => {
class Level1 {
level1() {
return 'level1';
}
}
class Level2 extends Level1 {
level2() {
return 'level2';
}
}
interface TestClass extends Level1, Level2 {}
@mix(Level2)
class TestClass extends BaseClass {}
const instance = new TestClass();
expect(instance.level1()).toBe('level1');
expect(instance.level2()).toBe('level2');
});
});