WiP: feat(xo-server-rework): new version from scratch

This commit is contained in:
Julien Fontanet
2018-02-05 14:30:21 +01:00
parent 20d1b7c481
commit 6f32a89015
62 changed files with 4091 additions and 165 deletions

2
.gitignore vendored
View File

@@ -24,6 +24,8 @@
/packages/xo-web/src/common/intl/locales/index.js
/packages/xo-web/src/common/themes/index.js
/packages/xo-server-rework/src/app/mixins/index.js
npm-debug.log
npm-debug.log.*
pnpm-debug.log

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -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})

View File

@@ -0,0 +1,47 @@
{
"private": true,
"name": "@xen-orchestra/async-fs",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/async-fs",
"bugs": "https://github.com/vatesfr/xo-web/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"promise-toolbox": "^0.9.5"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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"
}
}

View File

@@ -0,0 +1,10 @@
// @flow
import fs from 'fs'
import { promisifyAll } from 'promise-toolbox'
const NOT_PROMISIFIABLE_RE = /^(?:[_A-Z]|exists$)|(?:Async|Stream|Sync)$/
module.exports = promisifyAll(fs, {
mapper: name => !NOT_PROMISIFIABLE_RE.test(name) && name,
})

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -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})

View File

@@ -0,0 +1,50 @@
{
"private": true,
"name": "@xen-orchestra/async-map",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/async-map",
"bugs": "https://github.com/vatesfr/xo-web/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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"
}
}

View File

@@ -0,0 +1,36 @@
// @flow
import { map } from 'lodash'
// Similar to map() + Promise.all() but wait for all promises to
// settle before rejecting (with the first error)
const asyncMap = <T1, T2>(
collection: Array<T1> | Promise<Array<T1>>,
iteratee: (value: T1, key: number, collection: Array<T1>) => T2
): Promise<Array<T2>> => {
if (!Array.isArray(collection)) {
return collection.then(collection => asyncMap(collection, iteratee))
}
let errorContainer
const onError = error => {
if (errorContainer === undefined) {
errorContainer = { error }
}
}
return Promise.all(
map(collection, (item, key, collection) =>
new Promise(resolve => {
resolve(iteratee(item, key, collection))
}).catch(onError)
)
).then(values => {
if (errorContainer !== undefined) {
throw errorContainer.error
}
return values
})
}
export { asyncMap as default }

View File

@@ -7,11 +7,12 @@ const NODE_ENV = process.env.NODE_ENV || 'development'
const __PROD__ = NODE_ENV === 'production'
const __TEST__ = NODE_ENV === 'test'
module.exports = function (pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})
presets['@babel/preset-env'] = {
const configs = {
'@babel/plugin-proposal-decorators': {
legacy: true,
},
'@babel/preset-env' (pkg) {
return {
debug: !__TEST__,
loose: true,
shippedProposals: true,
@@ -29,12 +30,23 @@ module.exports = function (pkg, plugins, presets) {
: { browsers: '', node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}
},
}
const getConfig = (key, ...args) => {
const config = configs[key]
return config === undefined ? {} : typeof config === 'function' ? config(...args) : config
}
module.exports = function (pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})
Object.keys(pkg.devDependencies || {}).forEach(name => {
if (!(name in presets) && PLUGINS_RE.test(name)) {
plugins[name] = {}
plugins[name] = getConfig(name, pkg)
} else if (!(name in presets) && PRESETS_RE.test(name)) {
presets[name] = {}
presets[name] = getConfig(name, pkg)
}
})

View File

@@ -41,10 +41,10 @@
"moment-timezone": "^0.5.14"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},
@@ -53,7 +53,7 @@
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -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})

View File

@@ -0,0 +1,48 @@
{
"private": true,
"name": "@xen-orchestra/defined",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/defined",
"bugs": "https://github.com/vatesfr/xo-web/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": ">=4"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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"
}
}

View File

@@ -0,0 +1,65 @@
// @flow
// Usage:
//
// ```js
// const httpProxy = defined(
// process.env.HTTP_PROXY,
// process.env.http_proxy
// )
//
// const httpProxy = defined([
// process.env.HTTP_PROXY,
// process.env.http_proxy
// ])
// ```
export default function defined () {
let args = arguments
let n = args.length
if (n === 1) {
args = arguments[0]
n = args.length
}
for (let i = 0; i < n; ++i) {
let arg = arguments[i]
if (typeof arg === 'function') {
arg = get(arg)
}
if (arg !== undefined) {
return arg
}
}
}
// Usage:
//
// ```js
// const friendName = get(() => props.user.friends[0].name)
//
// // this form can be used to avoid recreating functions:
// const getFriendName = _ => _.friends[0].name
// const friendName = get(getFriendName, props.user)
// ```
export const get = (accessor: (input: ?any) => any, arg: ?any) => {
try {
return accessor(arg)
} catch (error) {
if (!(error instanceof TypeError)) { // avoid hidding other errors
throw error
}
}
}
// Usage:
//
// ```js
// const httpAgent = ifDef(
// process.env.HTTP_PROXY,
// _ => new ProxyAgent(_)
// )
// ```
export const ifDef = (value: ?any, thenFn: (value: any) => any) =>
value !== undefined
? thenFn(value)
: value

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -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})

View File

@@ -0,0 +1,48 @@
{
"private": true,
"name": "@xen-orchestra/emit-async",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/emit-async",
"bugs": "https://github.com/vatesfr/xo-web/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": ">=4"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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"
}
}

View File

@@ -0,0 +1,24 @@
export default function emitAsync (event) {
let opts
let i = 1
// an option object has been passed as first param
if (typeof event !== 'string') {
opts = event
event = arguments[i++]
}
const n = arguments.length - i
const args = new Array(n)
for (let j = 0; j < n; ++j) {
args[j] = arguments[j + i]
}
const onError = opts != null && opts.onError
return Promise.all(this.listeners(event).map(
listener => new Promise(resolve => {
resolve(listener.apply(this, args))
}).catch(onError)
))
}

View File

@@ -1,41 +1,3 @@
'use strict'
const NODE_ENV = process.env.NODE_ENV || 'development'
const __PROD__ = NODE_ENV === 'production'
const __TEST__ = NODE_ENV === 'test'
const pkg = require('./package')
let nodeCompat = (pkg.engines || {}).node
if (nodeCompat === undefined) {
nodeCompat = '6'
} else {
const trimChars = '^=>~'
while (trimChars.includes(nodeCompat[0])) {
nodeCompat = nodeCompat.slice(1)
}
}
module.exports = {
comments: !__PROD__,
ignore: __TEST__ ? undefined : [/\.spec\.js$/],
plugins: ['lodash'],
presets: [
[
'@babel/env',
{
debug: !__TEST__,
loose: true,
shippedProposals: true,
targets: __PROD__
? {
browsers: '>2%',
node: nodeCompat,
}
: { node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
},
],
'@babel/flow',
],
}
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -6,7 +6,7 @@
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/log",
"bugs": "https://github.com/vatesfr/xo-web/issues",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
@@ -21,19 +21,22 @@
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.39",
"@babel/polyfill": "7.0.0-beta.46",
"lodash": "^4.17.4",
"promise-toolbox": "^0.9.5"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.39",
"@babel/core": "7.0.0-beta.39",
"@babel/preset-env": "7.0.0-beta.39",
"@babel/preset-flow": "7.0.0-beta.39",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
@@ -44,7 +47,7 @@
"clean": "rimraf dist/",
"dev": "index-modules --cjs-lazy src/transports && cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -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})

View File

@@ -0,0 +1,50 @@
{
"private": true,
"name": "@xen-orchestra/mixin",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/mixin",
"bugs": "https://github.com/vatesfr/xo-web/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": ">=4"
},
"dependencies": {
"bind-property-descriptor": "^1.0.0"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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"
}
}

View File

@@ -0,0 +1,128 @@
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 (!(
// Special properties are not defined...
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 }

View File

@@ -1,6 +1,6 @@
{
"devDependencies": {
"@babel/register": "^7.0.0-beta.44",
"@babel/register": "^7.0.0-beta.46",
"babel-7-jest": "^21.3.2",
"babel-eslint": "^8.1.2",
"benchmark": "^2.1.4",

View File

@@ -30,9 +30,9 @@
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.1",
"rimraf": "^2.6.2"

View File

@@ -28,10 +28,10 @@
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},

View File

@@ -28,7 +28,7 @@
"node": ">=6"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.44",
"@babel/polyfill": "7.0.0-beta.46",
"bluebird": "^3.5.1",
"chalk": "^2.2.0",
"event-to-promise": "^0.8.0",
@@ -49,10 +49,10 @@
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"

View File

@@ -26,10 +26,10 @@
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "^7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "^7.0.0-beta.46",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -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__/

View File

@@ -0,0 +1,51 @@
# xo-server [![Build Status](https://travis-ci.org/vatesfr/xo-server.png?branch=master)](https://travis-ci.org/vatesfr/xo-server)
> Server part of [Xen Orchestra](https://xen-orchestra.com)
## Install
Installation of the [npm package](https://npmjs.org/package/xo-server):
```
> npm install --global xo-server
```
## Usage
```
> xo-server
```
## 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](https://github.com/vatesfr/xo-web/issues)
you've encountered;
- fork and create a pull request.
## License
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -0,0 +1,40 @@
// Vendor config: DO NOT TOUCH!
//
// See sample.config.yaml to override.
{
// Should users be created on first sign in?
//
// Necessary for external authentication providers.
"createUserOnFirstSignin": true,
"datadir": "/var/lib/xo-server/data",
"http": {
"listen": [
{
"port": 80
}
],
"mounts": {},
// Ciphers to use.
//
// These are the default ciphers in Node 4.2.6, we are setting
// them explicitly for older Node versions.
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
// Tell Node to respect the cipher order.
"honorCipherOrder": true,
// Specify to use at least TLSv1.1.
// See: https://github.com/certsimple/minimum-tls-version
"secureOptions": 117440512
},
"jwt": {
"expiresIn": "7d",
"secret": "P],7x#cRhuy,wCR'$}'N?<2yOQ3v6.!b*|1B2P36(wKsYICH|6"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
{
"private": true,
"name": "xo-server-rework",
"version": "0.0.0",
"license": "AGPL-3.0",
"description": "Server part of Xen Orchestra",
"keywords": [
"orchestra",
"server",
"xen",
"xen-orchestra"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-rework",
"bugs": "https://github.com/vatesfr/xo-web/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": true,
"main": "dist/",
"bin": {
"xo-server": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=6"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.46",
"app-conf": "^0.5.0",
"base64url": "^2.0.0",
"bind-property-descriptor": "^1.0.0",
"bluebird": "^3.5.1",
"cuid": "^2.0.2",
"dataloader": "^1.3.0",
"event-to-promise": "^0.8.0",
"golike-defer": "^0.4.1",
"graphql": "^0.13.0",
"http-request-plus": "^0.5.0",
"http-server-plus": "^0.9.0",
"immutable": "^4.0.0-rc.4",
"index-modules": "^0.3.0",
"jsonwebtoken": "^8.1.0",
"lodash": "^4.17.4",
"mnemonist": "^0.21.0",
"promise-toolbox": "^0.9.5",
"proxy-agent": "^2.1.0",
"spdy": "^3.4.7",
"uuid": "^3.1.0",
"zen-observable": "^0.8.6"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/plugin-proposal-decorators": "7.0.0-beta.46",
"@babel/plugin-proposal-optional-chaining": "7.0.0-beta.46",
"@babel/plugin-proposal-pipeline-operator": "7.0.0-beta.46",
"@babel/plugin-proposal-throw-expressions": "7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"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 && index-modules --auto src/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"start": "./dist/index.js"
}
}

View File

@@ -0,0 +1,8 @@
import EventEmitter from 'events'
import mixin from '@xen-orchestra/mixin'
import { values } from 'lodash'
import Mixins from './mixins'
@mixin(values(Mixins))
export default class App extends EventEmitter {}

View File

@@ -0,0 +1,51 @@
// @flow
import createLogger from '@xen-orchestra/log'
import emitAsync from '@xen-orchestra/emit-async'
const { debug, warn } = createLogger('hooks')
const makeSingletonHook = (hook, postEvent) => {
let promise
return function () {
if (promise === undefined) {
promise = runHook(this, hook)
promise.then(() => {
this.removeAllListeners(hook)
this.emit(postEvent)
this.removeAllListeners(postEvent)
})
}
return promise
}
}
const runHook = (app, hook) => {
debug(`${hook} start…`)
const promise = emitAsync.call(app, {
onError: error => warn(`${hook} failure`, error),
}, hook)
promise.then(() => {
debug(`${hook} finished`)
})
return promise
}
export default {
// Run *clean* async listeners.
//
// They normalize existing data, clear invalid entries, etc.
clean () {
return runHook(this, 'clean')
},
// Run *start* async listeners.
//
// They initialize the application.
start: makeSingletonHook('start', 'started'),
// Run *stop* async listeners.
//
// They close connections, unmount file systems, save states, etc.
stop: makeSingletonHook('stop', 'stopped'),
}

View File

@@ -0,0 +1,13 @@
import defined, { ifDef } from '@xen-orchestra/defined'
import hrp from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
export default class HttpRequest {
constructor (_, {
httpProxy = defined(process.env.http_proxy, process.env.HTTP_PROXY),
}) {
this.httpRequest = hrp.extend({
agent: ifDef(httpProxy, _ => new ProxyAgent(_)),
})
}
}

View File

@@ -0,0 +1,132 @@
// @flow
import type EventEmitter from 'events'
import type {
IncomingMessage,
Server,
ServerResponse,
} from 'http'
import createLogger from '@xen-orchestra/log'
import generateToken from 'generate-token'
import { fromCallback } from 'promise-toolbox'
import { once } from 'lodash'
const HTTP_REQUEST_VALIDITY = 1e3 * 60 * 60
const { warn } = createLogger('web-server')
type Handler = (IncomingMessage, ServerResponse, any) => void
type HandlerInfo = {|
data: any,
handler: Handler,
method: ?string,
once: boolean,
path: string,
unregister: () => void
|}
type HandlerInfoMap = { [url: string]: HandlerInfo }
export default class httpServer {
_handlers: HandlerInfoMap
constructor (app: EventEmitter, { httpServer }: { httpServer: Server }) {
const openConnections = new Set()
httpServer.on('connection', connection => {
openConnections.add(connection)
connection.once('close', () => {
openConnections.delete(connection)
})
})
app.on('stop', () => {
const timeout = setTimeout(() => {
openConnections.forEach(connection => {
connection.end()
})
}, 5e3).unref()
return fromCallback(cb => httpServer.close(cb)).then(() => {
clearTimeout(timeout)
})
})
const handlers = this._handlers = Object.create(null)
httpServer.on('request', (req, res) => {
const handler = handlers[req.url]
if (
handler !== undefined &&
(handler.method !== undefined || handler.method === req.method)
) {
if (handler.once) {
handler.unregister()
}
try {
handler.handler.call(app, req, res, handler.data)
} catch (error) {
warn('handler error', { error })
if (!res.headersSent) {
res.writeHead(500)
}
res.end()
}
return
}
res.writeHead(404)
res.end(`Page not found: ${req.url}`)
})
}
registerHttpHandler (
handler: Handler,
{ data, method, once: once_ = false, path, ttl }
) {
const handlers = this._handlers
if (path in handlers) {
throw new Error(`there is already an HTTP handler for ${path}`)
}
const unregister = once(() => {
delete handlers[path]
})
handlers[path] = {
data,
handler,
method: method && method.toUpperCase(),
once: once_,
path,
unregister,
}
if (ttl !== undefined) {
setTimeout(unregister, ttl)
}
return unregister
}
registerHttpRequest (
handler: Handler,
{ data, path, method = 'GET', suffix }
) {
return generateToken().then(token => {
let path = `/${token}`
if (suffix) {
path += `/${encodeURI(token)}`
}
this.registerHttpHandler(handler, {
data,
method,
once: true,
path,
ttl: HTTP_REQUEST_VALIDITY,
})
return path
})
}
}

View File

@@ -0,0 +1,28 @@
// @flow
import jwt from 'jsonwebtoken'
import { fromCallback } from 'promise-toolbox'
export default class JsonWebToken {
_encodeOpts: Object
_secret: string
constructor (_: any, {
config: { jwt: { expiresIn, secret } },
}: {
config: { jwt: { expiresIn?: string, secret: string } }
}) {
this._encodeOpts = { expiresIn }
this._secret = secret
}
decodeJwt (token: string): Promise<any> {
return fromCallback(cb => jwt.verify(token, this._secret, cb))
}
encodeJwt (payload: any): Promise<string> {
return fromCallback(cb =>
jwt.sign(payload, this._secret, this._encodeOpts, cb)
)
}
}

View File

@@ -0,0 +1,110 @@
// @flow
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
import { forEach, startsWith } from 'lodash'
import { join } from 'path'
import { readdir } from '@xen-orchestra/async-fs'
const { info, warn } = createLogger('plugins')
// ===================================================================
const LOOKUP_PATHS = [
'/usr/local/lib/node_modules',
join(__dirname, '../../../node_modules'),
]
// -------------------------------------------------------------------
export default class Plugins {
_plugins: Object
_prefix: string
constructor (app: any, { appName, safeMode }: { appName: string, safeMode: ?boolean }) {
this._plugins = {}
this._prefix = `${appName}-`
app.on('start', () => {
if (!safeMode) {
return this.discoverPlugins()
}
})
}
discoverPlugins () {
return asyncMap(this._listPlugins(), async (plugin, name) => {})
}
_listPlugins () {
const plugins = { __proto__: null }
const prefix = this._prefix
const prefixLength = prefix.length
return asyncMap(LOOKUP_PATHS, lookupPath =>
readdir(lookupPath).then(
basenames =>
forEach(basenames, basename => {
if (startsWith(basename, prefix)) {
const name = basename.slice(prefixLength)
const path = join(lookupPath, basename)
const previous = plugins[name]
if (name in plugins) {
warn(`duplicate plugins ${name}`, {
name,
paths: [previous.path, path].sort(),
})
return
}
let plugin
try {
plugin = require(path)
info(`successfully imported plugin ${name}`, {
name,
path,
})
} catch (error) {
warn(`failed to import plugin ${name}`, {
error,
name,
path,
})
return
}
let version
try {
;({ version } = require(join(path, 'package.json')))
} catch (_) {}
// Supports both “normal” CommonJS and Babel's ES2015 modules.
const {
default: factory = plugin,
configurationSchema,
configurationPresets,
} = plugin
plugins[name] = {
configurationPresets,
configurationSchema,
factory,
version,
}
}
}),
error => {
if (error.code !== 'ENOENT') {
warn('plugins', 'failed to read directory', {
error,
lookupPath,
})
}
}
)
)
}
}

View File

@@ -0,0 +1,58 @@
// import generateId from 'cuid'
// import mkdirp from 'simple-mkdirp'
// import { pCatch, pTap } from 'promise-toolbox'
// import { join } from 'path'
// import { readFile, writeFile } from '@xen-orchestra/async-fs'
// TODO: transition from in-memory database to a real database system.
export default class Store {
// constructor (app, { config: { datadir } }) {
// this._get = () => {
// const datafile = join(datadir, 'store.json')
//
// const promise = readFile(datafile)
// .then(JSON.parse)
// .catch(pCatch({ code: 'ENOENT' }, () => ({})))
// .then(pTap(data => {
// app.on('stop', data =>
// mkdirp(datadir)
// .then(() => writeFile(datafile, JSON.stringify(data)))
// )
// }))
//
// // Inline future accesses.
// this._get = () => promise
//
// return promise
// }
//
// this._types = {}
// }
//
// registerType (name, spec) {
// const types = this._types
//
// if (__DEV__ && name in types) {
// throw new Error(`type ${name} is already registered`)
// }
//
// types[name] = spec
// }
//
// async createObject ({ type, ...props }) {
// if (__DEV__ && !type) {
// throw new Error('missing type')
// }
//
// const db = await this._get()
// const byType = db.byType || (db.byType = {})
// const collection = byType[type] || (byType[type] = {})
//
// let { id } = props
// if (!id) {
// props.id = id = generateId()
// }
//
// collection[id] = props
// }
}

View File

@@ -0,0 +1,13 @@
export default class Users {
// constructor (app) {
// app.on('start', async () => {
//
// })
//
// app.on('export')
// }
//
// createUser ({ name }) {
//
// }
}

View File

@@ -0,0 +1,88 @@
// @flow
import { cpus as getCpus } from 'os'
import { ChildProcess, fork } from 'child_process'
const MAX = getCpus().length
const WORKER = `${__dirname}/../../worker.js`
class Task {
data: any
reject: (error: any) => void
resolve: (result: any) => void
constructor (data, resolve, reject) {
this.data = data
this.reject = reject
this.resolve = resolve
}
}
export default class Workers {
_idleWorker: ?ChildProcess
_nWorkers: number
_tasksQueue: Array<Task>
constructor () {
this._idleWorker = undefined
this._nWorkers = 0
this._tasksQueue = []
}
callWorker (data: any): any {
return new Promise((resolve, reject) => {
const task = new Task(data, resolve, reject)
const worker = this._getWorker()
if (worker !== undefined) {
this._submitTask(worker, task)
} else {
this._tasksQueue.push(task)
}
})
}
_getWorker () {
let worker = this._idleWorker
if (worker !== undefined) {
this._idleWorker = undefined
return worker
}
if (this._nWorkers < MAX) {
this._nWorkers++
worker = fork(WORKER)
worker.on('error', error => {
console.error('worker error', error)
})
worker.on('exit', (code, signal) => {
console.log('worker exit', code, signal)
this._nWorkers--
})
return worker
}
}
_submitTask (worker: ChildProcess, task: Task) {
worker.once('message', response => {
if ('error' in response) {
task.reject(response.error)
} else {
task.resolve(response.result)
}
const nextTask = this._tasksQueue.shift()
if (nextTask !== undefined) {
this._submitTask(worker, nextTask)
} else if (this._idleWorker !== undefined) {
worker.kill()
} else {
this._idleWorker = worker
}
})
worker.send(task.data)
}
}

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env node
const APP_NAME = 'xo-server'
// -------------------------------------------------------------------
const { info, warn } = require('@xen-orchestra/log').default('bootstrap')
process.on('unhandledRejection', reason => {
warn('possibly unhandled rejection', reason)
})
;(({ prototype }) => {
const { emit } = prototype
prototype.emit = function (event, error) {
event === 'error' && !this.listenerCount(event)
? warn('unhandled error event', error)
: emit.apply(this, arguments)
}
})(require('events').EventEmitter)
const Bluebird = require('bluebird')
Bluebird.longStackTraces()
global.Promise = Bluebird
// -------------------------------------------------------------------
const main = async args => {
info('starting')
const config = await require('app-conf').load(APP_NAME)
const httpServer = new (require('http-server-plus'))()
const readFile = Bluebird.promisify(require('fs').readFile)
await require('@xen-orchestra/async-map').default(
config.http.listen,
async ({
certificate,
// The properties was called `certificate` before.
cert = certificate,
key,
...opts
}) => {
if (cert !== undefined && key !== undefined) {
[opts.cert, opts.key] = await Promise.all([
readFile(cert),
readFile(key),
])
}
try {
const niceAddress = await httpServer.listen(opts)
info(`Web server listening on ${niceAddress}`)
} catch (error) {
if (error.niceAddress !== undefined) {
warn(`Web server could not listen on ${error.niceAddress}`)
const { code } = error
if (code === 'EACCES') {
warn(' Access denied.')
warn(' Ports < 1024 are often reserved to privileges users.')
} else if (code === 'EADDRINUSE') {
warn(' Address already in use.')
}
} else {
warn('Web server could not listen', error)
}
}
}
)
try {
const { group, user } = config
group != null && process.setgid(group)
user != null && process.setuid(user)
} catch (error) {
warn('failed to change group/user', error)
}
global.Observable = require('zen-observable')
const App = require('./app').default
const app = new App({
appName: APP_NAME,
config,
httpServer,
safeMode: require('lodash/includes')(args, '--safe-mode'),
})
await app.start()
// Gracefully shutdown on signals.
//
// TODO: implements a timeout? (or maybe it is the services launcher
// responsibility?)
require('lodash/forEach')(['SIGINT', 'SIGTERM'], signal => {
let alreadyCalled = false
process.on(signal, () => {
if (alreadyCalled) {
warn('forced exit')
process.exit(1)
}
alreadyCalled = true
info(`${signal} caught, closing…`)
app.stop()
})
})
return require('event-to-promise')(app, 'stopped')
}
main(process.argv.slice(2)).then(
() => info('bye :-)'),
error => warn('fatal error', error)
)

View File

@@ -0,0 +1,10 @@
// @flow
import base64url from 'base64url'
import { fromCallback } from 'promise-toolbox'
import { randomBytes } from 'crypto'
const generateSecureToken = (bytes: number = 32): Promise<string> =>
fromCallback(cb => randomBytes(bytes, cb)).then(base64url)
export { generateSecureToken as default }

103
packages/xo-server-rework/src/node_modules/mp-atch.js generated vendored Normal file
View File

@@ -0,0 +1,103 @@
import {
every,
forEach,
isArray,
isPlainObject,
some,
} from 'lodash'
// ===================================================================
const { hasOwnProperty } = Object.prototype
const getSingleKey = obj => {
let prop
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) {
if (prop !== undefined) {
return
}
prop = key
}
}
return prop
}
const OPERATORS = {
__not: (pattern, value) => !match(pattern, value),
__or: (pattern, value) => some(pattern, subpattern => match(subpattern, value)),
}
export const match = (pattern, value) => {
if (isPlainObject(pattern)) {
const key = getSingleKey(pattern)
let operator
if (key !== undefined && (operator = OPERATORS[key]) !== undefined) {
return operator(pattern[key], value)
}
return isPlainObject(value) && every(pattern, (subpattern, key) => (
value[key] !== undefined && match(subpattern, value[key])
))
}
if (isArray(pattern)) {
return isArray(value) && every(pattern, subpattern =>
some(value, subvalue => match(subpattern, subvalue))
)
}
return pattern === value
}
// -------------------------------------------------------------------
const createPredicate = pattern => pattern == null
? () => false
: value => match(pattern, value)
export const patch = (value, patchData) => {
if (isPlainObject(patchData)) {
if (isArray(value)) {
const toRemove = createPredicate(patchData['-'])
const tmp = []
forEach(value, (v, i) => {
if (i in patchData) {
const p = patchData[i]
if (p === null) {
return
}
tmp.push(patch(v, p))
} else if (!toRemove(v)) {
tmp.push(v)
}
})
const toAdd = patchData['+']
if (toAdd) {
tmp.push.apply(tmp, toAdd)
}
return tmp
}
if (isPlainObject(value)) {
value = { ...value }
forEach(patchData, (v, k) => {
if (v === null) {
delete value[k]
} else {
value[k] = patch(value[k], v)
}
})
return value
}
value = {}
forEach(patchData, (v, k) => {
if (v !== null) {
value[k] = patch(null, v)
}
})
return value
}
return patchData
}

View File

@@ -0,0 +1,103 @@
/* eslint-env jest */
import { forEach } from 'lodash'
import { match, patch } from './mp-atch'
// ===================================================================
describe('match()', () => {
const data = {
'matches object properties': {
pattern: { foo: 'bar' },
nope: [
null,
{ },
{ foo: 'baz' },
],
yep: [
{ foo: 'bar' },
{ foo: 'bar', bar: 'baz' },
],
},
'matches set items': {
pattern: [ 'foo', 'bar' ],
nope: [
[],
[ 'foo' ],
[ 'bar' ],
[ 'foo', 'baz' ],
],
yep: [
[ 'bar', 'foo' ],
[ 'bar', 'baz', 'foo' ],
],
},
'supports a __or operator': {
pattern: { __or: [
'foo',
{ },
] },
nope: [
'bar',
[],
],
yep: [
'foo',
{ 'foo': 'bar' },
],
},
}
forEach(data, ({ pattern, nope, yep }, desc) => {
if (!pattern) {
it(desc)
} else {
it(desc, () => {
forEach(nope, value => {
expect(match(pattern, value)).toBe(false)
})
forEach(yep, value => {
expect(match(pattern, value)).toBe(true)
})
})
}
})
it('supports a __not operator', () => {
forEach(data, ({ pattern, nope, yep }) => {
if (!pattern) {
return
}
pattern = { __not: pattern }
forEach(nope, value => {
expect(match(pattern, value)).toBe(true)
})
forEach(yep, value => {
expect(match(pattern, value)).toBe(false)
})
})
})
})
describe('patch', () => {
it('can patch arrays', () => {
expect(patch(
[ 'foo', 'bar', 'quuz' ],
{ 0: null, '-': 'quuz', '+': [ 'baz' ] }
)).toEqual(
[ 'bar', 'baz' ]
)
})
it('can patch objects', () => {
expect(patch(
{ foo: 1, bar: 2 },
{ foo: null, bar: 3, baz: 4 }
)).toEqual(
{ bar: 3, baz: 4 }
)
})
})

View File

@@ -0,0 +1,26 @@
// @flow
import { dirname } from 'path'
import { mkdir, stat } from 'fs'
import { promisify } from 'promise-toolbox'
const simpleMkdirp = (
path: string,
cb: (error: ?Error) => void
) => mkdir(path, undefined, error => {
if (error == null) {
return cb()
}
if (error.code === 'ENOENT') {
return simpleMkdirp(dirname(path), error =>
error != null ? cb(error) : simpleMkdirp(path, cb)
)
}
return stat(path, (_, stats) =>
stats != null && stats.isDirectory() ? cb() : cb(error)
)
})
export default promisify(simpleMkdirp)

View File

@@ -0,0 +1,71 @@
import toDecorator from './to-decorator'
import { EventEmitter } from 'events'
import { CancelToken } from 'promise-toolbox'
class Task extends EventEmitter {
constructor (name, { cancelToken, parent, steps } = {}) {
super()
this._cancelToken = cancelToken || (parent && parent.cancelToken)
this._name = name
this._step = 0
this._steps = steps || 0
}
get cancelToken () {
return this._cancelToken
}
plan (n) {
this._steps += n
}
step (stepName) {
this.emit('progress', {
step: ++this._step,
stepName,
steps: this._steps,
})
const token = this._cancelToken
token && token.throwIfRequested()
}
}
export default opts =>
toDecorator(
fn =>
function () {
const { name = fn.name, steps } = opts || {}
const n = arguments.length
const i = 0
let task
if (n !== 0) {
const arg = arguments[0]
if (arg instanceof Task) {
task = new Task(name, { parent: arg, steps })
} else if (CancelToken.isCancelToken(arg)) {
task = new Task(name, { cancelToken: arg, steps })
}
}
if (task === undefined) {
task = new Task(name, { steps })
}
const args = new Array(i + n)
args[0] = task
for (let j = 1; j < n; ++j) {
args[j] = arguments[j + i]
}
const promise = new Promise(resolve => resolve(fn.apply(this, args)))
promise.onProgress = cb => {
task.on('progress', cb)
return () => task.removeListener('progress', cb)
}
return promise
}
)

View File

@@ -0,0 +1,54 @@
/* eslint-env jest */
import { CancelToken } from 'promise-toolbox'
import task from './task'
// ===================================================================
const rejectionOf = promise =>
promise.then(result => {
throw result
}, reason => reason)
// ===================================================================
const sleep = () => new Promise(resolve => setTimeout(resolve, 0))
describe('@task', () => {
const fn = task()(async $task => {
await sleep()
$task.step('foo')
await sleep()
$task.step('bar')
await sleep()
$task.step('baz')
})
it('', () => {
const promise = fn()
promise.onProgress((...args) => console.log(args))
return promise
})
it('supports cancel tokens', async () => {
const { cancel, token } = CancelToken.source()
const promise = fn(token)
cancel('foo')
expect((await rejectionOf(promise)).message).toBe('foo')
})
it('supports subtasks', async () => {
const fn2 = task()(async $task => {
await sleep()
await fn($task)
await sleep()
await fn($task)
})
const promise = fn2()
await promise
})
})

View File

@@ -0,0 +1,23 @@
// @flow
type Descriptor = {
value: any
}
const toDecorator = (
wrapFunction: Function,
wrapMethod: Function = wrapFunction
) => (
target: Function | any,
key?: string,
descriptor?: Descriptor
) =>
descriptor === undefined
? wrapFunction(target)
: {
...descriptor,
value: (typeof target === 'function' ? wrapFunction : wrapMethod)(
descriptor.value
),
}
export { toDecorator as default }

View File

@@ -0,0 +1,23 @@
const METHODS = {
add: ([ a, b ]) => a + b,
sleep: duration => new Promise(resolve => setTimeout(resolve, duration)),
}
process.on('message', ({ method, arg }) => {
const fn = METHODS[method]
if (fn === undefined) {
return process.send(new Error('no such method'))
}
new Promise(resolve => resolve(fn(arg)))
.then(
result => process.send({ result }),
error => {
console.error(error)
process.send({ error })
}
)
.catch(error => {
console.error('worker error', error)
})
})

View File

@@ -31,7 +31,7 @@
"node": ">=6"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.44",
"@babel/polyfill": "7.0.0-beta.46",
"@marsaud/smb2-promise": "^0.2.1",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/fs": "^0.0.0",
@@ -123,17 +123,17 @@
"yazl": "^2.4.3"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-proposal-decorators": "7.0.0-beta.44",
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.44",
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.44",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.44",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.44",
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.46",
"@babel/core": "7.0.0-beta.46",
"@babel/plugin-proposal-decorators": "7.0.0-beta.46",
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.46",
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.46",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.46",
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.46",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.46",
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.46",
"@babel/preset-env": "7.0.0-beta.46",
"@babel/preset-flow": "7.0.0-beta.46",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",

910
yarn.lock

File diff suppressed because it is too large Load Diff