feat: cron (#34)
This commit is contained in:
parent
ff2c69102d
commit
348bc16d6d
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,6 +3,8 @@
|
||||
/lerna-debug.log
|
||||
/lerna-debug.log.*
|
||||
|
||||
/@xen-orchestra/*/dist/
|
||||
/@xen-orchestra/*/node_modules/
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
|
41
@xen-orchestra/cron/.babelrc.js
Normal file
41
@xen-orchestra/cron/.babelrc.js
Normal file
@ -0,0 +1,41 @@
|
||||
'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',
|
||||
],
|
||||
}
|
24
@xen-orchestra/cron/.npmignore
Normal file
24
@xen-orchestra/cron/.npmignore
Normal 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__/
|
92
@xen-orchestra/cron/README.md
Normal file
92
@xen-orchestra/cron/README.md
Normal file
@ -0,0 +1,92 @@
|
||||
# @xen-orchestra/cron [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Focused, well maintained, cron parser/scheduler
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/cron
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import * as Cron from '@xen-orchestra/cron'
|
||||
|
||||
Cron.parse('* * * jan,mar *')
|
||||
// → { month: [ 1, 3 ] }
|
||||
|
||||
Cron.next('* * * jan,mar *', 2, 'America/New_York')
|
||||
// → [ 2018-01-19T22:15:00.000Z, 2018-01-19T22:16:00.000Z ]
|
||||
|
||||
const stop = Cron.schedule('@hourly', () => {
|
||||
console.log(new Date())
|
||||
}, 'UTC+05:30')
|
||||
```
|
||||
|
||||
### Pattern syntax
|
||||
|
||||
```
|
||||
<minute> <hour> <day of month> <month> <day of week>
|
||||
```
|
||||
|
||||
|
||||
Each entry can be:
|
||||
|
||||
- a single value
|
||||
- a range (`0-23` or `*/2`)
|
||||
- a list of values/ranges (`1,8-12`)
|
||||
|
||||
A wildcard (`*`) can be used as a shortcut for the whole range
|
||||
(`first-last`).
|
||||
|
||||
Step values can be used in conjunctions with ranges. For instance,
|
||||
`1-7/2` is the same as `1,3,5,7`.
|
||||
|
||||
| Field | Allowed values |
|
||||
|------------------|----------------|
|
||||
| minute | 0-59 |
|
||||
| hour | 0-23 |
|
||||
| day of the month | 1-31 or 3-letter names (`jan`, `feb`, …) |
|
||||
| month | 0-11 |
|
||||
| day of week | 0-7 (0 and 7 both mean Sunday) or 3-letter names (`mon`, `tue`, …) |
|
||||
|
||||
> Note: the month range is 0-11 to be compatible with
|
||||
> [cron](https://github.com/kelektiv/node-cron), it does not appear to
|
||||
> be very standard though.
|
||||
|
||||
## 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
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
57
@xen-orchestra/cron/package.json
Normal file
57
@xen-orchestra/cron/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/cron",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "Focused, well maintained, cron parser/scheduler",
|
||||
"keywords": [
|
||||
"cron",
|
||||
"cronjob",
|
||||
"crontab",
|
||||
"job",
|
||||
"parser",
|
||||
"pattern",
|
||||
"schedule",
|
||||
"scheduling",
|
||||
"task"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/@xen-orchestra/cron",
|
||||
"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": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"luxon": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.38",
|
||||
"@babel/core": "7.0.0-beta.38",
|
||||
"@babel/preset-env": "7.0.0-beta.38",
|
||||
"@babel/preset-flow": "7.0.0-beta.38",
|
||||
"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 clean",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
40
@xen-orchestra/cron/src/index.js
Normal file
40
@xen-orchestra/cron/src/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import nextDate from './next'
|
||||
import parse from './parse'
|
||||
|
||||
const autoParse = pattern =>
|
||||
typeof pattern === 'string' ? parse(pattern) : schedule
|
||||
|
||||
export const next = (cronPattern, n = 1, zone = 'utc') => {
|
||||
const schedule = autoParse(cronPattern)
|
||||
|
||||
let date = DateTime.fromObject({ zone })
|
||||
const dates = []
|
||||
for (let i = 0; i < n; ++i) {
|
||||
dates.push((date = nextDate(schedule, date)).toJSDate())
|
||||
}
|
||||
return dates
|
||||
}
|
||||
|
||||
export { parse }
|
||||
|
||||
export const schedule = (cronPattern, fn, zone = 'utc') => {
|
||||
const wrapper = () => {
|
||||
fn()
|
||||
scheduleNextRun()
|
||||
}
|
||||
|
||||
let handle
|
||||
const schedule = autoParse(cronPattern)
|
||||
const scheduleNextRun = () => {
|
||||
const now = DateTime.fromObject({ zone })
|
||||
const nextRun = next(schedule, now)
|
||||
handle = setTimeout(wrapper, nextRun - now)
|
||||
}
|
||||
|
||||
scheduleNextRun()
|
||||
return () => {
|
||||
clearTimeout(handle)
|
||||
}
|
||||
}
|
90
@xen-orchestra/cron/src/next.js
Normal file
90
@xen-orchestra/cron/src/next.js
Normal file
@ -0,0 +1,90 @@
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const NEXT_MAPPING = {
|
||||
month: { year: 1 },
|
||||
day: { month: 1 },
|
||||
weekday: { week: 1 },
|
||||
hour: { day: 1 },
|
||||
minute: { hour: 1 },
|
||||
}
|
||||
|
||||
const getFirst = values => values !== undefined ? values[0] : 0
|
||||
|
||||
const setFirstAvailable = (date, unit, values) => {
|
||||
if (values === undefined) {
|
||||
return date
|
||||
}
|
||||
|
||||
const curr = date.get(unit)
|
||||
const next = values[sortedIndex(values, curr) % values.length]
|
||||
if (curr === next) {
|
||||
return date
|
||||
}
|
||||
|
||||
const newDate = date.set({ [unit]: next })
|
||||
return newDate > date ? newDate : newDate.plus(NEXT_MAPPING[unit])
|
||||
}
|
||||
|
||||
// returns the next run, after the passed date
|
||||
export default (schedule, fromDate) => {
|
||||
let date = fromDate
|
||||
.set({
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
})
|
||||
.plus({ minute: 1 })
|
||||
|
||||
const { minute, hour, dayOfMonth, month, dayOfWeek } = schedule
|
||||
date = setFirstAvailable(date, 'minute', minute)
|
||||
|
||||
let tmp
|
||||
|
||||
tmp = setFirstAvailable(date, 'hour', hour)
|
||||
if (tmp !== date) {
|
||||
date = tmp.set({
|
||||
minute: getFirst(minute),
|
||||
})
|
||||
}
|
||||
|
||||
let loop
|
||||
let i = 0
|
||||
do {
|
||||
loop = false
|
||||
|
||||
tmp = setFirstAvailable(date, 'month', month)
|
||||
if (tmp !== date) {
|
||||
date = tmp.set({
|
||||
day: 1,
|
||||
hour: getFirst(hour),
|
||||
minute: getFirst(minute),
|
||||
})
|
||||
}
|
||||
|
||||
if (dayOfMonth === undefined) {
|
||||
if (dayOfWeek !== undefined) {
|
||||
tmp = setFirstAvailable(date, 'weekday', dayOfWeek)
|
||||
}
|
||||
} else if (dayOfWeek === undefined) {
|
||||
tmp = setFirstAvailable(date, 'day', dayOfMonth)
|
||||
} else {
|
||||
tmp = DateTime.min(
|
||||
setFirstAvailable(date, 'day', dayOfMonth),
|
||||
setFirstAvailable(date, 'weekday', dayOfWeek)
|
||||
)
|
||||
}
|
||||
if (tmp !== date) {
|
||||
loop = tmp.month !== date.month
|
||||
date = tmp.set({
|
||||
hour: getFirst(hour),
|
||||
minute: getFirst(minute),
|
||||
})
|
||||
}
|
||||
} while (loop && ++i < 5)
|
||||
|
||||
if (loop) {
|
||||
throw new Error('no solutions found for this schedule')
|
||||
}
|
||||
|
||||
return date
|
||||
}
|
46
@xen-orchestra/cron/src/next.spec.js
Normal file
46
@xen-orchestra/cron/src/next.spec.js
Normal file
@ -0,0 +1,46 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import next from './next'
|
||||
import parse from './parse'
|
||||
|
||||
const N = (pattern, fromDate = '2018-04-09T06:25') =>
|
||||
next(parse(pattern), DateTime.fromISO(fromDate, { zone: 'utc' })).toISO({
|
||||
includeOffset: false,
|
||||
suppressMilliseconds: true,
|
||||
suppressSeconds: true,
|
||||
})
|
||||
|
||||
describe('next()', () => {
|
||||
mapValues(
|
||||
{
|
||||
minutely: ['* * * * *', '2018-04-09T06:26'],
|
||||
hourly: ['@hourly', '2018-04-09T07:00'],
|
||||
daily: ['@daily', '2018-04-10T00:00'],
|
||||
monthly: ['@monthly', '2018-05-01T00:00'],
|
||||
yearly: ['@yearly', '2019-01-01T00:00'],
|
||||
weekly: ['@weekly', '2018-04-15T00:00'],
|
||||
},
|
||||
([pattern, result], title) =>
|
||||
it(title, () => {
|
||||
expect(N(pattern)).toBe(result)
|
||||
})
|
||||
)
|
||||
|
||||
it('select first between month-day and week-day', () => {
|
||||
expect(N('* * 10 * wen')).toBe('2018-04-10T00:00')
|
||||
expect(N('* * 12 * wen')).toBe('2018-04-11T00:00')
|
||||
})
|
||||
|
||||
it('select the last available day of a month', () => {
|
||||
expect(N('* * 29 feb *')).toBe('2020-02-29T00:00')
|
||||
})
|
||||
|
||||
it('fails when no solutions has been found', () => {
|
||||
expect(() => N('0 0 30 feb *')).toThrow(
|
||||
'no solutions found for this schedule'
|
||||
)
|
||||
})
|
||||
})
|
198
@xen-orchestra/cron/src/parse.js
Normal file
198
@xen-orchestra/cron/src/parse.js
Normal file
@ -0,0 +1,198 @@
|
||||
const compareNumbers = (a, b) => a - b
|
||||
|
||||
const createParser = ({ fields: [...fields], presets: { ...presets } }) => {
|
||||
const m = fields.length
|
||||
|
||||
for (let j = 0; j < m; ++j) {
|
||||
const field = fields[j]
|
||||
let { aliases } = field
|
||||
if (aliases !== undefined) {
|
||||
let symbols = aliases
|
||||
|
||||
if (Array.isArray(aliases)) {
|
||||
aliases = {}
|
||||
const [start] = field.range
|
||||
symbols.forEach((alias, i) => {
|
||||
aliases[alias] = start + i
|
||||
})
|
||||
} else {
|
||||
symbols = Object.keys(aliases)
|
||||
}
|
||||
|
||||
fields[j] = {
|
||||
...field,
|
||||
aliases,
|
||||
aliasesRegExp: new RegExp(symbols.join('|'), 'y'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let field, i, n, pattern, schedule, values
|
||||
|
||||
const isDigit = c => c >= '0' && c <= '9'
|
||||
const match = c => pattern[i] === c && (++i, true)
|
||||
|
||||
const consumeWhitespaces = () => {
|
||||
let c
|
||||
while ((c = pattern[i]) === ' ' || c === '\t') {
|
||||
++i
|
||||
}
|
||||
}
|
||||
|
||||
const parseInteger = () => {
|
||||
let c
|
||||
const digits = []
|
||||
while (isDigit((c = pattern[i]))) {
|
||||
++i
|
||||
digits.push(c)
|
||||
}
|
||||
if (digits.length === 0) {
|
||||
throw new SyntaxError(`${field.name}: missing integer at character ${i}`)
|
||||
}
|
||||
return Number.parseInt(digits.join(''), 10)
|
||||
}
|
||||
|
||||
const parseValue = () => {
|
||||
let value
|
||||
|
||||
const { aliasesRegExp } = field
|
||||
if (aliasesRegExp === undefined || isDigit(pattern[i])) {
|
||||
value = parseInteger()
|
||||
const { post } = field
|
||||
if (post !== undefined) {
|
||||
value = post(value)
|
||||
}
|
||||
} else {
|
||||
aliasesRegExp.lastIndex = i
|
||||
const matches = aliasesRegExp.exec(pattern)
|
||||
if (matches === null) {
|
||||
throw new SyntaxError(
|
||||
`${field.name}: missing alias or integer at character ${i}`
|
||||
)
|
||||
}
|
||||
const [alias] = matches
|
||||
i += alias.length
|
||||
value = field.aliases[alias]
|
||||
}
|
||||
|
||||
const { range } = field
|
||||
if (value < range[0] || value > range[1]) {
|
||||
throw new SyntaxError(
|
||||
`${field.name}: ${value} is not between ${range[0]} and ${range[1]}`
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const parseRange = () => {
|
||||
let end, start, step
|
||||
if (match('*')) {
|
||||
if (!match('/')) {
|
||||
return
|
||||
}
|
||||
[start, end] = field.range
|
||||
step = parseInteger()
|
||||
} else {
|
||||
start = parseValue()
|
||||
if (!match('-')) {
|
||||
values.add(start)
|
||||
return
|
||||
}
|
||||
end = parseValue()
|
||||
step = match('/') ? parseInteger() : 1
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i += step) {
|
||||
values.add(i)
|
||||
}
|
||||
}
|
||||
|
||||
const parseSequence = () => {
|
||||
do {
|
||||
parseRange()
|
||||
} while (match(','))
|
||||
}
|
||||
|
||||
const parse = p => {
|
||||
{
|
||||
const schedule = presets[p]
|
||||
if (schedule !== undefined) {
|
||||
return typeof schedule === 'string'
|
||||
? (presets[p] = parse(schedule))
|
||||
: schedule
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
i = 0
|
||||
n = p.length
|
||||
pattern = p
|
||||
schedule = {}
|
||||
|
||||
for (let j = 0; j < m; ++j) {
|
||||
consumeWhitespaces()
|
||||
|
||||
field = fields[j]
|
||||
values = new Set()
|
||||
parseSequence()
|
||||
if (values.size !== 0) {
|
||||
schedule[field.name] = Array.from(values).sort(compareNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
consumeWhitespaces()
|
||||
if (i !== n) {
|
||||
throw new SyntaxError(
|
||||
`unexpected character at offset ${i}, expected end`
|
||||
)
|
||||
}
|
||||
|
||||
return schedule
|
||||
} finally {
|
||||
field = pattern = schedule = values = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return parse
|
||||
}
|
||||
|
||||
export default createParser({
|
||||
fields: [
|
||||
{
|
||||
name: 'minute',
|
||||
range: [0, 59],
|
||||
},
|
||||
{
|
||||
name: 'hour',
|
||||
range: [0, 23],
|
||||
},
|
||||
{
|
||||
name: 'dayOfMonth',
|
||||
range: [1, 31],
|
||||
},
|
||||
{
|
||||
aliases: 'jan feb mar apr may jun jul aug sep oct nov dec'.split(' '),
|
||||
name: 'month',
|
||||
range: [1, 12],
|
||||
|
||||
// this function is applied to numeric entries (not steps)
|
||||
//
|
||||
// currently parse month 0-11
|
||||
post: value => value + 1,
|
||||
},
|
||||
{
|
||||
aliases: 'mon tue wen thu fri sat sun'.split(' '),
|
||||
name: 'dayOfWeek',
|
||||
post: value => (value === 0 ? 7 : value),
|
||||
range: [1, 7],
|
||||
},
|
||||
],
|
||||
presets: {
|
||||
'@annually': '0 0 1 jan *',
|
||||
'@daily': '0 0 * * *',
|
||||
'@hourly': '0 * * * *',
|
||||
'@monthly': '0 0 1 * *',
|
||||
'@weekly': '0 0 * * sun',
|
||||
'@yearly': '0 0 1 jan *',
|
||||
},
|
||||
})
|
51
@xen-orchestra/cron/src/parse.spec.js
Normal file
51
@xen-orchestra/cron/src/parse.spec.js
Normal file
@ -0,0 +1,51 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import parse from './parse'
|
||||
|
||||
describe('parse()', () => {
|
||||
it('works', () => {
|
||||
expect(parse('0 0-10 */10 jan,2,4-11/3 *')).toEqual({
|
||||
minute: [0],
|
||||
hour: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
dayOfMonth: [1, 11, 21, 31],
|
||||
month: [1, 3, 5, 8, 11],
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly parse months', () => {
|
||||
expect(parse('* * * 0,11 *')).toEqual({
|
||||
month: [1, 12],
|
||||
})
|
||||
expect(parse('* * * jan,dec *')).toEqual({
|
||||
month: [1, 12],
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly parse days', () => {
|
||||
expect(parse('* * * * mon,sun')).toEqual({
|
||||
dayOfWeek: [1, 7],
|
||||
})
|
||||
})
|
||||
|
||||
it('reports missing integer', () => {
|
||||
expect(() => parse('*/a')).toThrow(
|
||||
'minute: missing integer at character 2'
|
||||
)
|
||||
expect(() => parse('*')).toThrow('hour: missing integer at character 1')
|
||||
})
|
||||
|
||||
it('reports invalid aliases', () => {
|
||||
expect(() => parse('* * * jan-foo *')).toThrow(
|
||||
'month: missing alias or integer at character 10'
|
||||
)
|
||||
})
|
||||
|
||||
it('dayOfWeek: 0 and 7 bind to sunday', () => {
|
||||
expect(parse('* * * * 0')).toEqual({
|
||||
dayOfWeek: [7],
|
||||
})
|
||||
expect(parse('* * * * 7')).toEqual({
|
||||
dayOfWeek: [7],
|
||||
})
|
||||
})
|
||||
})
|
@ -29,9 +29,10 @@
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"transform": {
|
||||
"complex-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"value-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"xo-cli/.+\\.jsx?$": "babel-7-jest",
|
||||
"/@xen-orchestra/cron/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/complex-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/value-matcher/.+\\.jsx?$": "babel-7-jest",
|
||||
"/packages/xo-cli/.+\\.jsx?$": "babel-7-jest",
|
||||
"\\.jsx?$": "babel-jest"
|
||||
}
|
||||
},
|
||||
@ -57,6 +58,7 @@
|
||||
"test": "jest && flow status"
|
||||
},
|
||||
"workspaces": [
|
||||
"@xen-orchestra/*",
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
|
@ -1,16 +1,25 @@
|
||||
const { forEach, fromCallback } = require('promise-toolbox')
|
||||
const fs = require('fs')
|
||||
|
||||
const PKGS_DIR = `${__dirname}/../packages`
|
||||
const ROOT_DIR = `${__dirname}/..`
|
||||
|
||||
const _getPackages = scope => {
|
||||
const inScope = scope !== undefined
|
||||
const dir = `${ROOT_DIR}/${inScope ? scope : 'packages'}`
|
||||
return fromCallback(cb => fs.readdir(dir, cb)).then(names =>
|
||||
names.map(name => ({
|
||||
dir: `${dir}/${name}`,
|
||||
name: inScope ? `${scope}/${name}` : name,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
exports.getPackages = (readPackageJson = false) => {
|
||||
const p = fromCallback(cb =>
|
||||
fs.readdir(PKGS_DIR, cb)
|
||||
).then(names => {
|
||||
const pkgs = names.map(name => ({
|
||||
dir: `${PKGS_DIR}/${name}`,
|
||||
name,
|
||||
}))
|
||||
const p = Promise.all([
|
||||
_getPackages(),
|
||||
_getPackages('@xen-orchestra'),
|
||||
]).then(pkgs => {
|
||||
pkgs = [].concat(...pkgs) // flatten
|
||||
return readPackageJson
|
||||
? Promise.all(pkgs.map(pkg =>
|
||||
readFile(`${pkg.dir}/package.json`).then(data => {
|
||||
|
10
yarn.lock
10
yarn.lock
@ -2681,6 +2681,10 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
|
||||
mkdirp ">=0.5 0"
|
||||
rimraf "2"
|
||||
|
||||
full-icu@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/full-icu/-/full-icu-1.2.0.tgz#6bd8bf565f696aab30df503de2d47b92d9f1046f"
|
||||
|
||||
function-bind@^1.0.2, function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
@ -4182,6 +4186,12 @@ ltx@^2.5.0, ltx@^2.6.2:
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
|
||||
luxon@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-0.4.0.tgz#e7146ce52a6db7e82448438827860092d75bb8d6"
|
||||
dependencies:
|
||||
full-icu "^1.2.0"
|
||||
|
||||
make-error@^1.0.2, make-error@^1.0.4, make-error@^1.2.1, make-error@^1.2.3, make-error@^1.3.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.2.tgz#8762ffad2444dd8ff1f7c819629fa28e24fea1c4"
|
||||
|
Loading…
Reference in New Issue
Block a user