diff --git a/README.md b/README.md index 7430ebd..d865165 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ JavaScript/TypeScript class inheritance tools. +#### `npm install lowclass` + +# Features + Lowclass is a lib that includes the following inheritance tools: -- A `multiple()` function for composing ES2015 `class`es together in a simple - ergonomic way. For example: +- A `multiple()` function for composing JavaScript `class`es together in a simple + ergonomic way as a simpler alternative to mixing functions. For example: ```js // define a few classes with unique features: @@ -69,11 +73,16 @@ Lowclass is a lib that includes the following inheritance tools: person.attack() ``` -- A `Mixin()` helper for making mixable ES2015 `class`es. Mixins are less - ergonomic than composing classes with the `multiple()` helper, but if you're - after performance, then mixins (made with or without the `Mixin()` helper) will have - faster instantiation and property lookup than classes composed with - `multiple()`. For example: +- A `Mixin()` function for creating mixable JavaScript `class`es as an alternative + to `multiple()`. Mixins are less ergonomic than composing classes with the + `multiple()` helper, but if you're after the most performance, then mixins (made + with or without the `Mixin()` helper) will have faster instantiation and + property lookup than classes composed with `multiple()` because `multiple()` + uses `Proxy` under the hood. In most cases, the performance difference matters + not, and using `multiple()` leads to cleaner and simpler code. + + The following example implements the same thing as the above example with + `multiple()`, and we can see it is more verbose: ```js import {Mixin} from 'lowclass' @@ -108,7 +117,7 @@ Lowclass is a lib that includes the following inheritance tools: } }) - // At this point Walker, Talker, and Barker are references to ES2015 `class`es. + // At this point Walker, Talker, and Barker are references to `class`es. // Now use them like regular classes by extending from them normally: @@ -148,754 +157,3 @@ Lowclass is a lib that includes the following inheritance tools: person.talk() person.attack() ``` - -- A `Class()` tool for creating classes with public, protected, and private members. For example: - - ```js - import Class from 'lowclass' - import Something from 'somewhere' - - export default Class('Thing').extends(Something, ({Protected, Private}) => ({ - doSomething() { - // this method is public - Protected(this).makeStuff() - } - - protected: { - makeStuff() { - // this method is protected - Private(this).stuffImpl() - } - }, - - private: { - stuffImpl() { - // this method is private - } - } - })) - ``` - - ```js - import Thing from './Thing' - - const Blob = Class('Blob').extends(Thing, ({Super, Protected, Private}) => ({ - doSomething() { - Super(this).doSomething() // works fine, makeStuff is public. - Protected(this).makeStuff() // works fine, makeStuff is protected and inherited - - // logs "undefined", private methods are not inherited - console.log(this.stuffImpl) - - // try to access it with the Private helper: - Private(this).stuffImpl() // error, can not read property "stuffImpl" of undefined. - }, - })) - - const blob = new Blob() - // access public members: - blob.doSomething() // it works - - // can not acecss protected or private members: - blob.makeStuff() // error, can not read property "makeStuff" of undefined. - blob.stuffImpl() // error, can not read property "stuffImpl" of undefined. - ``` - -#### `npm install lowclass --save` - -Lowclass let's us define classes with protected and private data similar to in -C++ (and similar to some some extent Java): - -- `Public` members can be accessed from outside the class. -- `Protected` members can be accessed in the class and its derived classes. -- `Private` members can be only accessed within the class. - -But there's an interesting difference (advantage) that lowclass private members -have over C++ private members: private functionality of a class made with -lowclass can be inherited by a derived subclass, but the functionality is still -scoped to the class where it is utilized, meaning that the inherited -functionality will operate on the private data of the class where the inherited -functionality is used without breaking private, protected, and public API -contracts. - -Lowclass supports - -- extending builtins like Array. (see - [`tests/extending-builtins.test.js`](./tests/extending-builtins.test.js)). -- extending native ES6 classes. (see - [`tests/extending-native-classes.test.js`](./tests/extending-native-classes.test.js)) -- extending builtins like `HTMLElement` and using the subclasses in native APIs - like Custom Elements. (see - [`tests/custom-elements.test.js`](./tests/custom-elements.test.js)). - -## Intro - -All of the intro examples are available as tests in -[`tests/readme-examples.test.js`](./tests/readme-examples.test.js), and -the other test files contain many more examples. - -### Hiding members of your existing classes - -You may already be using ES2015's native `class` syntax to define your classes, -for example: - -```js -class Thing { - constructor() { - // you might be using a convention like leading underscores to - // tell people some property is "protected" or "private" - this._protectedProperty = 'yoohoo' - } - - someMethod() { - return this._protectedProperty - } -} - -const instance = new Thing() - -instance.someMethod() // returns "yoohoo" - -// but the property is not actually protected: -console.log(instance._protectedProperty) // "yoohoo" -``` - -The good news is, you can use lowclass to add Protected and Private -functionality to your existing classes! - -Just wrap your class with lowclass to gain Protected or Private functionality: - -```js -import protect from 'lowclass' -// or const protect = require('lowclass') - -const Thing = protect(({Protected}) => { - return class Thing { - constructor() { - // make the property truly protected - Protected(this).protectedProperty = 'yoohoo' - } - - someMethod() { - console.log('Protected value is:', Protected(this).protectedProperty) - } - } -}) -``` - -We can make it a little cleaner: - -```js -const Thing = protect( - ({Protected}) => - class { - constructor() { - Protected(this).protectedProperty = 'yoohoo' - } - - someMethod() { - return Protected(this).protectedProperty - } - }, -) -``` - -If we were exporting this from a module, we could write it like this: - -```js -export default protect( - ({Protected}) => - class Thing { - constructor() { - Protected(this).protectedProperty = 'yoohoo' - } - - someMethod() { - return Protected(this).protectedProperty - } - }, -) -``` - -You might still be making ES5-style classes using `function() {}` instead of -`class`. In this case wrapping it would look like this: - -```js -const Thing = protect(({Protected}) => { - function Thing() { - Protected(this).protectedProperty = 'yoohoo' - } - - Thing.prototype = { - constructor: Thing, - - someMethod() { - return Protected(this).protectedProperty - }, - } - - return Thing -}) -``` - -And it works: - -```js -const t = new Thing() - -expect(t.someMethod()).toBe('yoohoo') - -// the value is not publicly accessible! -expect(t.protectedProperty).toBe(undefined) -``` - -But this is a fairly simple example. Let's show how inheritance of protected -members works, again wrapping a native ES6+ `class`. Suppose we have a derived -class that is also using the not-actually-protected underscore convention: - -```js -class Something extends Thing { - otherMethod() { - // we'll need to update this - return this._protectedProperty - } -} -``` - -We will wrap it with lowclass too, so that it can inherit the protected member: - -```js -const Something = protect( - ({Protected}) => - class extends Thing { - otherMethod() { - // access the inherited actually-protected member - return Protected(this).protectedProperty - } - }, -) -``` - -If you are writing ES5-style classes, it will look something like this: - -```js -const Something = protect(({Protected}) => { - function Something() { - Thing.call(this) - } - - Something.prototype = { - __proto__: Thing.prototype, - constructor: Something, - - otherMethod() { - // access the inherited actually-protected member - return Protected(this).protectedProperty - }, - } - - return Something -}) -``` - -And it works: - -```js -const s = new Something() -expect(s.protectedProperty).toBe(undefined) -expect(s.otherMethod()).toBe('yoohoo') -``` - -Nice, we can keep internal implementation hidden, and prevent people from using -our APIs in unexpected ways! - -### Private members - -Continuing from above, if we use a Private member instead of a Protected member -in a derived subclass, the subclass will not be able to access the private -member of the parent class (like C++ and Java). - -Here's an example that shows the concept, but this time we will define the -classes directly with lowclass, instead of wrapping a class: - -```js -import Class from 'lowclass' - -const Thing = Class(({Private}) => ({ - constructor() { - Private(this).privateProperty = 'yoohoo' - }, -})) - -const Something = Thing.subclass(({Private}) => ({ - otherMethod() { - return Private(this).privateProperty - }, -})) - -const something = new Something() - -// the private member can't be accessed by the subclass code: -expect(something.otherMethod()).toBe(undefined) -``` - -As you can see, code in the child class (`otherMethod`) is unable to access the -private value of the parent class. - -### Private Inheritance - -In the last example, We've learned that, like in C++ or Java, subclasses can -not access parent class private members. - -But lowclass offers something that C++ and Java do not: Private Inheritance. -Subclasses can inherit (make use of) private functionality from a parent class. -A subclass can call an inherited private method, but the interesting thing is -that the inherited private method _will operate on the private data of the -subclass, not of the parent class_. - -Let's illustrate this with an example, then we'll explain afterwords how it -works: - -```js -const Class = require('lowclass') -// or import Class from 'lowclass' - -const Thing = Class(({Private}) => ({ - constructor() { - Private(this).privateProperty = 'yoohoo' - }, - - someMethod() { - return Private(this).privateProperty - }, - - changeIt() { - Private(this).privateProperty = 'oh yeah' - }, -})) - -const Something = Class().extends(Thing, ({Private}) => ({ - otherMethod() { - return Private(this).privateProperty - }, - - makeItSo() { - Private(this).privateProperty = 'it is so' - }, -})) - -const instance = new Something() - -expect(instance.someMethod()).toBe('yoohoo') -expect(instance.otherMethod()).toBe(undefined) - -instance.changeIt() -expect(instance.someMethod()).toBe('oh yeah') -expect(instance.otherMethod()).toBe(undefined) - -instance.makeItSo() -expect(instance.someMethod()).toBe('oh yeah') -expect(instance.otherMethod()).toBe('it is so') -``` - -> Huh? What? - -In every class hierarchy, there is a private scope for each class in the -hierarchy (just like in C++ and Java). In this case, there's two private -scopes: one for `Thing`, and one for `Something`. `Thing.someMethod` and -`Thing.changeIt` are accessing the `privateProperty` of `Thing`, while -`Something.otherMethod` and `Something.makeItSo` are accessing the -`privateProperty` of `Something`. - -But unlike C++ and Java, lowclass has a concept of private inheritance, where a -subclass can re-use private logic of a parent class, but the logic will operate -on private members of the class scope where it is used. - -To use inheritable functionality, all that you have to do is run private code -in the code of a subclass. Let's make one more example to show what this means -in another way: - -```js -const Counter = Class(({Private}) => ({ - private: { - // this is a prototype property, the initial private value will be - // inherited by subclasses - count: 0, - - increment() { - this.count++ - }, - }, - - tick() { - Private(this).increment() - - return Private(this).count - }, - - getCountValue() { - return Private(this).count - }, -})) - -const DoubleCounter = Counter.subclass(({Private}) => ({ - doubleTick() { - // to use inherited private functionality in a subclass, simply use - // the functionality in the code of the subclass. - Private(this).increment() - Private(this).increment() - - return Private(this).count - }, - - getDoubleCountValue() { - return Private(this).count - }, -})) - -const counter = new Counter() - -expect(counter.tick()).toBe(1) - -const doubleCounter = new DoubleCounter() - -expect(doubleCounter.doubleTick()).toBe(2) -expect(doubleCounter.tick()).toBe(1) - -expect(doubleCounter.doubleTick()).toBe(4) -expect(doubleCounter.tick()).toBe(2) - -// There's a private `counter` member for the Counter class, and there's a -// separate private `counter` member for the `DoubleCounter` class (the -// initial value inherited from `Counter`): -expect(doubleCounter.getDoubleCountValue()).not.toBe(counter.getCountValue()) -expect(doubleCounter.getCountValue()).toBe(2) -expect(doubleCounter.getDoubleCountValue()).toBe(4) -``` - -The inherited private functionality has to be triggered directly, as triggering -it indirectly will make it behave like in C++ and Java. This is why when we -called `doubleCounter.tick()` the private functionality operated on the private -`count` property of the `Counter` class, not the `DoubleCounter` class. - -The key thing to learn from this is that when private code is used, it operates -on the class scope where the code is triggered. In the case of `DoubleCounter`, -we trigger the inherited functionality inside of the `DoubleCounter.doubleTick` -method, so this makes the inherited functionality operate on `DoubleCounter`'s -inherited private `count` property. - -### "friends" like in C++, or "package protected" like in Java - -Lowclass makes it possible to do something similar to "friend" in C++ or -"package protected" in Java. We can do these sorts of things by "leaking" the -access helpers to a scope that is outside a class definition. - -For example, in the following example, the `Counter` class has private data, -and the `Incrementor` class can access the protected member of the `Counter` -class although `Incrementor` is not derived from `Counter`. These two classes -are exported and then imported by another file which can not access the private -data, but can use the public API of both classes to make instances of the two -classes interact with eachother. - -```js -// Counter.js - -// show how to do something similar to "friend" in C++ or "package protected" -// in Java. - -import Class from 'lowclass' - -let CounterProtected - -const Counter = Class(({Private, Protected}) => { - // leak the Counter class Protected helper to outer scope - CounterProtected = Protected - - return { - value() { - return Private(this).count - }, - - private: { - count: 0, - }, - - protected: { - increment() { - Private(this).count++ - }, - }, - } -}) - -// note how Incrementor does not extend from Counter -const Incrementor = Class(({Private}) => ({ - constructor(counter) { - Private(this).counter = counter - }, - - increment() { - const counter = Private(this).counter - CounterProtected(counter).increment() - }, -})) - -export {Counter, Incrementor} -``` - -```js -// shows that functionality similar to "friend" in C++ or "package -// protected" can be done with lowclass. See `./Counter.js` to learn how it -// works. - -import {Counter, Incrementor} from './Counter' - -// in a real-world scenario, counter might be used here locally... -const counter = new Counter() - -// ...while incrementor might be passed to third party code. -const incrementor = new Incrementor(counter) - -// show that we can only access what is public -expect(counter.count).toBe(undefined) -expect(counter.increment).toBe(undefined) -expect(typeof counter.value).toBe('function') - -expect(incrementor.counter).toBe(undefined) -expect(typeof incrementor.increment).toBe('function') - -// show that it works: -expect(counter.value()).toBe(0) -incrementor.increment() -expect(counter.value()).toBe(1) -incrementor.increment() -expect(counter.value()).toBe(2) -``` - -## Forms of writing classes - -Working examples of the various forms depicted here are in -[`tests/syntaxes.test.js`](./tests/syntaxes.test.js). - -### Simple object literals - -If we will only use public members in our class, we can define a class with a -simple object literal in a few ways. - -Here's a named class, and in this case it is a little redundant as there are -two occurrences of "Thing" in the definition: - -```js -const Thing = Class( 'Thing', { - method() { ... } -}) -``` - -An anonymous class can avoid redundancy, and new engines are good at showing -you variable names in the console when classes or functions are anonymous: - -```js -const Thing = Class({ - method() { ... } -}) -``` - -A named class can be useful for debugging in older environments, and when used -with with direct exports as there's no redundancy: - -```js -export default Class( 'Thing', { - method() { ... } -}) -``` - -If you're not using Protected or Private members, you probably don't need to -even use lowclass, and native `class` syntax can give you all the Public -functionality that you need. - -### Definer functions give us access to access helpers. - -There's also a [proposal for private -members](https://github.com/tc39/proposal-class-fields) in the works, but who -knows how long until it makes its way into engines, if ever. - -Until then, we can use a "definer function" when defining a class with -lowclass, so that we can access Public, Protected, Private, and Super helpers. - -Instead of providing a simple object literal as above, we can provide a -function that receives access helpers. This function should then return the -object literal that contains the definition of the class, or should return a -custom-made class constructor. - -#### Returning an object literal - -```js -export default Class('Thing', function (Public, Protected, Private, Super) { - return { - method() { - // use any of the helpers inside the class code, as needed, f.e. - - // access Public members - this.foo = 'foo' - - // access Protected members - Protected(this).bar = 'bar' - - // access Private members - Private(this).baz = 'baz' - }, - } -}) -``` - -To make code shorter, you can combine arrow functions with destructuring of -arguments. In this exampe, we only need the Private helper: - -```js -export default Class('Thing', ({Private}) => ({ - method() { - // access Private members - Private(this).baz = 'baz' - }, -})) -``` - -#### Returning a class constructor - -If you want to make your classes in your own way, you can return a class from a -definer function, which is useful for wrapping existing classes in order to -give them protected and private functionality: - -```js -export default Class(({Private}) => { - return class { - method() { - Private(this).baz = 'baz' - } - } -}) - -// or - -export default Class( - ({Private}) => - class { - method() { - Private(this).baz = 'baz' - } - }, -) -``` - -### ES5-like assignment to prototype - -You might have lots of ES5-style code, so this form can be useful in porting -over to lowclass more quickly, or maybe you just like this form more. - -```js -export default Class('Thing', ({Public, Private}) => { - Public.prototype.method = function () { - Private(this).baz = 'baz' - } -}) -``` - -### Subclasses - -We can make a subclass in a couple ways, with ot without names, and using -object literals or definer functions. We'll use the `Super` helper to access -super methods. - -#### With `.extends` - -This way is more similar to native classes: - -```js -const Something = Class().extends(Thing, ({Super}) => ({ - method() { - Super(this).method() - }, -})) -``` - -And as before, naming the class can be useful: - -```js -export default Class('Something').extends(Thing, ({Private}) => ({ - method() { - Super(this).method() - }, -})) -``` - -#### With `.subclass` - -Here's same subclass example using `.subclass`: - -```js -const Something = Thing.subclass(({Super}) => ({ - method() { - Super(this).method() - }, -})) -``` - -And as before, naming the class can be useful: - -```js -export default Thing.subclass('Something', ({Super}) => ({ - method() { - Super(this).method() - }, -})) -``` - -We can also stick lowclass onto any constructor, and use it just like the -previous example: - -```js -import Class from 'lowclass' - -Array.subclass = Class - -const MyArray = Array.subclass( ({ Super, Private }) => { - constructor() { - const self = super.constructor(...args) - self.__proto__ = MyArray.prototype - - Private(self).message = 'I am Array!' - - return self - }, -}) -``` - -See the full Array example in -[`test/extending-builtins.test.js`](./test/extending-builtins.test.js). - -## Differences between lowclass and other languages - -### C++ - -C++ and lowclass are basically the same (including "friend" classes). Where -they differ is that lowclass offers "Private Inheritance" as described above -while C++ does not. - -See [here](https://www.geeksforgeeks.org/access-modifiers-in-c/) for an -explainer on C++ access modifers which is effectively the same for lowclass. - -### Java - -The differences between lowclass' and Java's access modifiers are basically the -same as the differences between C++ and Java. Lowclass additionally has -"Private Inheritance". Lowclass also has a concept similar to "package -protected" which is similar to "friend" in C++. - -See [here](https://www.javatpoint.com/access-modifiers) for an explainer of -Java access modifiers. We can compare this against C++, and therefore also -against lowclass. - -## TODO - -- [ ] public/protected/private/super helpers for static members -- [ ] ability to make classes "final" diff --git a/index.html b/index.html index 8868f56..86e1efc 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,5 @@ diff --git a/src/Class.ts b/src/Class.ts deleted file mode 100644 index 8f0e233..0000000 --- a/src/Class.ts +++ /dev/null @@ -1,889 +0,0 @@ -// TODO -// [x] remove the now-unnecessary modes (leave just what was 'es5' mode) -// [x] link helpers to each other, making it possible to destructure the arguments to definer functions -// [x] let access helper prototype objects extend from Object, otherwise common tools are not available. -// [x] accept a function as return value of function definer, to be treated as a class to derive the definition from, so that it can have access to Protected and Private helpers -// [x] let the returned class define protected and private getters which return the protected and private definitions. -// [ ] protected and private static members -// [ ] no `any` types -// [ ] other TODOs in the code - -import { - Constructor, - copyDescriptors, - setDefaultStaticDescriptors, - setDefaultPrototypeDescriptors, - hasPrototype, -} from './utils.js' - -import type {Id} from './types.js' - -type ImplementationKeys = 'static' | 'private' | 'protected' - -type FunctionToConstructor = T extends (...a: infer A) => void ? new (...a: A) => TReturn : never - -// Note, void also works the same in place of unknown -type ReplaceCtorReturn = T extends new (...a: infer A) => unknown ? new (...a: A) => TReturn : never - -type ConstructorOrDefault = T extends {constructor: infer TCtor} ? TCtor : () => void - -// Although the SuperType type definiton already checks that T extends from -// Constructor, the additional check in the generic paramters is useful so -// that we don't get an error about "never" which is hard to track down. The -// generic paramter will cause a more helpful and understandable error. -// TODO ensure that T is InstanceType of TBase -// prettier-ignore -type SuperType<_T, TSuper extends Constructor> = TSuper extends Constructor - ? {constructor: (...a: A) => I} & InstanceType - : never -// type SuperType< -// T extends InstanceType, -// TSuper extends Constructor -// > = TSuper extends Constructor -// ? T extends InstanceType -// ? {constructor: (...a: A) => I} & Id> -// : never -// : never - -type SuperHelper = (self: T) => SuperType -type PrivateHelper = (self: T) => T extends {__: {private: infer TPrivate}} ? TPrivate : never -type PublicHelper = (self: T) => Omit // TODO validate instance is public? -type ProtectedHelper = (self: T) => T extends {__: {protected: infer TProtected}} ? TProtected : never -// type ProtectedHelper = (self: T) => T extends {protected: infer TProtected} ? TProtected : never -type Statics = T extends {static: infer TStatic} ? TStatic : {} -type SaveInheritedProtected = T extends {protected: infer TProtected} ? TProtected : {} - -// there's a missing link here: if the super class of T is a native class -// that extends from a lowclass class, then we don't inherit those protected -// members. Any ideas? -type StaticsAndProtected = Id & {__: {protected: SaveInheritedProtected}}> - -type ExtractInheritedProtected = T extends {__: infer TProtected} ? TProtected : {} -type PickImplementationKeys = Pick> // similar to Pick, but not quite - -// this moves the implementation keys off the constructor return type and -// onto a fake __ property, so that we can reference the __ type within the -// implementatin code, but so that the outside (public) doesn't see the fake -// __ property. -type LowClassThis = Id & {__: PickImplementationKeys}> - -type OmitImplementationKeys = Omit - -import { - getFunctionBody, - setDescriptor, - propertyIsAccessor, - getInheritedDescriptor, - getInheritedPropertyNames, - WeakTwoWayMap, -} from './utils.js' - -export const staticBlacklist = ['subclass', 'extends', ...Object.getOwnPropertyNames(new Function())] - -const publicProtoToProtectedProto = new WeakMap() -const publicProtoToPrivateProto = new WeakMap() - -// A two-way map to associate public instances with protected instances. -// There is one protected instance per public instance -const publicToProtected = new WeakTwoWayMap() - -// so we can get the class scope associated with a private instance -const privateInstanceToClassScope = new WeakMap() - -const brandToPublicPrototypes = new WeakMap() -const brandToProtectedPrototypes = new WeakMap() -const brandToPrivatePrototypes = new WeakMap() -const brandToPublicsPrivates = new WeakMap() - -const defaultOptions = { - // es5 class inheritance is simple, nice, easy, and robust - // There was another mode, but it has been removed - mode: 'es5', - - // false is better for performance, but true will use Function (similar to - // eval) to name your class functions in the most accurate way. - nativeNaming: false, - - // similar to ES6 classes: - prototypeWritable: false, - defaultClassDescriptor: { - writable: true, - enumerable: false, - configurable: true, - }, - setClassDescriptors: true, -} - -export class InvalidSuperAccessError extends Error {} -export class InvalidAccessError extends Error {} - -export const Class = createClassHelper() - -export function createClassHelper(options?: any) { - options = options ? {...defaultOptions, ...options} : defaultOptions - - options.defaultClassDescriptor = { - ...defaultOptions.defaultClassDescriptor, - ...options.defaultClassDescriptor, - } - - const {mode, prototypeWritable, setClassDescriptors, nativeNaming} = options - - /* - * this is just the public interface adapter for createClass(). Depending - * on how you call this interface, you can do various things like: - * - * - anonymous empty class - * - * Class() - * - * - named empty class - * - * Class('Foo') - * - * - base class named Foo - * - * Class('Foo', (Public, Protected, Private) => { - * someMethod() { ... }, - * }) - * - * - anonymous base class - * - * Class((Public, Protected, Private) => { - * someMethod() { ... }, - * }) - * - * Class('Foo').extends(OtherClass, (Public, Protected, Private) => ({ - * someMethod() { ... }, - * })) - * - * OtherClass.subclass = Class - * const Bar = OtherClass.subclass((Public, Protected, Private) => { - * ... - * }) - * - * - any class made with lowclass has a static subclass if you prefer using - * that: - * - * Bar.subclass('Baz', (Public, Protected, Private) => {...}) - * - * - but you could as well do - * - * Class('Baz').extends(Bar, (Public, Protected, Private) => {...}) - */ - function Class(): typeof Object - // export function Class( - function Class(name: string): { - extends( - base: TBase, - members: (helpers: { - Super: SuperHelper - Public: PublicHelper - Protected: ProtectedHelper - Private: PrivateHelper - }) => T & - Partial> & - ThisType & ExtractInheritedProtected>>, - brand?: object, - ): T extends {constructor: infer _TCtor} - ? FunctionToConstructor, Id & OmitImplementationKeys>> & - Id & Pick> - : ReplaceCtorReturn>> & Id & Pick> - } - function Class( - name: string, - members: ( - helpers: { - Public: PublicHelper - Protected: ProtectedHelper - Private: PrivateHelper - Super: never - }, // TODO Super is actually Object - ) => T & ThisType>, - brand?: object, - ): FunctionToConstructor, Id>> & Id> - function Class( - name: string, - members: T & ThisType>, - brand?: object, - ): FunctionToConstructor, Id>> & Id> - function Class(this: any, ...args: any[]) { - let usingStaticSubclassMethod = false - - // if called as SomeConstructor.subclass, or bound to SomeConstructor - if (typeof this === 'function') usingStaticSubclassMethod = true - - // f.e. `Class()`, `Class('Foo')`, `Class('Foo', {...})` , `Class('Foo', - // {...}, Brand)`, similar to `class {}`, `class Foo {}`, class Foo - // {...}, and class Foo {...} with branding (see comments on classBrand - // below regarding positional privacy) - if (args.length <= 3) { - let name = '' - let definer: any = null - let classBrand: any = null - - // f.e. `Class('Foo')` - if (typeof args[0] === 'string') name = args[0] - // f.e. `Class((pub, prot, priv) => ({ ... }))` - else if (typeof args[0] === 'function' || typeof args[0] === 'object') { - definer = args[0] - classBrand = args[1] - } - - // f.e. `Class('Foo', (pub, prot, priv) => ({ ... }))` - if (typeof args[1] === 'function' || typeof args[1] === 'object') { - definer = args[1] - classBrand = args[2] - } - - // Make a class in case we wanted to do just `Class()` or - // `Class('Foo')`... - const Ctor = usingStaticSubclassMethod - ? createClass.call(this, name, definer, classBrand) - : createClass(name, definer, classBrand) - - // ...but add the extends helper in case we wanted to do like: - // Class().extends(OtherClass, (Public, Protected, Private) => ({ - // ... - // })) - Ctor.extends = function (ParentClass: any, def: any, brand: any) { - def = def || definer - brand = brand || classBrand - return createClass.call(ParentClass, name, def, brand) - } - - return Ctor - } - - throw new TypeError('invalid args') - } - - return Class - - /** - * @param {string} className The name that the class being defined should - * have. - * @param {Function} definer A function or object for defining the class. - * If definer a function, it is passed the Public, Protected, Private, and - * Super helpers. Methods and properties can be defined on the helpers - * directly. An object containing methods and properties can also be - * returned from the function. If definer is an object, the object should - * be in the same format as the one returned if definer were a function. - */ - function createClass(this: any, className: string, definer: (...args: any[]) => any, classBrand: object) { - 'use strict' - - // f.e. ParentClass.subclass((Public, Protected, Private) => {...}) - let ParentClass = this - - if (typeof className !== 'string') { - throw new TypeError(` - You must specify a string for the 'className' argument. - `) - } - - let definition = null - - // f.e. Class('Foo', { ... }) - if (definer && typeof definer === 'object') { - definition = definer - } - - // Return early if there's no definition or parent class, just a simple - // extension of Object. f.e. when doing just `Class()` or - // `Class('Foo')` - else if (!ParentClass && (!definer || (typeof definer !== 'function' && typeof definer !== 'object'))) { - let Ctor: CtorWithSubclass & Function - - if (nativeNaming && className) Ctor = new Function(`return function ${className}() {}`)() - else { - // force anonymous even in ES6+ - Ctor = (() => function () {})() as unknown as CtorWithSubclass - - if (className) setDescriptor(Ctor, 'name', {value: className}) - } - - Ctor.prototype = {__proto__: Object.prototype, constructor: Ctor} - - // no static inheritance here, just like with `class Foo {}` - - setDescriptor(Ctor, 'subclass', { - value: Class, - writable: true, // TODO maybe let's make this non writable - enumerable: false, - configurable: false, - }) - - return Ctor - } - - // A two-way map to associate public instances with private instances. - // Unlike publicToProtected, this is inside here because there is one - // private instance per class scope per instance (or to say it another - // way, each instance has as many private instances as the number of - // classes that the given instance has in its inheritance chain, one - // private instance per class) - const scopedPublicsToPrivates = classBrand ? void undefined : new WeakTwoWayMap() - - if (classBrand) { - if (!brandToPublicsPrivates.get(classBrand)) brandToPublicsPrivates.set(classBrand, new WeakTwoWayMap()) - } - - // if no brand provided, then we use the most fine-grained lexical - // privacy. Lexical privacy is described at - // https://github.com/tc39/proposal-class-fields/issues/60 - // - // TODO make prototypes non-configurable so that the clasds-brand system - // can't be tricked. For now, it's good enough, most people aren't going - // to go out of their way to mangle with the prototypes in order to - // force invalid private access. - classBrand = classBrand || {brand: 'lexical'} - - // the class "scope" that we will bind to the helper functions - const scope = { - className, // convenient for debugging - - get publicToPrivate() { - return scopedPublicsToPrivates ? scopedPublicsToPrivates : brandToPublicsPrivates.get(classBrand) - }, - - classBrand, - - // we use these to memoize the Public/Protected/Private access - // helper results, to make subsequent accessses faster. - cachedPublicAccesses: new WeakMap(), - cachedProtectedAccesses: new WeakMap(), - cachedPrivateAccesses: new WeakMap(), - } as any - - // create the super helper for this class scope - const supers = new WeakMap() - const Super = superHelper.bind(null, supers, scope) - - // bind this class' scope to the helper functions - const Public = getPublicMembers.bind(null, scope) as any - const Protected = getProtectedMembers.bind(null, scope) as any - const Private = getPrivateMembers.bind(null, scope) as any - - Public.prototype = {} - Protected.prototype = {} - Private.prototype = {} - - // alows the user to destructure arguments to definer functions - Public.Public = Public - Public.Protected = Protected - Public.Private = Private - Public.Super = Super - Protected.Public = Public - Protected.Protected = Protected - Protected.Private = Private - Protected.Super = Super - // Private and Super are never passed as first argument - - // pass the helper functions to the user's class definition function - definition = definition || (definer && definer(Public, Protected, Private, Super)) - - // the user has the option of returning an object that defines which - // properties are public/protected/private. - if (definition && typeof definition !== 'object' && typeof definition !== 'function') { - throw new TypeError(` - The return value of a class definer function, if any, should be - an object, or a class constructor. - `) - } - - // if a function was returned, we assume it is a class from which we - // get the public definition from. - let customClass = null - if (typeof definition === 'function') { - customClass = definition - definition = definition.prototype - ParentClass = customClass.prototype.__proto__.constructor - } - - let staticMembers - - // if functions were provided for the public/protected/private - // properties of the definition object, execute them with their - // respective access helpers, and use the objects returned from them. - if (definition) { - staticMembers = definition.static - delete definition.static - - if (typeof definition.public === 'function') { - definition.public = definition.public(Protected, Private) - } - - if (typeof definition.protected === 'function') { - definition.protected = definition.protected(Public, Private) - } - - if (typeof definition.private === 'function') { - definition.private = definition.private(Public, Protected) - } - } - - ParentClass = ParentClass || Object - - // extend the parent class - const parentPublicPrototype = ParentClass.prototype - const publicPrototype = (definition && definition.public) || definition || Object.create(parentPublicPrototype) - if (publicPrototype.__proto__ !== parentPublicPrototype) publicPrototype.__proto__ = parentPublicPrototype - - // extend the parent protected prototype - const parentProtectedPrototype = getParentProtectedPrototype(parentPublicPrototype) - const protectedPrototype = (definition && definition.protected) || Object.create(parentProtectedPrototype) - if (protectedPrototype.__proto__ !== parentProtectedPrototype) - protectedPrototype.__proto__ = parentProtectedPrototype - publicProtoToProtectedProto.set(publicPrototype, protectedPrototype) - - // private prototype inherits from parent, but each private instance is - // private only for the class of this scope - const parentPrivatePrototype = getParentPrivatePrototype(parentPublicPrototype) - const privatePrototype = (definition && definition.private) || Object.create(parentPrivatePrototype) - if (privatePrototype.__proto__ !== parentPrivatePrototype) privatePrototype.__proto__ = parentPrivatePrototype - publicProtoToPrivateProto.set(publicPrototype, privatePrototype) - - if (!brandToPublicPrototypes.get(classBrand)) brandToPublicPrototypes.set(classBrand, new Set()) - if (!brandToProtectedPrototypes.get(classBrand)) brandToProtectedPrototypes.set(classBrand, new Set()) - if (!brandToPrivatePrototypes.get(classBrand)) brandToPrivatePrototypes.set(classBrand, new Set()) - - brandToPublicPrototypes.get(classBrand).add(publicPrototype) - brandToProtectedPrototypes.get(classBrand).add(protectedPrototype) - brandToPrivatePrototypes.get(classBrand).add(privatePrototype) - - scope.publicPrototype = publicPrototype - scope.privatePrototype = privatePrototype - scope.protectedPrototype = protectedPrototype - scope.parentPublicPrototype = parentPublicPrototype - scope.parentProtectedPrototype = parentProtectedPrototype - scope.parentPrivatePrototype = parentPrivatePrototype - - // the user has the option of assigning methods and properties to the - // helpers that we passed in, to let us know which methods and - // properties are public/protected/private so we can assign them onto - // the respective prototypes. - copyDescriptors(Public.prototype, publicPrototype) - copyDescriptors(Protected.prototype, protectedPrototype) - copyDescriptors(Private.prototype, privatePrototype) - - if (definition) { - // delete these so we don't expose them on the class' public - // prototype - delete definition.public - delete definition.protected - delete definition.private - - // if a `public` object was also supplied, we treat that as the public - // prototype instead of the base definition object, so we copy the - // definition's props to the `public` object - // - // TODO For now we copy from the definition object to the 'public' - // object (publicPrototype), but this won't work with native `super`. - // Maybe later, we can use a Proxy to read props from both the root - // object and the public object, so that `super` works from both. - // Another option is to not allow a `public` object, only protected - // and private - if (definition !== publicPrototype) { - // copy whatever remains - copyDescriptors(definition, publicPrototype) - } - } - - if (customClass) { - if (staticMembers) copyDescriptors(staticMembers, customClass) - return customClass - } - - const userConstructor = publicPrototype.hasOwnProperty('constructor') ? publicPrototype.constructor : null - - let NewClass!: CtorWithSubclass & Function - let newPrototype = null - - // ES5 version (which seems to be so much better) - if (mode === 'es5') { - NewClass = (() => - function (this: any) { - let ret = null - - let constructor = null - - if (userConstructor) constructor = userConstructor - else constructor = ParentClass - - // Object is a special case because otherwise - // `Object.apply(this)` returns a different object and we don't - // want to deal with return value in that case - if (constructor !== Object) ret = constructor.apply(this, arguments) - - if (ret && (typeof ret === 'object' || typeof ret === 'function')) { - // XXX should we set ret.__proto__ = constructor.prototype - // here? Or let the user deal with that? - return ret - } - - return this - })() as unknown as CtorWithSubclass - - newPrototype = publicPrototype - } else { - throw new TypeError(` - The lowclass "mode" option can only be 'es5' for now. - `) - } - - if (className) { - if (nativeNaming) { - const code = getFunctionBody(NewClass) - const proto = NewClass.prototype - - NewClass = new Function( - ` userConstructor, ParentClass `, - ` - return function ${className}() { ${code} } - `, - )(userConstructor, ParentClass) - - NewClass.prototype = proto - } else { - setDescriptor(NewClass, 'name', {value: className}) - } - } - - if (userConstructor && userConstructor.length) { - // length is not writable, only configurable, therefore the value - // has to be set with a descriptor update - setDescriptor(NewClass, 'length', { - value: userConstructor.length, - }) - } - - // static stuff { - - // static inheritance - NewClass.__proto__ = ParentClass - - if (staticMembers) copyDescriptors(staticMembers, NewClass) - - // allow users to make subclasses. When subclass is called on a - // constructor, it defines `this` which is assigned to ParentClass - // above. - setDescriptor(NewClass, 'subclass', { - value: Class, - writable: true, - enumerable: false, - configurable: false, - }) - - // } - - // prototype stuff { - - NewClass.prototype = newPrototype - - NewClass.prototype.constructor = NewClass - - // } - - if (setClassDescriptors) { - setDefaultStaticDescriptors(NewClass, options, staticBlacklist) - setDescriptor(NewClass, 'prototype', {writable: prototypeWritable}) - setDefaultPrototypeDescriptors(NewClass.prototype, options) - setDefaultPrototypeDescriptors(protectedPrototype, options) - setDefaultPrototypeDescriptors(privatePrototype, options) - } - - scope.constructor = NewClass // convenient for debugging - - return NewClass - } -} - -// XXX PERFORMANCE: instead of doing multiple prototype traversals with -// hasPrototype in the following access helpers, maybe we can do a single -// traversal and check along the way? -// -// Worst case examples: -// -// currently: -// If class hierarchy has 20 classes -// If we detect which instance we have in order of public, protected, private -// If the instance we're checking is the private instance of the middle class (f.e. class 10) -// We'll traverse 20 public prototypes with 20 conditional checks -// We'll traverse 20 protected prototypes with 20 conditional checks -// And finally we'll traverse 10 private prototypes with 10 conditional checks -// TOTAL: We traverse over 50 prototypes with 50 conditional checks -// -// proposed: -// If class hierarchy has 20 classes -// If we detect which instance we have in order of public, protected, private -// If the instance we're checking is the private instance of the middle class (f.e. class 10) -// We'll traverse 10 public prototypes with 3 conditional checks at each prototype -// TOTAL: We traverse over 10 prototypes with 30 conditional checks -// BUT: The conditional checking will involve reading WeakMaps instead of -// checking just reference equality. If we can optimize how this part -// works, it might be worth it. -// -// Can the tradeoff (less traversal and conditional checks) outweigh the -// heavier conditional checks? -// -// XXX PERFORMANCE: We can also cache the access-helper results, which requires more memory, -// but will make use of access helpers much faster, especially important for -// animations. - -function getParentProtectedPrototype(parentPublicPrototype: any) { - // look up the prototype chain until we find a parent protected prototype, if any. - - let parentProtectedProto - let currentPublicProto = parentPublicPrototype - - while (currentPublicProto && !parentProtectedProto) { - parentProtectedProto = publicProtoToProtectedProto.get(currentPublicProto) - currentPublicProto = currentPublicProto.__proto__ - } - - // TODO, now that we're finding the nearest parent protected proto, - // we might not need to create an empty object for each class if we - // don't find one, to avoid prototype lookup depth, as we'll connect - // to the nearest one we find, if any. - return parentProtectedProto || {} -} - -function getParentPrivatePrototype(parentPublicPrototype: any) { - // look up the prototype chain until we find a parent protected prototype, if any. - - let parentPrivateProto - let currentPublicProto = parentPublicPrototype - - while (currentPublicProto && !parentPrivateProto) { - parentPrivateProto = publicProtoToPrivateProto.get(currentPublicProto) - currentPublicProto = currentPublicProto.__proto__ - } - - // TODO, now that we're finding the nearest parent protected proto, - // we might not need to create an empty object for each class if we - // don't find one, to avoid prototype lookup depth, as we'll connect - // to the nearest one we find, if any. - return parentPrivateProto || {} -} - -function getPublicMembers(scope: any, instance: any) { - let result = scope.cachedPublicAccesses.get(instance) - - if (result) return result - - // check only for the private instance of this class scope - if (isPrivateInstance(scope, instance)) - scope.cachedPublicAccesses.set(instance, (result = getSubclassScope(instance).publicToPrivate.get(instance))) - // check for an instance of the class (or its subclasses) of this scope - else if (isProtectedInstance(scope, instance)) - scope.cachedPublicAccesses.set(instance, (result = publicToProtected.get(instance))) - // otherwise just return whatever was passed in, it's public already! - else scope.cachedPublicAccesses.set(instance, (result = instance)) - - return result -} - -function getProtectedMembers(scope: any, instance: any) { - let result = scope.cachedProtectedAccesses.get(instance) - - if (result) return result - - // check for an instance of the class (or its subclasses) of this scope - // This allows for example an instance of an Animal base class to access - // protected members of an instance of a Dog child class. - if (isPublicInstance(scope, instance)) - scope.cachedProtectedAccesses.set( - instance, - (result = publicToProtected.get(instance) || createProtectedInstance(instance)), - ) - // check for a private instance inheriting from this class scope - else if (isPrivateInstance(scope, instance)) { - const publicInstance = getSubclassScope(instance).publicToPrivate.get(instance) - scope.cachedProtectedAccesses.set( - instance, - (result = publicToProtected.get(publicInstance) || createProtectedInstance(publicInstance)), - ) - } - - // return the protected instance if it was passed in - else if (isProtectedInstance(scope, instance)) scope.cachedProtectedAccesses.set(instance, (result = instance)) - - if (!result) throw new InvalidAccessError('invalid access of protected member') - - return result -} - -function getSubclassScope(privateInstance: any) { - return privateInstanceToClassScope.get(privateInstance) -} - -function createProtectedInstance(publicInstance: any) { - // traverse instance proto chain, find first protected prototype - const protectedPrototype = findLeafmostProtectedPrototype(publicInstance) - - // make the protected instance from the found protected prototype - const protectedInstance = Object.create(protectedPrototype) - publicToProtected.set(publicInstance, protectedInstance) - return protectedInstance -} - -function findLeafmostProtectedPrototype(publicInstance: any) { - let result = null - let currentProto = publicInstance.__proto__ - - while (currentProto) { - result = publicProtoToProtectedProto.get(currentProto) - if (result) return result - currentProto = currentProto.__proto__ - } - - return result -} - -function getPrivateMembers(scope: any, instance: any) { - let result = scope.cachedPrivateAccesses.get(instance) - - if (result) return result - - // check for a public instance that is or inherits from this class - if (isPublicInstance(scope, instance)) - scope.cachedPrivateAccesses.set( - instance, - (result = scope.publicToPrivate.get(instance) || createPrivateInstance(scope, instance)), - ) - // check for a protected instance that is or inherits from this class' - // protectedPrototype - else if (isProtectedInstance(scope, instance)) { - const publicInstance = publicToProtected.get(instance) - scope.cachedPrivateAccesses.set( - instance, - (result = scope.publicToPrivate.get(publicInstance) || createPrivateInstance(scope, publicInstance)), - ) - } - - // return the private instance if it was passed in - else if (isPrivateInstance(scope, instance)) scope.cachedPrivateAccesses.set(instance, (result = instance)) - - if (!result) throw new InvalidAccessError('invalid access of private member') - - return result -} - -function createPrivateInstance(scope: any, publicInstance: any) { - const privateInstance = Object.create(scope.privatePrototype) - scope.publicToPrivate.set(publicInstance, privateInstance) - privateInstanceToClassScope.set(privateInstance, scope) // TODO use WeakTwoWayMap - return privateInstance -} - -function isPublicInstance(scope: any, instance: any, brandedCheck = true) { - if (!brandedCheck) return hasPrototype(instance, scope.publicPrototype) - - for (const proto of Array.from(brandToPublicPrototypes.get(scope.classBrand))) { - if (hasPrototype(instance, proto)) return true - } - - return false -} - -function isProtectedInstance(scope: any, instance: any, brandedCheck = true) { - if (!brandedCheck) return hasPrototype(instance, scope.protectedPrototype) - - for (const proto of Array.from(brandToProtectedPrototypes.get(scope.classBrand))) { - if (hasPrototype(instance, proto)) return true - } - - return false -} - -function isPrivateInstance(scope: any, instance: any, brandedCheck = true) { - if (!brandedCheck) return hasPrototype(instance, scope.privatePrototype) - - for (const proto of Array.from(brandToPrivatePrototypes.get(scope.classBrand))) { - if (hasPrototype(instance, proto)) return true - } - - return false -} - -function superHelper(supers: any, scope: any, instance: any) { - const {parentPublicPrototype, parentProtectedPrototype, parentPrivatePrototype} = scope - - if (isPublicInstance(scope, instance, false)) return getSuperHelperObject(instance, parentPublicPrototype, supers) - - if (isProtectedInstance(scope, instance, false)) - return getSuperHelperObject(instance, parentProtectedPrototype, supers) - - if (isPrivateInstance(scope, instance, false)) return getSuperHelperObject(instance, parentPrivatePrototype, supers) - - throw new InvalidSuperAccessError('invalid super access') -} - -function getSuperHelperObject(instance: any, parentPrototype: any, supers: any) { - let _super = supers.get(instance) - - // XXX PERFORMANCE: there's probably some ways to improve speed here using caching - if (!_super) { - supers.set(instance, (_super = Object.create(parentPrototype))) - - const keys = getInheritedPropertyNames(parentPrototype) - let i = keys.length - - while (i--) { - const key = keys[i] - - setDescriptor( - _super, - key, - { - get: function () { - let value: any = void undefined - - const descriptor = getInheritedDescriptor(parentPrototype, key) - - if (descriptor && propertyIsAccessor(descriptor)) { - const getter = descriptor.get - if (getter) value = getter.call(instance) - } else { - value = parentPrototype[key] - } - - if (value && value.call && typeof value === 'function') { - value = value.bind(instance) - } - - return value - }, - - // like native `super`, setting a super property does nothing. - set: function (value) { - const descriptor = getInheritedDescriptor(parentPrototype, key) - - if (descriptor && propertyIsAccessor(descriptor)) { - const setter = descriptor.set - if (setter) value = setter.call(instance, value) - } else { - // just like native `super` - instance[key] = value - } - }, - }, - true, - ) - } - } - - return _super -} - -export default Class - -type CtorWithSubclass = Constructor< - object, - any[], - { - subclass: Constructor - __proto__: CtorWithSubclass - } -> diff --git a/src/Mixin.ts b/src/Mixin.ts index 0e37e32..c738085 100644 --- a/src/Mixin.ts +++ b/src/Mixin.ts @@ -1,8 +1,6 @@ // TODO no any types // TODO no @ts-ignore -import Class from './Class.js' - import type {Constructor} from './utils.js' // export type MixinFunction = (BaseClass: T) => T @@ -22,7 +20,7 @@ export function Mixin(mixinFn: T, DefaultBase?: Constru // @ts-ignore TS v4 introduced a type error mixinFn = Dedupe(mixinFn) // @ts-ignore TS v4 introduced a type error - mixinFn = WithDefault(mixinFn, DefaultBase || Class()) + mixinFn = WithDefault(mixinFn, DefaultBase || class {}) mixinFn = ApplyDefault(mixinFn) // @ts-ignore diff --git a/src/WIP-test-lowclass-types.ts.off b/src/WIP-test-lowclass-types.ts.off deleted file mode 100644 index dfa0aa9..0000000 --- a/src/WIP-test-lowclass-types.ts.off +++ /dev/null @@ -1,152 +0,0 @@ -import Class from 'lowclass' - -const Animal = Class('Animal', { - sound: '', - constructor(sound: string) { - this.sound = sound - }, - makeSound() { - console.log(this.sound) - }, -}) - -const a = new Animal('') - -a.makeSound() - -const Dog = Class('Dog').extends(Animal, ({ Super }) => ({ - constructor(size: 'small' | 'big') { - if (size === 'small') Super(this).constructor('woof') - if (size === 'big') Super(this).constructor('WOOF') - }, - - // makeSound(d: number) { - // console.log(this.sound, d) - // }, - makeSound() { - Super(this).makeSound() - console.log(this.sound) - }, - - bark() { - this.makeSound() - }, - other() { - this.bark() - }, -})) -type Dog = InstanceType - -const smallDog: Dog = new Dog('small') -smallDog.bark() // "woof" - -const bigDog = new Dog('big') -bigDog.bark() // "WOOF" - -bigDog.bark() -bigDog.makeSound() - -const Foo = Class('Foo', ({ Public, Protected, Private }) => ({ - constructor(s: string) { - console.log(s) - }, - - static: { - staticProp: 123, - - staticMethod() { - console.log(Foo.staticProp) // 123 - }, - }, - - publicProp: 'blah', - - publicMethod(a: string) { - // TODO TS, this should work like Foo.staticMethod() - // this.constructor.staticMethod() - - console.log(a) - }, - - protected: { - protectedProp: 456, - - protectedMethod(a: string) { - console.log(a) - console.log(Protected(this).protectedProp) - }, - }, - - private: { - privateProp: 789, - - privateMethod() { - Public(this).publicProp - Public(this).publicMethod('') // Should this be allowed? Or do we just the below line to work ? - Public(this).publicMethod('') - }, - }, - - test() { - this.publicMethod('') - - console.log(this.publicProp) // 'blah' - - let p = Protected(this) - console.log(p) - p.protectedMethod('') - console.log(p.protectedProp) // 456 - - Private(this).privateMethod() - console.log(Private(this).privateProp) // 789 - return Private(this).privateProp - }, -})) - -let foo = new Foo('') - -console.log(foo.publicProp) -// console.log(foo.protectedProp) -// console.log(foo.privateProp) - -Foo.staticMethod() -Foo.staticProp - -const Bar = Class('Bar').extends(Foo, ({ Public, Protected, Private, Super }) => ({ - constructor(sound: string) { - sound - }, - static: { - derivedStatic: 10, - }, - private: { - derivedPrivate: 10, - }, - protected: { - derivedProtected: 10, - }, - derivedPublicMethod() { - Protected(this).protectedMethod('') - Protected(this).protectedProp - Protected(this).derivedProtected - Private(this).derivedPrivate - this.test() - this.test() - }, - test() { - const b = {} - - // TODO TS This call should only allow `Super(this)` or `Super(Protected(this))`. - Super(b).test() - - return Super(this).test() - }, -})) - -var bar = new Bar('') -bar.derivedPublicMethod() -bar.test() -// bar.derivedPrivate // should be error -Bar.derivedStatic - -Bar.staticMethod() diff --git a/src/index.ts b/src/index.ts index 1492b42..f0d3ca8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,3 @@ -// the bread and butter -export * from './Class.js' -import Class from './Class.js' -export default Class - // mix and match your classes! export * from './multiple.js' export * from './Mixin.js' diff --git a/src/tests/Counter.js b/src/tests/Counter.js deleted file mode 100644 index e8166cf..0000000 --- a/src/tests/Counter.js +++ /dev/null @@ -1,41 +0,0 @@ -// show how to do something similar to "friend" in C++ or "package protected" -// in Java, using intentionally leaked access helpers - -import Class from '../index.js' - -let CounterProtected - -const Counter = Class(({Private, Protected}) => { - // leak the Counter class Protected helper to outer scope - CounterProtected = Protected - - return { - value() { - return Private(this).count - }, - - private: { - count: 0, - }, - - protected: { - increment() { - Private(this).count++ - }, - }, - } -}) - -// note how Incrementor does not extend from Counter -const Incrementor = Class(({Private}) => ({ - constructor(counter) { - Private(this).counter = counter - }, - - increment() { - const counter = Private(this).counter - CounterProtected(counter).increment() - }, -})) - -export {Counter, Incrementor} diff --git a/src/tests/Counter2.js b/src/tests/Counter2.js deleted file mode 100644 index 895a8c1..0000000 --- a/src/tests/Counter2.js +++ /dev/null @@ -1,48 +0,0 @@ -// show how to do something similar to "friend" in C++ or "package protected" -// in Java, using intentionally shared class brands - -import Class from '../index.js' - -// an empty object used as a brand key by the Class() helper -// -// NOTE Too bad Symbols aren't supported by WeakMaps, otherwise we could use a -// Symbol here, which would be cleaner. -let FriendKey = {} - -const Counter2 = Class( - 'Counter2', - ({Private}) => ({ - value() { - return Private(this).count - }, - - private: { - count: 0, - }, - - protected: { - increment() { - Private(this).count++ - }, - }, - }), - FriendKey, -) - -// note how Incrementor2 does not extend from Counter2 -const Incrementor2 = Class( - 'Incrementor2', - ({Private, Protected}) => ({ - constructor(counter) { - Private(this).counter = counter - }, - - increment() { - const counter = Private(this).counter - Protected(counter).increment() - }, - }), - FriendKey, -) - -export {Counter2, Incrementor2} diff --git a/src/tests/basics.test.js b/src/tests/basics.test.js deleted file mode 100644 index ef47c7c..0000000 --- a/src/tests/basics.test.js +++ /dev/null @@ -1,1062 +0,0 @@ -import {Class, InvalidAccessError, InvalidSuperAccessError} from '../index.js' - -import {native} from '../native.js' - -const test = it - -describe('basics', () => { - test('only public members can be read/written from outside code', () => { - const Dog = Class('Dog', ({Protected, Private}) => ({ - setFoo() { - this.foo = 'woo hoo' - }, - checkFoo() { - expect(this.foo === 'weee').toBeTruthy() - }, - setBar() { - Protected(this).bar = 'yippee' - }, - checkBar() { - expect(Protected(this).bar === 'yippee').toBeTruthy() - }, - setBaz() { - Private(this).baz = 'oh yeah' - }, - checkBaz() { - expect(Private(this).baz === 'oh yeah').toBeTruthy() - }, - })) - - const dog = new Dog() - dog.setFoo() - dog.foo = 'weee' - expect(dog.foo === 'weee').toBeTruthy() - dog.checkFoo() - - dog.bar = 'yoohoo' - dog.setBar() - expect(dog.bar === 'yoohoo').toBeTruthy() - dog.checkBar() - - dog.baz = 'hee hee' - dog.setBaz() - expect(dog.baz === 'hee hee').toBeTruthy() - dog.checkBaz() - }) - - test('we should not be able to access protected members from an unrelated class', () => { - const Dog = Class('Dog', ({Protected}) => { - Protected.prototype.sound = 'Woof!' - }) - - const UnrelatedClass = Class(function UnrelatedClass(Public, Protected) { - Public.prototype.testInvalidAccess = function () { - const dog = new Dog() - console.log('should fail:', Protected(dog).sound) - } - }) - - let u = new UnrelatedClass() - - expect(() => { - u.testInvalidAccess() - }).toThrowError(InvalidAccessError) - }) - - test('we should not be able to access private members from an unrelated class', () => { - const Dog = Class('Dog', ({Private}) => { - Private.prototype.breed = 'labrador' - }) - - const UnrelatedClass = Class(function UnrelatedClass({Public, Private}) { - Public.prototype.testInvalidAccess = function () { - const dog = new Dog() - console.log('should fail:', Private(dog).breed) - } - }) - - const u = new UnrelatedClass() - - expect(() => { - u.testInvalidAccess() - }).toThrowError(InvalidAccessError) - }) - - test('we can access a child class protected member from a super class', () => { - const Animal = Class('Animal', ({Protected}) => ({ - getDogSound: function talk() { - const dog = new Dog() - return Protected(dog).sound - }, - })) - - const Dog = Animal.subclass('Dog', ({Protected}) => { - Protected.prototype.sound = 'Woof!' - }) - - const animal = new Animal() - const dogSound = animal.getDogSound() - - expect(dogSound === 'Woof!').toBeTruthy() - }) - - test('we can access a super class protected member from a child class', () => { - const Animal = Class('Animal', ({Protected}) => { - Protected.prototype.alive = true - }) - - const Dog = Animal.subclass('Dog', ({Protected}) => ({ - isAlive() { - return Protected(this).alive - }, - })) - - const dog = new Dog() - expect(dog.isAlive() === true).toBeTruthy() - }) - - test('we can not access a private member of an instance of a child class from a parent class instance', () => { - let AnimalPrivate = null - - const Animal = Class('Animal', ({Private}) => { - AnimalPrivate = Private - - return { - public: { - foo: function talk() { - const dog = new Dog() - - // like in C++, accessing the private variable of a child class does not work. - expect(Private(dog).sound === undefined).toBeTruthy() - - // like in C++, we can only access the private members associated with the class that we are currently in: - expect(Private(dog).bar === 'BAR').toBeTruthy() - - Private(dog).sound = 'Awoooo!' - dog.verifySound() - dog.changeSound() - expect(Private(dog).sound === 'Awoooo!').toBeTruthy() - - Private(dog).bar = 'of soap' - dog.checkBar() // dog's is still "BAR" - - Private(this).bar = 'of soap' - dog.checkBar() // dog's is still "BAR" - - dog.exposePrivate() - expect(Private(dog) !== dogPrivate).toBeTruthy() - expect(Private(this) !== dogPrivate).toBeTruthy() - expect(Private(this) !== Private(dog)).toBeTruthy() - }, - }, - - private: { - bar: 'BAR', - }, - } - }) - - let dogPrivate = null - - const Dog = Animal.subclass(function Dog({Public, Private}) { - Private.prototype.sound = 'Woof!' - Public.prototype.verifySound = function () { - expect(Private(this).sound === 'Woof!').toBeTruthy() - } - Public.prototype.changeSound = function () { - Private(this).sound = 'grrr!' - } - Public.prototype.checkBar = function () { - // the private instance for the Dog class is not the same instance a for the Animal class - expect(Private(this) !== AnimalPrivate(this)).toBeTruthy() - - // private bar was inherited, but the instance is still private - expect(Private(this).bar === 'BAR').toBeTruthy() - - // and therefore this value is different, because it's a different instance - expect(AnimalPrivate(this).bar === 'of soap').toBeTruthy() - } - Public.prototype.exposePrivate = function () { - dogPrivate = Private(this) - } - }) - - const animal = new Animal('Ranchuu') - animal.foo() - }) - - test('we can not access a private member of an instance of a parent class from a child class instance', () => { - const Animal = Class('Animal', ({Private}) => ({ - private: { - bar: 'BAR', - }, - changeBar() { - Private(this).bar = 'hokey pokey' - }, - })) - - const Dog = Animal.subclass(function Dog({Public, Private}) { - Private.prototype.sound = 'Woof!' - Public.prototype.foo = function () { - // we should not be able to access Animal's private bar property - this.changeBar() // changed Animal's private bar property - - // 'BAR' is inherited, and is unique to Dog code, so the value is - // not 'hokey pokey' - expect(Private(this).bar === 'BAR').toBeTruthy() - - expect(this.bar === undefined).toBeTruthy() - } - }) - - const dog = new Dog() - dog.foo() - }) - - test('further example, private members are isolated to their classes', () => { - const Animal = Class('Animal', ({Private}) => ({ - public: { - test: function () { - const dog = new Dog() - dog.bar = 'bar' - dog.setBar() - expect(Private(this).bar === 'oh yeah').toBeTruthy() - Private(this).bar = 'mmm hmmm' - dog.checkBar() - expect(Private(this).bar === 'mmm hmmm').toBeTruthy() - }, - }, - - private: { - bar: 'oh yeah', - }, - })) - - const Dog = Animal.subclass('Dog', ({Private}) => ({ - setBar: function () { - Private(this).bar = 'yippee' - }, - checkBar: function () { - expect(Private(this).bar === 'yippee').toBeTruthy() - Private(this).bar = 'woohoo' - }, - })) - - const animal = new Animal() - animal.foo = 'foo' - animal.test() - }) - - const publicAccesses = [] - const protectedAccesses = [] - const privateAccesses = [] - - let SomeClassPrivate - let someClassPublicInstance - let someClassProtectedInstance - let someClassPrivateInstance - - let foundPrivate - - const SomeClass = Class('SomeClass', (Public, Protected, Private) => { - SomeClassPrivate = Private - - return { - foo: 'foo', - - constructor() { - someClassPublicInstance = this - someClassProtectedInstance = Protected(this) - someClassPrivateInstance = Private(this) - }, - - publicMethod: function () { - expect(this === Public(this)).toBeTruthy() - expect(this.foo === Public(this).foo).toBeTruthy() - - expect(this.foo === 'foo').toBeTruthy() - expect(Public(this).foo === 'foo').toBeTruthy() - expect(Protected(this).bar === 'bar').toBeTruthy() - expect(Private(this).baz === 'baz').toBeTruthy() - - expect(Public(this) !== Protected(this)).toBeTruthy() - expect(Public(this) !== Private(this)).toBeTruthy() - expect(Protected(this) !== Private(this)).toBeTruthy() - - publicAccesses.push(Public(this)) - protectedAccesses.push(Protected(this)) - privateAccesses.push(Private(this)) - - Protected(this).protectedMethod() - }, - - protected: { - bar: 'bar', - - protectedMethod: function () { - expect(this === Protected(this)).toBeTruthy() - expect(this.bar === Protected(this).bar).toBeTruthy() - - expect(this.bar === 'bar').toBeTruthy() - expect(Public(this).foo === 'foo').toBeTruthy() - expect(Protected(this).bar === 'bar').toBeTruthy() - expect(Private(this).baz === 'baz').toBeTruthy() - - expect(Protected(this) !== Public(this)).toBeTruthy() - expect(Protected(this) !== Private(this)).toBeTruthy() - expect(Public(this) !== Private(this)).toBeTruthy() - - publicAccesses.push(Public(this)) - protectedAccesses.push(Protected(this)) - privateAccesses.push(Private(this)) - - // this is calling SomeClass.privateMethod in the scope of SomeClass - Private(this).privateMethod() - }, - }, - - private: { - baz: 'baz', - - privateMethod: function () { - foundPrivate = Private(this) - expect(this === Private(this)).toBeTruthy() - expect(this.baz === Private(this).baz).toBeTruthy() - - expect(this.baz === 'baz').toBeTruthy() - expect(Public(this).foo === 'foo').toBeTruthy() - expect(Protected(this).bar === 'bar').toBeTruthy() - expect(Private(this).baz === 'baz').toBeTruthy() - - expect(Private(this) !== Public(this)).toBeTruthy() - expect(Private(this) !== Protected(this)).toBeTruthy() - expect(Public(this) !== Protected(this)).toBeTruthy() - - publicAccesses.push(Public(this)) - protectedAccesses.push(Protected(this)) - privateAccesses.push(Private(this)) - }, - }, - } - }) - - test('check that various ways to access public/protected/private members work inside a single base class', () => { - const o = new SomeClass() - o.publicMethod() - - expect(o.protectedMethod === undefined).toBeTruthy() - expect(o.privateMethod === undefined).toBeTruthy() - - expect(publicAccesses.length === 3).toBeTruthy() - expect(protectedAccesses.length === 3).toBeTruthy() - expect(privateAccesses.length === 3).toBeTruthy() - - expect(publicAccesses.every((instance, i, accesses) => instance === accesses[0])).toBeTruthy() - expect(protectedAccesses.every((instance, i, accesses) => instance === accesses[0])).toBeTruthy() - expect(privateAccesses.every((instance, i, accesses) => instance === accesses[0])).toBeTruthy() - - publicAccesses.length = 0 - protectedAccesses.length = 0 - privateAccesses.length = 0 - }) - - test('check that various ways to access public/protected/private members work inside a subclass', () => { - let subClassPublicInstance - let subClassProtectedInstance - let subClassPrivateInstance - - const SubClass = SomeClass.subclass(({Super, Private, Protected}) => ({ - constructor() { - Super(this).constructor() - - subClassPublicInstance = this - subClassProtectedInstance = Protected(this) - subClassPrivateInstance = Private(this) - }, - - publicMethod() { - Super(this).publicMethod() - }, - - protected: { - protectedMethod() { - Super(this).protectedMethod() - Private(this).privateMethod() - }, - }, - - private: { - privateMethod() { - // Private Inheritance! - // - // This is calling SomeClass.privateMethod in the scope of - // SubClass, so any operations on private members will be - // on the private members of SubClass (members which have - // been in herited from SomeClass). - Super(this).privateMethod() - - // This helps explain the magic regarding Private Inheritance - // - // this proves that private functionality works like - // `private` in C++, except that functionality can be - // inherited, and the inherited functionality operates on - // the private data of the class that initiated the method - // call (in this case SubClass initiated the call to - // SomeClass.privateMethod with Super(this).privateMethod(), so if - // SomeClass.privateMethod modifies any private data, it - // will modify the data associated with SubClass, not - // SomeClass). - expect(this).toBe(SomeClassPrivate(this)) - expect(this).not.toBe(someClassPrivateInstance) - - // (Just in case you didn't realize yet, `this` is - // equivalent to `Private(this)` in a private method) - expect(this).toBe(Private(this)) - }, - }, - })) - - const o = new SubClass() - o.publicMethod() - - expect(publicAccesses.length === 4).toBeTruthy() - expect(protectedAccesses.length === 4).toBeTruthy() - expect(privateAccesses.length === 4).toBeTruthy() - - expect(publicAccesses.every(instance => instance === subClassPublicInstance)).toBeTruthy() - expect(protectedAccesses.every(instance => instance === subClassProtectedInstance)).toBeTruthy() - - // this is where things diverge from the previous baseclass test, - // giving you a hint at how Private Inheritance works - // - // the first time SomeClass.privateMethod is called, it is called in - // the scope of SomeClass, so Private(this) in that method refers to - // the private members of SomeClass. - privateAccesses.slice(0, 3).forEach(instance => expect(instance).toBe(someClassPrivateInstance)) - // - // and the second time SomeClass.privateMethod is called, it is called - // in the scope of SubClass (as Super(this).privateMethod()) so in this - // case Private(this) in that method refers to the private members of - // SubClass, and if the method modifies any data, it will modify data - // associated with SubClass, not SomeClass) - expect(privateAccesses[3] === subClassPrivateInstance) - - publicAccesses.length = 0 - protectedAccesses.length = 0 - privateAccesses.length = 0 - }) - - test('Show how Super works with private members (Private Inheritance)', () => { - const Foo = Class(({Private, Protected, Public}) => ({ - fooThought() { - return Private(this).thought - }, - - modifyPrivateDataInFoo() { - Private(this).think('hmmmmm') - }, - - private: { - thought: 'weeeee', - - think(value) { - this.thought = value - }, - }, - })) - - const Bar = Class().extends(Foo, ({Private, Super}) => ({ - barThought() { - return Private(this).thought - }, - - modifyPrivateDataInBar() { - Private(this).thinkAgain('yeaaaaah') - }, - - private: { - // Thought you knew private members? Think again! - thinkAgain(value) { - // code re-use, but modifies data of Bar class, not Foo class - Super(this).think(value) - }, - }, - })) - - const b = new Bar() - - // shows that the initial private value of `thought` in Bar is - // inherited from Foo - expect(b.fooThought()).toBe('weeeee') - expect(b.barThought()).toBe('weeeee') - - b.modifyPrivateDataInFoo() - b.modifyPrivateDataInBar() - - // the private member in Foo hasn't changed: - expect(b.fooThought()).toBe('hmmmmm') - - // but the private member in Bar has: - expect(b.barThought()).toBe('yeaaaaah') - - // native `super` works too: - const Baz = Class().extends(Bar, ({Super}) => ({ - private: { - think() { - super.think() - }, - }, - })) - - const baz = new Baz() - - expect(baz.fooThought()).toBe('weeeee') - expect(baz.barThought()).toBe('weeeee') - - baz.modifyPrivateDataInFoo() - baz.modifyPrivateDataInBar() - - // oh yes! This is great! - expect(baz.fooThought()).toBe('hmmmmm') - expect(baz.barThought()).toBe('yeaaaaah') - }) - - test('double check: super spaghetti soup works', () => { - // I like super spaghetti soup. - - const SomeClass = Class(({Protected, Private}) => ({ - // default access is public, like C++ structs - publicMethod() { - Protected(this).protectedMethod() - }, - - checkPrivateProp() { - expect(Private(this).lorem === 'foo').toBeTruthy() - }, - - protected: { - protectedMethod() { - Private(this).lorem = 'foo' - }, - }, - - private: { - lorem: 'blah', - }, - })) - - const SubClass = Class().extends(SomeClass, ({Private, Super}) => ({ - publicMethod() { - Super(this).publicMethod() - Private(this).lorem = 'baaaaz' - this.checkPrivateProp() - }, - - checkPrivateProp() { - Super(this).checkPrivateProp() - expect(Private(this).lorem === 'baaaaz').toBeTruthy() - }, - - protected: { - protectedMethod() { - Super(this).protectedMethod() - }, - }, - - private: { - lorem: 'bar', - }, - })) - - const GrandChildClass = SubClass.subclass((Public, Protected, Private, Super) => ({ - test() { - Private(this).begin() - }, - - reallyBegin() { - Protected(this).reallyReallyBegin() - }, - - protected: { - reallyReallyBegin() { - Super(Public(this)).publicMethod() - }, - }, - - private: { - begin() { - Public(this).reallyBegin() - }, - }, - })) - - const o = new GrandChildClass() - o.test() - - expect(typeof o.test === 'function').toBeTruthy() - expect(o.reallyReallyBegin === undefined).toBeTruthy() - expect(o.begin === undefined).toBeTruthy() - }) - - test('static members and static inheritance', () => { - // only public access for static members for now (TODO protected/private) - - const Car = Class({ - wheels: [1, 2, 3, 4], - static: { - isCar(obj) { - return obj.wheels.length === 4 - }, - }, - }) - - const car = new Car() - expect(Car.isCar(car)).toBeTruthy() - - const Buggy = Class().extends(Car) - - const buggy = new Car() - expect(Car.isCar(buggy)).toBeTruthy() - - // inheritance - const DuneBuggy = Class().extends(Buggy) - expect(DuneBuggy.isCar(buggy)).toBe(true) - }) - - test("implicitly extending Object doesn't inherit static members, like ES6 classes", () => { - // extends Object by default, but doesn't inherit static features, - // similar to ES6 `class {}` - const Lorem = Class() - const l = new Lorem() - - // we have Object prototype methods - expect(l instanceof Object && typeof l.hasOwnProperty === 'function').toBeTruthy() - - // but not static methods - expect(typeof Lorem.create === 'undefined').toBeTruthy() - }) - - test('explicitly extending Object inherits static members, like ES6 classes', () => { - // extending Object directly inherits static features, similar to - // `class extends Object {}` - const Lorem = Class().extends(Object) - const l = new Lorem() - - // we have Object prototype methods - expect(l instanceof Object && typeof l.hasOwnProperty === 'function').toBeTruthy() - - // and static methods - expect(typeof Lorem.create === 'function').toBeTruthy() - }) - - test('make sure generated constructor has same `.length` as the supplied constructor', () => { - const Foo = Class({ - constructor(a, b, c, d) {}, - }) - - expect(Foo.length === 4).toBeTruthy() - }) - - test("make sure calling a super method that isn't on the direct parent class works", () => { - // (f.e. a grand parent method will be called if the parent class doesn't - // have the method) - - const Foo = Class({ - method() { - return 'it works' - }, - }) - - const Bar = Class().extends(Foo) - - const Baz = Class().extends(Bar, ({Super}) => ({ - test() { - return Super(this).method() - }, - })) - - const b = new Baz() - - expect(b.test() === 'it works').toBeTruthy() - }) - - test('make sure getters/setters work', () => { - const Foo = Class(({Protected}) => ({ - get foo() { - return Protected(this).foo - }, - set foo(value) { - Protected(this).foo = value - }, - })) - - const f = new Foo() - - f.foo = 1 - - expect(f.foo === 1).toBeTruthy() - - const Bar = Class().extends(Foo, { - test() { - this.foo = 10 - return this.foo - }, - }) - - const bar = new Bar() - - expect(bar.test() === 10).toBeTruthy() - - const Baz = Class().extends(Foo, ({Super}) => ({ - test() { - Super(this).foo = 20 - return Super(this).foo - }, - })) - - const baz = new Baz() - - expect(baz.test() === 20).toBeTruthy() - - let count = 0 - - const Lorem = Class().extends(Foo, ({Super, Protected}) => ({ - get foo() { - count++ - return Super(this).foo - }, - set foo(value) { - count++ - Super(this).foo = value - }, - protectedFoo() { - return Protected(this).foo - }, - })) - - const l = new Lorem() - - l.foo = 15 - expect(l.foo === 15).toBeTruthy() - expect(count === 2).toBeTruthy() - expect(l.protectedFoo() === 15).toBeTruthy() - - const Ipsum = Class().extends(Lorem, (Public, Protected) => ({ - protected: { - get bar() { - return Public(this).foo * 2 - }, - set bar(value) { - Public(this).foo = value - }, - }, - - test() { - Protected(this).bar = 50 - return Protected(this).bar - }, - })) - - const i = new Ipsum() - - i.foo = 33 - expect(i.foo === 33).toBeTruthy() - expect(count === 4).toBeTruthy() - expect(i.test() === 100).toBeTruthy() - expect(i.protectedFoo() === 50).toBeTruthy() - }) - - test('show that the protected instance in different code of a class hierarchy are the same instance', () => { - let fooProtectedGetter - let fooProtected - const Foo = Class((Public, Protected) => { - fooProtectedGetter = Protected - Protected.prototype.foo = 'foo' - Public.prototype.constructor = function () { - fooProtected = Protected(this) - } - }) - - let barProtectedGetter - let barProtected - const Bar = Class().extends(Foo, ({Super, Public, Protected}) => { - barProtectedGetter = Protected - Protected.prototype.bar = 'bar' - Public.prototype.constructor = function () { - Super(this).constructor() - barProtected = Protected(this) - } - Public.prototype.test = function () { - const f = new Foo() - Protected(f) - } - }) - - expect(fooProtectedGetter !== barProtectedGetter).toBeTruthy() - - const f = new Foo() - const b = new Bar() - - expect(fooProtected === barProtected).toBeTruthy() - expect(fooProtectedGetter(b) === barProtectedGetter(b)).toBeTruthy() - - expect(() => { - b.test() - }).toThrowError(InvalidAccessError) - }) - - test('valid vs invalid Super access', () => { - //const verifyDimensionCall = jest.fn() - const verifyDimensionCall = jasmine.createSpy() - - // PhysicalObject implicitly extends from Object (no pun intended!): - const PhysicalObject = Class({ - getDimensions() { - // see below - expect(this instanceof Piano).toBeTruthy() - - verifyDimensionCall() - }, - }) - - const Instrument = Class().extends(PhysicalObject, ({Super}) => ({ - sound: '', - - makeSound() { - return this.sound - }, - - testFromInstrumentClass() { - const piano = new Piano() - - // This Super call works because piano is instance of - // Instrument, but the Super will be relative to this class - // (Instrument). Because Instrument inherits from - // PhysicalObject, calling `Super(piano)` will give you access - // to PhysicalObject properties and methods with piano as - // context. - // - // Who knows, there might be some interesting use case for - // being able to call super on some other instance, - // something that we can't do with native `super`, and this - // doesn't break the protected or private API access - // contracts. - // - expect(Super(piano).makeSound).toBe(undefined) - Super(piano).getDimensions() - - // Do you want a super piano? - }, - })) - - const Piano = Class().extends(Instrument, { - sound: 'ping', // how do you describe piano sound? - }) - - const Oboe = Class().extends(Instrument, ({Super}) => ({ - sound: 'wooo', // or an oboe sound? - - testFromOboeClass() { - const piano = new Piano() - - expect(() => { - // fails because piano isn't an instance of Oboe, so there - // isn't any set of super props/methods for piano based on - // the scope of the Oboe class. - Super(piano).makeSound() - }).toThrowError(InvalidSuperAccessError) - - // wish I had a super piano. - - let sound - - expect(() => { - sound = Super(this).makeSound() - }).not.toThrow() - - // Oboes are already super though! - - return sound - }, - })) - - const oboe = new Oboe() - - oboe.testFromInstrumentClass() - expect(verifyDimensionCall).toHaveBeenCalled() - - expect(oboe.testFromOboeClass()).toBe('wooo') - }) - - // Based on an example that's been floating around, f.e. at - // https://stackoverflow.com/a/11199220/454780 and other places - test("there's no recursive problem, using Super helper", () => { - const A = Class({ - foo: function (n) { - return n - }, - }) - - const B = A.subclass(({Super}) => ({ - foo: function (n) { - if (n > 100) return -1 - return Super(this).foo(n + 1) - }, - })) - - const C = B.subclass(({Super}) => ({ - foo: function (n) { - return Super(this).foo(n + 2) - }, - })) - - var c = new C() - expect(c.foo(0) === 3).toBeTruthy() - }) - - // Slightly modifying the previous example, using builtin `super` also works - // (for ES6+ environments) - test("there's no recursive problem, using native super", () => { - const A = Class({ - foo: function (n) { - return n - }, - }) - - const B = A.subclass({ - foo(n) { - if (n > 100) return -1 - return super.foo(n + 1) - }, - }) - - const C = B.subclass({ - foo(n) { - return super.foo(n + 2) - }, - }) - - var c = new C() - expect(c.foo(0) === 3).toBeTruthy() - }) - - test('performing tricks with leaked access helpers', () => { - // If we leak the access helper as follows, we can export imagine that we - // can export multiple classes from a module file, so the members are still - // private, but acecssible to all the classes in the file. We could call - // this pattern "module private". - // - // What could be a use case for this? It may be similar to package scope in - // Java. - - let fooPrivate - - const Foo = Class(({Private}) => { - fooPrivate = Private - - Private.prototype.foo = 'foo' - }) - - const Bar = Foo.subclass(({Private}) => ({ - test() { - expect(fooPrivate(this).foo === 'foo') // "foo".toBeTruthy() - expect(Private(this).foo === 'bar') // "bar".toBeTruthy() - }, - private: { - foo: 'bar', - }, - })) - - const bar = new Bar() - bar.test() - }) - - test("'private' and 'protected' definition objects should not be left on the 'public' definition object", () => { - // Make sure that the 'private' and 'protected' definition objects (when - // defining a class, f.e. `protected: { ... }`) are not visible on the - // 'public' prototype - - const definition = { - foo: 'foo', - protected: { - bar: 'bar', - }, - private: { - baz: 'baz', - }, - } - - const Foo = Class(definition) - - // lowclass uses the class definition as the class prototype directly (this - // allows `super` to work in ES6+ environments) - expect(Foo.prototype === definition).toBeTruthy() - - // lowclass also uses the protected and private sub-objects as the internal - // protected and private prototypes as well, but they shouldn't be visible - // on the public prototype: - expect(typeof definition.protected === 'undefined').toBeTruthy() - expect(typeof Foo.prototype.protected === 'undefined').toBeTruthy() - expect(typeof definition.private === 'undefined').toBeTruthy() - expect(typeof Foo.prototype.private === 'undefined').toBeTruthy() - - // prove the previous comment about directly using protected and private - // sub-objects as prototypes is true { - - const protectedDefinition = {bar: 'bar'} - const privateDefinition = {baz: 'baz'} - - const Bar = Class((Public, Protected, Private) => ({ - foo: 'foo', - - test() { - return [Protected(this), Private(this)] - }, - - protected: protectedDefinition, - private: privateDefinition, - })) - - const b = new Bar() - - expect(b.test()[0].__proto__ === protectedDefinition).toBeTruthy() - expect(b.test()[1].__proto__ === privateDefinition).toBeTruthy() - - // } - }) - - test("using native `super` in public methods of a root definition object won't work if there's also a 'public' definition object", () => { - const Person = Class({ - fly() { - return 'fly' - }, - }) - - // does not (can not) work - const Man = Class().extends(Person, { - fly() { - expect(super.fly).toBe(undefined) - return 'failed' - }, - - public: { - unusedMethod() {}, - }, - }) - - const man = new Man() - expect(man.fly()).toBe('failed') - - // works - const SuperMan = Class().extends(Person, ({Super}) => ({ - fly() { - return Super(this).fly() - }, - })) - - const superman = new SuperMan() - expect(superman.fly()).toBe('fly') - - // I guess SuperMan is Super fly! - }) -}) diff --git a/src/tests/class-branding.test.js b/src/tests/class-branding.test.js deleted file mode 100644 index e0070b3..0000000 --- a/src/tests/class-branding.test.js +++ /dev/null @@ -1,181 +0,0 @@ -import {Class, InvalidAccessError, InvalidSuperAccessError} from '../index.js' -import Mixin from '../Mixin.js' - -const test = it - -describe('Class branding and positional privacy vs lexical privacy', () => { - test(` - Private/protected access works across instances of a class generated - from multiple applications of a mixin passed the same base class. - `, () => { - // this test works because the following mixin applications are - // memoized, so calling `Foo.mixin()` twice without supplying differeing - // args causes the same class constructor to be returned both times. - - let count = 0 - - const Foo = Mixin((Base = Class()) => { - return Class('Foo').extends(Base, ({Super, Private}) => ({ - constructor() { - Super(this).constructor() - Private(this).foo = ++count - }, - - getPrivateFromOther(other) { - return Private(other).foo - }, - })) - // ^ a brand is not passed in here - }) - - const A = Foo.mixin() - const B = Foo.mixin() - - const a = new A() - const b = new B() - - expect(a.getPrivateFromOther(b)).toBe(2) - }) - - test(` - If no brand is provided, private/protected access does NOT work across - instances of a class generated from multiple applications of a mixin - passed differing base classes. - `, () => { - // This test shows private access does not work. The following two calls - // of `Foo.mixin()` are passed different base classes, so the return - // values are two differeing class constructors. Not passing a brand - // means that the private access will not be shared across these classes - // (this is called "lexical privates" according to - // https://github.com/tc39/proposal-class-fields/issues/60. - - let count = 0 - - const Foo = Mixin((Base = Class()) => { - return Class('Foo').extends(Base, ({Super, Private}) => ({ - constructor() { - Super(this).constructor() - Private(this).foo = ++count - }, - - getPrivateFromOther(other) { - return Private(other).foo - }, - })) - // ^ a brand is not passed in here - }) - - const BaseA = Class() - const BaseB = Class() - - const A = Foo.mixin(BaseA) - const B = Foo.mixin(BaseB) - - const a = new A() - const b = new B() - - // this won't work, the implementation will treat a and b as if they - // were made from two unrelated class definitions - expect(() => a.getPrivateFromOther(b)).toThrowError(InvalidAccessError) - }) - - test(` - If a brand is provided, private/protected access should work across - instances of the same class generated from multiple applications of a - mixin passed differing base classes. - `, () => { - // To make privacy work unlike in the previous example, we need to - // define a brand for the classes generated by the mixins. The brand is - // an object, and the Content of it doesn't matter (we could leave it - // empty, it's just used internally as a WeakMap key). It tells the - // Class implementation to share privacy across instances made from - // classes that share the brand. This let's us achieve "positional - // privacy" as described in - // https://github.com/tc39/proposal-class-fields/issues/60 - const FooBrand = {brand: 'FooBrand'} - - let count = 0 - let proto = 0 - - const Foo = Mixin((Base = Class()) => { - return Class('Foo').extends( - Base, - ({Super, Private}) => ({ - proto: ++proto, - - constructor() { - Super(this).constructor() - Private(this).foo = ++count - }, - - getPrivateFromOther(other) { - return Private(other).foo - }, - }), - FooBrand, - ) - // ^ passing the brand enables behavior similar to "positional privacy" - }) - - const BaseA = Class() - const BaseB = Class() - - const A = Foo.mixin(BaseA) - const B = Foo.mixin(BaseB) - - const a = new A() - const b = new B() - - // although a and b were created from two different class constructors - // due to the mixin calls, private access still works, thanks to the - // brand which marks them as "from the same Foo class", similar to - // privacy based on source position. - expect(a.getPrivateFromOther(b)).toBe(2) - }) - - test(`the Super helper should not work across instances of a branded class`, () => { - const FooBrand = {brand: 'FooBrand'} - - const Foo = Mixin(Base => { - return Class('Foo').extends( - Base, - ({Super, Private}) => ({ - constructor() { - Super(this).constructor() - }, - - callSuperOnOther(other) { - Super(other) - }, - }), - FooBrand, - ) - // ^ passing the brand should not make Super behave like the access helpers - }) - - const BaseA = Class() - const BaseB = Class() - - const A = Foo.mixin(BaseA) - const B = Foo.mixin(BaseB) - - const a = new A() - const b = new B() - - expect(() => a.callSuperOnOther(b)).toThrowError(InvalidSuperAccessError) - - // but Super should work across instances of the exact same class. - - const BaseC = Class() - - // C and D are the exact same class because the mixin returns a cached - // class when the same base class is passed - const C = Foo.mixin(BaseC) - const D = Foo.mixin(BaseC) - - const c = new C() - const d = new D() - - expect(() => c.callSuperOnOther(d)).not.toThrowError(InvalidSuperAccessError) - }) -}) diff --git a/src/tests/configuration.test.js b/src/tests/configuration.test.js deleted file mode 100644 index 897b332..0000000 --- a/src/tests/configuration.test.js +++ /dev/null @@ -1,224 +0,0 @@ -import {Class, createClassHelper, staticBlacklist} from '../index.js' - -const test = it - -describe('configuration', () => { - test('ensure that class prototype and static descriptors are like ES6 classes', () => { - const Duck = Class(({Protected, Private}) => ({ - constructor() {}, - add() {}, - get foo() {}, - - protected: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - private: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - static: { - foo: 'foo', - add() {}, - set foo(v) {}, - }, - - test() { - checkDescriptors(Protected(this).__proto__) - checkDescriptors(Private(this).__proto__) - }, - })) - - const protoDescriptor = Object.getOwnPropertyDescriptor(Duck, 'prototype') - expect(!protoDescriptor.writable).toBeTruthy() - expect(!protoDescriptor.enumerable).toBeTruthy() - expect(!protoDescriptor.configurable).toBeTruthy() - - checkDescriptors(Duck) - checkDescriptors(Duck.prototype) - - const duck = new Duck() - duck.test() - }) - - test('Show how to change class creation configuration', () => { - // for example suppose we want static and prototype props/methods to be - // enumerable, and the prototype to be writable. - - const Class = createClassHelper({ - prototypeWritable: true, - defaultClassDescriptor: { - enumerable: true, - configurable: false, - }, - }) - - const AwesomeThing = Class(({Protected, Private}) => ({ - constructor() {}, - add() {}, - get foo() {}, - - protected: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - private: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - static: { - foo: 'foo', - add() {}, - set foo(v) {}, - }, - - test() { - checkDescriptors(Protected(this).__proto__, true, false) - checkDescriptors(Private(this).__proto__, true, false) - }, - })) - - const protoDescriptor = Object.getOwnPropertyDescriptor(AwesomeThing, 'prototype') - expect(protoDescriptor.writable).toBeTruthy() - expect(!protoDescriptor.enumerable).toBeTruthy() - expect(!protoDescriptor.configurable).toBeTruthy() - - checkDescriptors(AwesomeThing, true, false) - checkDescriptors(AwesomeThing.prototype, true, false) - - const thing = new AwesomeThing() - thing.test() - }) - - test('Show how to disable setting of descriptors', () => { - // leaving them like ES5 classes (gives better performance while defining - // classes too, if you don't need the stricter descriptors) - - const Class = createClassHelper({ - setClassDescriptors: false, - }) - - const PeanutBrittle = Class(({Protected, Private}) => ({ - constructor() {}, - add() {}, - get foo() {}, - - protected: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - private: { - foo: 'foo', - add() {}, - get foo() {}, - }, - - static: { - foo: 'foo', - add() {}, - set foo(v) {}, - }, - - test() { - checkDescriptors(Protected(this).__proto__, true, true) - checkDescriptors(Private(this).__proto__, true, true) - }, - })) - - const protoDescriptor = Object.getOwnPropertyDescriptor(PeanutBrittle, 'prototype') - expect(protoDescriptor.writable).toBeTruthy() - expect(!protoDescriptor.enumerable).toBeTruthy() - expect(!protoDescriptor.configurable).toBeTruthy() - - checkDescriptors(PeanutBrittle, true, true) - checkDescriptors(PeanutBrittle.prototype, true, true) - - const thing = new PeanutBrittle() - thing.test() - }) - - function checkDescriptors(obj, enumerable = false, configurable = true) { - const useBlacklist = typeof obj === 'function' - - const descriptors = Object.getOwnPropertyDescriptors(obj) - let descriptor - - expect(Object.keys(descriptors).length).toBeTruthy() - - for (const key in descriptors) { - if (useBlacklist && staticBlacklist.includes(key)) continue - - descriptor = descriptors[key] - - if ('writable' in descriptor) expect(descriptor.writable).toBeTruthy() - else expect('get' in descriptor).toBeTruthy() - - expect(descriptor.enumerable === enumerable).toBeTruthy() - expect(descriptor.configurable === configurable).toBeTruthy() - } - } - - test('name classes natively (default is false)', () => { - // without native naming - { - const Class = createClassHelper({ - nativeNaming: false, // default - }) - - // anonymous: - const Something = Class() - expect(Something.name === '').toBeTruthy() - - // named: - const OtherThing = Class('OtherThing') - expect(OtherThing.name === 'OtherThing').toBeTruthy() - - expect(!OtherThing.toString().includes('OtherThing')).toBeTruthy() - - // make sure works with non-simple classes (because different code path) - const AwesomeThing = Class({method() {}}) - expect(AwesomeThing.name).toBe('') - const AwesomeThing2 = Class('AwesomeThing2', {method() {}}) - expect(AwesomeThing2.name).toBe('AwesomeThing2') - expect(!AwesomeThing2.toString().includes('AwesomeThing2')).toBeTruthy() - } - - // with native naming - { - // this config causes functions to be created using naming that is - // native to the engine, by doing something like this: - // new Function(` return function ${ className }() { ... } `) - const Class = createClassHelper({ - nativeNaming: true, - }) - - // anonymous: - const AnotherThing = Class() - expect(AnotherThing.name === '').toBeTruthy() - - // named: - const YetAnotherThing = Class('YetAnotherThing') - expect(YetAnotherThing.name === 'YetAnotherThing').toBeTruthy() - - // here's the difference - expect(YetAnotherThing.toString().includes('YetAnotherThing')).toBeTruthy() - - // make sure works with non-simple classes (because different code path) - const AwesomeThing = Class({method() {}}) - expect(AwesomeThing.name).toBe('') - const AwesomeThing2 = Class('AwesomeThing2', {method() {}}) - expect(AwesomeThing2.name).toBe('AwesomeThing2') - expect(AwesomeThing2.toString().includes('AwesomeThing2')).toBeTruthy() - } - }) -}) diff --git a/src/tests/custom-elements.test.js b/src/tests/custom-elements.test.js deleted file mode 100644 index a30eba2..0000000 --- a/src/tests/custom-elements.test.js +++ /dev/null @@ -1,209 +0,0 @@ -import Class from '../index.js' -import {native} from '../native.js' - -describe('Custom Elements', () => { - // example of extending HTMLElement for use with customElements.define - // (Custom Elements) - it('works with custom elements', () => { - // full example, wraps builting HTMLElement class with the native helper - { - const MyEL = Class().extends(native(HTMLElement), ({Super}) => ({ - static: { - observedAttributes: ['foo'], - }, - - connected: false, - disconnected: true, - - constructor() { - // return is needed for it to work - return Super(this).constructor() - - // native super works too - //return super.constructor() - }, - - connectedCallback() { - this.connected = true - this.disconnected = false - }, - - disconnectedCallback() { - this.connected = false - this.disconnected = true - }, - - attributeChangedCallback(attr, oldVal, newVal) { - this[attr] = newVal - }, - })) - - customElements.define('my-el', MyEL) - - const el = document.createElement('my-el') - - document.body.appendChild(el) - expect(el.connected).toBe(true) - expect(el.disconnected).toBe(false) - - el.setAttribute('foo', 'bar') - expect(el.foo).toBe('bar') - - document.body.removeChild(el) - expect(el.connected).toBe(false) - expect(el.disconnected).toBe(true) - } - - // other ways to do it too: - - // with Reflect.construct and builtin HTMLElement, no native helper. - // The native helper uses Reflect.construct internally to achieve a - // similar effect. - { - const MyEl = Class().extends(window.HTMLElement, ({Super}) => ({ - constructor() { - // Reflect.construct is needed to be used manually if we - // don't use the native helper - return Reflect.construct(Super(this).constructor, [], this.constructor) - - // using native super would work here too - //return Reflect.construct(super.constructor, [], this.constructor) - - // we could also construct HTMLElement directly - //return Reflect.construct(HTMLElement, [], this.constructor) - - // don't use new.target, it doesn't work (for now at least) - //return Reflect.construct(super.constructor, [], new.target) - }, - connectedCallback() { - this.connected = true - }, - })) - - customElements.define('my-el1', MyEl) - const el = new MyEl() - document.body.appendChild(el) - - expect(el.connected).toBe(true) - - document.body.removeChild(el) - } - - // extending a Custom Elements class. - { - const MyEl = Class().extends(native(HTMLElement), { - constructor() { - return super.constructor() - }, - connectedCallback() { - this.connected = true - }, - }) - - const MyEl2 = Class().extends(MyEl, { - constructor() { - return super.constructor() - }, - connectedCallback() { - super.connectedCallback() - }, - }) - - customElements.define('my-el2', MyEl2) - const el = document.createElement('my-el2') - - expect(el instanceof MyEl2).toBe(true) - - document.body.appendChild(el) - - expect(el.connected).toBe(true) - - document.body.removeChild(el) - } - - // When using `Reflect.construct`, use `this.constructor` in place of - // `new.target` - { - const MyEl = Class().extends(native(HTMLElement), { - constructor() { - return Reflect.construct(super.constructor, [], this.constructor) - }, - connectedCallback() { - this.connected = true - }, - }) - - const MyEl2 = Class().extends(MyEl, { - constructor() { - return Reflect.construct(super.constructor, [], this.constructor) - }, - connectedCallback() { - super.connectedCallback() - }, - }) - - customElements.define('my-el3', MyEl2) - const el = document.createElement('my-el3') - - expect(el instanceof MyEl2).toBe(true) - - document.body.appendChild(el) - - expect(el.connected).toBe(true) - - document.body.removeChild(el) - } - - // if you provide your own classes, you can do it any way you want, - // including using Reflect.construct with new.target - { - const MyEl = Class( - ({Protected}) => - class extends HTMLElement { - constructor() { - return Reflect.construct(HTMLElement, [], new.target) - } - connectedCallback() { - Protected(this).connected = true - } - - getProtectedMember() { - return Protected(this).connected - } - - // define initial protected values - static protected() { - return { - connected: false, - } - } - }, - ) - - const MyEl2 = Class( - ({Protected}) => - class extends MyEl { - constructor() { - return Reflect.construct(MyEl, [], new.target) - } - - connectedCallback() { - super.connectedCallback() - } - }, - ) - - customElements.define('my-el6', MyEl2) - const el = document.createElement('my-el6') - - expect(el instanceof MyEl2).toBe(true) - - document.body.appendChild(el) - - expect(el.connected).toBe(undefined) - expect(el.getProtectedMember()).toBe(true) - - document.body.removeChild(el) - } - }) -}) diff --git a/src/tests/empty-classes.test.js b/src/tests/empty-classes.test.js deleted file mode 100644 index 5a51098..0000000 --- a/src/tests/empty-classes.test.js +++ /dev/null @@ -1,135 +0,0 @@ -import Class from '../index.js' - -const test = it - -describe('empty classes', () => { - test('anonymous empty base classes', () => { - const Constructor = Class() - const instance = new Constructor() - expect(instance instanceof Constructor).toBeTruthy() - expect(Constructor.name === '').toBeTruthy() - expect(Constructor.prototype.__proto__ === Object.prototype).toBeTruthy() - }) - - test('named empty base class', () => { - const Foo = Class('Foo') - const foo = new Foo() - expect(foo instanceof Foo).toBeTruthy() - expect(Foo.name === 'Foo').toBeTruthy() - expect(Foo.prototype.__proto__ === Object.prototype).toBeTruthy() - }) - - test('anonymous non-empty base class', () => { - const Dog = Class(() => ({ - method() {}, - })) - - expect(Dog.name === '').toBeTruthy() - expect(Dog.prototype.__proto__ === Object.prototype).toBeTruthy() - - const dog = new Dog() - expect(dog instanceof Dog).toBeTruthy() - expect(typeof dog.method === 'function').toBeTruthy() - }) - - test('named non-empty base class', () => { - const Dog = Class('Dog', () => ({ - method() {}, - })) - - expect(Dog.name === 'Dog').toBeTruthy() - expect(Dog.prototype.__proto__ === Object.prototype).toBeTruthy() - - const dog = new Dog() - expect(dog instanceof Dog).toBeTruthy() - expect(typeof dog.method === 'function').toBeTruthy() - }) - - test('anonymous empty subclass', () => { - const LivingThing = Class() - const Alien = Class().extends(LivingThing) - expect(Alien.name === '').toBeTruthy() - expect(Alien.prototype.__proto__ === LivingThing.prototype).toBeTruthy() - - const a = new Alien() - expect(a instanceof Alien).toBeTruthy() - }) - - test('named empty subclass', () => { - const LivingThing = Class('LivingThing') - const Alien = Class('Alien').extends(LivingThing) - expect(Alien.name === 'Alien').toBeTruthy() - expect(Alien.prototype.__proto__ === LivingThing.prototype).toBeTruthy() - - const a = new Alien() - expect(a instanceof Alien).toBeTruthy() - }) - - test('anonymous non-empty subclass', () => { - const LivingThing = Class(() => ({ - method1() {}, - })) - const Alien = Class().extends(LivingThing, () => ({ - method2() {}, - })) - expect(Alien.name === '').toBeTruthy() - expect(Alien.prototype.__proto__ === LivingThing.prototype).toBeTruthy() - - const a = new Alien() - expect(a instanceof Alien).toBeTruthy() - expect(a.method1).toBeTruthy() - expect(a.method2).toBeTruthy() - }) - - test('named non-empty subclass', () => { - const LivingThing = Class('LivingThing', () => ({ - method1() {}, - })) - const Alien = Class('Alien').extends(LivingThing, () => ({ - method2() {}, - })) - expect(Alien.name === 'Alien').toBeTruthy() - expect(Alien.prototype.__proto__ === LivingThing.prototype).toBeTruthy() - - const a = new Alien() - expect(a instanceof Alien).toBeTruthy() - expect(typeof a.method1 === 'function').toBeTruthy() - expect(typeof a.method2 === 'function').toBeTruthy() - }) - - test('anonymous subclass with extends at the end', () => { - const SeaCreature = Class(() => ({ - method1() {}, - })) - - const Shark = Class(() => ({ - method2() {}, - })).extends(SeaCreature) - - expect(Shark.name === '').toBeTruthy() - expect(Shark.prototype.__proto__ === SeaCreature.prototype).toBeTruthy() - - const shark = new Shark() - expect(shark instanceof Shark).toBeTruthy() - expect(typeof shark.method1 === 'function').toBeTruthy() - expect(typeof shark.method2 === 'function').toBeTruthy() - }) - - test('named subclass with extends at the end', () => { - const SeaCreature = Class(() => ({ - method1() {}, - })) - - const Shark = Class('Shark', () => ({ - method2() {}, - })).extends(SeaCreature) - - expect(Shark.name === 'Shark').toBeTruthy() - expect(Shark.prototype.__proto__ === SeaCreature.prototype).toBeTruthy() - - const shark = new Shark() - expect(shark instanceof Shark).toBeTruthy() - expect(typeof shark.method1 === 'function').toBeTruthy() - expect(typeof shark.method2 === 'function').toBeTruthy() - }) -}) diff --git a/src/tests/extending-builtins.test.js b/src/tests/extending-builtins.test.js deleted file mode 100644 index 5537d6b..0000000 --- a/src/tests/extending-builtins.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import Class from '../index.js' -import {native} from '../native.js' - -const test = it - -describe('extending builtins', () => { - test('extending native Array', () => { - const MyArray = Class().extends(native(Array), (Public, Protected, Private) => ({ - constructor(...args) { - const self = super.constructor(...args) - self.__proto__ = MyArray.prototype - - Private(self).message = 'I am Array!' - - return self - }, - add(...args) { - return Protected(this).add(...args) - }, - protected: { - add(...args) { - return Public(this).push(...args) - }, - }, - - showMessage() { - return Private(this).message - }, - })) - - const a = new MyArray() - expect(a instanceof Array).toBeTruthy() - expect(a instanceof MyArray).toBeTruthy() - - expect(a.showMessage()).toBe('I am Array!') - - expect(a.add(1, 2, 3) === 3).toBeTruthy() - expect(a.length === 3).toBeTruthy() - expect(a.concat(4, 5, 6).length === 6).toBeTruthy() - expect(a.concat(4, 5, 6) instanceof MyArray).toBeTruthy() - expect(Array.isArray(a)).toBeTruthy() - }) -}) diff --git a/src/tests/extending-native-classes.test.js b/src/tests/extending-native-classes.test.js deleted file mode 100644 index 109f93c..0000000 --- a/src/tests/extending-native-classes.test.js +++ /dev/null @@ -1,67 +0,0 @@ -import Class from '../index.js' -import {native} from '../native.js' - -const test = it - -describe('extending native classes', () => { - test('extend native class, and using Super helper', () => { - class Foo { - constructor(msg) { - this.message = msg - } - - method() { - return this.message - } - } - - // TODO auto-detect `class`es - const Bar = Class().extends(native(Foo), ({Super}) => ({ - constructor(msg) { - Super(this).constructor(msg) - - this.message += '!' - }, - - method() { - return Super(this).method() - }, - })) - - const b = new Bar('it works') - - expect(b instanceof Bar).toBeTruthy() - expect(b instanceof Foo).toBeTruthy() - expect(b.method() === 'it works!').toBeTruthy() - }) - - test('extend native class, and using native `super`', () => { - class Foo { - constructor(msg) { - this.message = msg - } - - method() { - return this.message - } - } - - const Bar = Class().extends(native(Foo), { - constructor(msg) { - super.constructor(msg) - - this.message += '!' - }, - - method() { - return super.method() - }, - }) - - const b = new Bar('it works') - - expect(b instanceof Bar).toBeTruthy() - expect(b instanceof Foo).toBeTruthy() - expect(b.method() === 'it works!').toBeTruthy() - }) -}) diff --git a/src/tests/friends.test.js b/src/tests/friends.test.js deleted file mode 100644 index 8b6e225..0000000 --- a/src/tests/friends.test.js +++ /dev/null @@ -1,66 +0,0 @@ -import {Counter, Incrementor} from './Counter.js' -import {Counter2, Incrementor2} from './Counter2.js' - -const test = it - -describe('Friends', () => { - test(` - functionality similar to "friend" in C++ or "package protected" in Java, - by means of intentionally leaked access helpers - `, () => { - // shows that functionality similar to "friend" in C++ or "package - // protected" can be done with lowclass. See `./Counter.js` to learn how it - // works. - - // in a real-world scenario, counter might be used here locally... - const counter = new Counter() - - // ...while incrementor might be passed to third party code. - const incrementor = new Incrementor(counter) - - // show that we can only access what is public - expect(counter.count).toBe(undefined) - expect(counter.increment).toBe(undefined) - expect(typeof counter.value).toBe('function') - - expect(incrementor.counter).toBe(undefined) - expect(typeof incrementor.increment).toBe('function') - - // show that it works: - expect(counter.value()).toBe(0) - incrementor.increment() - expect(counter.value()).toBe(1) - incrementor.increment() - expect(counter.value()).toBe(2) - }) - - test(` - functionality similar to "friend" in C++ or "package protected" in Java, - by means of intentionally shared class brands - `, () => { - // shows that functionality similar to "friend" in C++ or "package - // protected" can be done with lowclass. See `./Counter2.js` to learn how it - // works. - - // in a real-world scenario, counter might be used here locally... - const counter = new Counter2() - - // ...while incrementor might be passed to third party code. - const incrementor = new Incrementor2(counter) - - // show that we can only access what is public - expect(counter.count).toBe(undefined) - expect(counter.increment).toBe(undefined) - expect(typeof counter.value).toBe('function') - - expect(incrementor.counter).toBe(undefined) - expect(typeof incrementor.increment).toBe('function') - - // show that it works: - expect(counter.value()).toBe(0) - incrementor.increment() - expect(counter.value()).toBe(1) - incrementor.increment() - expect(counter.value()).toBe(2) - }) -}) diff --git a/src/tests/readme-examples.test.js b/src/tests/readme-examples.test.js index b801cb7..584f561 100644 --- a/src/tests/readme-examples.test.js +++ b/src/tests/readme-examples.test.js @@ -1,209 +1,6 @@ -import Class from '../index.js' - -const test = it - -describe('README examples', () => { - test('use a real protected member instead of the underscore convention, ES2015 classes', () => { - // an alias, which semantically more meaningful when wrapping a native - // `class` that already contains the "class" keyword. - const protect = Class - - const Thing = protect( - ({Protected}) => - class { - constructor() { - // stop using underscore and make it truly protected: - Protected(this).protectedProperty = 'yoohoo' - } - - someMethod() { - return Protected(this).protectedProperty - } - }, - ) - - const t = new Thing() - - expect(t.someMethod()).toBe('yoohoo') - - // the value is not publicly accessible! - expect(t.protectedProperty).toBe(undefined) - - const Something = protect( - ({Protected}) => - class extends Thing { - otherMethod() { - // access the inherited actually-protected member - return Protected(this).protectedProperty - } - }, - ) - - const s = new Something() - expect(s.protectedProperty).toBe(undefined) - expect(s.otherMethod()).toBe('yoohoo') - }) - - test('use a real protected member instead of the underscore convention, ES5 classes', () => { - // an alias, which semantically more meaningful when wrapping a native - // `class` that already contains the "class" keyword. - const protect = Class - - const Thing = protect(({Protected}) => { - function Thing() { - Protected(this).protectedProperty = 'yoohoo' - } - - Thing.prototype = { - constructor: Thing, - - someMethod() { - return Protected(this).protectedProperty - }, - } - - return Thing - }) - - const t = new Thing() - - expect(t.someMethod()).toBe('yoohoo') - - // the value is not publicly accessible! - expect(t.protectedProperty).toBe(undefined) - - const Something = protect(({Protected}) => { - function Something() { - Thing.call(this) - } - - Something.prototype = { - __proto__: Thing.prototype, - constructor: Something, - - otherMethod() { - // access the inherited actually-protected member - return Protected(this).protectedProperty - }, - } - - return Something - }) - - const s = new Something() - expect(s.protectedProperty).toBe(undefined) - expect(s.otherMethod()).toBe('yoohoo') - }) - - test('no access of parent private data in subclass', () => { - const Thing = Class(({Private}) => ({ - constructor() { - Private(this).privateProperty = 'yoohoo' - }, - - someMethod() { - return Private(this).privateProperty - }, - - changeIt() { - Private(this).privateProperty = 'oh yeah' - }, - })) - - const Something = Class().extends(Thing, ({Private}) => ({ - otherMethod() { - return Private(this).privateProperty - }, - - makeItSo() { - Private(this).privateProperty = 'it is so' - }, - })) - - const instance = new Something() - - expect(instance.someMethod()).toBe('yoohoo') - expect(instance.otherMethod()).toBe(undefined) - - instance.changeIt() - expect(instance.someMethod()).toBe('oh yeah') - expect(instance.otherMethod()).toBe(undefined) - - instance.makeItSo() - expect(instance.someMethod()).toBe('oh yeah') - expect(instance.otherMethod()).toBe('it is so') - }) - - test('no access of parent private data in subclass', () => { - const Thing = Class(({Private}) => ({ - constructor() { - Private(this).privateProperty = 'yoohoo' - }, - })) - - const Something = Thing.subclass(({Private}) => ({ - otherMethod() { - return Private(this).privateProperty - }, - })) - - const something = new Something() - - expect(something.otherMethod()).toBe(undefined) - }) - - test('private inheritance', () => { - const Counter = Class(({Private}) => ({ - private: { - // this is a prototype prop, the initial value will be inherited by subclasses - count: 0, - - increment() { - this.count++ - }, - }, - - tick() { - Private(this).increment() - - return Private(this).count - }, - - getCountValue() { - return Private(this).count - }, - })) - - const DoubleCounter = Counter.subclass(({Private}) => ({ - doubleTick() { - Private(this).increment() - Private(this).increment() - - return Private(this).count - }, - - getDoubleCountValue() { - return Private(this).count - }, - })) - - const counter = new Counter() - - expect(counter.tick()).toBe(1) - - const doubleCounter = new DoubleCounter() - - expect(doubleCounter.doubleTick()).toBe(2) - expect(doubleCounter.tick()).toBe(1) - - expect(doubleCounter.doubleTick()).toBe(4) - expect(doubleCounter.tick()).toBe(2) - - // There's a private `counter` member for the Counter class, and there's a - // separate private `counter` member for the `DoubleCounter` class (the - // initial value inherited from `Counter`): - expect(doubleCounter.getDoubleCountValue()).not.toBe(counter.getCountValue()) - expect(doubleCounter.getCountValue()).toBe(2) - expect(doubleCounter.getDoubleCountValue()).toBe(4) - }) -}) +var test = it; +describe('README examples', function () { + test('they work', function () { + // TODO + }); +}); diff --git a/src/tests/syntaxes.test.js b/src/tests/syntaxes.test.js deleted file mode 100644 index 1b6ccf5..0000000 --- a/src/tests/syntaxes.test.js +++ /dev/null @@ -1,329 +0,0 @@ -// various forms of writing classes ("syntaxes") - -import Class from '../index.js' -import {native} from '../native.js' - -const test = it - -describe('various forms of writing classes', () => { - test('object literal', () => { - const Foo = Class({ - constructor() { - this.bar = 'bar' - }, - foo() { - expect(this.bar === 'bar').toBeTruthy() - }, - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('definer function (arrow function), returning an object literal', () => { - const Foo = Class(({Super}) => ({ - constructor() { - this.bar = 'bar' - }, - foo() { - expect(Super(this).hasOwnProperty('bar')).toBe(true) - expect(this.bar === 'bar').toBeTruthy() - }, - })) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('definer function (non-arrow), returning an object literal', () => { - const Foo = Class(function ({Super}) { - return { - constructor() { - this.bar = 'bar' - }, - foo() { - expect(Super(this).hasOwnProperty('bar')).toBe(true) - expect(this.bar === 'bar').toBeTruthy() - }, - } - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('definer function (arrow function), setting ES5-like prototype assignment', () => { - const Foo = Class(({Super, Public}) => { - Public.prototype.constructor = function () { - this.bar = 'bar' - } - Public.prototype.foo = function () { - expect(Super(this).hasOwnProperty('bar')).toBe(true) - expect(this.bar === 'bar').toBeTruthy() - } - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('wrap a native class', () => { - const Foo = Class( - () => - class { - constructor() { - this.bar = 'bar' - } - foo() { - expect(this.bar === 'bar').toBeTruthy() - } - }, - ) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('wrap an ES5 class', () => { - const Foo = Class(() => { - function Foo() { - this.bar = 'bar' - } - - Foo.prototype = { - constructor: Foo, - foo() { - expect(this.bar === 'bar').toBeTruthy() - }, - } - - return Foo - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - f.foo() - }) - - test('object literal with access helpers on each access definition', () => { - const Foo = Class({ - public: (Protected, Private) => ({ - constructor() { - this.bar = 'bar' - }, - foo() { - expect(this.bar === 'bar').toBeTruthy() - expect(Protected(this).foo() === 'barbar3').toBeTruthy() - expect(Private(this).foo() === 'barbar2').toBeTruthy() - return 'it works' - }, - }), - protected: (Public, Private) => ({ - bar: 'bar2', - foo() { - return Public(this).bar + Private(this).bar - }, - }), - private: (Public, Protected) => ({ - bar: 'bar3', - foo() { - return Public(this).bar + Protected(this).bar - }, - }), - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - expect(f.foo() === 'it works').toBeTruthy() - - const Bar = Foo.subclass({ - public: Protected => ({ - test() { - return Protected(this).test() - }, - }), - protected: ({Super, Public}) => ({ - test() { - return Super(Public(this)).foo() - }, - }), - }) - - const b = new Bar() - expect(b instanceof Bar).toBeTruthy() - expect(b.foo() === 'it works').toBeTruthy() - }) - - test('definer function and ES5-like prototype assignment', () => { - const Foo = Class(({Protected, Private, Public}) => { - Public.prototype.constructor = function () { - this.bar = 'bar' - } - - Public.prototype.foo = function () { - expect(this.bar === 'bar').toBeTruthy() - expect(Protected(this).foo() === 'barbar3').toBeTruthy() - expect(Private(this).foo() === 'barbar2').toBeTruthy() - return 'it works' - } - - Protected.prototype.bar = 'bar2' - - Protected.prototype.foo = function () { - return Public(this).bar + Private(this).bar - } - - Private.prototype.bar = 'bar3' - - Private.prototype.foo = function () { - return Public(this).bar + Protected(this).bar - } - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - expect(f.foo() === 'it works').toBeTruthy() - - const Bar = Foo.subclass(({Public, Protected, Super}) => { - Public.prototype.test = function () { - return Protected(this).test() - } - - Protected.prototype.test = function () { - return Super(Public(this)).foo() - } - }) - - const b = new Bar() - expect(b instanceof Bar).toBeTruthy() - expect(b.foo() === 'it works').toBeTruthy() - }) - - test('definer function and ES5-like prototype object literals', () => { - const Foo = Class(({Protected, Private, Public}) => { - Public.prototype = { - constructor() { - this.bar = 'bar' - }, - - foo() { - expect(this.bar === 'bar').toBeTruthy() - expect(Protected(this).foo() === 'barbar3').toBeTruthy() - expect(Private(this).foo() === 'barbar2').toBeTruthy() - return 'it works' - }, - } - - Protected.prototype = { - bar: 'bar2', - - foo() { - return Public(this).bar + Private(this).bar - }, - } - - Private.prototype = { - bar: 'bar3', - - foo() { - return Public(this).bar + Protected(this).bar - }, - } - }) - - const f = new Foo() - expect(f instanceof Foo).toBeTruthy() - expect(f.foo() === 'it works').toBeTruthy() - - const Bar = Foo.subclass(({Public, Protected, Super}) => { - Public.prototype = { - test() { - return Protected(this).test() - }, - } - - Protected.prototype = { - test() { - return Super(Public(this)).foo() - }, - } - }) - - const b = new Bar() - expect(b instanceof Bar).toBeTruthy() - expect(b.foo() === 'it works').toBeTruthy() - }) - - test('different ways to make a subclass', () => { - let Foo = Class() - - let Bar = Class().extends(Foo, { - method() {}, - }) - let bar = new Bar() - - expect(bar instanceof Foo).toBeTruthy() - expect(bar instanceof Bar).toBeTruthy() - expect(typeof bar.method).toBe('function') - - Bar = Class({ - method() {}, - }).extends(Foo) - bar = new Bar() - - expect(bar instanceof Foo).toBeTruthy() - expect(bar instanceof Bar).toBeTruthy() - expect(typeof bar.method).toBe('function') - - Bar = Foo.subclass({ - method() {}, - }) - bar = new Bar() - - expect(bar instanceof Foo).toBeTruthy() - expect(bar instanceof Bar).toBeTruthy() - expect(typeof bar.method).toBe('function') - - // TODO these doesn't work yet, but they should so that it is easy to work with existing code bases { - - //Foo = class {} - //Foo.subclass = Class - //Bar = Foo.subclass({ - //method() {} - //}) - //bar = new Bar - - //expect( bar instanceof Foo ).toBeTruthy() - //expect( bar instanceof Bar ).toBeTruthy() - //expect( typeof bar.method ).toBe( 'function' ) - - //Foo = native( class {} ) - //Foo.subclass = Class - //Bar = Foo.subclass({ - //method() {} - //}) - //bar = new Bar - - //expect( bar instanceof Foo ).toBeTruthy() - //expect( bar instanceof Bar ).toBeTruthy() - //expect( typeof bar.method ).toBe( 'function' ) - - //Foo = Class( () => class {} ) - //Foo.subclass = Class - //Bar = Foo.subclass({ - //method() {} - //}) - //bar = new Bar - - //expect( bar instanceof Foo ).toBeTruthy() - //expect( bar instanceof Bar ).toBeTruthy() - //expect( typeof bar.method ).toBe( 'function' ) - - // } - }) -}) diff --git a/src/tests/wrap-custom-classes.test.js b/src/tests/wrap-custom-classes.test.js deleted file mode 100644 index 88adaa1..0000000 --- a/src/tests/wrap-custom-classes.test.js +++ /dev/null @@ -1,118 +0,0 @@ -import Class from '../index.js' - -const test = it - -describe('wrap existing classes', () => { - test('protected and private members for custom-made ES5 classes', () => { - const Foo = Class(({Protected, Private}) => { - // make and return our own es5-style base class, with Protected and - // Private helpers in scope. - - function Foo() { - this.foo = 'foo' - } - - Foo.prototype = { - constructor: Foo, - test() { - expect(this.foo === 'foo').toBeTruthy() - expect(Private(this).bar === 'bar').toBeTruthy() - expect(Protected(this).baz === 'baz').toBeTruthy() - }, - - // define access just like with regular class definitions - private: { - bar: 'bar', - }, - protected: { - baz: 'baz', - }, - } - - return Foo - }) - - const foo = new Foo() - foo.test() - - const Bar = Class(({Super, Private}) => { - // make and return our own es5-style subclass - - const prototype = { - __proto__: Foo.prototype, - - constructor: function () { - Super(this).constructor() - }, - - test() { - super.test() - expect(Private(this).who === 'you').toBeTruthy() - }, - - private: { - who: 'you', - }, - } - - prototype.constructor.prototype = prototype - - return prototype.constructor - }) - - const bar = new Bar() - bar.test() - }) - - test('protected and private members for custom-made native ES6+ classes', () => { - // wrap our own es6 native-style base class with access helpers in scope. - const Lorem = Class( - ({Protected, Private}) => - class { - constructor() { - this.foo = 'foo' - } - - test() { - expect(this.foo === 'foo').toBeTruthy() - expect(Private(this).bar === 'bar').toBeTruthy() - expect(Protected(this).baz === 'baz').toBeTruthy() - } - - get private() { - return { - bar: 'bar', - } - } - - get protected() { - return { - baz: 'baz', - } - } - }, - ) - - const lorem = new Lorem() - lorem.test() - - // wrap our own es6 native-style subclass with the access helpers in scope - const Ipsum = Class(({Private}) => { - return class extends Lorem { - test() { - super.test() - expect(Private(this).secret === 'he did it').toBeTruthy() - } - - get private() { - return { - secret: 'he did it', - } - } - } - }) - - const ip = new Ipsum() - ip.test() - }) -})