Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 902-zone_unit_tests
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Jimenez Ruiz 2020-06-23 11:41:18 +02:00
commit 1b683f5dc0
29 changed files with 7498 additions and 1161 deletions

4
Jenkinsfile vendored
View File

@ -54,6 +54,10 @@ pipeline {
} }
} }
stage('Test') { stage('Test') {
when { not { anyOf {
branch 'test'
branch 'master'
}}}
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }

View File

@ -13,8 +13,15 @@ Required applications.
You will need to install globally the following items. You will need to install globally the following items.
``` ```
# sudo npm install -g jest gulp-cli $ sudo npm install -g jest gulp-cli
``` ```
For the usage of jest --watch on macOs.
```
$ brew install watchman
```
* [watchman](https://facebook.github.io/watchman/)
## Linux Only Prerequisites ## Linux Only Prerequisites
Your user must be on the docker group to use it so you will need to run this command: Your user must be on the docker group to use it so you will need to run this command:

View File

@ -4,3 +4,4 @@ apps:
instances: 1 instances: 1
max_restarts: 3 max_restarts: 3
restart_delay: 15000 restart_delay: 15000
node_args: --tls-min-v1.0

View File

@ -54,7 +54,7 @@ module.exports = class Docker {
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort']; this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];
} }
await this.waitForHealthy(); if (runChown) await this.wait();
} catch (err) { } catch (err) {
if (this.isRandom) if (this.isRandom)
await this.rm(); await this.rm();

View File

@ -1,6 +1,6 @@
describe('Component vnField', () => { describe('Component vnField', () => {
let $element;
let controller; let controller;
let $element;
beforeEach(ngModule('vnCore')); beforeEach(ngModule('vnCore'));
@ -13,12 +13,84 @@ describe('Component vnField', () => {
$element.remove(); $element.remove();
}); });
// Remove this block describe('field get/set', () => {
describe('clearDisabled binding', () => { it('should do nothing when trying to set the same value again', () => {
it(`should enable the show property`, () => { controller.field = '';
controller.clearDisabled = true; jest.spyOn(controller, 'validateValue');
expect(controller.clearDisabled).toEqual(true); controller.field = '';
expect(controller.validateValue).toHaveBeenCalledTimes(0);
expect(controller.classList).not.toContain('not-empty');
});
it('should add the class no empty and then call validateValue()', () => {
controller.field = '';
jest.spyOn(controller, 'validateValue');
controller.field = 'someField';
expect(controller.validateValue).toHaveBeenCalledTimes(1);
expect(controller.classList).toContain('not-empty');
});
});
describe('refreshHint()', () => {
it('should add the class invalid if there is an error in the controller', () => {
controller._error = true;
controller.refreshHint();
expect(controller.classList).toContain('invalid');
});
});
describe('onFocus()', () => {
it('should add the class focus', () => {
controller.onFocus(true);
expect(controller.classList).toContain('focused');
});
it('should not add the class focus', () => {
controller.onFocus(false);
expect(controller.classList).not.toContain('focuses');
});
});
describe('buildInput()', () => {
it('should build an input based on the received type', () => {
controller.buildInput('number');
expect(controller.input.tagName).toEqual('INPUT');
expect(controller.input.type).toEqual('number');
});
});
describe('validateValue()', () => {
it('should do nothing if there is no new error to show', () => {
jest.spyOn(controller, 'refreshHint');
controller.inputError = 'old validation message';
controller.buildInput('number');
controller.input.setCustomValidity('old validation message');
controller.validateValue();
expect(controller.refreshHint).not.toHaveBeenCalled();
expect(controller.inputError).toEqual('old validation message');
});
it('should update the input error and call refreshHint', () => {
jest.spyOn(controller, 'refreshHint');
controller.inputError = 'OLD validation message';
controller.buildInput('number');
controller.input.setCustomValidity('NEW validation message');
controller.validateValue();
expect(controller.refreshHint).toHaveBeenCalled();
expect(controller.inputError).toEqual('NEW validation message');
}); });
}); });
}); });

View File

@ -73,7 +73,7 @@ export function config($translateProvider, $translatePartialLoaderProvider, $htt
return locale; return locale;
if (langOptions.langAliases[locale]) if (langOptions.langAliases[locale])
return langOptions.langAliases[locale]; return langOptions.langAliases[locale];
return fallbackLang; return langOptions.fallbackLang;
}); });
$translatePartialLoaderProvider.addPart(appName); $translatePartialLoaderProvider.addPart(appName);

View File

@ -48,7 +48,7 @@ function backWatch(done) {
// XXX: Workaround to avoid nodemon bug // XXX: Workaround to avoid nodemon bug
// https://github.com/remy/nodemon/issues/1346 // https://github.com/remy/nodemon/issues/1346
let commands = ['node --inspect ./node_modules/gulp/bin/gulp.js']; let commands = ['node --tls-min-v1.0 --inspect ./node_modules/gulp/bin/gulp.js'];
if (!isWindows) commands.unshift('sleep 1'); if (!isWindows) commands.unshift('sleep 1');
nodemon({ nodemon({
@ -136,7 +136,7 @@ function backTest(done) {
const nodemon = require('gulp-nodemon'); const nodemon = require('gulp-nodemon');
nodemon({ nodemon({
exec: ['node ./node_modules/gulp/bin/gulp.js'], exec: ['node --tls-min-v1.0 ./node_modules/gulp/bin/gulp.js'],
args: ['backTestOnce'], args: ['backTestOnce'],
watch: backSources, watch: backSources,
done: done done: done

View File

@ -15,4 +15,4 @@ Show Pickup order: Ver orden de recogida
Search claim by id or client name: Buscar reclamaciones por identificador o nombre de cliente Search claim by id or client name: Buscar reclamaciones por identificador o nombre de cliente
Claim deleted!: Reclamación eliminada! Claim deleted!: Reclamación eliminada!
claim: reclamación claim: reclamación
Photos: Fotos Photos: Fotos

View File

@ -0,0 +1,27 @@
const app = require('vn-loopback/server/server');
describe('client sendSms()', () => {
let createdLog;
afterAll(async done => {
await app.models.ClientLog.destroyById(createdLog.id);
done();
});
it('should send a message and log it', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let id = 101;
let destination = 222222222;
let message = 'this is the message created in a test';
let sms = await app.models.Client.sendSms(ctx, id, destination, message);
logId = sms.logId;
createdLog = await app.models.ClientLog.findById(logId);
let json = JSON.parse(JSON.stringify(createdLog.newInstance));
expect(json.message).toEqual(message);
});
});

View File

@ -0,0 +1,37 @@
const app = require('vn-loopback/server/server');
const soap = require('soap');
describe('sms send()', () => {
it('should return the expected message and status code', async() => {
const code = 200;
const smsConfig = await app.models.SmsConfig.findOne();
const soapClient = await soap.createClientAsync(smsConfig.uri);
spyOn(soap, 'createClientAsync').and.returnValue(soapClient);
spyOn(soapClient, 'sendSMSAsync').and.returnValue([{
result: {
$value:
`<xtratelecom-sms-response>
<sms>
<codigo>
${code}
</codigo>
<descripcion>
Envio en procesamiento
</descripcion>
<messageId>
1
</messageId>
</sms>
<procesoId>
444328681
</procesoId>
</xtratelecom-sms-response>`
}
}]);
let ctx = {req: {accessToken: {userId: 1}}};
let result = await app.models.Sms.send(ctx, 105, 'destination', 'My SMS Body');
expect(result.statusCode).toEqual(200);
expect(result.status).toContain('Fake response');
});
});

View File

@ -9,7 +9,7 @@ class Controller extends Descriptor {
set entity(value) { set entity(value) {
super.entity = value; super.entity = value;
if (this.$params.sendSMS) if (value && this.$params.sendSMS)
this.showSMSDialog(); this.showSMSDialog();
} }

View File

@ -14,7 +14,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: 'string', type: 'Object',
root: true root: true
}, },
http: { http: {
@ -53,6 +53,6 @@ module.exports = Self => {
if (!isAllowed) if (!isAllowed)
throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED'); throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
return await models.TicketTracking.create(params); return models.TicketTracking.create(params);
}; };
}; };

View File

@ -0,0 +1,50 @@
module.exports = Self => {
Self.remoteMethodCtx('setDelivered', {
description: 'Changes the state of the received ticket ids to delivered',
accessType: 'WRITE',
accepts: [
{
arg: 'ticketIds',
description: 'the array of ticket ids to set as delivered',
type: ['Number'],
required: true,
http: {source: 'body'}
}
],
returns: {
type: 'Object',
root: true
},
http: {
path: `/setDelivered`,
verb: 'POST'
}
});
Self.setDelivered = async(ctx, ticketIds) => {
let userId = ctx.req.accessToken.userId;
let models = Self.app.models;
let state = await models.State.findOne({
where: {
code: 'delivered'
},
fields: ['id', 'name', 'alertLevel', 'code']
});
let worker = await models.Worker.findOne({where: {userFk: userId}});
let promises = [];
for (let id of ticketIds) {
let promise = models.TicketTracking.changeState(ctx, {
stateFk: state.id,
workerFk: worker.id,
ticketFk: id
});
promises.push(promise);
}
await Promise.all(promises);
return state;
};
};

View File

@ -0,0 +1,36 @@
const app = require('vn-loopback/server/server');
describe('ticket setDelivered()', () => {
let ticketOne;
let ticketTwo;
beforeAll(async done => {
let originalTicketOne = await app.models.Ticket.findById(8);
let originalTicketTwo = await app.models.Ticket.findById(10);
originalTicketOne.id = null;
originalTicketTwo.id = null;
ticketOne = await app.models.Ticket.create(originalTicketOne);
ticketTwo = await app.models.Ticket.create(originalTicketTwo);
done();
});
afterAll(async done => {
await app.models.Ticket.destroyById(ticketOne.id);
await app.models.Ticket.destroyById(ticketTwo.id);
done();
});
it('should return the state which has been applied to the given tickets', async() => {
let ctx = {req: {accessToken: {userId: 49}}};
let delivered = await app.models.State.findOne({where: {code: 'delivered'}, fields: ['id']});
let params = [ticketOne.id, ticketTwo.id];
let state = await app.models.TicketTracking.setDelivered(ctx, params);
expect(state.id).toEqual(delivered.id);
});
});

View File

@ -0,0 +1,27 @@
const app = require('vn-loopback/server/server');
describe('ticket sendSms()', () => {
let logId;
afterAll(async done => {
await app.models.TicketLog.destroyById(logId);
done();
});
it('should send a message and log it', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let id = 11;
let destination = 222222222;
let message = 'this is the message created in a test';
let sms = await app.models.Ticket.sendSms(ctx, id, destination, message);
logId = sms.logId;
let createdLog = await app.models.TicketLog.findById(logId);
let json = JSON.parse(JSON.stringify(createdLog.newInstance));
expect(json.message).toEqual(message);
});
});

View File

@ -1,5 +1,6 @@
module.exports = function(Self) { module.exports = function(Self) {
require('../methods/ticket-tracking/changeState')(Self); require('../methods/ticket-tracking/changeState')(Self);
require('../methods/ticket-tracking/setDelivered')(Self);
Self.validatesPresenceOf('stateFk', {message: 'State cannot be blank'}); Self.validatesPresenceOf('stateFk', {message: 'State cannot be blank'});
Self.validatesPresenceOf('workerFk', {message: 'Worker cannot be blank'}); Self.validatesPresenceOf('workerFk', {message: 'Worker cannot be blank'});

View File

@ -5,6 +5,7 @@
<vn-item <vn-item
ng-click="addTurn.show()" ng-click="addTurn.show()"
vn-acl="buyer" vn-acl="buyer"
ng-show="$ctrl.isEditable"
vn-acl-action="remove" vn-acl-action="remove"
name="addTurn" name="addTurn"
translate> translate>
@ -60,7 +61,7 @@
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="makeInvoiceConfirmation.show()" ng-click="makeInvoiceConfirmation.show()"
ng-show="!$ctrl.isInvoiced" ng-show="$ctrl.isEditable"
vn-acl="invoicing" vn-acl="invoicing"
vn-acl-action="remove" vn-acl-action="remove"
name="makeInvoice" name="makeInvoice"

View File

@ -8,6 +8,7 @@ class Controller extends Descriptor {
set ticket(value) { set ticket(value) {
this.entity = value; this.entity = value;
this.isTicketEditable();
} }
get entity() { get entity() {
@ -22,14 +23,6 @@ class Controller extends Descriptor {
this.showSMSDialog(); this.showSMSDialog();
} }
get isEditable() {
try {
return !this.ticket.tracking.state.alertLevel;
} catch (e) {}
return true;
}
get isInvoiced() { get isInvoiced() {
return this.ticket.refFk !== null; return this.ticket.refFk !== null;
} }
@ -45,6 +38,12 @@ class Controller extends Descriptor {
return this.ticket.stowaway || this.ticket.ship; return this.ticket.stowaway || this.ticket.ship;
} }
isTicketEditable() {
this.$http.get(`Tickets/${this.$state.params.id}/isEditable`).then(res => {
this.isEditable = res.data;
});
}
showChangeShipped() { showChangeShipped() {
this.newShipped = this.ticket.shipped; this.newShipped = this.ticket.shipped;
this.$.changeShippedDialog.show(); this.$.changeShippedDialog.show();

View File

@ -28,8 +28,10 @@ describe('Ticket Component vnTicketDescriptor', () => {
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, _$state_) => { beforeEach(angular.mock.inject(($componentController, _$httpBackend_, _$state_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$httpBackend.whenGET(`Tickets/${ticket.id}/canHaveStowaway`).respond(true); $httpBackend.whenGET(`Tickets/${ticket.id}/canHaveStowaway`).respond(true);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond(true);
$state = _$state_; $state = _$state_;
$state.params.id = 1;
$state.getCurrentPath = () => [null, {state: {name: 'ticket'}}]; $state.getCurrentPath = () => [null, {state: {name: 'ticket'}}];
controller = $componentController('vnTicketDescriptor', {$element: null}, {ticket}); controller = $componentController('vnTicketDescriptor', {$element: null}, {ticket});

View File

@ -89,7 +89,7 @@
<vn-td class="expendable">{{::ticket.province}}</vn-td> <vn-td class="expendable">{{::ticket.province}}</vn-td>
<vn-td> <vn-td>
<span class="chip {{$ctrl.stateColor(ticket)}}"> <span class="chip {{$ctrl.stateColor(ticket)}}">
{{::ticket.state}} {{ticket.state}}
</span> </span>
</vn-td> </vn-td>
<vn-td>{{::ticket.agencyMode}}</vn-td> <vn-td>{{::ticket.agencyMode}}</vn-td>
@ -120,6 +120,13 @@
</vn-data-viewer> </vn-data-viewer>
<div fixed-bottom-right> <div fixed-bottom-right>
<vn-vertical style="align-items: center;"> <vn-vertical style="align-items: center;">
<vn-button class="round sm vn-mb-sm"
icon="print"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.setDelivered()"
vn-tooltip="Set as delivered and open delivery note(s)"
tooltip-position="left">
</vn-button>
<vn-button class="round sm vn-mb-sm" <vn-button class="round sm vn-mb-sm"
icon="icon-recovery" icon="icon-recovery"
ng-show="$ctrl.totalChecked > 0" ng-show="$ctrl.totalChecked > 0"

View File

@ -4,6 +4,37 @@ import UserError from 'core/lib/user-error';
import './style.scss'; import './style.scss';
export default class Controller extends Section { export default class Controller extends Section {
constructor($element, $, vnReport) {
super($element, $);
this.vnReport = vnReport;
}
setDelivered() {
const checkedTickets = this.checked;
let ids = [];
for (let ticket of checkedTickets)
ids.push(ticket.id);
this.$http.post('TicketTrackings/setDelivered', ids).then(res => {
let state = res.data;
for (let ticket of checkedTickets) {
ticket.stateFk = state.id;
ticket.state = state.name;
ticket.alertLevel = state.alertLevel;
ticket.alertLevelCode = state.code;
}
this.openDeliveryNotes(ids);
});
}
openDeliveryNotes(ids) {
for (let id of ids) {
this.vnReport.show('delivery-note', {
ticketId: id,
});
}
}
openBalanceDialog() { openBalanceDialog() {
const checkedTickets = this.checked; const checkedTickets = this.checked;
const description = []; const description = [];
@ -125,6 +156,7 @@ export default class Controller extends Section {
return [minHour, maxHour]; return [minHour, maxHour];
} }
} }
Controller.$inject = ['$element', '$scope', 'vnReport'];
ngModule.component('vnTicketIndex', { ngModule.component('vnTicketIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -1,6 +1,7 @@
import './index.js'; import './index.js';
describe('Component vnTicketIndex', () => { describe('Component vnTicketIndex', () => {
let controller; let controller;
let $httpBackend;
let $window; let $window;
let tickets = [{ let tickets = [{
id: 1, id: 1,
@ -21,7 +22,8 @@ describe('Component vnTicketIndex', () => {
beforeEach(ngModule('ticket')); beforeEach(ngModule('ticket'));
beforeEach(inject(($componentController, _$window_) => { beforeEach(inject(($componentController, _$window_, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$window = _$window_; $window = _$window_;
const $element = angular.element('<vn-ticket-index></vn-ticket-index>'); const $element = angular.element('<vn-ticket-index></vn-ticket-index>');
controller = $componentController('vnTicketIndex', {$element}); controller = $componentController('vnTicketIndex', {$element});
@ -85,6 +87,21 @@ describe('Component vnTicketIndex', () => {
}); });
}); });
describe('setDelivered()/openDeliveryNotes()', () => {
it('should perform a post to setDelivered and open tabs with the delivery notes', () => {
controller.$.model = {data: tickets, refresh: () => {}};
$window.open = jest.fn();
$httpBackend.expect('POST', 'TicketTrackings/setDelivered').respond('ok');
controller.setDelivered();
$httpBackend.flush();
expect($window.open).toHaveBeenCalledWith(`api/report/delivery-note?ticketId=${tickets[1].id}`);
expect($window.open).toHaveBeenCalledWith(`api/report/delivery-note?ticketId=${tickets[2].id}`);
});
});
describe('checked()', () => { describe('checked()', () => {
it('should return an array of checked tickets', () => { it('should return an array of checked tickets', () => {
controller.$.model = {data: tickets}; controller.$.model = {data: tickets};

View File

@ -2,6 +2,7 @@ Weekly tickets: Tickets programados
Go to lines: Ir a lineas Go to lines: Ir a lineas
Not available: No disponible Not available: No disponible
Payment on account...: Pago a cuenta... Payment on account...: Pago a cuenta...
Set as delivered and open delivery note(s): Marcar como servido/s y abrir albarán/es
Closure: Cierre Closure: Cierre
You cannot make a payment on account from multiple clients: No puedes realizar un pago a cuenta de clientes diferentes You cannot make a payment on account from multiple clients: No puedes realizar un pago a cuenta de clientes diferentes
Filter by selection: Filtro por selección Filter by selection: Filtro por selección

View File

@ -44,7 +44,7 @@ class Controller extends Component {
let salesIds = []; let salesIds = [];
let modified = false; let modified = false;
if (!this.newDiscount) return; if (this.newDiscount == null) return;
for (let i = 0; i < this.edit.length; i++) { for (let i = 0; i < this.edit.length; i++) {
if (this.newDiscount != this.edit[0].discount || this.bulk || !this.newDiscount) { if (this.newDiscount != this.edit[0].discount || this.bulk || !this.newDiscount) {

View File

@ -99,8 +99,8 @@
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath}}/50x50/{{sale.image}}" ng-src="{{::$root.imagePath}}/catalog/50x50/{{sale.image}}"
zoom-image="{{::$root.imagePath}}/1600x900/{{sale.image}}" zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{sale.image}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td vn-focus number> <vn-td vn-focus number>

View File

@ -56,15 +56,19 @@ module.exports = Self => {
} }
}); });
let entitlementRate = 0;
absences.forEach(absence => { absences.forEach(absence => {
const isHoliday = absence.absenceType().code === 'holiday'; const absenceType = absence.absenceType();
const isHalfHoliday = absence.absenceType().code === 'halfHoliday'; const isHoliday = absenceType.code === 'holiday';
const isHalfHoliday = absenceType.code === 'halfHoliday';
if (isHoliday) if (isHoliday)
calendar.holidaysEnjoyed += 1; calendar.holidaysEnjoyed += 1;
if (isHalfHoliday) if (isHalfHoliday)
calendar.holidaysEnjoyed += 0.5; calendar.holidaysEnjoyed += 0.5;
entitlementRate += absenceType.holidayEntitlementRate;
absence.dated = new Date(absence.dated); absence.dated = new Date(absence.dated);
absence.dated.setHours(0, 0, 0, 0); absence.dated.setHours(0, 0, 0, 0);
}); });
@ -119,6 +123,9 @@ module.exports = Self => {
workedDays += Math.floor((endedTime - startedTime) / dayTimestamp); workedDays += Math.floor((endedTime - startedTime) / dayTimestamp);
if (workedDays > daysInYear())
workedDays = daysInYear();
// Workcenter holidays // Workcenter holidays
let holidayList = contract.workCenter().holidays(); let holidayList = contract.workCenter().holidays();
for (let day of holidayList) { for (let day of holidayList) {
@ -137,10 +144,22 @@ module.exports = Self => {
const maxHolidays = currentContract.holidays().days; const maxHolidays = currentContract.holidays().days;
calendar.totalHolidays = maxHolidays; calendar.totalHolidays = maxHolidays;
if (workedDays < 365) workedDays -= entitlementRate;
calendar.totalHolidays = Math.round(2 * maxHolidays * (workedDays) / 365) / 2;
if (workedDays < daysInYear())
calendar.totalHolidays = Math.round(2 * maxHolidays * (workedDays) / daysInYear()) / 2;
}
function daysInYear() {
const year = yearStarted.getFullYear();
return isLeapYear(year) ? 366 : 365;
} }
return [calendar, absences, holidays]; return [calendar, absences, holidays];
}; };
function isLeapYear(year) {
return year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
}
}; };

View File

@ -19,6 +19,9 @@
}, },
"code": { "code": {
"type": "String" "type": "String"
},
"holidayEntitlementRate": {
"type": "Number"
} }
}, },
"acls": [ "acls": [

8246
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@
"@babel/preset-env": "^7.7.7", "@babel/preset-env": "^7.7.7",
"@babel/register": "^7.7.7", "@babel/register": "^7.7.7",
"angular-mocks": "^1.7.9", "angular-mocks": "^1.7.9",
"babel-jest": "^24.9.0", "babel-jest": "^26.0.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
@ -70,7 +70,7 @@
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"jasmine-reporters": "^2.3.2", "jasmine-reporters": "^2.3.2",
"jasmine-spec-reporter": "^4.2.1", "jasmine-spec-reporter": "^4.2.1",
"jest": "^24.9.0", "jest": "^26.0.1",
"jest-junit": "^8.0.0", "jest-junit": "^8.0.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"merge-stream": "^1.0.1", "merge-stream": "^1.0.1",