├── .gitignore ├── .eslintrc.json ├── test ├── .eslintrc.json └── test.js ├── CONTRIBUTING.md ├── .editorconfig ├── index.js ├── package.json ├── HISTORY.md ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── code-of-conduct.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "qubyte/ES2018-module" 3 | } 4 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please review the [code of conduct]('code-of-conduct.md') before opening issues 4 | or pull requests. 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default function createMixin(propertyDescriptors) { 2 | const mixed = new WeakSet(); 3 | 4 | function mixin(obj) { 5 | Object.defineProperties(obj, propertyDescriptors); 6 | mixed.add(obj); 7 | return obj; 8 | } 9 | 10 | function checkInstance(obj) { 11 | for (let o = obj; o; o = Object.getPrototypeOf(o)) { 12 | if (mixed.has(o)) { 13 | return true; 14 | } 15 | } 16 | } 17 | 18 | Object.defineProperty(mixin, Symbol.hasInstance, { value: checkInstance }); 19 | 20 | return mixin; 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixomatic", 3 | "version": "5.0.0", 4 | "description": "Create mixins which work with instanceof (friendly for unit tests).", 5 | "author": "Mark S. Everitt", 6 | "license": "MIT", 7 | "repository": "github:qubyte/mixomatic", 8 | "main": "index.js", 9 | "module": "index.js", 10 | "type": "module", 11 | "exports": { 12 | ".": "./index.js" 13 | }, 14 | "files": [ 15 | "index.js" 16 | ], 17 | "keywords": [ 18 | "multiple inheritance", 19 | "mixins", 20 | "instanceof" 21 | ], 22 | "scripts": { 23 | "test": "node --test", 24 | "lint": "eslint ." 25 | }, 26 | "devDependencies": { 27 | "eslint": "^8.28.0", 28 | "eslint-config-qubyte": "^5.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## v5.0.0 4 | 5 | Drops support for Node 14. 6 | 7 | ## v4.0.1 8 | 9 | Adds an export map. 10 | 11 | ## v4.0.0 12 | 13 | Drop support for UMD modules and Node 10. Use v3 if you still need that 14 | functionality (this module is otherwise unchanged). In the past this would have 15 | had an impact on Node.js projects, but Node (since v12) now has good support for 16 | ES modules. 17 | 18 | ## v3.0.0 19 | 20 | Mixins will now consider an object an instance of themselves if the object or 21 | any member the prototype chain of the object has had the mixin applied to it. 22 | 23 | ## v2.0.0 24 | 25 | Now implemented as a function which returns a function rather than as a class. 26 | 27 | ## v1.0.0 28 | 29 | Initial release. 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: [18, 20, 22] 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@main 18 | - name: use node ${{ matrix.node }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm test 23 | lint: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: checkout 27 | uses: actions/checkout@main 28 | - name: use node 22 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: 22.x 32 | - run: npm ci 33 | - run: npm run lint 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark S. Everitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { describe, it, beforeEach } from 'node:test'; 3 | import createMixin from 'mixomatic'; 4 | 5 | describe('Mixin', () => { 6 | it('is a function', () => { 7 | assert.ok(createMixin instanceof Function); 8 | }); 9 | 10 | describe('mixins', () => { 11 | let descriptors; 12 | let mixin; 13 | 14 | beforeEach(() => { 15 | descriptors = { 16 | aProperty: { 17 | value: 'abc', 18 | enumerable: true, 19 | configurable: true, 20 | writable: true 21 | }, 22 | aMethod: { 23 | value() { 24 | return 'A method'; 25 | }, 26 | enumerable: false, 27 | configurable: false, 28 | writable: false 29 | } 30 | }; 31 | 32 | mixin = createMixin(descriptors); 33 | }); 34 | 35 | it('makes the Symbol.hasInstance property of the mixin not configurable, not enumerable, and not writable', () => { 36 | const { configurable, enumerable, writable } = Object.getOwnPropertyDescriptor(mixin, Symbol.hasInstance); 37 | 38 | assert.equal(configurable, false); 39 | assert.equal(enumerable, false); 40 | assert.equal(writable, false); 41 | }); 42 | 43 | it('returns the object passed to it', () => { 44 | const obj = {}; 45 | 46 | assert.equal(obj, mixin(obj)); 47 | }); 48 | 49 | it('Appends properties to the object according to descriptors', () => { 50 | const obj = {}; 51 | 52 | mixin(obj); 53 | 54 | assert.deepEqual(Object.getOwnPropertyDescriptors(obj), descriptors); 55 | }); 56 | 57 | it('does not drop existing properties', () => { 58 | const obj = { a: 1, b: 2, c: 3 }; 59 | 60 | mixin(obj); 61 | 62 | const allDescriptors = { 63 | ...descriptors, 64 | a: { 65 | value: 1, 66 | configurable: true, 67 | writable: true, 68 | enumerable: true 69 | }, 70 | b: { 71 | value: 2, 72 | configurable: true, 73 | writable: true, 74 | enumerable: true 75 | }, 76 | c: { 77 | value: 3, 78 | configurable: true, 79 | writable: true, 80 | enumerable: true 81 | } 82 | }; 83 | 84 | assert.deepEqual(Object.getOwnPropertyDescriptors(obj), allDescriptors); 85 | }); 86 | 87 | it('considers mixed objects to be instances of itself', () => { 88 | const obj = {}; 89 | 90 | mixin(obj); 91 | 92 | assert.equal(obj instanceof mixin, true); 93 | }); 94 | 95 | it('considers unmixed objects not to be instances of itself', () => { 96 | const obj = {}; 97 | 98 | assert.equal(obj instanceof mixin, false); 99 | }); 100 | 101 | it('works with classes via prototypes', () => { 102 | class MyClass {} 103 | 104 | mixin(MyClass.prototype); 105 | 106 | assert.equal(new MyClass() instanceof mixin, true); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mark.s.everitt+contributor-covenant@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository has been [moved to codeburg](https://codeberg.org/qubyte/mixomatic). 2 | 3 | # mixomatic 4 | 5 | Create mixins which work with `instanceof` (friendly for unit tests). Internally 6 | references are handled by a `WeakSet` instances so there's no need to manually 7 | keep records of which objects have been mixed onto and risk memory leaks. 8 | 9 | ## Install 10 | 11 | With `npm`: 12 | ``` 13 | npm install --save mixomatic 14 | ``` 15 | 16 | With `yarn`: 17 | ``` 18 | yarn add mixomatic 19 | ``` 20 | 21 | Or alternatively, in a browser or deno you can use it directly in a page via 22 | [unpkg][0] as a module (not recommended for production use): 23 | ```javascript 24 | import mixomatic from 'https://unpkg.com/mixomatic'; 25 | ``` 26 | 27 | ## Usage 28 | 29 | Make a new mixin which appends [`propertyDescriptors`][1] to an object. 30 | ```javascript 31 | import mixomatic from 'mixomatic'; 32 | 33 | const myMixin = mixomatic(propertyDescriptors); 34 | ``` 35 | 36 | Mix onto an object. 37 | ```javascript 38 | const obj = {}; 39 | 40 | myMixin(obj); 41 | ``` 42 | 43 | Check if an object has been modified by a given mixin: 44 | 45 | ```javascript 46 | obj instanceof myMixin; // true 47 | ``` 48 | 49 | Also works with classes! 50 | 51 | ```javascript 52 | class MyClass {} 53 | 54 | myMixin(MyClass.prototype); 55 | 56 | const obj = new MyClass(); 57 | 58 | obj instanceof MyClass; // true 59 | obj instanceof myMixin; // true 60 | ``` 61 | 62 | And inheritance! 63 | 64 | ```javascript 65 | class MyChildClass extends MyClass {} 66 | 67 | const obj = new MyChildClass(); 68 | 69 | obj instanceof MyChildClass; // true 70 | obj instanceof MyClass; // true 71 | obj instanceof myMixin; // true 72 | ``` 73 | 74 | ## Example 75 | 76 | You're making a game with a little ship which shoots space-bound rocks before 77 | they can bash into it. Both the ship and the rocks have position and velocity 78 | properties. You _could_ make a class, which provides a `move` method, which they 79 | would both inherit from. However, that could be the beginning of a class 80 | hierarchy and you've heard bad things about those being hard to modify in the 81 | future. JavaScript also has no way to do multiple inheritance with classes, so 82 | your options are limited with classes anyway. 83 | 84 | Instead you make the wise choice to use `mixomatic`! You use mixomatic to create 85 | a mixin called `movable`, which takes a time difference and uses it to update 86 | the position of its host object. 87 | 88 | ```javascript 89 | const movable = mixomatic({ 90 | move: { 91 | value(dt) { 92 | this.position.x += dt * this.velocity.x; 93 | this.position.y += dt * this.velocity.y; 94 | }, 95 | configurable: true, 96 | enumerable: false, 97 | writable: true 98 | } 99 | }); 100 | ``` 101 | 102 | Since there'll only be one ship, you define it directly as an object and apply 103 | `movable` to it to give it the `move` method. 104 | 105 | ```javascript 106 | const ship = { 107 | position: { x: 0, y: 0 }, 108 | velocity: { x: 0, y: 0 } 109 | }; 110 | 111 | movable(ship); 112 | ``` 113 | 114 | Asteroids are more numerous and can appear in all sorts of places, so you decide 115 | to go with a class for those. 116 | 117 | ```javascript 118 | class Asteroid { 119 | constructor(position, velocity) { 120 | this.position = { x: position.x, y: position.y }; 121 | this.velocity = { x: velocity.x, y: velocity.y }; 122 | } 123 | } 124 | 125 | movable(Asteroid.prototype); 126 | ``` 127 | 128 | Now both `ship` and `Asteroid` instances will have the `move` method, and will 129 | both appear to be instances of `movable`, yet are not part of the same class 130 | hierarchy. All sorts of behaviour can be written as mixins (for example, the 131 | ship can fire missiles, and so can UFOs). 132 | 133 | This is useful because mixins can be tested in isolation, and you can avoid 134 | duplication of tests for mixed properties by using an `instanceof` check in the 135 | test suites of host objects like `ship` and `Asteroid`. 136 | 137 | [0]: https://unpkg.com/ 138 | [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties 139 | --------------------------------------------------------------------------------