diff --git a/.codexdocsrc b/.codexdocsrc new file mode 100644 index 0000000..f42c13c --- /dev/null +++ b/.codexdocsrc @@ -0,0 +1,9 @@ +{ + "title": "CodeX Editor   🤩🧦🤨", + "menu": [ + "Guides", + "API", + "Plugins", + {"title": "Support Project", "uri": "/support"} + ] +} diff --git a/.gitignore b/.gitignore index ee81716..1a1c337 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ typings/ # Database files .db/ +.testdb/ # Cache of babel and others .cache/ diff --git a/bin/www b/bin/www index a2fd294..ab2a467 100755 --- a/bin/www +++ b/bin/www @@ -6,11 +6,12 @@ const app = require('../src/app'); const debug = require('debug')('codex.editor.docs:server'); const http = require('http'); +const config = require('../config'); /** * Get port from environment and store in Express. */ -const port = normalizePort(process.env.PORT || '3000'); +const port = normalizePort(config.port || '3000'); app.set('port', port); @@ -30,16 +31,16 @@ server.on('listening', onListening); * Normalize a port into a number, string, or false. */ function normalizePort(val) { - const port = parseInt(val, 10); + const value = parseInt(val, 10); - if (isNaN(port)) { + if (isNaN(value)) { // named pipe return val; } - if (port >= 0) { + if (value >= 0) { // port number - return port; + return value; } return false; @@ -62,11 +63,9 @@ function onError(error) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); - break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); - break; default: throw error; } diff --git a/config/development.json b/config/development.json new file mode 100644 index 0000000..62c4ac6 --- /dev/null +++ b/config/development.json @@ -0,0 +1,4 @@ +{ + "port": 3000, + "database": ".db" +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..42e364b --- /dev/null +++ b/config/index.js @@ -0,0 +1,22 @@ +/** + * This module reads configuration file depending on NODE_ENV + * + * @type {module} + */ + +const fs = require('fs'); +const path = require('path'); +const NODE_ENV = process.env.NODE_ENV || 'development'; +const configPath = `./${NODE_ENV}.json`; +let config; + +if (fs.existsSync(path.resolve(__dirname, configPath))) { + config = require(configPath); +} else { + config = { + database: '.db', + port: 3000 + }; +} + +module.exports = config; diff --git a/config/production.json b/config/production.json new file mode 100644 index 0000000..62c4ac6 --- /dev/null +++ b/config/production.json @@ -0,0 +1,4 @@ +{ + "port": 3000, + "database": ".db" +} diff --git a/config/testing.json b/config/testing.json new file mode 100644 index 0000000..25957e0 --- /dev/null +++ b/config/testing.json @@ -0,0 +1,5 @@ +{ + "port": 3001, + "database": ".testdb", + "rcFile": "./test/.codexdocsrc" +} diff --git a/package.json b/package.json index dbde531..5d3351e 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "nodemon ./bin/www", - "test": "mocha --recursive ./test", + "start": "cross-env NODE_ENV=production nodemon ./bin/www", + "start:dev": "cross-env NODE_ENV=development nodemon ./bin/www", + "test": "cross-env NODE_ENV=testing mocha --recursive ./test", "lint": "eslint --fix --cache ./src/**/*.js", "build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --watch", "precommit": "yarn lint && yarn test --exit" @@ -36,6 +37,7 @@ "babel-loader": "^8.0.2", "chai": "^4.1.2", "chai-http": "^4.0.0", + "cross-env": "^5.2.0", "css-loader": "^1.0.0", "cssnano": "^4.1.0", "eslint": "^5.3.0", @@ -44,6 +46,7 @@ "husky": "^0.14.3", "mini-css-extract-plugin": "^0.4.3", "mocha": "^5.2.0", + "mocha-sinon": "^2.1.0", "nyc": "^12.0.2", "postcss": "^7.0.2", "postcss-apply": "^0.11.0", @@ -59,6 +62,8 @@ "postcss-nested-ancestors": "^2.0.0", "postcss-nesting": "^6.0.0", "postcss-smart-import": "^0.7.6", + "rimraf": "^2.6.2", + "sinon": "^6.3.5", "webpack": "^4.17.1", "webpack-cli": "^3.1.0" } diff --git a/src/app.js b/src/app.js index 276a796..65beda0 100644 --- a/src/app.js +++ b/src/app.js @@ -3,10 +3,14 @@ const express = require('express'); const path = require('path'); const cookieParser = require('cookie-parser'); const logger = require('morgan'); +const rcParser = require('./utils/rcparser'); const routes = require('./routes'); const app = express(); +const config = rcParser.getConfiguration(); + +app.locals.config = config; // view engine setup app.set('views', path.join(__dirname, 'views')); diff --git a/src/utils/database/pages.js b/src/utils/database/pages.js index c169825..493ab25 100644 --- a/src/utils/database/pages.js +++ b/src/utils/database/pages.js @@ -1,5 +1,6 @@ const Datastore = require('nedb'); +const config = require('../../../config'); -const db = new Datastore({filename: './.db/pages.db', autoload: true}); +const db = new Datastore({filename: `./${config.database}/pages.db`, autoload: true}); module.exports = db; diff --git a/src/utils/rcparser.js b/src/utils/rcparser.js new file mode 100644 index 0000000..392fbee --- /dev/null +++ b/src/utils/rcparser.js @@ -0,0 +1,102 @@ +const fs = require('fs'); +const path = require('path'); +const config = require('../../config'); +const rcPath = path.resolve(__dirname, '../../', config.rcFile || './.codexdocsrc'); + +/** + * @typedef {Object} RCData + * @property {string} title - website title + * @property {object[]} menu - options for website menu + * @property {string} menu[].title - menu option title + * @property {string} menu[].uri - menu option href + */ + +/** + * @class RCParser + * @classdesc Class to parse runtime configuration file for CodeX Docs engine + */ +module.exports = class RCParser { + /** + * Default CodeX Docs configuration + * + * @static + * @return {{title: string, menu: Array}} + */ + static get DEFAULTS() { + return { + title: 'CodeX Docs', + menu: [] + }; + } + + /** + * Find and parse runtime configuration file + * + * @static + * @return {{title: string, menu: []}} + */ + static getConfiguration() { + if (!fs.existsSync(rcPath)) { + return RCParser.DEFAULTS; + } + + const file = fs.readFileSync(rcPath, {encoding: 'UTF-8'}); + const rConfig = {}; + let userConfig; + + try { + userConfig = JSON.parse(file); + } catch (e) { + console.log('CodeX Docs rc file should be in JSON format.'); + return RCParser.DEFAULTS; + } + + rConfig.title = userConfig.title || RCParser.DEFAULTS.title; + rConfig.menu = userConfig.menu || RCParser.DEFAULTS.menu; + + if (!(rConfig.menu instanceof Array)) { + console.log('Menu section in the rc file must be an array.'); + rConfig.menu = RCParser.DEFAULTS.menu; + } + + rConfig.menu = rConfig.menu.filter((option, i) => { + i = i + 1; + if (typeof option === 'string') { + return true; + } + + if (!option || option instanceof Array || typeof option !== 'object') { + console.log(`Menu option #${i} in rc file must be a string or an object`); + return false; + } + + const {title, uri} = option; + + if (!title || typeof title !== 'string') { + console.log(`Menu option #${i} title must be a string.`); + return false; + } + + if (!uri || typeof uri !== 'string') { + console.log(`Menu option #${i} uri must be a string.`); + return false; + } + + return true; + }); + + rConfig.menu = rConfig.menu.map(option => { + if (typeof option === 'string') { + return { + title: option, + /* Replace all non alpha- and numeric-symbols with '-' */ + uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-') + }; + } + + return option; + }); + + return rConfig; + } +}; diff --git a/src/views/components/header.twig b/src/views/components/header.twig index 51f0734..fd7d722 100644 --- a/src/views/components/header.twig +++ b/src/views/components/header.twig @@ -1,6 +1,6 @@
diff --git a/test/database.js b/test/database.js index 5caa43e..af4e306 100644 --- a/test/database.js +++ b/test/database.js @@ -1,11 +1,12 @@ const fs = require('fs'); +const config = require('../config'); const {expect} = require('chai'); const {class: Database} = require('../src/utils/database'); const Datastore = require('nedb'); describe('Database', () => { - const pathToDB = './.db/test.db'; + const pathToDB = `./${config.database}/test.db`; let nedbInstance; let db; diff --git a/test/models/page.js b/test/models/page.js index 764da23..f134562 100644 --- a/test/models/page.js +++ b/test/models/page.js @@ -1,8 +1,19 @@ const {expect} = require('chai'); +const fs = require('fs'); +const path = require('path'); +const config = require('../../config'); const Page = require('../../src/models/page'); const {pages} = require('../../src/utils/database'); describe('Page model', () => { + after(() => { + const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); + + if (fs.existsSync(pathToDB)) { + fs.unlinkSync(pathToDB); + } + }); + it('Working with empty model', async () => { let page = new Page(); diff --git a/test/rcparser.js b/test/rcparser.js new file mode 100644 index 0000000..053952c --- /dev/null +++ b/test/rcparser.js @@ -0,0 +1,262 @@ +const {expect} = require('chai'); + +require('mocha-sinon'); +const fs = require('fs'); +const path = require('path'); +const config = require('../config'); +const rcParser = require('../src/utils/rcparser'); + +const rcPath = path.resolve(process.cwd(), config.rcFile); + +describe('RC file parser test', () => { + beforeEach(function () { + this.sinon.stub(console, 'log'); + }); + + afterEach(() => { + if (fs.existsSync(rcPath)) { + fs.unlinkSync(rcPath); + } + }); + + it('Default config', async () => { + const parsedConfig = rcParser.getConfiguration(); + + expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS); + }); + + it('Invalid JSON formatted config', () => { + const invalidJson = '{title: "Codex Docs"}'; + + fs.writeFileSync(rcPath, invalidJson, 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('CodeX Docs rc file should be in JSON format.')).to.be.true; + + expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS); + }); + + it('Normal config', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + {title: 'Option 1', uri: '/option1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(parsedConfig).to.be.deep.equal(normalConfig); + }); + + it('Missed title', () => { + const normalConfig = { + menu: [ + {title: 'Option 1', uri: '/option1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(parsedConfig.menu).to.be.deep.equal(normalConfig.menu); + expect(parsedConfig.title).to.be.equal(rcParser.DEFAULTS.title); + }); + + it('Missed menu', () => { + const normalConfig = { + title: 'Documentation' + }; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu); + }); + + it('Menu is not an array', () => { + const normalConfig = { + title: 'Documentation', + menu: { + 0: {title: 'Option 1', uri: '/option1'}, + 1: {title: 'Option 2', uri: '/option2'}, + 2: {title: 'Option 3', uri: '/option3'} + } + }; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu section in the rc file must be an array.')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu); + }); + + it('Menu option is a string', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + 'Option 1', + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 1', uri: '/option-1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); + + it('Menu option is not a string or an object', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + [ {title: 'Option 1', uri: '/option1'} ], + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu option #1 in rc file must be a string or an object')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); + + it('Menu option title is undefined', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + {uri: '/option1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu option #1 title must be a string.')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); + + it('Menu option title is not a string', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + {title: [], uri: '/option1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu option #1 title must be a string.')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); + + it('Menu option uri is undefined', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + {title: 'Option 1'}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu option #1 uri must be a string.')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); + + it('Menu option title is not a string', () => { + const normalConfig = { + title: 'Documentation', + menu: [ + {title: 'Option 1', uri: []}, + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ] + }; + + const expectedMenu = [ + {title: 'Option 2', uri: '/option2'}, + {title: 'Option 3', uri: '/option3'} + ]; + + fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + + const parsedConfig = rcParser.getConfiguration(); + + expect(console.log.calledOnce).to.be.true; + expect(console.log.calledWith('Menu option #1 uri must be a string.')).to.be.true; + + expect(parsedConfig.title).to.be.equal(normalConfig.title); + expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + }); +}); diff --git a/test/rest/pages.js b/test/rest/pages.js index cbb960c..38c06e4 100644 --- a/test/rest/pages.js +++ b/test/rest/pages.js @@ -1,6 +1,9 @@ const {app} = require('../../bin/www'); const model = require('../../src/models/page'); +const fs = require('fs'); +const path = require('path'); +const config = require('../../config'); const chai = require('chai'); const chaiHTTP = require('chai-http'); const {expect} = chai; @@ -14,6 +17,14 @@ describe('Pages REST: ', () => { agent = chai.request.agent(app); }); + after(async () => { + const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); + + if (fs.existsSync(pathToDB)) { + fs.unlinkSync(pathToDB); + } + }); + it('Creating page', async () => { const body = { blocks: [ diff --git a/yarn.lock b/yarn.lock index 4650296..cdc6477 100644 --- a/yarn.lock +++ b/yarn.lock @@ -615,6 +615,28 @@ version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" +"@sinonjs/commons@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.0.2.tgz#3e0ac737781627b8844257fadc3d803997d0526e" + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@3.0.0", "@sinonjs/formatio@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.0.0.tgz#9d282d81030a03a03fa0c5ce31fd8786a4da311a" + dependencies: + "@sinonjs/samsam" "2.1.0" + +"@sinonjs/samsam@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.0.tgz#b8b8f5b819605bd63601a6ede459156880f38ea3" + dependencies: + array-from "^2.1.1" + +"@sinonjs/samsam@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.2.tgz#16947fce5f57258d01f1688fdc32723093c55d3f" + "@types/chai@4": version "4.1.4" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.4.tgz#5ca073b330d90b4066d6ce18f60d57f2084ce8ca" @@ -911,6 +933,10 @@ array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -1678,6 +1704,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" + dependencies: + cross-spawn "^6.0.5" + is-windows "^1.0.0" + cross-spawn@^4: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -2009,7 +2042,7 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff@3.5.0: +diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3372,7 +3405,7 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^1.0.2: +is-windows@^1.0.0, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3492,6 +3525,10 @@ json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +just-extend@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3606,6 +3643,10 @@ lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -3618,6 +3659,10 @@ lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" +lolex@^2.3.2, lolex@^2.7.5: + version "2.7.5" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733" + loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -3873,6 +3918,10 @@ mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: dependencies: minimist "0.0.8" +mocha-sinon@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mocha-sinon/-/mocha-sinon-2.1.0.tgz#61e92727e577bee44cac6f32162dcec33c1301e5" + mocha@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" @@ -3993,6 +4042,16 @@ nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" +nise@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.5.tgz#979a97a19c48d627bb53703726ae8d53ce8d4b3e" + dependencies: + "@sinonjs/formatio" "3.0.0" + just-extend "^3.0.0" + lolex "^2.3.2" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + node-libs-browser@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" @@ -4446,6 +4505,12 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -5530,6 +5595,20 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sinon@^6.3.5: + version "6.3.5" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0" + dependencies: + "@sinonjs/commons" "^1.0.2" + "@sinonjs/formatio" "^3.0.0" + "@sinonjs/samsam" "^2.1.2" + diff "^3.5.0" + lodash.get "^4.4.2" + lolex "^2.7.5" + nise "^1.4.5" + supports-color "^5.5.0" + type-detect "^4.0.8" + slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" @@ -5810,7 +5889,7 @@ supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" dependencies: @@ -5878,6 +5957,10 @@ test-exclude@^4.2.0: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" +text-encoding@^0.6.4: + version "0.6.4" + resolved "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -5975,7 +6058,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"