├── doc ├── screenshot.png └── about_control_server_data.md ├── release ├── pkg-config.json ├── package.json ├── index.js └── yarn.lock ├── .editorconfig ├── .gitignore ├── ui ├── css │ ├── main.css │ ├── _playback.css │ ├── _layout.css │ ├── _volume.css │ ├── _trackDetails.css │ ├── _variables.css │ ├── _base.css │ └── _controlButton.css ├── tsconfig.json ├── index.tsx ├── format.ts ├── TrackDetails.tsx ├── Volume.tsx ├── Playback.tsx ├── App.tsx └── Icon.tsx ├── server ├── test │ ├── .eslintrc.json │ ├── fixtures.ts │ ├── Message.test.ts │ └── api.test.ts ├── tsconfig.json ├── Util.ts ├── Logger.ts ├── Server.ts ├── ControlServer.ts ├── main.ts ├── Foobar.ts ├── MockControlServer │ ├── Update.ts │ └── index.ts ├── Models.ts └── Message.ts ├── tsconfig.json ├── .gitattributes ├── .github └── workflows │ ├── validate.yml │ └── release.yml ├── LICENSE.md ├── .eslintrc.js ├── webpack.config.js ├── package.json └── README.md /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klemola/foobar2000-web-ui/HEAD/doc/screenshot.png -------------------------------------------------------------------------------- /release/pkg-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": "main.js", 3 | "assets": "../build/static/**/*" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | *.log 4 | .DS_Store 5 | .vscode 6 | build/ 7 | dist/ 8 | logs/ 9 | tsconfig.tsbuildinfo 10 | -------------------------------------------------------------------------------- /ui/css/main.css: -------------------------------------------------------------------------------- 1 | @import './_variables.css'; 2 | @import './_base.css'; 3 | @import './_layout.css'; 4 | @import './_playback.css'; 5 | @import './_volume.css'; 6 | @import './_trackDetails.css'; 7 | @import './_controlButton.css'; 8 | -------------------------------------------------------------------------------- /server/test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true, "optionalDependencies": true}], 4 | "promise/catch-or-return": 0, 5 | "promise/always-return": 0, 6 | "global-require": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /release/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foobar2000-web-ui-release", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "package": "node index.js" 7 | }, 8 | "dependencies": { 9 | "pkg": "4.4.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "../build", 6 | "target": "es5", 7 | "module": "commonjs", 8 | "lib": ["esnext", "es2015.iterable"], 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./main.ts", "./test/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "skipLibCheck": true, 5 | "allowSyntheticDefaultImports": true, 6 | "allowJs": false, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noEmitOnError": true, 11 | "incremental": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # By default disable end-of-line normalization 2 | * !text 3 | 4 | # Explicitly declare text files to always use UNIX newlines 5 | *.txt eol=lf 6 | *.js eol=lf 7 | *.css eol=lf 8 | *.scss eol=lf 9 | *.html eol=lf 10 | *.json eol=lf 11 | *.sh eol=lf 12 | 13 | # Declare all files that are truly binary and should not be modified. 14 | *.png binary 15 | *.jpg binary 16 | *.gif binary 17 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "target": "es5", 6 | "module": "es6", 7 | "lib": ["dom", "dom.iterable", "esnext", "es2015.iterable"], 8 | "resolveJsonModule": true, 9 | "jsx": "react", 10 | "jsxFactory": "h" 11 | }, 12 | "include": ["./**/*.tsx", "./**/*.ts"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /server/Util.ts: -------------------------------------------------------------------------------- 1 | import { Failure, Success } from 'runtypes' 2 | 3 | export const failure = (message: string): Failure => ({ 4 | success: false, 5 | message 6 | }) 7 | 8 | export const success = (value: T): Success => ({ 9 | success: true, 10 | value 11 | }) 12 | 13 | export const mapSuccess = ( 14 | s: Success, 15 | fn: (curr: T) => A 16 | ): Success => ({ 17 | ...s, 18 | value: fn(s.value) 19 | }) 20 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*" 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: "12.x" 20 | - run: yarn 21 | - run: yarn test 22 | -------------------------------------------------------------------------------- /release/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { exec } = require('pkg') 3 | 4 | const SERVER_ENTRYPOINT = '../build/main.js' 5 | const TARGET = 'node12-win-x64' 6 | const PKG_OUTPUT = './dist/foobar2000-web-ui.exe' 7 | 8 | exec([ 9 | SERVER_ENTRYPOINT, 10 | '--target', 11 | TARGET, 12 | '--output', 13 | PKG_OUTPUT, 14 | '--config', 15 | 'pkg-config.json' 16 | ]) 17 | .then(() => console.info('✅ .exe built!')) 18 | .catch(console.error) 19 | -------------------------------------------------------------------------------- /ui/css/_playback.css: -------------------------------------------------------------------------------- 1 | .playback { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .playback__current-track { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | flex: 1; 11 | min-height: calc(var(--vh, 1vh) * 50); 12 | } 13 | 14 | .playback__controls--main, 15 | .playback__controls--secondary { 16 | display: flex; 17 | padding: var(--spacing-s); 18 | } 19 | 20 | .playback__controls--secondary__spacer { 21 | flex: 1; 22 | } 23 | 24 | @media (max-width: 480px) { 25 | .playback { 26 | flex: 1; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact' 2 | 3 | import App from './App' 4 | 5 | require('modern-css-reset') 6 | require('./css/main.css') 7 | 8 | // Replaces the native VH CSS unit with a custom variable so that browser chrome doesn't overlap with app controls. 9 | const setRealVH = () => { 10 | let vh = window.innerHeight * 0.01 11 | document.documentElement.style.setProperty('--vh', `${vh}px`) 12 | } 13 | 14 | window.addEventListener('resize', setRealVH) 15 | window.addEventListener('load', setRealVH) 16 | 17 | // Briefly "activates" buttons on tap 18 | document.addEventListener('touchstart', function() {}, false) 19 | 20 | render(, document.body) 21 | -------------------------------------------------------------------------------- /ui/css/_layout.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .app__init { 7 | width: 100vw; 8 | height: calc(var(--vh, 1vh) * 100); 9 | display: flex; 10 | flex-direction: column; 11 | align-content: center; 12 | justify-content: center; 13 | text-align: center; 14 | } 15 | 16 | .app__wrapper { 17 | width: 100vw; 18 | max-width: var(--app-max-width); 19 | margin: 0 auto; 20 | display: flex; 21 | flex-direction: column; 22 | padding: var(--spacing-s); 23 | } 24 | 25 | @media (max-width: 480px) { 26 | .app__wrapper { 27 | height: calc(var(--vh, 1vh) * 100); 28 | max-height: calc(var(--vh, 1vh) * 100); 29 | max-width: 100vw; 30 | overflow: hidden; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /doc/about_control_server_data.md: -------------------------------------------------------------------------------- 1 | # About data sent by foobar_controlserver 2 | 3 | Initial message upon connection looks like this: 4 | 5 | Message 1: 6 | 7 | ``` 8 | 999|Connected to foobar2000 Control Server v1.0.1| 9 | 999|Accepted client from 127.0.0.1| 10 | 999|There are currently 2/10 clients connected| 11 | 999|Type '?' or 'help' for command information| 12 | ``` 13 | 14 | Message 2 (and subsequent playback status messages): 15 | 16 | ``` 17 | 113|3|282|2.73|FLAC|605|Imaginary Friends|Bronchitis|2013|Post-rock|?|Bronchitis (entire)|745| 18 | ``` 19 | 20 | If music is not playing when user connects, messages 1 and 2 will be merged. 21 | 22 | Changes in volume look like this: 23 | 24 | ``` 25 | 222|-1.58| 26 | ``` 27 | 28 | When volume is muted, `-100` is the volume message value. 29 | -------------------------------------------------------------------------------- /ui/format.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../server/Models' 2 | 3 | function pad(num: number): string { 4 | return `${num < 10 ? '0' : ''}${num}` 5 | } 6 | 7 | export function formatDuration(ms: number): string { 8 | if (isNaN(ms)) return '00:00' 9 | 10 | let remaining = ms 11 | 12 | const seconds = Math.floor(remaining % 60) 13 | remaining /= 60 14 | 15 | const minutes = Math.floor(remaining % 60) 16 | remaining /= 60 17 | 18 | const hours = Math.floor(remaining % 24) 19 | 20 | return [hours, minutes, seconds] 21 | .filter((num, i) => num > 0 || i > 0) 22 | .map(pad) 23 | .join(':') 24 | } 25 | 26 | export function formatVolume(volume: Volume): string { 27 | if (volume.type === 'muted') { 28 | return 'Muted.' 29 | } 30 | 31 | return `${volume.volume} dB` 32 | } 33 | -------------------------------------------------------------------------------- /ui/css/_volume.css: -------------------------------------------------------------------------------- 1 | .volume { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | background: var(--color-bg-box); 6 | border: 1px solid var(--color-border); 7 | border-radius: var(--border-radius); 8 | } 9 | 10 | .volume__value, 11 | .volume__controls { 12 | display: flex; 13 | padding: var(--spacing-xs) var(--spacing-s); 14 | align-items: center; 15 | } 16 | 17 | .volume__value { 18 | flex: 1; 19 | user-select: none; 20 | } 21 | 22 | .volume__controls { 23 | width: 240px; 24 | max-width: 240px; 25 | } 26 | 27 | @media (max-width: 480px) { 28 | .volume { 29 | width: 100vw; 30 | margin-left: calc(0px - var(--spacing-s)); 31 | margin-bottom: calc(0px - var(--spacing-s)); 32 | padding-bottom: var(--spacing-s); 33 | border-radius: 0; 34 | border-width: 1px 0 0 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/test/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { TrackInfo } from '../Models' 2 | 3 | export const mockTrack1: TrackInfo = { 4 | status: 111, 5 | secondsPlayed: 1, 6 | codec: 'FLAC', 7 | bitrate: 605, 8 | artist: 'Mock Artist', 9 | album: 'Mock Album', 10 | date: '2019', 11 | genre: 'Test', 12 | trackNumber: '01', 13 | track: 'Mock Track #1', 14 | trackLength: 120, 15 | state: 'playing', 16 | } 17 | 18 | export const mockTrack2: TrackInfo = { 19 | status: 111, 20 | secondsPlayed: 1, 21 | codec: 'FLAC', 22 | bitrate: 605, 23 | artist: 'Mock Artist', 24 | album: 'Mock Album', 25 | date: '2019', 26 | genre: 'Test', 27 | trackNumber: '02', 28 | track: 'Mock Track #2', 29 | trackLength: 30, 30 | state: 'playing', 31 | } 32 | 33 | export const initialMsg = `999|Connected to foobar2000 Control Server v1.0.1|\r 34 | 999|Accepted client from 127.0.0.1|\r 35 | 999|There are currently 1/10 clients connected|\r 36 | 999|Type '?' or 'help' for command information|\r` 37 | -------------------------------------------------------------------------------- /ui/css/_trackDetails.css: -------------------------------------------------------------------------------- 1 | .track-details, 2 | .track-details--stopped { 3 | padding: var(--spacing-s); 4 | font-size: var(--font-size-h3); 5 | } 6 | 7 | .track-details--stopped { 8 | text-align: center; 9 | font-weight: var(--font-weight-bold); 10 | } 11 | 12 | .track-details__info { 13 | list-style: none; 14 | padding: 0; 15 | text-align: center; 16 | } 17 | 18 | .track-details__time { 19 | text-align: center; 20 | padding: var(--spacing-s); 21 | } 22 | 23 | .track-details__info__track, 24 | .track-details__info__artist, 25 | .track-details__info__album { 26 | padding: var(--spacing-xs) var(--spacing-s) 0; 27 | } 28 | 29 | .track-details__info__track { 30 | font-weight: var(--font-weight-bold); 31 | } 32 | 33 | .track-details__info__artist { 34 | color: var(--color-highlight); 35 | } 36 | 37 | .track-details__info__track__number { 38 | color: var(--color-text-muted); 39 | margin-right: var(--spacing-xs); 40 | } 41 | 42 | .track-details__time, 43 | .track-details__info__track__number { 44 | user-select: none; 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matias Klemola 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 -------------------------------------------------------------------------------- /server/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | import 'winston-daily-rotate-file' 3 | 4 | import { Env } from './Models' 5 | export type Logger = winston.Logger 6 | 7 | export const create = ( 8 | env: Env = 'production', 9 | service: string = 'fb2k-web-ui-server' 10 | ): Logger => { 11 | const [fileLogLevel, consoleLogLevel] = Env.match( 12 | prod => ['info', 'error'], 13 | dev => ['debug', 'info'], 14 | test => ['debug', 'error'] 15 | )(env) 16 | 17 | return winston.createLogger({ 18 | format: winston.format.json(), 19 | defaultMeta: { service }, 20 | transports: [ 21 | new winston.transports.DailyRotateFile({ 22 | filename: `%DATE%-${service}-${env}.log`, 23 | dirname: 'logs', 24 | datePattern: 'YYYY-MM-DD', 25 | maxSize: '10m', 26 | maxFiles: '3', 27 | level: fileLogLevel 28 | }), 29 | new winston.transports.Console({ 30 | format: winston.format.cli(), 31 | level: consoleLogLevel 32 | }) 33 | ] 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint', 'import'], 6 | env: { 7 | browser: true, 8 | mocha: true 9 | }, 10 | extends: [ 11 | 'prettier', 12 | 'plugin:import/errors', 13 | 'plugin:import/warnings', 14 | 'plugin:import/typescript', 15 | 'plugin:@typescript-eslint/recommended' 16 | ], 17 | parserOptions: { 18 | project: path.resolve(__dirname, './tsconfig.json'), 19 | tsconfigRootDir: __dirname, 20 | ecmaVersion: 2018, 21 | sourceType: 'module' 22 | }, 23 | rules: { 24 | 'import/no-unresolved': 'off', 25 | '@typescript-eslint/no-unused-vars': 'off', 26 | '@typescript-eslint/indent': 'off', 27 | '@typescript-eslint/no-use-before-define': 'off', 28 | '@typescript-eslint/camelcase': 'off', 29 | '@typescript-eslint/member-delimiter-style': 'off', 30 | '@typescript-eslint/explicit-member-accessibility': 'off', 31 | '@typescript-eslint/explicit-function-return-type': [ 32 | 'warn', 33 | { 34 | allowExpressions: true, 35 | allowHigherOrderFunctions: true 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/TrackDetails.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from 'preact' 2 | 3 | import { TrackInfo } from '../server/Models' 4 | import { formatDuration } from './format' 5 | 6 | interface Props { 7 | trackInfo: TrackInfo 8 | className?: string 9 | } 10 | 11 | const TrackDetails: FunctionalComponent = (props: Props) => { 12 | const { trackInfo } = props 13 | const trackDuration = `${formatDuration( 14 | trackInfo.secondsPlayed 15 | )} / ${formatDuration(trackInfo.trackLength)}` 16 | const className = props.className || '' 17 | 18 | return trackInfo.state === 'stopped' ? ( 19 |
Stopped.
20 | ) : ( 21 |
22 |
    23 |
  • 24 | {trackInfo.artist} 25 |
  • 26 |
  • 27 | {trackInfo.album} 28 |
  • 29 |
  • 30 | 31 | {trackInfo.trackNumber} 32 | 33 | {trackInfo.track} 34 |
  • 35 |
36 |
{trackDuration}
37 |
38 | ) 39 | } 40 | 41 | export default TrackDetails 42 | -------------------------------------------------------------------------------- /server/Server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import path from 'path' 3 | import express from 'express' 4 | import * as bodyparser from 'body-parser' 5 | import socketio from 'socket.io' 6 | 7 | import * as Foobar from './Foobar' 8 | import { Context } from './Models' 9 | 10 | function createErrorHandler(ctx: Context, io: socketio.Server) { 11 | return () => 12 | io.sockets.emit( 13 | 'controlServerError', 14 | 'Connection to Foobar control server ended.' 15 | ) 16 | } 17 | 18 | export function create() { 19 | const app = express() 20 | const server = http.createServer(app) 21 | const io = socketio(server) 22 | 23 | return { 24 | server, 25 | app, 26 | io 27 | } 28 | } 29 | 30 | export function configureStatic(ctx: Context, app: express.Application) { 31 | const staticDir = path.join(__dirname, 'static') 32 | 33 | app.use(bodyparser.json()) 34 | app.use(express.static(staticDir)) 35 | 36 | app.locals.pretty = true 37 | 38 | app.get('/', (req, res) => res.sendFile(path.join(staticDir, 'index.html'))) 39 | } 40 | 41 | export function configureWebsockets(ctx: Context, io: socketio.Server) { 42 | const handleErr = createErrorHandler(ctx, io) 43 | 44 | io.sockets.on('connection', socket => { 45 | ctx.logger.info('Web client connected', { socketId: socket.id }) 46 | 47 | socket.on('disconnect', () => { 48 | ctx.logger.info('Web client disconnected', { socketId: socket.id }) 49 | }) 50 | 51 | socket.on('foobarCommand', command => 52 | Foobar.sendCommand(ctx, io, command) 53 | ) 54 | 55 | // TODO: query current volume 56 | Foobar.queryTrackInfo(ctx, io) 57 | }) 58 | 59 | ctx.client.on('data', Foobar.onData(ctx, io)) 60 | ctx.client.on('end', handleErr) 61 | ctx.client.on('error', handleErr) 62 | } 63 | -------------------------------------------------------------------------------- /ui/Volume.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from 'preact' 2 | import classnames from 'classnames' 3 | 4 | import { Volume, Action } from '../server/Models' 5 | import { formatVolume } from './format' 6 | import * as Icon from './Icon' 7 | 8 | interface Props { 9 | currentVolume: Volume 10 | onFoobarCommand: (action: Action) => Action 11 | } 12 | 13 | const VolumeControl: FunctionalComponent = (props: Props) => { 14 | const { currentVolume, onFoobarCommand } = props 15 | const volumePresentation = formatVolume(currentVolume) 16 | const atMaxVolume = 17 | currentVolume.type === 'audible' && currentVolume.volume === 0 18 | 19 | return ( 20 |
21 |
{volumePresentation}
22 |
23 | 33 | 39 | 46 |
47 |
48 | ) 49 | } 50 | 51 | export default VolumeControl 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.**" 7 | 8 | jobs: 9 | package: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Set env 16 | run: | 17 | echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:10}) 18 | echo ::set-env name=ARCHIVE_NAME::$(echo "foobar2000-web-ui-${GITHUB_REF:10}.zip") 19 | 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: "12.x" 23 | 24 | - name: Build 25 | run: | 26 | yarn 27 | yarn build 28 | 29 | - name: Package 30 | id: package 31 | run: | 32 | cd ./release 33 | yarn 34 | yarn package 35 | zip -j dist/${{ env.ARCHIVE_NAME }} dist/foobar2000-web-ui.exe 36 | 37 | - name: Create Release 38 | id: create_release 39 | uses: actions/create-release@v1.0.0 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tag_name: ${{ env.RELEASE_VERSION }} 44 | release_name: Release ${{ env.RELEASE_VERSION }} 45 | draft: false 46 | prerelease: false 47 | 48 | - name: Upload Release Asset 49 | id: upload-release-asset 50 | uses: actions/upload-release-asset@v1.0.1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | upload_url: ${{ steps.create_release.outputs.upload_url }} 55 | asset_path: "./release/dist/${{ env.ARCHIVE_NAME }}" 56 | asset_name: ${{ env.ARCHIVE_NAME }} 57 | asset_content_type: application/zip 58 | -------------------------------------------------------------------------------- /server/ControlServer.ts: -------------------------------------------------------------------------------- 1 | import * as Net from 'net' 2 | 3 | import { Context } from './Models' 4 | import { Logger } from 'Logger' 5 | 6 | const connectionError = new Error('Could not connect to control server') 7 | 8 | export function probe(port: number): Promise { 9 | const sock = new Net.Socket() 10 | let retriesLeft = 3 11 | 12 | sock.setTimeout(5000) 13 | 14 | return new Promise((resolve, reject) => { 15 | const tryConnect = () => { 16 | retriesLeft = retriesLeft - 1 17 | 18 | if (retriesLeft === 0) { 19 | return reject(connectionError) 20 | } 21 | 22 | return setTimeout(() => sock.connect(port, '127.0.0.1'), 1000) 23 | } 24 | 25 | sock.on('connect', () => { 26 | sock.destroy() 27 | return resolve() 28 | }) 29 | 30 | sock.on('error', tryConnect) 31 | sock.on('timeout', () => tryConnect) 32 | 33 | tryConnect() 34 | }) 35 | } 36 | 37 | export function connect(port: number, logger: Logger): Promise { 38 | const onConnectionError = (socket: Net.Socket) => (e: Error) => { 39 | logger.warn('Error in control server connection', e) 40 | socket.destroy() 41 | process.exit(1) 42 | } 43 | 44 | return new Promise(resolve => { 45 | const client: Net.Socket = Net.connect({ port }, () => { 46 | client.setKeepAlive(true, 10000) 47 | 48 | client.on('end', onConnectionError(client)) 49 | client.on('error', onConnectionError(client)) 50 | 51 | return resolve(client) 52 | }) 53 | }) 54 | } 55 | 56 | export function sendCommand(ctx: Context, command: string) { 57 | try { 58 | ctx.client.write(`${command}\r\n`) 59 | ctx.logger.debug(`Control server command sent for action ${command}`) 60 | } catch (error) { 61 | ctx.logger.warn('Could not reach control server') 62 | 63 | if (error) { 64 | ctx.logger.error(error) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/css/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Colors */ 3 | --color-dark: #000; 4 | --color-semidark: #2a2a2a; 5 | --color-semidark-dim: #1a1a1a; 6 | --color-neutral: #444; 7 | --color-light: #eaeaea; 8 | --color-light-dim: #9a9a9a; 9 | --color-primary: var(--color-light); 10 | --color-primary-active: #ffd900; 11 | --color-secondary: var(--color-dark); 12 | --color-secondary-active: var(--color-semidark); 13 | --color-highlight: #00ccff; 14 | 15 | --color-text: var(--color-primary); 16 | --color-text-placeholder: var(--color-text); 17 | --color-text-inverse: var(--color-dark); 18 | --color-text-tooltip: var(--color-dark); 19 | --color-text-muted: var(--color-light-dim); 20 | 21 | --color-bg-app: var(--color-dark); 22 | --color-bg-box: var(--color-semidark-dim); 23 | --color-border: var(--color-semidark); 24 | 25 | /* Text */ 26 | --default-font: 'HelveticaNeue', 'Helvetica Neue', 'Helvetica', 27 | 'TeXGyreHerosRegular', 'Tahoma', 'Geneva', 'Arial', sans-serif; 28 | --font-family-heading: var(--default-font); 29 | --font-family-body: var(--default-font); 30 | --font-size-text-small: 0.8em; 31 | --line-height-text-small: 1em; 32 | --font-size-text: 1em; 33 | --line-height-text: 1.4; 34 | --font-size-h4: 1em; 35 | --line-height-h4: 1.4; 36 | --font-size-h3: 1.306em; 37 | --line-height-h3: 1; 38 | --font-size-h2: 1.618em; 39 | --line-height-h2: 1; 40 | --font-size-h1: 2.118em; 41 | --line-height-h1: 1; 42 | --font-weight-bold: 800; 43 | --font-weight-semibold: 600; 44 | --font-weight-book: 500; 45 | --font-weight-regular: 400; 46 | 47 | /* Sizing */ 48 | --spacing-xxs: 0.25rem; 49 | --spacing-xs: 0.5rem; 50 | --spacing-s: 1rem; 51 | --spacing-m: 2rem; 52 | --spacing-l: 3rem; 53 | --border-radius: 5px; 54 | --app-max-width: 35em; 55 | --app-padding: var(--spacing-s); 56 | --size-button-small: 1.6rem; 57 | --size-button-medium: 2rem; 58 | --size-button-large: 3.2rem; 59 | } 60 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import * as Logger from './Logger' 2 | import * as Foobar from './Foobar' 3 | import * as Server from './Server' 4 | import * as ControlServer from './ControlServer' 5 | import { Context, Env, Config } from './Models' 6 | 7 | const DEFAULT_FOOBAR_PATH = 'C:/Program Files (x86)/foobar2000' 8 | // foo_controlserver port (default is '3333' in component configuration). 9 | const DEFAULT_CONTROL_SERVER_PORT = 3333 10 | // Web UI port. 11 | const DEFAULT_WEBSERVER_PORT = 3000 12 | // By default foo_controlserver uses '|' as a separator 13 | const controlServerMessageSeparator = '|' 14 | const environment: Env = Env.guard(process.env.NODE_ENV) 15 | ? process.env.NODE_ENV 16 | : 'production' 17 | const config: Config = { 18 | foobarPath: DEFAULT_FOOBAR_PATH, 19 | controlServerPort: DEFAULT_CONTROL_SERVER_PORT, 20 | webserverPort: DEFAULT_WEBSERVER_PORT, 21 | controlServerMessageSeparator, 22 | environment 23 | } 24 | const logger = Logger.create(config.environment) 25 | 26 | logger.debug('Initializing', config) 27 | 28 | Foobar.launch(config, logger) 29 | .then(instance => { 30 | logger.debug('Foobar launched') 31 | return Promise.all([ 32 | instance, 33 | ControlServer.connect(config.controlServerPort, logger) 34 | ]) 35 | }) 36 | .then(([instance, client]) => { 37 | const context: Context = { 38 | config, 39 | logger, 40 | client, 41 | instance 42 | } 43 | const { server, app, io } = Server.create() 44 | 45 | Server.configureStatic(context, app) 46 | Server.configureWebsockets(context, io) 47 | 48 | setInterval(() => Foobar.queryTrackInfo(context, io), 1000) 49 | server.listen(config.webserverPort) 50 | logger.debug('Initialization complete') 51 | logger.info(`Server listening on port ${config.webserverPort}`) 52 | }) 53 | .catch((err: Error) => { 54 | logger.error( 55 | 'Could not initialize server and/or connect to control server. Make sure the configuration is correct.', 56 | err 57 | ) 58 | process.exit(1) 59 | }) 60 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require('path') 4 | 5 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') 6 | const HtmlWebpackPlugin = require('html-webpack-plugin') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | 9 | const INPUT_DIR = path.resolve(__dirname, 'ui') 10 | const OUTPUT_DIR = path.resolve(__dirname, 'build', 'static') 11 | 12 | const webpackConfig = { 13 | context: INPUT_DIR, 14 | entry: { 15 | main: './index.tsx' 16 | }, 17 | output: { 18 | filename: 'ui.js', 19 | path: OUTPUT_DIR 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.ts', '.tsx', '.css', '.json', '.html'] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | loader: 'ts-loader', 29 | options: { 30 | transpileOnly: true 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: MiniCssExtractPlugin.loader, 38 | options: { 39 | modules: true, 40 | importLoaders: 1, 41 | localIdentName: '[name][hash:base64]', 42 | sourceMap: true, 43 | minimize: true 44 | } 45 | }, 46 | 'css-loader' 47 | ] 48 | } 49 | ] 50 | }, 51 | plugins: [ 52 | new ForkTsCheckerWebpackPlugin(), 53 | new HtmlWebpackPlugin({ 54 | title: 'Foobar 2000 Web UI', 55 | meta: { 56 | viewport: 'width=device-width, initial-scale=1', 57 | 'apple-mobile-web-app-capable': 'yes' 58 | } 59 | }), 60 | new MiniCssExtractPlugin({ 61 | filename: 'styles.css', 62 | chunkFilename: 'styles.css' 63 | }) 64 | ] 65 | } 66 | 67 | module.exports = webpackConfig 68 | -------------------------------------------------------------------------------- /ui/css/_base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | -moz-box-sizing: border-box; 5 | -webkit-box-sizing: border-box; 6 | box-sizing: border-box; 7 | } 8 | 9 | html, 10 | body { 11 | height: 100%; 12 | width: 100%; 13 | margin: 0; 14 | padding: 0; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | body { 21 | background: var(--color-bg-app); 22 | color: var(--color-text); 23 | font-family: var(--font-family-body); 24 | line-height: var(--line-height-text); 25 | } 26 | 27 | a { 28 | color: var(--color-primary); 29 | text-decoration: none; 30 | } 31 | 32 | a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | h1 { 37 | margin-top: var(--spacing-m); 38 | font-size: var(--font-size-h1); 39 | line-height: var(--line-height-h1); 40 | } 41 | 42 | h2 { 43 | margin-top: var(--spacing-l); 44 | font-size: var(--font-size-h2); 45 | line-height: var(--line-height-h2); 46 | } 47 | 48 | h3 { 49 | margin-top: var(--spacing-m); 50 | font-size: var(--font-size-h3); 51 | line-height: var(--line-height-h3); 52 | } 53 | 54 | h4 { 55 | font-size: var(--font-size-h4); 56 | line-height: var(--line-height-h4); 57 | } 58 | 59 | h1, 60 | h2, 61 | h3, 62 | h4 { 63 | font-family: var(--font-family-heading); 64 | letter-spacing: 1px; 65 | word-wrap: break-word; 66 | } 67 | 68 | h2 + h3 { 69 | margin-top: 0; 70 | } 71 | 72 | hr { 73 | border: 1px solid var(--color-bg-box); 74 | border-radius: var(--border-radius); 75 | width: 100%; 76 | } 77 | 78 | img { 79 | width: 100%; 80 | } 81 | 82 | li { 83 | padding: var(--spacing-xxs) 0; 84 | } 85 | 86 | button { 87 | background: var(--color-bg-box); 88 | color: var(--color-primary); 89 | padding: var(--spacing-xxs) var(--spacing-xs); 90 | border: 1px solid var(--color-border); 91 | border-radius: var(--border-radius); 92 | cursor: pointer; 93 | user-select: none; 94 | } 95 | 96 | button:hover, 97 | button:active { 98 | color: var(--color-primary-active); 99 | } 100 | 101 | button:disabled { 102 | background: var(--color-semidark-dim); 103 | color: var(--color-text-muted); 104 | } 105 | -------------------------------------------------------------------------------- /ui/Playback.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from 'preact' 2 | import classnames from 'classnames' 3 | 4 | import { TrackInfo, Action } from '../server/Models' 5 | import TrackDetails from './TrackDetails' 6 | import * as Icon from './Icon' 7 | 8 | interface Props { 9 | currentTrack: TrackInfo 10 | onFoobarCommand: (action: Action) => Action 11 | } 12 | 13 | const Playback: FunctionalComponent = (props: Props) => { 14 | const { currentTrack, onFoobarCommand } = props 15 | const isPlaying = currentTrack.state === 'playing' 16 | const isStopped = currentTrack.state === 'stopped' 17 | const playPauseAction = currentTrack.state === 'playing' ? 'pause' : 'play' 18 | 19 | return ( 20 |
21 | 25 |
26 | 32 | 38 | 44 |
45 |
46 | 55 |
56 | 62 |
63 |
64 | ) 65 | } 66 | 67 | export default Playback 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foobar2000-web-ui", 3 | "version": "0.1.0", 4 | "author": "Matias Klemola ", 5 | "description": "Control foobar2000 from a browser.", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/klemola/foobar2000-web-ui.git" 10 | }, 11 | "keywords": [ 12 | "foobar2000", 13 | "remote control", 14 | "ui", 15 | "server" 16 | ], 17 | "scripts": { 18 | "build-ui": "webpack --mode production --config webpack.config.js", 19 | "build-server": "tsc -p server/tsconfig.json", 20 | "build": "yarn build-server && yarn build-ui", 21 | "start": "cross-env-shell NODE_ENV=development \"yarn build && node build/main.js\"", 22 | "test-server": "mocha --exit ./build/**/*.test.js", 23 | "test": "yarn build && yarn test-server", 24 | "format": "prettier --write server/**/*.ts ui/**/*.ts", 25 | "lint": "eslint server/**/*.ts ui/**/*.ts" 26 | }, 27 | "dependencies": { 28 | "body-parser": "1.19.0", 29 | "classnames": "2.2.6", 30 | "express": "4.17.1", 31 | "modern-css-reset": "1.1.0", 32 | "preact": "10.4.4", 33 | "prelude-ts": "1.0.0", 34 | "runtypes": "4.3.0", 35 | "socket.io": "2.3.0", 36 | "socket.io-client": "2.3.0", 37 | "winston": "3.3.3", 38 | "winston-daily-rotate-file": "4.5.0" 39 | }, 40 | "devDependencies": { 41 | "@types/body-parser": "^1.19.0", 42 | "@types/chai": "^4.2.11", 43 | "@types/classnames": "^2.2.10", 44 | "@types/express": "^4.17.6", 45 | "@types/mocha": "^7.0.2", 46 | "@types/node": "^12.12.47", 47 | "@types/socket.io": "^2.1.8", 48 | "@types/socket.io-client": "^1.4.33", 49 | "@typescript-eslint/eslint-plugin": "3.4.0", 50 | "@typescript-eslint/parser": "3.4.0", 51 | "chai": "4.2.0", 52 | "cross-env": "7.0.2", 53 | "css-loader": "3.6.0", 54 | "eslint": "7.3.1", 55 | "eslint-config-prettier": "6.11.0", 56 | "eslint-plugin-import": "2.21.2", 57 | "fork-ts-checker-webpack-plugin": "5.0.4", 58 | "html-webpack-plugin": "4.3.0", 59 | "mini-css-extract-plugin": "0.9.0", 60 | "mocha": "8.0.1", 61 | "prettier": "2.0.5", 62 | "ts-loader": "7.0.5", 63 | "typescript": "3.9.5", 64 | "webpack": "4.43.0", 65 | "webpack-cli": "3.3.12" 66 | }, 67 | "prettier": { 68 | "semi": false, 69 | "singleQuote": true 70 | }, 71 | "engines": { 72 | "node": ">=12.18.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/css/_controlButton.css: -------------------------------------------------------------------------------- 1 | .control-button, 2 | .control-button--activated, 3 | .control-button--small, 4 | .control-button--small--activated, 5 | .control-button--large, 6 | .control-button--large--activated { 7 | flex: 1; 8 | margin: var(--spacing-xs); 9 | padding: 0; 10 | border: none; 11 | background: none; 12 | } 13 | 14 | .control-button:first-of-type, 15 | .control-button--activated:first-of-type, 16 | .control-button--small:first-of-type, 17 | .control-button--small--activated:first-of-type, 18 | .control-button--large:first-of-type, 19 | .control-button--large--activated:first-of-type { 20 | margin-left: 0; 21 | } 22 | 23 | .control-button:last-of-type, 24 | .control-button--activated:last-of-type, 25 | .control-button--small:last-of-type, 26 | .control-button--small--activated:last-of-type, 27 | .control-button--large:last-of-type, 28 | .control-button--large--activated:last-of-type { 29 | margin-right: 0; 30 | } 31 | 32 | .control-button svg, 33 | .control-button--activated svg { 34 | height: var(--size-button-medium); 35 | } 36 | 37 | .control-button--small svg, 38 | .control-button--small--activated svg { 39 | height: var(--size-button-small); 40 | } 41 | 42 | .control-button--large svg, 43 | .control-button--large--activated svg { 44 | height: var(--size-button-large); 45 | } 46 | 47 | .control-button svg, 48 | .control-button--small svg, 49 | .control-button--large svg { 50 | fill: var(--color-primary); 51 | } 52 | 53 | .control-button--activated svg, 54 | .control-button--small--activated svg, 55 | .control-button--large--activated svg { 56 | fill: var(--color-primary-active); 57 | } 58 | 59 | .control-button:disabled, 60 | .control-button--activated:disabled, 61 | .control-button--small:disabled, 62 | .control-button--small--activated:disabled, 63 | .control-button--large:disabled, 64 | .control-button--large--activated:disabled { 65 | background: none; 66 | cursor: not-allowed; 67 | pointer-events: none; 68 | } 69 | 70 | .control-button:disabled svg, 71 | .control-button--activated:disabled svg, 72 | .control-button--small:disabled svg, 73 | .control-button--small--activated:disabled svg, 74 | .control-button--large:disabled svg, 75 | .control-button--large--activated:disabled svg { 76 | fill: var(--color-text-muted); 77 | } 78 | 79 | @media (max-width: 480px) { 80 | .control-button:active svg, 81 | .control-button--small:active svg, 82 | .control-button--large:active svg { 83 | fill: var(--color-primary-active); 84 | } 85 | } 86 | 87 | @media (min-width: 481px) { 88 | .control-button:hover svg, 89 | .control-button--small:hover svg, 90 | .control-button--large:hover svg { 91 | fill: var(--color-primary-active); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui/App.tsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact' 2 | import io from 'socket.io-client' 3 | import { Option } from 'prelude-ts' 4 | 5 | import { 6 | Message, 7 | TrackInfo, 8 | Volume as VolumeType, 9 | Action 10 | } from '../server/Models' 11 | import Playback from './Playback' 12 | import Volume from './Volume' 13 | 14 | // TODO: refactor into an union type 15 | interface AppState { 16 | connected: boolean 17 | currentTrack: Option 18 | volume: VolumeType 19 | } 20 | 21 | const initialState: AppState = { 22 | connected: false, 23 | currentTrack: Option.none(), 24 | volume: { 25 | type: 'audible', 26 | volume: 0 27 | } 28 | } 29 | 30 | export default class App extends Component<{}, AppState> { 31 | socket = io('/', { 32 | autoConnect: false 33 | }) 34 | state = initialState 35 | 36 | componentDidMount() { 37 | this.socket.on('message', (message: Message) => { 38 | switch (message.type) { 39 | case 'playback': 40 | return this.setState({ 41 | currentTrack: Option.of(message.data) 42 | }) 43 | case 'volume': 44 | return this.setState({ 45 | volume: message.data 46 | }) 47 | } 48 | }) 49 | this.socket.on('connect', () => this.setState({ connected: true })) 50 | this.socket.on('disconnect', () => this.setState({ connected: false })) 51 | this.socket.open() 52 | } 53 | 54 | componentWillUnmount() { 55 | this.socket.close() 56 | } 57 | 58 | onFoobarCommand = (action: Action): Action => { 59 | this.socket.emit('foobarCommand', action) 60 | return action 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 | {!this.state.connected || this.state.currentTrack.isNone() ? ( 67 |
68 |

Foobar2000 Web UI

69 |

Connecting

70 |
71 | ) : ( 72 |
73 | 77 | 81 |
82 | )} 83 |
84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/Foobar.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | import * as child_process from 'child_process' 4 | import * as fs from 'fs' 5 | import * as path from 'path' 6 | import * as os from 'os' 7 | 8 | import * as Message from './Message' 9 | import * as ControlServer from './ControlServer' 10 | import { Context, Config, FB2KInstance, Action } from './Models' 11 | import { Logger } from 'Logger' 12 | 13 | export function launch(config: Config, logger: Logger): Promise { 14 | if (os.platform() !== 'win32') { 15 | const MockControlServer = require('./MockControlServer') 16 | MockControlServer.createServer('127.0.0.1', config.controlServerPort) 17 | 18 | return new Promise(resolve => resolve(null)) 19 | } 20 | 21 | const normalizedPath = `${path.normalize(config.foobarPath)}/` 22 | 23 | if (fs.readdirSync(normalizedPath).indexOf('foobar2000.exe') === -1) { 24 | throw new Error( 25 | 'Foobar2000.exe was not found in the path specified in configuration' 26 | ) 27 | } 28 | 29 | const fb2kInstance: FB2KInstance = child_process.spawn('foobar2000.exe', { 30 | cwd: config.foobarPath, 31 | detached: true 32 | }) 33 | 34 | return ControlServer.probe(config.controlServerPort).then(() => { 35 | logger.info('Control server detected') 36 | return fb2kInstance 37 | }) 38 | } 39 | 40 | export function queryTrackInfo(ctx: Context, io: SocketIO.Server) { 41 | return sendCommand(ctx, io, 'trackinfo') 42 | } 43 | 44 | // TODO move launchFoobar logic out of this module 45 | export function sendCommand( 46 | ctx: Context, 47 | io: SocketIO.Server, 48 | command: Action | 'launchFoobar' 49 | ) { 50 | ctx.logger.debug('Command received', { command }) 51 | 52 | if (command === 'launchFoobar') { 53 | return launch(ctx.config, ctx.logger) 54 | .then(i => { 55 | ctx.instance = i 56 | io.sockets.emit('foobarStarted') 57 | }) 58 | .catch(() => { 59 | ctx.logger.warn('Could not launch Foobar') 60 | io.sockets.emit('controlServerError', 'Could not launch Foobar') 61 | }) 62 | } 63 | 64 | return ControlServer.sendCommand(ctx, command) 65 | } 66 | 67 | export function onData(ctx: Context, io: SocketIO.Server) { 68 | return function controlDataHandler(data: Buffer) { 69 | const dataStr = data.toString('utf-8') 70 | const messages = Message.parseControlData(dataStr) 71 | 72 | ctx.logger.debug('Received message(s) from control server', { 73 | raw: dataStr, 74 | messages 75 | }) 76 | 77 | messages.forEach(message => { 78 | io.sockets.emit('message', message) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /server/MockControlServer/Update.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net' 2 | 3 | import { mockTrack1, mockTrack2 } from '../test/fixtures' 4 | import { 5 | TrackInfo, 6 | VolumeAction, 7 | Volume, 8 | PlaybackAction, 9 | Action, 10 | } from '../Models' 11 | 12 | export interface State { 13 | currentTrack: TrackInfo 14 | currentVolume: Volume 15 | previousVolume: Volume 16 | sockets: Array 17 | } 18 | 19 | const MAX_VOLUME = 0 20 | const MUTED_VOLUME = -100 21 | const initialVolume: Volume = { 22 | type: 'audible', 23 | volume: 0.0, 24 | } 25 | 26 | const updatePlayback = ( 27 | currentTrack: TrackInfo, 28 | action: PlaybackAction 29 | ): TrackInfo => { 30 | switch (action) { 31 | case 'play': 32 | return { 33 | ...currentTrack, 34 | state: 'playing', 35 | status: 111, 36 | } 37 | case 'pause': 38 | return { 39 | ...currentTrack, 40 | state: 'paused', 41 | status: 113, 42 | } 43 | case 'stop': 44 | return { 45 | ...currentTrack, 46 | secondsPlayed: 0, 47 | state: 'stopped', 48 | status: 112, 49 | } 50 | case 'prev': 51 | case 'next': 52 | case 'rand': 53 | return currentTrack.track === mockTrack1.track 54 | ? mockTrack2 55 | : mockTrack1 56 | } 57 | } 58 | 59 | const updateVolume = ( 60 | currentVolume: Volume, 61 | previousVolume: Volume, 62 | action: VolumeAction 63 | ): Volume => { 64 | const previousVolumeLevel = 65 | previousVolume.type === 'audible' ? previousVolume.volume : MUTED_VOLUME 66 | const volumeLevel = 67 | currentVolume.type === 'muted' 68 | ? previousVolumeLevel 69 | : currentVolume.volume 70 | 71 | switch (action) { 72 | case 'vol mute': 73 | return currentVolume.type === 'audible' 74 | ? { 75 | type: 'muted', 76 | } 77 | : { 78 | type: 'audible', 79 | volume: volumeLevel, 80 | } 81 | case 'vol up': 82 | return { 83 | type: 'audible', 84 | volume: volumeLevel < MAX_VOLUME ? volumeLevel + 1 : MAX_VOLUME, 85 | } 86 | case 'vol down': 87 | return { 88 | type: 'audible', 89 | volume: 90 | volumeLevel > MUTED_VOLUME ? volumeLevel - 1 : MUTED_VOLUME, 91 | } 92 | } 93 | } 94 | 95 | export const init = (): State => ({ 96 | currentTrack: { ...mockTrack1 }, 97 | currentVolume: initialVolume, 98 | previousVolume: initialVolume, 99 | sockets: [], 100 | }) 101 | 102 | export const update = (state: State, action: Action): State => { 103 | switch (action) { 104 | case 'trackinfo': 105 | return state 106 | case 'play': 107 | case 'pause': 108 | case 'stop': 109 | case 'prev': 110 | case 'next': 111 | case 'rand': 112 | return { 113 | ...state, 114 | currentTrack: updatePlayback(state.currentTrack, action), 115 | } 116 | case 'vol mute': 117 | case 'vol up': 118 | case 'vol down': 119 | return { 120 | ...state, 121 | currentVolume: updateVolume( 122 | state.currentVolume, 123 | state.previousVolume, 124 | action 125 | ), 126 | previousVolume: state.currentVolume, 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foobar2000 Web UI 2 | 3 | ## Notice 4 | 5 | > The codebase is being updated to meet 2019 standards. While the goal is too keep the master branch bug-free, I can't guarantee that everything works. Some minor features might be missing from the new version, and will likely be re-instated later. 6 | > 7 | > The new version requires a recent version of NodeJS and a modern browser. NodeJS will later be bundled with the application (the application will be self-contained and can be installed without Git). The content below has not been updated yet. 8 | 9 | Foobar2000 Web UI application consists of two parts: 10 | 11 | - A Node.js server that controls foobar2000 music player using [native CLI commands](http://wiki.hydrogenaudio.org/index.php?title=Foobar2000:Commandline_Guide) and [foo_controlserver component](https://code.google.com/p/foo-controlserver/) (a tcp/ip server for foobar2000) 12 | 13 | - A web application that allows the user to send basic foobar2000 playback commands and adjust application volume level. Information about the track that is currently playing is also displayed and automatically updated when the track or playback status changes. 14 | 15 | ![ScreenShot](/doc/screenshot.png) 16 | 17 | Multiple devices can connect to the server using the local network, and it's up to the user to block unwanted connections. By default the foobar plugin and server allow any connection from the network. 18 | 19 | ## Features 20 | 21 | - supports all basic foobar2000 controls 22 | - playback (play/pause/stop, next/prev and random track) 23 | - volume (mute, up/down) 24 | - current track is displayed (along with artist/album information) 25 | - the track view is updated automatically when track changes 26 | - notifications about server and foobar2000 status 27 | - user is notified is the server disconnects 28 | - user is notified when foobar2000 application is closed 29 | - user can start foobar2000 from the UI 30 | - extremely fast and reactive UI 31 | - no delay in controls 32 | - volume and track status are updated real-time 33 | 34 | ## Requirements 35 | 36 | Requires Node.js version 12+ and foobar2000 v1+. For older versions of Node (down to 0.10.23), see [this tag](https://github.com/klemola/foobar2000-web-ui/tree/legacy_nodejs). 37 | foobar2000 component foo_controlserver is also required. Download the component [from Google code](https://code.google.com/p/foo-controlserver/downloads/list). 38 | 39 | Since foobar2000 is only available for Windows, other operating systems are not supported for the server. User is assumed to run the server on the machine that foobar2000 runs on. 40 | 41 | ## Installation 42 | 43 | - download [foo_controlserver](https://code.google.com/p/foo-controlserver/downloads/list) component and copy the .dll to "components" directory in foobar2000 installation directory (restart foobar2000 if needed) 44 | - clone this repository 45 | - (optional) open config.js in an editor and make necessary changes 46 | - navigate to the directory of this project using terminal (cmd.exe) or PowerShell 47 | - run command `npm install` 48 | 49 | ## Starting the server 50 | 51 | - start foobar2000 52 | - run command `npm start` in terminal 53 | 54 | ## Running tests 55 | 56 | - run command `npm test` in terminal 57 | 58 | ## Web application browser support 59 | 60 | Web UI was tested on newest stable version of 61 | 62 | - Google Chrome 63 | - Firefox 64 | - Safari 65 | 66 | Generally speaking only recent versions of modern browsers are supported. 67 | 68 | ## Known issues 69 | 70 | Foo_controlserver doesn't update track status if it's playing a track it can't "follow". This happens if the user queues tracks from media library and not from a playlist, or "cursor follows playback" option is not enabled in foobar2000. Since this is a bug / missing feature in the component, I can't fix the issue. 71 | 72 | I will add issues to the issue tracker for things that I'd like to improve or are not working yet. 73 | 74 | ## Planned features 75 | 76 | - track history 77 | - album art through a web service (Discogs, Last.fm) 78 | - search music library and queue music / playlist 79 | -------------------------------------------------------------------------------- /server/Models.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Number, 3 | String, 4 | Literal, 5 | Record, 6 | Union, 7 | Unknown, 8 | Runtype, 9 | Static, 10 | Null 11 | } from 'runtypes' 12 | import { Socket } from 'net' 13 | import { ChildProcessWithoutNullStreams } from 'child_process' 14 | 15 | import { Logger } from './Logger' 16 | 17 | export const Env = Union( 18 | Literal('production'), 19 | Literal('development'), 20 | Literal('test') 21 | ) 22 | 23 | export type Env = Static 24 | 25 | export const StatusType = Union( 26 | Literal('playing'), 27 | Literal('stopped'), 28 | Literal('paused'), 29 | Literal('volumeChange'), 30 | Literal('info'), 31 | Literal('unknown') 32 | ) 33 | 34 | export type StatusType = Static 35 | 36 | export const PlaybackAction = Union( 37 | Literal('play'), 38 | Literal('pause'), 39 | Literal('stop'), 40 | Literal('prev'), 41 | Literal('next'), 42 | Literal('rand') 43 | ) 44 | 45 | export type PlaybackAction = Static 46 | export const playbackActions: readonly PlaybackAction[] = [ 47 | 'play', 48 | 'pause', 49 | 'stop', 50 | 'prev', 51 | 'next', 52 | 'rand' 53 | ] 54 | 55 | export const VolumeAction = Union( 56 | Literal('vol mute'), 57 | Literal('vol down'), 58 | Literal('vol up') 59 | ) 60 | 61 | export type VolumeAction = Static 62 | export const volumeActions: readonly VolumeAction[] = [ 63 | 'vol mute', 64 | 'vol down', 65 | 'vol up' 66 | ] 67 | 68 | export const MetaAction = Union(Literal('trackinfo')) 69 | export type MetaAction = Static 70 | 71 | export const Action = Union(PlaybackAction, VolumeAction, MetaAction) 72 | export type Action = Static 73 | 74 | export const Config = Record({ 75 | foobarPath: String, 76 | controlServerPort: Number, 77 | webserverPort: Number, 78 | controlServerMessageSeparator: String, 79 | environment: Env 80 | }) 81 | 82 | export type Config = Static 83 | 84 | export const FB2KInstance = (Unknown as Runtype< 85 | ChildProcessWithoutNullStreams 86 | >).Or(Null) 87 | 88 | export type FB2KInstance = Static 89 | 90 | const Context = Record({ 91 | config: Config, 92 | logger: Unknown as Runtype, 93 | client: Unknown as Runtype, 94 | instance: FB2KInstance 95 | }) 96 | 97 | export type Context = Static 98 | 99 | export const PlaybackState = Union( 100 | Literal('playing'), 101 | Literal('paused'), 102 | Literal('stopped') 103 | ) 104 | export type PlaybackState = Static 105 | 106 | export const TrackInfo = Record({ 107 | status: Number, 108 | secondsPlayed: Number, 109 | codec: String, 110 | bitrate: Number, 111 | artist: String, 112 | album: String, 113 | date: String, 114 | genre: String, 115 | trackNumber: String, 116 | track: String, 117 | trackLength: Number, 118 | state: PlaybackState 119 | }) 120 | 121 | export type TrackInfo = Static 122 | 123 | export const Muted = Record({ 124 | type: Literal('muted') 125 | }) 126 | 127 | export type Muted = Static 128 | 129 | export const Audible = Record({ 130 | type: Literal('audible'), 131 | volume: Number 132 | }) 133 | 134 | export type Audible = Static 135 | 136 | export const Volume = Union(Muted, Audible) 137 | 138 | export type Volume = Static 139 | 140 | export const InfoMessage = Record({ 141 | type: Literal('info'), 142 | data: String 143 | }) 144 | 145 | export type InfoMessage = Static 146 | 147 | export const PlaybackMessage = Record({ 148 | type: Literal('playback'), 149 | data: TrackInfo 150 | }) 151 | 152 | export type PlaybackMessage = Static 153 | 154 | export const VolumeMessage = Record({ 155 | type: Literal('volume'), 156 | data: Volume 157 | }) 158 | 159 | export type VolumeMessage = Static 160 | 161 | export const Message = Union(InfoMessage, PlaybackMessage, VolumeMessage) 162 | 163 | export type Message = Static 164 | -------------------------------------------------------------------------------- /server/MockControlServer/index.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net' 2 | import { Vector } from 'prelude-ts' 3 | 4 | import * as Logger from '../Logger' 5 | import { init, update, State } from './Update' 6 | import { initialMsg } from '../test/fixtures' 7 | import { TrackInfo, Volume, Action } from '../Models' 8 | 9 | class StateWrapper { 10 | state: State = init() 11 | sockets: Array = [] 12 | 13 | get(): State { 14 | return this.state 15 | } 16 | 17 | set(next: State): State { 18 | this.state = next 19 | return this.state 20 | } 21 | 22 | addSocket(socket: net.Socket): Array { 23 | this.sockets = this.sockets.concat([socket]) 24 | return this.listSockets() 25 | } 26 | 27 | listSockets(): Array { 28 | return this.sockets 29 | } 30 | } 31 | 32 | const mockTrackInfoResponse = (t: TrackInfo): string => 33 | [ 34 | t.status, 35 | '3', 36 | '282', 37 | t.secondsPlayed, 38 | t.codec, 39 | t.bitrate, 40 | t.artist, 41 | t.album, 42 | t.date, 43 | t.genre, 44 | t.trackNumber, 45 | t.track, 46 | t.trackLength, 47 | ].join('|') 48 | 49 | const mockVolumeResponse = Volume.match( 50 | (muted) => `222|-100.00|`, 51 | (audible) => `222|${audible.volume.toFixed(2)}|` 52 | ) 53 | 54 | const onConnection = (stateWrapper: StateWrapper, logger: Logger.Logger) => ( 55 | socket: net.Socket 56 | ): void => { 57 | socket.write( 58 | [ 59 | initialMsg, 60 | mockTrackInfoResponse(stateWrapper.get().currentTrack), 61 | ].join('\r\n') 62 | ) 63 | 64 | // TODO: Remove socket when connection is closed 65 | stateWrapper.addSocket(socket) 66 | 67 | socket.on('data', (data) => { 68 | const state = stateWrapper.get() 69 | const stringData = Vector.ofIterable(data.toString()) 70 | // get rid of CRLF 71 | .dropRight(2) 72 | .mkString('') 73 | 74 | if (Action.guard(stringData)) { 75 | const nextState = stateWrapper.set(update(state, stringData)) 76 | 77 | socket.write( 78 | [ 79 | mockTrackInfoResponse(nextState.currentTrack), 80 | mockVolumeResponse(nextState.currentVolume), 81 | ].join('\r\n') 82 | ) 83 | 84 | return logger.debug('Received command', { 85 | action: stringData, 86 | state: nextState, 87 | }) 88 | } else { 89 | return logger.warn('Unknown command', { 90 | action: stringData, 91 | }) 92 | } 93 | }) 94 | } 95 | 96 | export const createServer = (host: string, port: number): net.Server => { 97 | const stateWrapper = new StateWrapper() 98 | const logger = Logger.create('test', 'mock-control-server') 99 | const server = net.createServer(onConnection(stateWrapper, logger)) 100 | 101 | const tick = setInterval(() => { 102 | const state = stateWrapper.get() 103 | const nextSecondsPlayed = 104 | state.currentTrack.state === 'playing' 105 | ? state.currentTrack.secondsPlayed + 1 106 | : state.currentTrack.secondsPlayed 107 | 108 | const trackEnded = state.currentTrack.trackLength <= nextSecondsPlayed 109 | 110 | stateWrapper.set( 111 | trackEnded 112 | ? update(state, 'next') 113 | : { 114 | ...state, 115 | currentTrack: { 116 | ...state.currentTrack, 117 | secondsPlayed: nextSecondsPlayed, 118 | }, 119 | } 120 | ) 121 | 122 | if (trackEnded) { 123 | stateWrapper 124 | .listSockets() 125 | .forEach((socket) => 126 | socket.write( 127 | mockTrackInfoResponse(stateWrapper.get().currentTrack) 128 | ) 129 | ) 130 | } 131 | }, 1000) 132 | 133 | server.on('close', () => clearInterval(tick)) 134 | server.listen(port, host) 135 | 136 | return server 137 | } 138 | -------------------------------------------------------------------------------- /server/test/Message.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | 3 | import { assert } from 'chai' 4 | 5 | import * as Message from '../Message' 6 | import { 7 | TrackInfo, 8 | InfoMessage, 9 | PlaybackMessage, 10 | VolumeMessage, 11 | Volume, 12 | } from '../Models' 13 | 14 | describe('Message', () => { 15 | it('should parse an information block', () => { 16 | const lines = [ 17 | '999|Connected to foobar2000 Control Server v1.0.1|', 18 | '999|Accepted client from 127.0.0.1|', 19 | '999|There are currently 2/10 clients connected|', 20 | "999|Type '?' or 'help' for command information|", 21 | ] 22 | const message = lines.join('\r\n') 23 | const expectedMessage = lines.join('\n') 24 | const messages = Message.parseControlData(message) 25 | const infoMessage = InfoMessage.check(messages[0]) 26 | 27 | assert.equal(messages.length, 1) 28 | assert.equal(infoMessage.data, expectedMessage) 29 | }) 30 | 31 | it('should parse a playback status message', () => { 32 | const message = 33 | '111|3|282|2.73|FLAC|605|Imaginary Friends|Bronchitis|2013|Post-rock|01|Bronchitis (entire)|745|' 34 | const expectedTrackData: TrackInfo = { 35 | status: 111, 36 | secondsPlayed: 2.73, 37 | codec: 'FLAC', 38 | bitrate: 605, 39 | artist: 'Imaginary Friends', 40 | album: 'Bronchitis', 41 | date: '2013', 42 | genre: 'Post-rock', 43 | trackNumber: '01', 44 | track: 'Bronchitis (entire)', 45 | trackLength: 745, 46 | state: 'playing', 47 | } 48 | const messages = Message.parseControlData(message) 49 | const playbackMessage = PlaybackMessage.check(messages[0]) 50 | 51 | assert.equal(messages.length, 1) 52 | assert.deepEqual(playbackMessage.data, expectedTrackData) 53 | }) 54 | 55 | it('should set state "playing" for code "111"', () => { 56 | const message = 57 | '111|3|282|2.73|FLAC|605|Imaginary Friends|Bronchitis|2013|Post-rock|01|Bronchitis (entire)|745|' 58 | const messages = Message.parseControlData(message) 59 | const playbackMessage = PlaybackMessage.check(messages[0]) 60 | 61 | assert.equal(messages.length, 1) 62 | assert.equal(playbackMessage.data.state, 'playing') 63 | }) 64 | 65 | it('should set state "stopped" for code "112"', () => { 66 | const message = 67 | '112|3|282|2.73|FLAC|605|Imaginary Friends|Bronchitis|2013|Post-rock|01|Bronchitis (entire)|745|' 68 | const messages = Message.parseControlData(message) 69 | const playbackMessage = PlaybackMessage.check(messages[0]) 70 | 71 | assert.equal(messages.length, 1) 72 | assert.equal(playbackMessage.data.state, 'stopped') 73 | }) 74 | 75 | it('should set state "paused" for code "113"', () => { 76 | const message = 77 | '113|3|282|2.73|FLAC|605|Imaginary Friends|Bronchitis|2013|Post-rock|01|Bronchitis (entire)|745|' 78 | const messages = Message.parseControlData(message) 79 | const playbackMessage = PlaybackMessage.check(messages[0]) 80 | 81 | assert.equal(messages.length, 1) 82 | assert.equal(playbackMessage.data.state, 'paused') 83 | }) 84 | 85 | it('should parse a volume message', () => { 86 | const message = '222|-1.58|' 87 | const mockStatus: Volume = { 88 | type: 'audible', 89 | volume: -1.58, 90 | } 91 | 92 | const messages = Message.parseControlData(message) 93 | const volumeMessage = VolumeMessage.check(messages[0]) 94 | 95 | assert.equal(messages.length, 1) 96 | assert.deepEqual(volumeMessage.data, mockStatus) 97 | }) 98 | 99 | it('should parse a volume message when muted', () => { 100 | const message = '222|-100.00|' 101 | const mockStatus: Volume = { 102 | type: 'muted', 103 | } 104 | 105 | const messages = Message.parseControlData(message) 106 | const volumeMessage = VolumeMessage.check(messages[0]) 107 | 108 | assert.equal(messages.length, 1) 109 | assert.deepEqual(volumeMessage.data, mockStatus) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /server/Message.ts: -------------------------------------------------------------------------------- 1 | import { Vector, HashMap, Option } from 'prelude-ts' 2 | import { Result, Failure } from 'runtypes' 3 | 4 | import { 5 | Message, 6 | TrackInfo, 7 | InfoMessage, 8 | StatusType, 9 | VolumeMessage 10 | } from './Models' 11 | import { failure, success, mapSuccess } from './Util' 12 | 13 | const statusCodes: HashMap = HashMap.of( 14 | ['111', 'playing'], 15 | ['112', 'stopped'], 16 | ['113', 'paused'], 17 | ['222', 'volumeChange'], 18 | ['999', 'info'] 19 | ) 20 | 21 | const trackInfoKeys: (keyof TrackInfo)[] = [ 22 | 'status', 23 | 'secondsPlayed', 24 | 'codec', 25 | 'bitrate', 26 | 'artist', 27 | 'album', 28 | 'date', 29 | 'genre', 30 | 'trackNumber', 31 | 'track', 32 | 'trackLength' 33 | ] 34 | 35 | const trackInfoKeysWithNumberValue: (keyof TrackInfo)[] = [ 36 | 'status', 37 | 'secondsPlayed', 38 | 'bitrate', 39 | 'trackLength' 40 | ] 41 | 42 | function statusCodeToName(code: string): StatusType { 43 | const lookupResult = statusCodes.findAny((key, _) => code === key) 44 | 45 | if (lookupResult.isNone()) { 46 | return 'unknown' 47 | } 48 | 49 | return lookupResult.get()[1] 50 | } 51 | 52 | function parseTrackData(text: string): Result { 53 | const items = Vector.ofIterable(text.split('|')) 54 | const status = items.head() 55 | // Get rid of fields that we don't care about 56 | const otherValues = items.drop(3) 57 | 58 | if (status.isNone() || otherValues.length() < trackInfoKeys.length - 1) { 59 | return failure('Could not parse track data') 60 | } 61 | 62 | const values = otherValues.prepend(status.get()) 63 | const trackInfoEntries = Vector.zip(trackInfoKeys, values) 64 | const trackInfo = HashMap.ofIterable(trackInfoEntries) 65 | .filterKeys(k => !k.startsWith('unknown')) 66 | .map((k, v) => [k, mapTrackInfoValue(k, v)]) 67 | .put('state', statusCodeToName(status.get())) 68 | .toObjectDictionary(x => x) 69 | 70 | return TrackInfo.validate(trackInfo) 71 | } 72 | 73 | function mapTrackInfoValue(k: keyof TrackInfo, v: string): string | number { 74 | return trackInfoKeysWithNumberValue.includes(k) ? Number(v) : v 75 | } 76 | 77 | function nextVolume(currentVolumeValue: string): VolumeMessage { 78 | return { 79 | type: 'volume', 80 | data: currentVolumeValue.startsWith('-100') 81 | ? { 82 | type: 'muted' 83 | } 84 | : { 85 | type: 'audible', 86 | volume: Number(currentVolumeValue) 87 | } 88 | } 89 | } 90 | 91 | function parseMessage(raw: string): Result { 92 | const parseMessageFailure: Failure = failure('Could not parse message') 93 | const messageCode = raw.substring(0, 3) 94 | 95 | switch (statusCodeToName(messageCode)) { 96 | case 'info': 97 | return success({ 98 | type: 'info', 99 | data: raw 100 | }) 101 | 102 | case 'volumeChange': 103 | const vol = Vector.ofIterable(raw.split('|')) 104 | .filter(v => v !== '') 105 | .last() 106 | 107 | return vol.isSome() 108 | ? success(nextVolume(vol.get())) 109 | : parseMessageFailure 110 | 111 | case 'playing': 112 | case 'paused': 113 | case 'stopped': 114 | const trackInfo = parseTrackData(raw) 115 | return trackInfo.success 116 | ? mapSuccess(trackInfo, (value: TrackInfo) => ({ 117 | type: 'playback', 118 | data: value 119 | })) 120 | : trackInfo 121 | default: 122 | return parseMessageFailure 123 | } 124 | } 125 | 126 | export function parseControlData(text: string): Message[] { 127 | const lines: string[] = text.split('\r\n') 128 | const messageList: Vector = Vector.ofIterable(lines).mapOption( 129 | l => { 130 | const messageResult = parseMessage(l) 131 | return messageResult.success 132 | ? Option.of(messageResult.value) 133 | : Option.none() 134 | } 135 | ) 136 | 137 | if (messageList.allMatch(InfoMessage.guard)) { 138 | return [ 139 | { 140 | type: 'info', 141 | data: messageList.foldLeft( 142 | '', 143 | (data, message) => 144 | `${data}${data.length === 0 ? '' : '\n'}${message.data}` 145 | ) 146 | } 147 | ] 148 | } 149 | 150 | return messageList.toArray() 151 | } 152 | -------------------------------------------------------------------------------- /ui/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from 'preact' 2 | 3 | export const Forward: FunctionalComponent<{}> = () => ( 4 | 5 | 6 | 7 | ) 8 | 9 | export const Backward: FunctionalComponent<{}> = () => ( 10 | 11 | 12 | 13 | ) 14 | 15 | export const Play: FunctionalComponent<{}> = () => ( 16 | 17 | 18 | 19 | ) 20 | export const Pause: FunctionalComponent<{}> = () => ( 21 | 22 | 23 | 24 | ) 25 | export const Stop: FunctionalComponent<{}> = () => ( 26 | 27 | 28 | 29 | ) 30 | export const Random: FunctionalComponent<{}> = () => ( 31 | 32 | 33 | 34 | ) 35 | 36 | export const VolumeDown: FunctionalComponent<{}> = () => ( 37 | 38 | 39 | 40 | ) 41 | export const VolumeUp: FunctionalComponent<{}> = () => ( 42 | 43 | 44 | 45 | ) 46 | export const VolumeMute: FunctionalComponent<{}> = () => ( 47 | 48 | 49 | 50 | ) 51 | export const VolumeOff: FunctionalComponent<{}> = () => ( 52 | 53 | 54 | 55 | ) 56 | -------------------------------------------------------------------------------- /server/test/api.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any */ 2 | 3 | import { assert } from 'chai' 4 | import * as net from 'net' 5 | import { describe } from 'mocha' 6 | import SocketIOClient from 'socket.io-client' 7 | 8 | import { createServer } from '../MockControlServer' 9 | import * as Server from '../Server' 10 | import * as ControlServer from '../ControlServer' 11 | import { mockTrack1 } from './fixtures' 12 | import { 13 | Context, 14 | Env, 15 | Message, 16 | PlaybackMessage, 17 | VolumeMessage, 18 | Config, 19 | } from '../Models' 20 | import * as Logger from '../Logger' 21 | 22 | const ioOptions = { 23 | transports: ['websocket'], 24 | forceNew: true, 25 | autoConnect: false, 26 | reconnection: false, 27 | } 28 | const testControlServerPort = 6666 29 | const testServerPort = 9999 30 | const environment: Env = 'test' 31 | const config: Config = { 32 | foobarPath: 'C:/tmp', 33 | webserverPort: testServerPort, 34 | controlServerPort: testControlServerPort, 35 | controlServerMessageSeparator: '|', 36 | environment, 37 | } 38 | 39 | describe('API', () => { 40 | let mockControlServer: net.Server 41 | let testServer: any 42 | let ioInstance: SocketIO.Server 43 | 44 | before((done) => { 45 | const logger = Logger.create(config.environment) 46 | mockControlServer = createServer('127.0.0.1', testControlServerPort) 47 | 48 | ControlServer.connect(config.controlServerPort, logger).then( 49 | (client) => { 50 | const context: Context = { 51 | config: config, 52 | logger, 53 | client, 54 | instance: null, 55 | } 56 | 57 | const { server, io } = Server.create() 58 | Server.configureWebsockets(context, io) 59 | 60 | ioInstance = io 61 | testServer = server 62 | testServer.listen(testServerPort) 63 | done() 64 | } 65 | ) 66 | }) 67 | 68 | after(() => { 69 | mockControlServer.close() 70 | testServer.close() 71 | }) 72 | 73 | it('should initialize', (done) => { 74 | assert.ok(ioInstance !== null) 75 | done() 76 | }) 77 | 78 | it('should send foobar2000 status info upon connecting ', (done) => { 79 | const ioClient = SocketIOClient( 80 | `http://127.0.0.1:${testServerPort}/`, 81 | ioOptions 82 | ) 83 | 84 | const messages: Message[] = [] 85 | 86 | ioClient.on('message', (message: Message) => { 87 | messages.push(message) 88 | }) 89 | 90 | setTimeout(() => { 91 | const playbackMessage = messages.find((m) => m.type === 'playback') 92 | const volumeMessage = messages.find((m) => m.type === 'volume') 93 | 94 | assert.ok( 95 | playbackMessage && 96 | playbackMessage.type === 'playback' && 97 | playbackMessage.data.state === mockTrack1.state && 98 | playbackMessage.data.track === mockTrack1.track 99 | ) 100 | assert.ok( 101 | volumeMessage && 102 | volumeMessage.type === 'volume' && 103 | volumeMessage.data.type === 'audible' 104 | ) 105 | 106 | ioClient.disconnect() 107 | done() 108 | }, 100) 109 | 110 | ioClient.connect() 111 | }) 112 | 113 | // TODO improve test 114 | it('should send foobar2000 playback info when a playback action is triggered', (done) => { 115 | const ioClient = SocketIOClient( 116 | `http://127.0.0.1:${testServerPort}/`, 117 | ioOptions 118 | ) 119 | 120 | const messages: PlaybackMessage[] = [] 121 | 122 | ioClient.on('message', (message: Message) => { 123 | if (message.type === 'playback') { 124 | messages.push(message) 125 | } 126 | }) 127 | 128 | ioClient.connect() 129 | ioClient.emit('foobarCommand', 'stop') 130 | 131 | setTimeout(() => { 132 | const playbackMessage = messages[1] 133 | 134 | assert.ok(messages.length === 2) 135 | assert.ok(playbackMessage.data.status === 112) 136 | 137 | ioClient.disconnect() 138 | done() 139 | }, 100) 140 | }) 141 | 142 | // TODO improve test 143 | it('should send foobar2000 status info when volume is changed', (done) => { 144 | const ioClient = SocketIOClient( 145 | `http://127.0.0.1:${testServerPort}/`, 146 | ioOptions 147 | ) 148 | 149 | const messages: VolumeMessage[] = [] 150 | 151 | ioClient.on('message', (message: Message) => { 152 | if (message.type === 'volume') { 153 | messages.push(message) 154 | } 155 | }) 156 | 157 | ioClient.connect() 158 | ioClient.emit('foobarCommand', 'vol mute') 159 | 160 | setTimeout(() => { 161 | const volumeMessage = messages[1] 162 | 163 | assert.ok(messages.length === 2) 164 | assert.ok(volumeMessage.data.type === 'muted') 165 | 166 | ioClient.disconnect() 167 | done() 168 | }, 100) 169 | }) 170 | }) 171 | -------------------------------------------------------------------------------- /release/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/parser@~7.4.4": 6 | version "7.4.5" 7 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" 8 | integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== 9 | 10 | "@babel/runtime@~7.4.4": 11 | version "7.4.5" 12 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" 13 | integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== 14 | dependencies: 15 | regenerator-runtime "^0.13.2" 16 | 17 | "@mrmlnc/readdir-enhanced@^2.2.1": 18 | version "2.2.1" 19 | resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" 20 | integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== 21 | dependencies: 22 | call-me-maybe "^1.0.1" 23 | glob-to-regexp "^0.3.0" 24 | 25 | "@nodelib/fs.stat@^1.1.2": 26 | version "1.1.3" 27 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" 28 | integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== 29 | 30 | "@types/events@*": 31 | version "3.0.0" 32 | resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" 33 | integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== 34 | 35 | "@types/glob@^7.1.1": 36 | version "7.1.1" 37 | resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" 38 | integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== 39 | dependencies: 40 | "@types/events" "*" 41 | "@types/minimatch" "*" 42 | "@types/node" "*" 43 | 44 | "@types/minimatch@*": 45 | version "3.0.3" 46 | resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" 47 | integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== 48 | 49 | "@types/node@*": 50 | version "12.12.8" 51 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.8.tgz#dab418655af39ce2fa99286a0bed21ef8072ac9d" 52 | integrity sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w== 53 | 54 | ajv@^6.5.5: 55 | version "6.10.2" 56 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" 57 | integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== 58 | dependencies: 59 | fast-deep-equal "^2.0.1" 60 | fast-json-stable-stringify "^2.0.0" 61 | json-schema-traverse "^0.4.1" 62 | uri-js "^4.2.2" 63 | 64 | ansi-styles@^3.2.1: 65 | version "3.2.1" 66 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 67 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 68 | dependencies: 69 | color-convert "^1.9.0" 70 | 71 | arr-diff@^4.0.0: 72 | version "4.0.0" 73 | resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" 74 | integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= 75 | 76 | arr-flatten@^1.1.0: 77 | version "1.1.0" 78 | resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" 79 | integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== 80 | 81 | arr-union@^3.1.0: 82 | version "3.1.0" 83 | resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" 84 | integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= 85 | 86 | array-union@^1.0.2: 87 | version "1.0.2" 88 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" 89 | integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= 90 | dependencies: 91 | array-uniq "^1.0.1" 92 | 93 | array-uniq@^1.0.1: 94 | version "1.0.3" 95 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 96 | integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= 97 | 98 | array-unique@^0.3.2: 99 | version "0.3.2" 100 | resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" 101 | integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= 102 | 103 | asn1@~0.2.3: 104 | version "0.2.4" 105 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 106 | integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 107 | dependencies: 108 | safer-buffer "~2.1.0" 109 | 110 | assert-plus@1.0.0, assert-plus@^1.0.0: 111 | version "1.0.0" 112 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 113 | integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 114 | 115 | assign-symbols@^1.0.0: 116 | version "1.0.0" 117 | resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" 118 | integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= 119 | 120 | asynckit@^0.4.0: 121 | version "0.4.0" 122 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 123 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 124 | 125 | atob@^2.1.1: 126 | version "2.1.2" 127 | resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" 128 | integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== 129 | 130 | aws-sign2@~0.7.0: 131 | version "0.7.0" 132 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 133 | integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 134 | 135 | aws4@^1.8.0: 136 | version "1.8.0" 137 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" 138 | integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== 139 | 140 | balanced-match@^1.0.0: 141 | version "1.0.0" 142 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 143 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 144 | 145 | base@^0.11.1: 146 | version "0.11.2" 147 | resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" 148 | integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== 149 | dependencies: 150 | cache-base "^1.0.1" 151 | class-utils "^0.3.5" 152 | component-emitter "^1.2.1" 153 | define-property "^1.0.0" 154 | isobject "^3.0.1" 155 | mixin-deep "^1.2.0" 156 | pascalcase "^0.1.1" 157 | 158 | bcrypt-pbkdf@^1.0.0: 159 | version "1.0.2" 160 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 161 | integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= 162 | dependencies: 163 | tweetnacl "^0.14.3" 164 | 165 | brace-expansion@^1.1.7: 166 | version "1.1.11" 167 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 168 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 169 | dependencies: 170 | balanced-match "^1.0.0" 171 | concat-map "0.0.1" 172 | 173 | braces@^2.3.1: 174 | version "2.3.2" 175 | resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" 176 | integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== 177 | dependencies: 178 | arr-flatten "^1.1.0" 179 | array-unique "^0.3.2" 180 | extend-shallow "^2.0.1" 181 | fill-range "^4.0.0" 182 | isobject "^3.0.1" 183 | repeat-element "^1.1.2" 184 | snapdragon "^0.8.1" 185 | snapdragon-node "^2.0.1" 186 | split-string "^3.0.2" 187 | to-regex "^3.0.1" 188 | 189 | byline@~5.0.0: 190 | version "5.0.0" 191 | resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" 192 | integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= 193 | 194 | cache-base@^1.0.1: 195 | version "1.0.1" 196 | resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" 197 | integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== 198 | dependencies: 199 | collection-visit "^1.0.0" 200 | component-emitter "^1.2.1" 201 | get-value "^2.0.6" 202 | has-value "^1.0.0" 203 | isobject "^3.0.1" 204 | set-value "^2.0.0" 205 | to-object-path "^0.3.0" 206 | union-value "^1.0.0" 207 | unset-value "^1.0.0" 208 | 209 | call-me-maybe@^1.0.1: 210 | version "1.0.1" 211 | resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" 212 | integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= 213 | 214 | caseless@~0.12.0: 215 | version "0.12.0" 216 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 217 | integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 218 | 219 | chalk@~2.4.1, chalk@~2.4.2: 220 | version "2.4.2" 221 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 222 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 223 | dependencies: 224 | ansi-styles "^3.2.1" 225 | escape-string-regexp "^1.0.5" 226 | supports-color "^5.3.0" 227 | 228 | class-utils@^0.3.5: 229 | version "0.3.6" 230 | resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" 231 | integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== 232 | dependencies: 233 | arr-union "^3.1.0" 234 | define-property "^0.2.5" 235 | isobject "^3.0.0" 236 | static-extend "^0.1.1" 237 | 238 | collection-visit@^1.0.0: 239 | version "1.0.0" 240 | resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" 241 | integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= 242 | dependencies: 243 | map-visit "^1.0.0" 244 | object-visit "^1.0.0" 245 | 246 | color-convert@^1.9.0: 247 | version "1.9.3" 248 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 249 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 250 | dependencies: 251 | color-name "1.1.3" 252 | 253 | color-name@1.1.3: 254 | version "1.1.3" 255 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 256 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 257 | 258 | combined-stream@^1.0.6, combined-stream@~1.0.6: 259 | version "1.0.8" 260 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 261 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 262 | dependencies: 263 | delayed-stream "~1.0.0" 264 | 265 | component-emitter@^1.2.1: 266 | version "1.3.0" 267 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" 268 | integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== 269 | 270 | concat-map@0.0.1: 271 | version "0.0.1" 272 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 273 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 274 | 275 | copy-descriptor@^0.1.0: 276 | version "0.1.1" 277 | resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" 278 | integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= 279 | 280 | core-util-is@1.0.2, core-util-is@~1.0.0: 281 | version "1.0.2" 282 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 283 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 284 | 285 | dashdash@^1.12.0: 286 | version "1.14.1" 287 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 288 | integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= 289 | dependencies: 290 | assert-plus "^1.0.0" 291 | 292 | debug@^2.2.0, debug@^2.3.3: 293 | version "2.6.9" 294 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 295 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 296 | dependencies: 297 | ms "2.0.0" 298 | 299 | decode-uri-component@^0.2.0: 300 | version "0.2.0" 301 | resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" 302 | integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= 303 | 304 | deep-is@~0.1.3: 305 | version "0.1.3" 306 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 307 | integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= 308 | 309 | define-property@^0.2.5: 310 | version "0.2.5" 311 | resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" 312 | integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= 313 | dependencies: 314 | is-descriptor "^0.1.0" 315 | 316 | define-property@^1.0.0: 317 | version "1.0.0" 318 | resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" 319 | integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= 320 | dependencies: 321 | is-descriptor "^1.0.0" 322 | 323 | define-property@^2.0.2: 324 | version "2.0.2" 325 | resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" 326 | integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== 327 | dependencies: 328 | is-descriptor "^1.0.2" 329 | isobject "^3.0.1" 330 | 331 | delayed-stream@~1.0.0: 332 | version "1.0.0" 333 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 334 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 335 | 336 | dir-glob@^2.2.2: 337 | version "2.2.2" 338 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" 339 | integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== 340 | dependencies: 341 | path-type "^3.0.0" 342 | 343 | ecc-jsbn@~0.1.1: 344 | version "0.1.2" 345 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 346 | integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 347 | dependencies: 348 | jsbn "~0.1.0" 349 | safer-buffer "^2.1.0" 350 | 351 | escape-string-regexp@^1.0.5: 352 | version "1.0.5" 353 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 354 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 355 | 356 | escodegen@~1.11.1: 357 | version "1.11.1" 358 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" 359 | integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== 360 | dependencies: 361 | esprima "^3.1.3" 362 | estraverse "^4.2.0" 363 | esutils "^2.0.2" 364 | optionator "^0.8.1" 365 | optionalDependencies: 366 | source-map "~0.6.1" 367 | 368 | esprima@^3.1.3: 369 | version "3.1.3" 370 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" 371 | integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= 372 | 373 | estraverse@^4.2.0: 374 | version "4.3.0" 375 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 376 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 377 | 378 | esutils@^2.0.2: 379 | version "2.0.3" 380 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 381 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 382 | 383 | expand-brackets@^2.1.4: 384 | version "2.1.4" 385 | resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" 386 | integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= 387 | dependencies: 388 | debug "^2.3.3" 389 | define-property "^0.2.5" 390 | extend-shallow "^2.0.1" 391 | posix-character-classes "^0.1.0" 392 | regex-not "^1.0.0" 393 | snapdragon "^0.8.1" 394 | to-regex "^3.0.1" 395 | 396 | expand-template@~2.0.3: 397 | version "2.0.3" 398 | resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" 399 | integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== 400 | 401 | extend-shallow@^2.0.1: 402 | version "2.0.1" 403 | resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" 404 | integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= 405 | dependencies: 406 | is-extendable "^0.1.0" 407 | 408 | extend-shallow@^3.0.0, extend-shallow@^3.0.2: 409 | version "3.0.2" 410 | resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" 411 | integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= 412 | dependencies: 413 | assign-symbols "^1.0.0" 414 | is-extendable "^1.0.1" 415 | 416 | extend@~3.0.2: 417 | version "3.0.2" 418 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 419 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 420 | 421 | extglob@^2.0.4: 422 | version "2.0.4" 423 | resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" 424 | integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== 425 | dependencies: 426 | array-unique "^0.3.2" 427 | define-property "^1.0.0" 428 | expand-brackets "^2.1.4" 429 | extend-shallow "^2.0.1" 430 | fragment-cache "^0.2.1" 431 | regex-not "^1.0.0" 432 | snapdragon "^0.8.1" 433 | to-regex "^3.0.1" 434 | 435 | extsprintf@1.3.0: 436 | version "1.3.0" 437 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 438 | integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= 439 | 440 | extsprintf@^1.2.0: 441 | version "1.4.0" 442 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 443 | integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 444 | 445 | fast-deep-equal@^2.0.1: 446 | version "2.0.1" 447 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 448 | integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 449 | 450 | fast-glob@^2.2.6: 451 | version "2.2.7" 452 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" 453 | integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== 454 | dependencies: 455 | "@mrmlnc/readdir-enhanced" "^2.2.1" 456 | "@nodelib/fs.stat" "^1.1.2" 457 | glob-parent "^3.1.0" 458 | is-glob "^4.0.0" 459 | merge2 "^1.2.3" 460 | micromatch "^3.1.10" 461 | 462 | fast-json-stable-stringify@^2.0.0: 463 | version "2.0.0" 464 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 465 | integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= 466 | 467 | fast-levenshtein@~2.0.6: 468 | version "2.0.6" 469 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 470 | integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 471 | 472 | fill-range@^4.0.0: 473 | version "4.0.0" 474 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" 475 | integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= 476 | dependencies: 477 | extend-shallow "^2.0.1" 478 | is-number "^3.0.0" 479 | repeat-string "^1.6.1" 480 | to-regex-range "^2.1.0" 481 | 482 | for-in@^1.0.2: 483 | version "1.0.2" 484 | resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 485 | integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= 486 | 487 | forever-agent@~0.6.1: 488 | version "0.6.1" 489 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 490 | integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 491 | 492 | form-data@~2.3.2: 493 | version "2.3.3" 494 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 495 | integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== 496 | dependencies: 497 | asynckit "^0.4.0" 498 | combined-stream "^1.0.6" 499 | mime-types "^2.1.12" 500 | 501 | fragment-cache@^0.2.1: 502 | version "0.2.1" 503 | resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" 504 | integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= 505 | dependencies: 506 | map-cache "^0.2.2" 507 | 508 | from2@^2.3.0: 509 | version "2.3.0" 510 | resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" 511 | integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= 512 | dependencies: 513 | inherits "^2.0.1" 514 | readable-stream "^2.0.0" 515 | 516 | fs-extra@~7.0.1: 517 | version "7.0.1" 518 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" 519 | integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== 520 | dependencies: 521 | graceful-fs "^4.1.2" 522 | jsonfile "^4.0.0" 523 | universalify "^0.1.0" 524 | 525 | fs.realpath@^1.0.0: 526 | version "1.0.0" 527 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 528 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 529 | 530 | get-value@^2.0.3, get-value@^2.0.6: 531 | version "2.0.6" 532 | resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" 533 | integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= 534 | 535 | getpass@^0.1.1: 536 | version "0.1.7" 537 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 538 | integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= 539 | dependencies: 540 | assert-plus "^1.0.0" 541 | 542 | glob-parent@^3.1.0: 543 | version "3.1.0" 544 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" 545 | integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= 546 | dependencies: 547 | is-glob "^3.1.0" 548 | path-dirname "^1.0.0" 549 | 550 | glob-to-regexp@^0.3.0: 551 | version "0.3.0" 552 | resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" 553 | integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= 554 | 555 | glob@^7.1.3: 556 | version "7.1.6" 557 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 558 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 559 | dependencies: 560 | fs.realpath "^1.0.0" 561 | inflight "^1.0.4" 562 | inherits "2" 563 | minimatch "^3.0.4" 564 | once "^1.3.0" 565 | path-is-absolute "^1.0.0" 566 | 567 | globby@~9.2.0: 568 | version "9.2.0" 569 | resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" 570 | integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== 571 | dependencies: 572 | "@types/glob" "^7.1.1" 573 | array-union "^1.0.2" 574 | dir-glob "^2.2.2" 575 | fast-glob "^2.2.6" 576 | glob "^7.1.3" 577 | ignore "^4.0.3" 578 | pify "^4.0.1" 579 | slash "^2.0.0" 580 | 581 | graceful-fs@^4.1.2, graceful-fs@^4.1.6: 582 | version "4.2.3" 583 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" 584 | integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== 585 | 586 | har-schema@^2.0.0: 587 | version "2.0.0" 588 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 589 | integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 590 | 591 | har-validator@~5.1.0: 592 | version "5.1.3" 593 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 594 | integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 595 | dependencies: 596 | ajv "^6.5.5" 597 | har-schema "^2.0.0" 598 | 599 | has-flag@^3.0.0: 600 | version "3.0.0" 601 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 602 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 603 | 604 | has-value@^0.3.1: 605 | version "0.3.1" 606 | resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" 607 | integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= 608 | dependencies: 609 | get-value "^2.0.3" 610 | has-values "^0.1.4" 611 | isobject "^2.0.0" 612 | 613 | has-value@^1.0.0: 614 | version "1.0.0" 615 | resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" 616 | integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= 617 | dependencies: 618 | get-value "^2.0.6" 619 | has-values "^1.0.0" 620 | isobject "^3.0.0" 621 | 622 | has-values@^0.1.4: 623 | version "0.1.4" 624 | resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" 625 | integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= 626 | 627 | has-values@^1.0.0: 628 | version "1.0.0" 629 | resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" 630 | integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= 631 | dependencies: 632 | is-number "^3.0.0" 633 | kind-of "^4.0.0" 634 | 635 | http-signature@~1.2.0: 636 | version "1.2.0" 637 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 638 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 639 | dependencies: 640 | assert-plus "^1.0.0" 641 | jsprim "^1.2.2" 642 | sshpk "^1.7.0" 643 | 644 | ignore@^4.0.3: 645 | version "4.0.6" 646 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" 647 | integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== 648 | 649 | inflight@^1.0.4: 650 | version "1.0.6" 651 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 652 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 653 | dependencies: 654 | once "^1.3.0" 655 | wrappy "1" 656 | 657 | inherits@2, inherits@^2.0.1, inherits@~2.0.3: 658 | version "2.0.4" 659 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 660 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 661 | 662 | into-stream@~5.1.0: 663 | version "5.1.1" 664 | resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" 665 | integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== 666 | dependencies: 667 | from2 "^2.3.0" 668 | p-is-promise "^3.0.0" 669 | 670 | is-accessor-descriptor@^0.1.6: 671 | version "0.1.6" 672 | resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" 673 | integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= 674 | dependencies: 675 | kind-of "^3.0.2" 676 | 677 | is-accessor-descriptor@^1.0.0: 678 | version "1.0.0" 679 | resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" 680 | integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== 681 | dependencies: 682 | kind-of "^6.0.0" 683 | 684 | is-buffer@^1.1.5: 685 | version "1.1.6" 686 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 687 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 688 | 689 | is-data-descriptor@^0.1.4: 690 | version "0.1.4" 691 | resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" 692 | integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= 693 | dependencies: 694 | kind-of "^3.0.2" 695 | 696 | is-data-descriptor@^1.0.0: 697 | version "1.0.0" 698 | resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" 699 | integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== 700 | dependencies: 701 | kind-of "^6.0.0" 702 | 703 | is-descriptor@^0.1.0: 704 | version "0.1.6" 705 | resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" 706 | integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== 707 | dependencies: 708 | is-accessor-descriptor "^0.1.6" 709 | is-data-descriptor "^0.1.4" 710 | kind-of "^5.0.0" 711 | 712 | is-descriptor@^1.0.0, is-descriptor@^1.0.2: 713 | version "1.0.2" 714 | resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" 715 | integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== 716 | dependencies: 717 | is-accessor-descriptor "^1.0.0" 718 | is-data-descriptor "^1.0.0" 719 | kind-of "^6.0.2" 720 | 721 | is-extendable@^0.1.0, is-extendable@^0.1.1: 722 | version "0.1.1" 723 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 724 | integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= 725 | 726 | is-extendable@^1.0.1: 727 | version "1.0.1" 728 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" 729 | integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== 730 | dependencies: 731 | is-plain-object "^2.0.4" 732 | 733 | is-extglob@^2.1.0, is-extglob@^2.1.1: 734 | version "2.1.1" 735 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 736 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 737 | 738 | is-glob@^3.1.0: 739 | version "3.1.0" 740 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" 741 | integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= 742 | dependencies: 743 | is-extglob "^2.1.0" 744 | 745 | is-glob@^4.0.0: 746 | version "4.0.1" 747 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 748 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 749 | dependencies: 750 | is-extglob "^2.1.1" 751 | 752 | is-number@^3.0.0: 753 | version "3.0.0" 754 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" 755 | integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= 756 | dependencies: 757 | kind-of "^3.0.2" 758 | 759 | is-plain-object@^2.0.3, is-plain-object@^2.0.4: 760 | version "2.0.4" 761 | resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" 762 | integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== 763 | dependencies: 764 | isobject "^3.0.1" 765 | 766 | is-typedarray@~1.0.0: 767 | version "1.0.0" 768 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 769 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 770 | 771 | is-windows@^1.0.2: 772 | version "1.0.2" 773 | resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" 774 | integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== 775 | 776 | isarray@1.0.0, isarray@~1.0.0: 777 | version "1.0.0" 778 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 779 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 780 | 781 | isobject@^2.0.0: 782 | version "2.1.0" 783 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" 784 | integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= 785 | dependencies: 786 | isarray "1.0.0" 787 | 788 | isobject@^3.0.0, isobject@^3.0.1: 789 | version "3.0.1" 790 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 791 | integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= 792 | 793 | isstream@~0.1.2: 794 | version "0.1.2" 795 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 796 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 797 | 798 | jsbn@~0.1.0: 799 | version "0.1.1" 800 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 801 | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= 802 | 803 | json-schema-traverse@^0.4.1: 804 | version "0.4.1" 805 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 806 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 807 | 808 | json-schema@0.2.3: 809 | version "0.2.3" 810 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 811 | integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 812 | 813 | json-stringify-safe@~5.0.1: 814 | version "5.0.1" 815 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 816 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 817 | 818 | jsonfile@^4.0.0: 819 | version "4.0.0" 820 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" 821 | integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= 822 | optionalDependencies: 823 | graceful-fs "^4.1.6" 824 | 825 | jsprim@^1.2.2: 826 | version "1.4.1" 827 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 828 | integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= 829 | dependencies: 830 | assert-plus "1.0.0" 831 | extsprintf "1.3.0" 832 | json-schema "0.2.3" 833 | verror "1.10.0" 834 | 835 | kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: 836 | version "3.2.2" 837 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" 838 | integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= 839 | dependencies: 840 | is-buffer "^1.1.5" 841 | 842 | kind-of@^4.0.0: 843 | version "4.0.0" 844 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" 845 | integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= 846 | dependencies: 847 | is-buffer "^1.1.5" 848 | 849 | kind-of@^5.0.0: 850 | version "5.1.0" 851 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" 852 | integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== 853 | 854 | kind-of@^6.0.0, kind-of@^6.0.2: 855 | version "6.0.2" 856 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" 857 | integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== 858 | 859 | levn@~0.3.0: 860 | version "0.3.0" 861 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 862 | integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 863 | dependencies: 864 | prelude-ls "~1.1.2" 865 | type-check "~0.3.2" 866 | 867 | map-cache@^0.2.2: 868 | version "0.2.2" 869 | resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" 870 | integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= 871 | 872 | map-visit@^1.0.0: 873 | version "1.0.0" 874 | resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" 875 | integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= 876 | dependencies: 877 | object-visit "^1.0.0" 878 | 879 | merge2@^1.2.3: 880 | version "1.3.0" 881 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" 882 | integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== 883 | 884 | micromatch@^3.1.10: 885 | version "3.1.10" 886 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" 887 | integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== 888 | dependencies: 889 | arr-diff "^4.0.0" 890 | array-unique "^0.3.2" 891 | braces "^2.3.1" 892 | define-property "^2.0.2" 893 | extend-shallow "^3.0.2" 894 | extglob "^2.0.4" 895 | fragment-cache "^0.2.1" 896 | kind-of "^6.0.2" 897 | nanomatch "^1.2.9" 898 | object.pick "^1.3.0" 899 | regex-not "^1.0.0" 900 | snapdragon "^0.8.1" 901 | to-regex "^3.0.2" 902 | 903 | mime-db@1.42.0: 904 | version "1.42.0" 905 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" 906 | integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== 907 | 908 | mime-types@^2.1.12, mime-types@~2.1.19: 909 | version "2.1.25" 910 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" 911 | integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== 912 | dependencies: 913 | mime-db "1.42.0" 914 | 915 | minimatch@^3.0.4: 916 | version "3.0.4" 917 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 918 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 919 | dependencies: 920 | brace-expansion "^1.1.7" 921 | 922 | minimist@0.0.8: 923 | version "0.0.8" 924 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 925 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 926 | 927 | minimist@~1.2.0: 928 | version "1.2.0" 929 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 930 | integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 931 | 932 | mixin-deep@^1.2.0: 933 | version "1.3.2" 934 | resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" 935 | integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== 936 | dependencies: 937 | for-in "^1.0.2" 938 | is-extendable "^1.0.1" 939 | 940 | mkdirp@^0.5.1: 941 | version "0.5.1" 942 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 943 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 944 | dependencies: 945 | minimist "0.0.8" 946 | 947 | ms@2.0.0: 948 | version "2.0.0" 949 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 950 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 951 | 952 | multistream@~2.1.1: 953 | version "2.1.1" 954 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c" 955 | integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ== 956 | dependencies: 957 | inherits "^2.0.1" 958 | readable-stream "^2.0.5" 959 | 960 | nanomatch@^1.2.9: 961 | version "1.2.13" 962 | resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" 963 | integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== 964 | dependencies: 965 | arr-diff "^4.0.0" 966 | array-unique "^0.3.2" 967 | define-property "^2.0.2" 968 | extend-shallow "^3.0.2" 969 | fragment-cache "^0.2.1" 970 | is-windows "^1.0.2" 971 | kind-of "^6.0.2" 972 | object.pick "^1.3.0" 973 | regex-not "^1.0.0" 974 | snapdragon "^0.8.1" 975 | to-regex "^3.0.1" 976 | 977 | oauth-sign@~0.9.0: 978 | version "0.9.0" 979 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 980 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 981 | 982 | object-copy@^0.1.0: 983 | version "0.1.0" 984 | resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" 985 | integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= 986 | dependencies: 987 | copy-descriptor "^0.1.0" 988 | define-property "^0.2.5" 989 | kind-of "^3.0.3" 990 | 991 | object-visit@^1.0.0: 992 | version "1.0.1" 993 | resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" 994 | integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= 995 | dependencies: 996 | isobject "^3.0.0" 997 | 998 | object.pick@^1.3.0: 999 | version "1.3.0" 1000 | resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" 1001 | integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= 1002 | dependencies: 1003 | isobject "^3.0.1" 1004 | 1005 | once@^1.3.0: 1006 | version "1.4.0" 1007 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1008 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 1009 | dependencies: 1010 | wrappy "1" 1011 | 1012 | optionator@^0.8.1: 1013 | version "0.8.3" 1014 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" 1015 | integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== 1016 | dependencies: 1017 | deep-is "~0.1.3" 1018 | fast-levenshtein "~2.0.6" 1019 | levn "~0.3.0" 1020 | prelude-ls "~1.1.2" 1021 | type-check "~0.3.2" 1022 | word-wrap "~1.2.3" 1023 | 1024 | os-tmpdir@^1.0.1: 1025 | version "1.0.2" 1026 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 1027 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 1028 | 1029 | p-is-promise@^3.0.0: 1030 | version "3.0.0" 1031 | resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" 1032 | integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== 1033 | 1034 | pascalcase@^0.1.1: 1035 | version "0.1.1" 1036 | resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" 1037 | integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= 1038 | 1039 | path-dirname@^1.0.0: 1040 | version "1.0.2" 1041 | resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" 1042 | integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= 1043 | 1044 | path-is-absolute@^1.0.0: 1045 | version "1.0.1" 1046 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1047 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 1048 | 1049 | path-parse@^1.0.5: 1050 | version "1.0.6" 1051 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 1052 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 1053 | 1054 | path-type@^3.0.0: 1055 | version "3.0.0" 1056 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" 1057 | integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== 1058 | dependencies: 1059 | pify "^3.0.0" 1060 | 1061 | performance-now@^2.1.0: 1062 | version "2.1.0" 1063 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 1064 | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 1065 | 1066 | pify@^3.0.0: 1067 | version "3.0.0" 1068 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" 1069 | integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= 1070 | 1071 | pify@^4.0.1: 1072 | version "4.0.1" 1073 | resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" 1074 | integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== 1075 | 1076 | pkg-fetch@~2.6.2: 1077 | version "2.6.2" 1078 | resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.2.tgz#bad65a1f77f3bbd371be332a8da05a1d0c7edc7c" 1079 | integrity sha512-7DN6YYP1Kct02mSkhfblK0HkunJ7BJjGBkSkFdIW/QKIovtAMaICidS7feX+mHfnZ98OP7xFJvBluVURlrHJxA== 1080 | dependencies: 1081 | "@babel/runtime" "~7.4.4" 1082 | byline "~5.0.0" 1083 | chalk "~2.4.1" 1084 | expand-template "~2.0.3" 1085 | fs-extra "~7.0.1" 1086 | minimist "~1.2.0" 1087 | progress "~2.0.0" 1088 | request "~2.88.0" 1089 | request-progress "~3.0.0" 1090 | semver "~6.0.0" 1091 | unique-temp-dir "~1.0.0" 1092 | 1093 | pkg@4.4.0: 1094 | version "4.4.0" 1095 | resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.0.tgz#9b6f2c77f891b6eb403681f5a8c1d2de09a976d3" 1096 | integrity sha512-bFNJ3v56QwqB6JtAl/YrczlmEKBPBVJ3n5nW905kgvG1ex9DajODpTs0kLAFxyLwoubDQux/RPJFL6WrnD/vpg== 1097 | dependencies: 1098 | "@babel/parser" "~7.4.4" 1099 | "@babel/runtime" "~7.4.4" 1100 | chalk "~2.4.2" 1101 | escodegen "~1.11.1" 1102 | fs-extra "~7.0.1" 1103 | globby "~9.2.0" 1104 | into-stream "~5.1.0" 1105 | minimist "~1.2.0" 1106 | multistream "~2.1.1" 1107 | pkg-fetch "~2.6.2" 1108 | progress "~2.0.3" 1109 | resolve "1.6.0" 1110 | stream-meter "~1.0.4" 1111 | 1112 | posix-character-classes@^0.1.0: 1113 | version "0.1.1" 1114 | resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" 1115 | integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= 1116 | 1117 | prelude-ls@~1.1.2: 1118 | version "1.1.2" 1119 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 1120 | integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= 1121 | 1122 | process-nextick-args@~2.0.0: 1123 | version "2.0.1" 1124 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" 1125 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== 1126 | 1127 | progress@~2.0.0, progress@~2.0.3: 1128 | version "2.0.3" 1129 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 1130 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 1131 | 1132 | psl@^1.1.24: 1133 | version "1.4.0" 1134 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" 1135 | integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== 1136 | 1137 | punycode@^1.4.1: 1138 | version "1.4.1" 1139 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 1140 | integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= 1141 | 1142 | punycode@^2.1.0: 1143 | version "2.1.1" 1144 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 1145 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 1146 | 1147 | qs@~6.5.2: 1148 | version "6.5.2" 1149 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 1150 | integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 1151 | 1152 | readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.1.4: 1153 | version "2.3.6" 1154 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 1155 | integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== 1156 | dependencies: 1157 | core-util-is "~1.0.0" 1158 | inherits "~2.0.3" 1159 | isarray "~1.0.0" 1160 | process-nextick-args "~2.0.0" 1161 | safe-buffer "~5.1.1" 1162 | string_decoder "~1.1.1" 1163 | util-deprecate "~1.0.1" 1164 | 1165 | regenerator-runtime@^0.13.2: 1166 | version "0.13.3" 1167 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" 1168 | integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== 1169 | 1170 | regex-not@^1.0.0, regex-not@^1.0.2: 1171 | version "1.0.2" 1172 | resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" 1173 | integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== 1174 | dependencies: 1175 | extend-shallow "^3.0.2" 1176 | safe-regex "^1.1.0" 1177 | 1178 | repeat-element@^1.1.2: 1179 | version "1.1.3" 1180 | resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" 1181 | integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== 1182 | 1183 | repeat-string@^1.6.1: 1184 | version "1.6.1" 1185 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 1186 | integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= 1187 | 1188 | request-progress@~3.0.0: 1189 | version "3.0.0" 1190 | resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" 1191 | integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= 1192 | dependencies: 1193 | throttleit "^1.0.0" 1194 | 1195 | request@~2.88.0: 1196 | version "2.88.0" 1197 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" 1198 | integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== 1199 | dependencies: 1200 | aws-sign2 "~0.7.0" 1201 | aws4 "^1.8.0" 1202 | caseless "~0.12.0" 1203 | combined-stream "~1.0.6" 1204 | extend "~3.0.2" 1205 | forever-agent "~0.6.1" 1206 | form-data "~2.3.2" 1207 | har-validator "~5.1.0" 1208 | http-signature "~1.2.0" 1209 | is-typedarray "~1.0.0" 1210 | isstream "~0.1.2" 1211 | json-stringify-safe "~5.0.1" 1212 | mime-types "~2.1.19" 1213 | oauth-sign "~0.9.0" 1214 | performance-now "^2.1.0" 1215 | qs "~6.5.2" 1216 | safe-buffer "^5.1.2" 1217 | tough-cookie "~2.4.3" 1218 | tunnel-agent "^0.6.0" 1219 | uuid "^3.3.2" 1220 | 1221 | resolve-url@^0.2.1: 1222 | version "0.2.1" 1223 | resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" 1224 | integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= 1225 | 1226 | resolve@1.6.0: 1227 | version "1.6.0" 1228 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c" 1229 | integrity sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw== 1230 | dependencies: 1231 | path-parse "^1.0.5" 1232 | 1233 | ret@~0.1.10: 1234 | version "0.1.15" 1235 | resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" 1236 | integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== 1237 | 1238 | safe-buffer@^5.0.1, safe-buffer@^5.1.2: 1239 | version "5.2.0" 1240 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 1241 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 1242 | 1243 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1244 | version "5.1.2" 1245 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1246 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 1247 | 1248 | safe-regex@^1.1.0: 1249 | version "1.1.0" 1250 | resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" 1251 | integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= 1252 | dependencies: 1253 | ret "~0.1.10" 1254 | 1255 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 1256 | version "2.1.2" 1257 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1258 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1259 | 1260 | semver@~6.0.0: 1261 | version "6.0.0" 1262 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" 1263 | integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== 1264 | 1265 | set-value@^2.0.0, set-value@^2.0.1: 1266 | version "2.0.1" 1267 | resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" 1268 | integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== 1269 | dependencies: 1270 | extend-shallow "^2.0.1" 1271 | is-extendable "^0.1.1" 1272 | is-plain-object "^2.0.3" 1273 | split-string "^3.0.1" 1274 | 1275 | slash@^2.0.0: 1276 | version "2.0.0" 1277 | resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" 1278 | integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== 1279 | 1280 | snapdragon-node@^2.0.1: 1281 | version "2.1.1" 1282 | resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" 1283 | integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== 1284 | dependencies: 1285 | define-property "^1.0.0" 1286 | isobject "^3.0.0" 1287 | snapdragon-util "^3.0.1" 1288 | 1289 | snapdragon-util@^3.0.1: 1290 | version "3.0.1" 1291 | resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" 1292 | integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== 1293 | dependencies: 1294 | kind-of "^3.2.0" 1295 | 1296 | snapdragon@^0.8.1: 1297 | version "0.8.2" 1298 | resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" 1299 | integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== 1300 | dependencies: 1301 | base "^0.11.1" 1302 | debug "^2.2.0" 1303 | define-property "^0.2.5" 1304 | extend-shallow "^2.0.1" 1305 | map-cache "^0.2.2" 1306 | source-map "^0.5.6" 1307 | source-map-resolve "^0.5.0" 1308 | use "^3.1.0" 1309 | 1310 | source-map-resolve@^0.5.0: 1311 | version "0.5.2" 1312 | resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" 1313 | integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== 1314 | dependencies: 1315 | atob "^2.1.1" 1316 | decode-uri-component "^0.2.0" 1317 | resolve-url "^0.2.1" 1318 | source-map-url "^0.4.0" 1319 | urix "^0.1.0" 1320 | 1321 | source-map-url@^0.4.0: 1322 | version "0.4.0" 1323 | resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" 1324 | integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= 1325 | 1326 | source-map@^0.5.6: 1327 | version "0.5.7" 1328 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 1329 | integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 1330 | 1331 | source-map@~0.6.1: 1332 | version "0.6.1" 1333 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 1334 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 1335 | 1336 | split-string@^3.0.1, split-string@^3.0.2: 1337 | version "3.1.0" 1338 | resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" 1339 | integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== 1340 | dependencies: 1341 | extend-shallow "^3.0.0" 1342 | 1343 | sshpk@^1.7.0: 1344 | version "1.16.1" 1345 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 1346 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 1347 | dependencies: 1348 | asn1 "~0.2.3" 1349 | assert-plus "^1.0.0" 1350 | bcrypt-pbkdf "^1.0.0" 1351 | dashdash "^1.12.0" 1352 | ecc-jsbn "~0.1.1" 1353 | getpass "^0.1.1" 1354 | jsbn "~0.1.0" 1355 | safer-buffer "^2.0.2" 1356 | tweetnacl "~0.14.0" 1357 | 1358 | static-extend@^0.1.1: 1359 | version "0.1.2" 1360 | resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" 1361 | integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= 1362 | dependencies: 1363 | define-property "^0.2.5" 1364 | object-copy "^0.1.0" 1365 | 1366 | stream-meter@~1.0.4: 1367 | version "1.0.4" 1368 | resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" 1369 | integrity sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0= 1370 | dependencies: 1371 | readable-stream "^2.1.4" 1372 | 1373 | string_decoder@~1.1.1: 1374 | version "1.1.1" 1375 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 1376 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== 1377 | dependencies: 1378 | safe-buffer "~5.1.0" 1379 | 1380 | supports-color@^5.3.0: 1381 | version "5.5.0" 1382 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 1383 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1384 | dependencies: 1385 | has-flag "^3.0.0" 1386 | 1387 | throttleit@^1.0.0: 1388 | version "1.0.0" 1389 | resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" 1390 | integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= 1391 | 1392 | to-object-path@^0.3.0: 1393 | version "0.3.0" 1394 | resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" 1395 | integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= 1396 | dependencies: 1397 | kind-of "^3.0.2" 1398 | 1399 | to-regex-range@^2.1.0: 1400 | version "2.1.1" 1401 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" 1402 | integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= 1403 | dependencies: 1404 | is-number "^3.0.0" 1405 | repeat-string "^1.6.1" 1406 | 1407 | to-regex@^3.0.1, to-regex@^3.0.2: 1408 | version "3.0.2" 1409 | resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" 1410 | integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== 1411 | dependencies: 1412 | define-property "^2.0.2" 1413 | extend-shallow "^3.0.2" 1414 | regex-not "^1.0.2" 1415 | safe-regex "^1.1.0" 1416 | 1417 | tough-cookie@~2.4.3: 1418 | version "2.4.3" 1419 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" 1420 | integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== 1421 | dependencies: 1422 | psl "^1.1.24" 1423 | punycode "^1.4.1" 1424 | 1425 | tunnel-agent@^0.6.0: 1426 | version "0.6.0" 1427 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1428 | integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 1429 | dependencies: 1430 | safe-buffer "^5.0.1" 1431 | 1432 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1433 | version "0.14.5" 1434 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1435 | integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 1436 | 1437 | type-check@~0.3.2: 1438 | version "0.3.2" 1439 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 1440 | integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= 1441 | dependencies: 1442 | prelude-ls "~1.1.2" 1443 | 1444 | uid2@0.0.3: 1445 | version "0.0.3" 1446 | resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" 1447 | integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= 1448 | 1449 | union-value@^1.0.0: 1450 | version "1.0.1" 1451 | resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" 1452 | integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== 1453 | dependencies: 1454 | arr-union "^3.1.0" 1455 | get-value "^2.0.6" 1456 | is-extendable "^0.1.1" 1457 | set-value "^2.0.1" 1458 | 1459 | unique-temp-dir@~1.0.0: 1460 | version "1.0.0" 1461 | resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385" 1462 | integrity sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U= 1463 | dependencies: 1464 | mkdirp "^0.5.1" 1465 | os-tmpdir "^1.0.1" 1466 | uid2 "0.0.3" 1467 | 1468 | universalify@^0.1.0: 1469 | version "0.1.2" 1470 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" 1471 | integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 1472 | 1473 | unset-value@^1.0.0: 1474 | version "1.0.0" 1475 | resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" 1476 | integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= 1477 | dependencies: 1478 | has-value "^0.3.1" 1479 | isobject "^3.0.0" 1480 | 1481 | uri-js@^4.2.2: 1482 | version "4.2.2" 1483 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 1484 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 1485 | dependencies: 1486 | punycode "^2.1.0" 1487 | 1488 | urix@^0.1.0: 1489 | version "0.1.0" 1490 | resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" 1491 | integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= 1492 | 1493 | use@^3.1.0: 1494 | version "3.1.1" 1495 | resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" 1496 | integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== 1497 | 1498 | util-deprecate@~1.0.1: 1499 | version "1.0.2" 1500 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1501 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 1502 | 1503 | uuid@^3.3.2: 1504 | version "3.3.3" 1505 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" 1506 | integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== 1507 | 1508 | verror@1.10.0: 1509 | version "1.10.0" 1510 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 1511 | integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 1512 | dependencies: 1513 | assert-plus "^1.0.0" 1514 | core-util-is "1.0.2" 1515 | extsprintf "^1.2.0" 1516 | 1517 | word-wrap@~1.2.3: 1518 | version "1.2.3" 1519 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" 1520 | integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== 1521 | 1522 | wrappy@1: 1523 | version "1.0.2" 1524 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1525 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1526 | --------------------------------------------------------------------------------