Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions src/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import createPrototypeProxy from './createPrototypeProxy';
import bindAutoBindMethods from './bindAutoBindMethods';
import deleteUnknownAutoBindMethods from './deleteUnknownAutoBindMethods';

const RESERVED_STATICS = [
'length',
'name',
'arguments',
'caller',
'prototype'
];

function isEqualDescriptor(a, b) {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let key in a) {
if (a[key] !== b[key]) {
return false;
}
}
return true;
}

export default function proxyClass(InitialClass) {
// Prevent double wrapping.
// Given a proxy class, return the existing proxy managing it.
Expand All @@ -12,6 +35,13 @@ export default function proxyClass(InitialClass) {
const prototypeProxy = createPrototypeProxy();
let CurrentClass;

let staticDescriptors = {};
function wasStaticModifiedByUser(key) {
// Compare the descriptor with the one we previously set ourselves.
const currentDescriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
return !isEqualDescriptor(staticDescriptors[key], currentDescriptor);
}

let ProxyClass;
try {
// Create a proxy constructor with matching name
Expand Down Expand Up @@ -49,8 +79,46 @@ export default function proxyClass(InitialClass) {
// Set up the constructor property so accessing the statics work
ProxyClass.prototype.constructor = ProxyClass;

// Naïvely proxy static methods and properties
ProxyClass.prototype.constructor.__proto__ = NextClass;
// Set up the same prototype for inherited statics
ProxyClass.__proto__ = NextClass.__proto__;

// Copy static methods and properties
Object.getOwnPropertyNames(NextClass).forEach(key => {
if (RESERVED_STATICS.indexOf(key) > -1) {
return;
}

const staticDescriptor = {
...Object.getOwnPropertyDescriptor(NextClass, key),
configurable: true
};

// Copy static unless user has redefined it at runtime
if (!wasStaticModifiedByUser(key)) {
Object.defineProperty(ProxyClass, key, staticDescriptor);
staticDescriptors[key] = staticDescriptor;
}
});

// Remove old static methods and properties
Object.getOwnPropertyNames(ProxyClass).forEach(key => {
// Skip statics that exist on the next class
if (NextClass.hasOwnProperty(key)) {
return;
}

// Skip non-configurable statics
const descriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
if (descriptor && !descriptor.configurable) {
return;
}

// Delete static unless user has redefined it at runtime
if (!wasStaticModifiedByUser(key)) {
delete ProxyClass[key];
delete staticDescriptors[key];
}
});

// Try to infer displayName
ProxyClass.displayName = NextClass.displayName || NextClass.name;
Expand Down
160 changes: 155 additions & 5 deletions test/inheritance.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import expect from 'expect';
import { createProxy, getForceUpdate } from '../src';

class Base1 {
static getY() {
return 42;
}

getX() {
return 42;
}
Expand All @@ -14,6 +18,10 @@ class Base1 {
}

class Base2 {
static getY() {
return 43;
}

getX() {
return 43;
}
Expand All @@ -40,7 +48,7 @@ describe('inheritance', () => {
});

describe('modern only', () => {
it('replaces a base method with proxied base and derived', () => {
it('replaces a base instance method with proxied base and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

Expand All @@ -60,7 +68,27 @@ describe('inheritance', () => {
expect(renderer.getRenderOutput().props.children).toEqual(430);
});

it('replaces a base method with proxied base only', () => {
it('replaces a base static method with proxied base and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

class Derived extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 10}</span>;
}
}

const derivedProxy = createProxy(Derived);
const DerivedProxy = derivedProxy.get();

const instance = renderer.render(<DerivedProxy />);
expect(renderer.getRenderOutput().props.children).toEqual(420);

baseProxy.update(Base2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual(430);
});

it('replaces a base instance method with proxied base only', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

Expand All @@ -77,7 +105,24 @@ describe('inheritance', () => {
expect(renderer.getRenderOutput().props.children).toEqual(430);
});

it('replaces a derived method with proxied base and derived', () => {
it('replaces a base static method with proxied base only', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

class Derived extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 10}</span>;
}
}

const instance = renderer.render(<Derived />);
expect(renderer.getRenderOutput().props.children).toEqual(420);

baseProxy.update(Base2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual(430);
});

it('replaces a derived instance method with proxied base and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

Expand All @@ -103,7 +148,33 @@ describe('inheritance', () => {
expect(renderer.getRenderOutput().props.children).toEqual(4200);
});

it('replaces a derived method with proxied derived only', () => {
it('replaces a derived static method with proxied base and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

class Derived1 extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 10}</span>;
}
}

class Derived2 extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 100}</span>;
}
}

const derivedProxy = createProxy(Derived1);
const DerivedProxy = derivedProxy.get();

const instance = renderer.render(<DerivedProxy />);
expect(renderer.getRenderOutput().props.children).toEqual(420);

derivedProxy.update(Derived2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual(4200);
});

it('replaces a derived instance method with proxied derived only', () => {
class Derived1 extends Base1 {
actuallyRender() {
return <span>{super.getX() * 10}</span>;
Expand All @@ -126,7 +197,30 @@ describe('inheritance', () => {
expect(renderer.getRenderOutput().props.children).toEqual(4200);
});

it('replaces any method with proxied base, middle and derived', () => {
it('replaces a derived static method with proxied derived only', () => {
class Derived1 extends Base1 {
actuallyRender() {
return <span>{this.constructor.getY() * 10}</span>;
}
}

class Derived2 extends Base1 {
actuallyRender() {
return <span>{this.constructor.getY() * 100}</span>;
}
}

const derivedProxy = createProxy(Derived1);
const DerivedProxy = derivedProxy.get();

const instance = renderer.render(<DerivedProxy />);
expect(renderer.getRenderOutput().props.children).toEqual(420);

derivedProxy.update(Derived2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual(4200);
});

it('replaces any instance method with proxied base, middle and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

Expand Down Expand Up @@ -181,5 +275,61 @@ describe('inheritance', () => {
baseProxy.update(Base1).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('420 lol');
});

it('replaces any static method with proxied base, middle and derived', () => {
const baseProxy = createProxy(Base1);
const BaseProxy = baseProxy.get();

class Middle1 extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 10}</span>;
}
}

class Middle2 extends BaseProxy {
actuallyRender() {
return <span>{this.constructor.getY() * 100}</span>;
}
}

const middleProxy = createProxy(Middle1);
const MiddleProxy = middleProxy.get();

class Derived1 extends MiddleProxy {
render() {
return <span>{super.render().props.children + ' lol'}</span>;
}
}

class Derived2 extends MiddleProxy {
render() {
return <span>{super.render().props.children + ' nice'}</span>;
}
}

const derivedProxy = createProxy(Derived1);
const DerivedProxy = derivedProxy.get();

const instance = renderer.render(<DerivedProxy />);
expect(renderer.getRenderOutput().props.children).toEqual('420 lol');

baseProxy.update(Base2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('430 lol');

middleProxy.update(Middle2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('4300 lol');

derivedProxy.update(Derived2).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('4300 nice');

derivedProxy.update(Derived1).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('4300 lol');

middleProxy.update(Middle1).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('430 lol');

baseProxy.update(Base1).forEach(forceUpdate);
expect(renderer.getRenderOutput().props.children).toEqual('420 lol');
});
});
});
7 changes: 7 additions & 0 deletions test/static-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ describe('static method', () => {
expect(Proxy.getAnswer()).toEqual(42);
});

it('is own on proxy class instance', () => {
const proxy = createProxy(StaticMethod);
const Proxy = proxy.get();
expect(Proxy.hasOwnProperty('getAnswer')).toEqual(true);
});

it('gets added', () => {
const proxy = createProxy(StaticMethodRemoval);
const Proxy = proxy.get();
Expand Down Expand Up @@ -141,6 +147,7 @@ describe('static method', () => {

proxy.update(StaticMethodUpdate);
renderer.render(<Proxy />);

expect(renderer.getRenderOutput().props.children).toEqual(42);
expect(Proxy.getAnswer()).toEqual(42);
expect(getAnswer()).toEqual(42);
Expand Down
6 changes: 6 additions & 0 deletions test/static-property.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ describe('static property', () => {
expect(Proxy.answer).toEqual(42);
});

it('is own on proxy class instance', () => {
const proxy = createProxy(StaticProperty);
const Proxy = proxy.get();
expect(Proxy.hasOwnProperty('answer')).toEqual(true);
});

it('is changed when not reassigned', () => {
const proxy = createProxy(StaticProperty);
const Proxy = proxy.get();
Expand Down