diff --git a/@xen-orchestra/mixin/.babelrc.js b/@xen-orchestra/mixin/.babelrc.js new file mode 100644 index 000000000..4e27ec135 --- /dev/null +++ b/@xen-orchestra/mixin/.babelrc.js @@ -0,0 +1,3 @@ +module.exports = require('../../@xen-orchestra/babel-config')( + require('./package.json') +) diff --git a/@xen-orchestra/mixin/.npmignore b/@xen-orchestra/mixin/.npmignore new file mode 100644 index 000000000..e058b6bc1 --- /dev/null +++ b/@xen-orchestra/mixin/.npmignore @@ -0,0 +1,24 @@ +/benchmark/ +/benchmarks/ +*.bench.js +*.bench.js.map + +/examples/ +example.js +example.js.map +*.example.js +*.example.js.map + +/fixture/ +/fixtures/ +*.fixture.js +*.fixture.js.map +*.fixtures.js +*.fixtures.js.map + +/test/ +/tests/ +*.spec.js +*.spec.js.map + +__snapshots__/ diff --git a/@xen-orchestra/mixin/README.md b/@xen-orchestra/mixin/README.md new file mode 100644 index 000000000..7d35dc618 --- /dev/null +++ b/@xen-orchestra/mixin/README.md @@ -0,0 +1,49 @@ +# ${pkg.name} [![Build Status](https://travis-ci.org/${pkg.shortGitHubPath}.png?branch=master)](https://travis-ci.org/${pkg.shortGitHubPath}) + +> ${pkg.description} + +## Install + +Installation of the [npm package](https://npmjs.org/package/${pkg.name}): + +``` +> npm install --save ${pkg.name} +``` + +## Usage + +**TODO** + +## Development + +``` +# Install dependencies +> yarn + +# Run the tests +> yarn test + +# Continuously compile +> yarn dev + +# Continuously run the tests +> yarn dev-test + +# Build for production (automatically called by npm install) +> yarn build +``` + +## Contributions + +Contributions are *very* welcomed, either on the documentation or on +the code. + +You may: + +- report any [issue](${pkg.bugs}) + you've encountered; +- fork and create a pull request. + +## License + +${pkg.license} © [${pkg.author.name}](${pkg.author.url}) diff --git a/@xen-orchestra/mixin/package.json b/@xen-orchestra/mixin/package.json new file mode 100644 index 000000000..b5077075c --- /dev/null +++ b/@xen-orchestra/mixin/package.json @@ -0,0 +1,49 @@ +{ + "name": "@xen-orchestra/mixin", + "version": "0.0.0", + "license": "ISC", + "description": "", + "keywords": [], + "homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/mixin", + "bugs": "https://github.com/vatesfr/xen-orchestra/issues", + "repository": { + "type": "git", + "url": "https://github.com/vatesfr/xen-orchestra.git" + }, + "author": { + "name": "Julien Fontanet", + "email": "julien.fontanet@vates.fr" + }, + "preferGlobal": false, + "main": "dist/", + "bin": {}, + "files": [ + "dist/" + ], + "browserslist": [ + ">2%" + ], + "engines": { + "node": ">=6" + }, + "dependencies": { + "bind-property-descriptor": "^1.0.0" + }, + "devDependencies": { + "@babel/cli": "7.0.0", + "@babel/core": "7.0.0", + "@babel/preset-env": "7.0.0", + "babel-plugin-dev": "^1.0.0", + "babel-plugin-lodash": "^3.3.2", + "cross-env": "^5.1.3", + "rimraf": "^2.6.2" + }, + "scripts": { + "build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/", + "clean": "rimraf dist/", + "dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/", + "prebuild": "yarn run clean", + "predev": "yarn run prebuild", + "prepublishOnly": "yarn run build" + } +} diff --git a/@xen-orchestra/mixin/src/index.js b/@xen-orchestra/mixin/src/index.js new file mode 100644 index 000000000..3d945dd07 --- /dev/null +++ b/@xen-orchestra/mixin/src/index.js @@ -0,0 +1,130 @@ +import { getBoundPropertyDescriptor } from 'bind-property-descriptor' + +// =================================================================== + +const { defineProperties, getOwnPropertyDescriptor } = Object + +const isIgnoredProperty = name => name[0] === '_' || name === 'constructor' + +const IGNORED_STATIC_PROPERTIES = { + __proto__: null, + + arguments: true, + caller: true, + length: true, + name: true, + prototype: true, +} +const isIgnoredStaticProperty = name => name in IGNORED_STATIC_PROPERTIES + +const ownKeys = + (typeof Reflect !== 'undefined' && Reflect.ownKeys) || + (({ getOwnPropertyNames: names, getOwnPropertySymbols: symbols }) => + symbols !== undefined ? obj => names(obj).concat(symbols(obj)) : names)( + Object + ) + +// ------------------------------------------------------------------- + +const mixin = Mixins => Class => { + if (__DEV__ && !Array.isArray(Mixins)) { + throw new TypeError('Mixins should be an array') + } + + const { name } = Class + + // Copy properties of plain object mix-ins to the prototype. + { + const allMixins = Mixins + Mixins = [] + const { prototype } = Class + const descriptors = { __proto__: null } + allMixins.forEach(Mixin => { + if (typeof Mixin === 'function') { + Mixins.push(Mixin) + return + } + + for (const prop of ownKeys(Mixin)) { + if (__DEV__ && prop in prototype) { + throw new Error(`${name}#${prop} is already defined`) + } + + ;(descriptors[prop] = getOwnPropertyDescriptor( + Mixin, + prop + )).enumerable = false // Object methods are enumerable but class methods are not. + } + }) + defineProperties(prototype, descriptors) + } + + const n = Mixins.length + + function DecoratedClass (...args) { + const instance = new Class(...args) + + for (let i = 0; i < n; ++i) { + const Mixin = Mixins[i] + const { prototype } = Mixin + const mixinInstance = new Mixin(instance, ...args) + const descriptors = { __proto__: null } + const props = ownKeys(prototype) + for (let j = 0, m = props.length; j < m; ++j) { + const prop = props[j] + + if (isIgnoredProperty(prop)) { + continue + } + + if (prop in instance) { + throw new Error(`${name}#${prop} is already defined`) + } + + descriptors[prop] = getBoundPropertyDescriptor( + prototype, + prop, + mixinInstance + ) + } + defineProperties(instance, descriptors) + } + + return instance + } + + // Copy original and mixed-in static properties on Decorator class. + const descriptors = { __proto__: null } + ownKeys(Class).forEach(prop => { + let descriptor + if ( + !( + isIgnoredStaticProperty(prop) && + // if they already exist... + (descriptor = getOwnPropertyDescriptor(DecoratedClass, prop)) !== + undefined && + // and are not configurable. + !descriptor.configurable + ) + ) { + descriptors[prop] = getOwnPropertyDescriptor(Class, prop) + } + }) + Mixins.forEach(Mixin => { + ownKeys(Mixin).forEach(prop => { + if (isIgnoredStaticProperty(prop)) { + return + } + + if (__DEV__ && prop in descriptors) { + throw new Error(`${name}.${prop} is already defined`) + } + + descriptors[prop] = getOwnPropertyDescriptor(Mixin, prop) + }) + }) + defineProperties(DecoratedClass, descriptors) + + return DecoratedClass +} +export { mixin as default } diff --git a/CHANGELOG.md b/CHANGELOG.md index 645e1eb67..6ab7a53f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Released packages +- @xen-orchestra/mixin v0.0.0 - @xen-orchestra/emit-async v0.0.0 - xo-server v5.27.0 - xo-web v5.27.0 diff --git a/packages/xo-server/package.json b/packages/xo-server/package.json index 20b90e41a..d20b19789 100644 --- a/packages/xo-server/package.json +++ b/packages/xo-server/package.json @@ -35,12 +35,12 @@ "@xen-orchestra/cron": "^1.0.3", "@xen-orchestra/emit-async": "^0.0.0", "@xen-orchestra/fs": "^0.3.0", + "@xen-orchestra/mixin": "^0.0.0", "ajv": "^6.1.1", "app-conf": "^0.5.0", "archiver": "^3.0.0", "async-iterator-to-stream": "^1.0.1", "base64url": "^3.0.0", - "bind-property-descriptor": "^1.0.0", "blocked": "^1.2.1", "bluebird": "^3.5.1", "body-parser": "^1.18.2", diff --git a/packages/xo-server/src/decorators.js b/packages/xo-server/src/decorators.js index 4def99c02..ea87c92df 100644 --- a/packages/xo-server/src/decorators.js +++ b/packages/xo-server/src/decorators.js @@ -1,13 +1,3 @@ -import { getBoundPropertyDescriptor } from 'bind-property-descriptor' - -import { isArray, isFunction } from './utils' - -// =================================================================== - -const { defineProperties, getOwnPropertyDescriptor } = Object - -// =================================================================== - // Debounce decorator for methods. // // See: https://github.com/wycats/javascript-decorators @@ -49,118 +39,3 @@ export const debounce = duration => (target, name, descriptor) => { descriptor.value = debounced return descriptor } - -// ------------------------------------------------------------------- - -const _ownKeys = - (typeof Reflect !== 'undefined' && Reflect.ownKeys) || - (({ getOwnPropertyNames: names, getOwnPropertySymbols: symbols }) => - symbols ? obj => names(obj).concat(symbols(obj)) : names)(Object) - -const _isIgnoredProperty = name => name[0] === '_' || name === 'constructor' - -const _IGNORED_STATIC_PROPERTIES = { - __proto__: null, - - arguments: true, - caller: true, - length: true, - name: true, - prototype: true, -} -const _isIgnoredStaticProperty = name => _IGNORED_STATIC_PROPERTIES[name] - -export const mixin = MixIns => Class => { - if (!isArray(MixIns)) { - MixIns = [MixIns] - } - - const { name } = Class - - // Copy properties of plain object mix-ins to the prototype. - { - const allMixIns = MixIns - MixIns = [] - const { prototype } = Class - const descriptors = { __proto__: null } - for (const MixIn of allMixIns) { - if (isFunction(MixIn)) { - MixIns.push(MixIn) - continue - } - - for (const prop of _ownKeys(MixIn)) { - if (prop in prototype) { - throw new Error(`${name}#${prop} is already defined`) - } - - ;(descriptors[prop] = getOwnPropertyDescriptor( - MixIn, - prop - )).enumerable = false // Object methods are enumerable but class methods are not. - } - } - defineProperties(prototype, descriptors) - } - - function Decorator (...args) { - const instance = new Class(...args) - - for (const MixIn of MixIns) { - const { prototype } = MixIn - const mixinInstance = new MixIn(instance, ...args) - const descriptors = { __proto__: null } - for (const prop of _ownKeys(prototype)) { - if (_isIgnoredProperty(prop)) { - continue - } - - if (prop in instance) { - throw new Error(`${name}#${prop} is already defined`) - } - - descriptors[prop] = getBoundPropertyDescriptor( - prototype, - prop, - mixinInstance - ) - } - defineProperties(instance, descriptors) - } - - return instance - } - - // Copy original and mixed-in static properties on Decorator class. - const descriptors = { __proto__: null } - for (const prop of _ownKeys(Class)) { - let descriptor - if ( - !( - _isIgnoredStaticProperty(prop) && - // if they already exist... - (descriptor = getOwnPropertyDescriptor(Decorator, prop)) && - // and are not configurable. - !descriptor.configurable - ) - ) { - descriptors[prop] = getOwnPropertyDescriptor(Class, prop) - } - } - for (const MixIn of MixIns) { - for (const prop of _ownKeys(MixIn)) { - if (_isIgnoredStaticProperty(prop)) { - continue - } - - if (prop in descriptors) { - throw new Error(`${name}.${prop} is already defined`) - } - - descriptors[prop] = getOwnPropertyDescriptor(MixIn, prop) - } - } - defineProperties(Decorator, descriptors) - - return Decorator -} diff --git a/packages/xo-server/src/xapi/index.js b/packages/xo-server/src/xapi/index.js index bf176e513..3ecd74e29 100644 --- a/packages/xo-server/src/xapi/index.js +++ b/packages/xo-server/src/xapi/index.js @@ -2,6 +2,7 @@ import concurrency from 'limit-concurrency-decorator' import deferrable from 'golike-defer' import fatfs from 'fatfs' +import mixin from '@xen-orchestra/mixin' import synchronized from 'decorator-synchronized' import tarStream from 'tar-stream' import vmdkToVhd from 'xo-vmdk-to-vhd' @@ -32,7 +33,6 @@ import { satisfies as versionSatisfies } from 'semver' import createSizeStream from '../size-stream' import fatfsBuffer, { init as fatfsBufferInit } from '../fatfs-buffer' -import { mixin } from '../decorators' import { asyncMap, camelToSnakeCase, diff --git a/packages/xo-server/src/xo.js b/packages/xo-server/src/xo.js index ca41e61d7..8deeaa00b 100644 --- a/packages/xo-server/src/xo.js +++ b/packages/xo-server/src/xo.js @@ -1,5 +1,6 @@ import XoCollection from 'xo-collection' import XoUniqueIndex from 'xo-collection/unique-index' +import mixin from '@xen-orchestra/mixin' import { createClient as createRedisClient } from 'redis' import { EventEmitter } from 'events' import { noSuchObject } from 'xo-common/api-errors' @@ -16,7 +17,6 @@ import { import mixins from './xo-mixins' import Connection from './connection' -import { mixin } from './decorators' import { generateToken, noop } from './utils' // ===================================================================