Browse Source

Much better serialization/deserialization for sprinklers

update-deps
Alex Mikhalev 7 years ago
parent
commit
e5cc6b86e7
  1. 1
      .vscode/launch.json
  2. 2
      common/logger.ts
  3. 2
      common/sprinklers/Program.ts
  4. 4
      common/sprinklers/SprinklersDevice.ts
  5. 245
      common/sprinklers/json/index.ts
  6. 65
      common/sprinklers/json/list.ts
  7. 142
      common/sprinklers/mqtt/index.ts
  8. 37
      common/sprinklers/schedule.ts
  9. 3
      package.json
  10. 3
      server/configureLogger.ts
  11. 8
      server/index.ts
  12. 10
      yarn.lock

1
.vscode/launch.json vendored

@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
"env": {
"NODE_ENV": "development"
},
"console": "integratedTerminal",
"program": "${workspaceRoot}/dist/server/index.js"
}
]

2
common/logger.ts

@ -135,7 +135,7 @@ let logger: pino.Logger = pino({ @@ -135,7 +135,7 @@ let logger: pino.Logger = pino({
});
export function setLogger(newLogger: pino.Logger) {
logger = newLogger;
exports.default = logger = newLogger;
}
export default logger;

2
common/sprinklers/Program.ts

@ -9,7 +9,7 @@ export class ProgramItem { @@ -9,7 +9,7 @@ export class ProgramItem {
// duration of the run
readonly duration: Duration;
constructor(section: number, duration: Duration) {
constructor(section: number = 0, duration: Duration = new Duration()) {
this.section = section;
this.duration = duration;
}

4
common/sprinklers/SprinklersDevice.ts

@ -17,6 +17,10 @@ export abstract class SprinklersDevice { @@ -17,6 +17,10 @@ export abstract class SprinklersDevice {
abstract pauseSectionRunner(): Promise<{}>;
abstract unpauseSectionRunner(): Promise<{}>;
abstract get sectionConstructor(): typeof Section;
abstract get sectionRunnerConstructor(): typeof SectionRunner;
abstract get programConstructor(): typeof Program;
toString(): string {
return `SprinklersDevice{id="${this.id}", connected=${this.connected}, ` +
`sections=[${this.sections}], ` +

245
common/sprinklers/json/index.ts

@ -1,175 +1,108 @@ @@ -1,175 +1,108 @@
/* tslint:disable:ordered-imports */
import { assign, pick } from "lodash";
import {
createSimpleSchema, createModelSchema, primitive, object, date, custom,
ModelSchema, PropSchema,
} from "serializr";
import list from "./list";
import * as s from "..";
export interface ISectionJSON {
name: string;
state: boolean;
}
const sectionProps = ["name", "state"];
export function sectionToJSON(sec: s.Section): ISectionJSON {
return pick(sec, sectionProps);
}
export function sectionFromJSON(sec: s.Section, json: ISectionJSON) {
assign(sec, pick(json, sectionProps));
}
export interface ITimeOfDayJSON {
hour: number;
minute: number;
second: number;
millisecond: number;
}
const timeOfDayProps = ["hour", "minute", "second", "millisecond"];
export function timeOfDayToJSON(timeOfDay: s.TimeOfDay): ITimeOfDayJSON {
return pick(timeOfDay, timeOfDayProps);
}
export function timeOfDayFromJSON(timeOfDay: s.TimeOfDay, json: ITimeOfDayJSON) {
assign(timeOfDay, pick(json, timeOfDayProps));
}
export interface IScheduleJSON {
times: ITimeOfDayJSON[];
weekdays: number[];
from?: string;
to?: string;
}
const scheduleProps = ["weekdays", "from", "to"];
export function scheduleToJSON(schedule: s.Schedule): IScheduleJSON {
return {
...pick(schedule, scheduleProps),
times: schedule.times.map(timeOfDayToJSON),
export const durationSchema: PropSchema = {
serializer: (duration: s.Duration | null) =>
duration != null ? duration.toSeconds() : null,
deserializer: (json: any) =>
typeof json === "number" ? s.Duration.fromSeconds(json) : null,
};
}
export function scheduleFromJSON(schedule: s.Schedule, json: IScheduleJSON) {
assign(schedule, pick(json, scheduleProps));
schedule.times.length = json.times.length;
schedule.times.forEach((timeOfDay, i) =>
timeOfDayFromJSON(timeOfDay, json.times[i]));
}
export interface IProgramItemJSON {
section: number;
duration: number;
}
const programItemProps = ["section"];
export function programItemToJSON(programItem: s.ProgramItem): IProgramItemJSON {
return {
...pick(programItem, programItemProps),
duration: programItem.duration.toSeconds(),
export const dateSchema: PropSchema = {
serializer: (jsDate: Date | null) => jsDate != null ?
jsDate.toISOString() : null,
deserializer: (json: any) => typeof json === "string" ?
new Date(json) : null,
};
}
export function programItemFromJSON(json: IProgramItemJSON): s.ProgramItem {
const duration = s.Duration.fromSeconds(json.duration);
return new s.ProgramItem(json.section, duration);
}
export interface IProgramJSON {
name: string;
enabled: boolean;
sequence: IProgramItemJSON[];
schedule: IScheduleJSON;
running: boolean;
}
const programProps = ["name", "enabled", "running"];
export function programToJSON(program: s.Program): IProgramJSON {
return {
...pick(program, programProps),
sequence: program.sequence.map(programItemToJSON),
schedule: scheduleToJSON(program.schedule),
export const dateOfYearSchema: ModelSchema<s.DateOfYear> = {
factory: () => new s.DateOfYear(),
props: {
year: primitive(),
month: primitive(), // this only works if it is represented as a # from 0-12
day: primitive(),
},
};
}
export function programFromJSON(program: s.Program, json: IProgramJSON) {
assign(program, pick(json, programProps));
program.sequence = json.sequence.map((programItemJson) =>
programItemFromJSON(programItemJson));
scheduleFromJSON(program.schedule, json.schedule);
}
export interface ISectionRunJSON {
id: number;
section: number;
duration: number;
startTime?: number;
pauseTime?: number;
}
const sectionRunProps = ["id", "section", "duration", "startTime", "pauseTime"];
export function sectionRunToJSON(sectionRun: s.SectionRun): ISectionRunJSON {
return {
...pick(sectionRun, sectionRunProps),
duration: sectionRun.duration.toSeconds(),
export const timeOfDaySchema: ModelSchema<s.TimeOfDay> = {
factory: () => new s.TimeOfDay(),
props: {
hour: primitive(),
minute: primitive(),
second: primitive(),
millisecond: primitive(),
},
};
}
export function sectionRunFromJSON(sectionRun: s.SectionRun, json: ISectionRunJSON) {
assign(sectionRun, pick(json, sectionRunProps));
sectionRun.duration = s.Duration.fromSeconds(json.duration);
}
export const sectionSchema: ModelSchema<s.Section> = {
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionConstructor(
c.parentContext.target, c.json.id),
props: {
name: primitive(),
state: primitive(),
},
};
interface ISectionRunnerJSON {
queue: ISectionRunJSON[];
current: ISectionRunJSON | null;
paused: boolean;
}
const sectionRunnerProps = ["paused"];
export const sectionRunSchema: ModelSchema<s.SectionRun> = {
factory: (c) => new s.SectionRun(c.json.id),
props: {
id: primitive(),
section: primitive(),
duration: durationSchema,
startTime: dateSchema,
endTime: dateSchema,
},
};
export function sectionRunnerToJSON(sectionRunner: s.SectionRunner): ISectionRunnerJSON {
return {
...pick(sectionRunner, sectionRunnerProps),
queue: sectionRunner.queue.map(sectionRunToJSON),
current: sectionRunner.current ? sectionRunToJSON(sectionRunner.current) : null,
export const sectionRunnerSchema: ModelSchema<s.SectionRunner> = {
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionRunnerConstructor(
c.parentContext.target),
props: {
queue: list(object(sectionRunSchema)),
current: object(sectionRunSchema),
paused: primitive(),
},
};
}
export function sectionRunnerFromJSON(sectionRunner: s.SectionRunner, json: ISectionRunnerJSON) {
assign(sectionRunner, pick(json, sectionRunnerProps));
sectionRunner.queue.length = json.queue.length;
sectionRunner.queue.forEach((sectionRun, i) =>
sectionRunFromJSON(sectionRun, json.queue[i]));
if (json.current == null) {
sectionRunner.current = null;
} else {
if (sectionRunner.current == null) {
sectionRunner.current = new s.SectionRun();
}
sectionRunFromJSON(sectionRunner.current, json.current);
}
}
export const scheduleSchema: ModelSchema<s.Schedule> = {
factory: () => new s.Schedule(),
props: {
times: list(object(timeOfDaySchema)),
weekdays: list(primitive()),
from: object(dateOfYearSchema),
to: object(dateOfYearSchema),
},
};
interface ISprinklersDeviceJSON {
connected: boolean;
sections: ISectionJSON[];
sectionRunner: ISectionRunnerJSON;
programs: IProgramJSON[];
}
const sprinklersDeviceProps = ["connected"];
export const programItemSchema: ModelSchema<s.ProgramItem> = {
factory: () => new s.ProgramItem(),
props: {
section: primitive(),
duration: durationSchema,
},
};
export function sprinklersDeviceToJSON(sprinklersDevice: s.SprinklersDevice): ISprinklersDeviceJSON {
return {
...pick(sprinklersDevice, sprinklersDeviceProps),
sections: sprinklersDevice.sections.map(sectionToJSON),
sectionRunner: sectionRunnerToJSON(sprinklersDevice.sectionRunner),
programs: sprinklersDevice.programs.map(programToJSON),
export const programSchema: ModelSchema<s.Program> = {
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).programConstructor(
c.parentContext.target, c.json.id),
props: {
name: primitive(),
enabled: primitive(),
schedule: object(scheduleSchema),
sequence: list(object(programItemSchema)),
running: primitive(),
},
};
}
export function sprinklersDeviceFromJSON(sprinklersDevice: s.SprinklersDevice, json: ISprinklersDeviceJSON) {
assign(sprinklersDevice, pick(json, sprinklersDeviceProps));
sprinklersDevice.sections.length = json.sections.length;
sprinklersDevice.sections.forEach((section, i) =>
sectionFromJSON(section, json.sections[i]));
sectionRunnerFromJSON(sprinklersDevice.sectionRunner, json.sectionRunner);
sprinklersDevice.programs.length = json.programs.length;
sprinklersDevice.programs.forEach((program, i) =>
programFromJSON(program, json.programs[i]));
}
export const sprinklersDeviceSchema = createSimpleSchema({
connected: primitive(),
sections: list(object(sectionSchema)),
sectionRunner: object(sectionRunnerSchema),
programs: list(object(programSchema)),
});

65
common/sprinklers/json/list.ts

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
import { primitive, PropSchema } from "serializr";
function invariant(cond: boolean, message?: string) {
if (!cond) {
throw new Error("[serializr] " + (message || "Illegal State"));
}
}
function isPropSchema(thing: any) {
return thing && thing.serializer && thing.deserializer;
}
function isAliasedPropSchema(propSchema: any) {
return typeof propSchema === "object" && !!propSchema.jsonname;
}
function parallel(ar: any[], processor: (item: any, done: any) => void, cb: any) {
if (ar.length === 0) {
return void cb(null, []);
}
let left = ar.length;
const resultArray: any[] = [];
let failed = false;
const processorCb = (idx: number, err: any, result: any) => {
if (err) {
if (!failed) {
failed = true;
cb(err);
}
} else if (!failed) {
resultArray[idx] = result;
if (--left === 0) {
cb(null, resultArray);
}
}
};
ar.forEach((value, idx) => processor(value, processorCb.bind(null, idx)));
}
export default function list(propSchema: PropSchema): PropSchema {
propSchema = propSchema || primitive();
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first");
return {
serializer(ar) {
invariant(ar && typeof ar.length === "number" && typeof ar.map === "function",
"expected array (like) object");
return ar.map(propSchema.serializer);
},
deserializer(jsonArray, done, context) {
if (jsonArray === null) { // sometimes go will return null in place of empty array
return void done(null, []);
}
if (!Array.isArray(jsonArray)) {
return void done("[serializr] expected JSON array", null);
}
parallel(
jsonArray,
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
propSchema.deserializer(item, itemDone, context, undefined),
done,
);
},
};
}

142
common/sprinklers/mqtt/index.ts

@ -1,23 +1,15 @@ @@ -1,23 +1,15 @@
import { cloneDeep } from "lodash";
import * as mqtt from "mqtt";
import { deserialize, update } from "serializr";
import logger from "@common/logger";
import {
Duration,
ISprinklersApi,
Program,
ProgramItem,
Schedule,
Section,
SectionRun,
SectionRunner,
SprinklersDevice,
TimeOfDay,
} from "@common/sprinklers";
import * as s from "@common/sprinklers";
import * as schema from "@common/sprinklers/json";
import { checkedIndexOf } from "@common/utils";
const log = logger.child({ source: "mqtt" });
export class MqttApiClient implements ISprinklersApi {
export class MqttApiClient implements s.ISprinklersApi {
readonly mqttUri: string;
client: mqtt.Client;
connected: boolean;
@ -54,7 +46,7 @@ export class MqttApiClient implements ISprinklersApi { @@ -54,7 +46,7 @@ export class MqttApiClient implements ISprinklersApi {
});
}
getDevice(prefix: string): SprinklersDevice {
getDevice(prefix: string): s.SprinklersDevice {
if (/\//.test(prefix)) {
throw new Error("Prefix cannot contain a /");
}
@ -84,7 +76,8 @@ export class MqttApiClient implements ISprinklersApi { @@ -84,7 +76,8 @@ export class MqttApiClient implements ISprinklersApi {
}
}
private processMessage(topic: string, payload: Buffer, packet: mqtt.Packet) {
private processMessage(topic: string, payloadBuf: Buffer, packet: mqtt.Packet) {
const payload = payloadBuf.toString("utf8");
log.trace({ topic, payload }, "message arrived: ");
const topicIdx = topic.indexOf("/"); // find the first /
const prefix = topic.substr(0, topicIdx); // assume prefix does not contain a /
@ -108,7 +101,7 @@ const subscriptions = [ @@ -108,7 +101,7 @@ const subscriptions = [
"/section_runner",
];
class MqttSprinklersDevice extends SprinklersDevice {
class MqttSprinklersDevice extends s.SprinklersDevice {
readonly apiClient: MqttApiClient;
readonly prefix: string;
@ -127,6 +120,10 @@ class MqttSprinklersDevice extends SprinklersDevice { @@ -127,6 +120,10 @@ class MqttSprinklersDevice extends SprinklersDevice {
return this.prefix;
}
get sectionConstructor() { return MqttSection; }
get sectionRunnerConstructor() { return MqttSectionRunner; }
get programConstructor() { return MqttProgram; }
doSubscribe() {
const topics = subscriptions.map((filter) => this.prefix + filter);
this.apiClient.client.subscribe(topics, { qos: 1 });
@ -142,8 +139,7 @@ class MqttSprinklersDevice extends SprinklersDevice { @@ -142,8 +139,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
* @param topic The topic, with prefix removed
* @param payload The payload buffer
*/
onMessage(topic: string, payloadBuf: Buffer) {
const payload = payloadBuf.toString("utf8");
onMessage(topic: string, payload: string) {
if (topic === "connected") {
this.connected = (payload === "true");
log.trace(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`);
@ -207,7 +203,7 @@ class MqttSprinklersDevice extends SprinklersDevice { @@ -207,7 +203,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
log.warn({ topic }, "MqttSprinklersDevice recieved invalid message");
}
runSection(section: Section | number, duration: Duration) {
runSection(section: s.Section | number, duration: s.Duration) {
const sectionNum = checkedIndexOf(section, this.sections, "Section");
const payload: IRunSectionJSON = {
duration: duration.toSeconds(),
@ -215,9 +211,9 @@ class MqttSprinklersDevice extends SprinklersDevice { @@ -215,9 +211,9 @@ class MqttSprinklersDevice extends SprinklersDevice {
return this.makeRequest(`sections/${sectionNum}/run`, payload);
}
runProgram(program: Program | number) {
runProgram(program: s.Program | number) {
const programNum = checkedIndexOf(program, this.programs, "Program");
return this.makeRequest(`programs/${programNum}/run`, {});
return this.makeRequest(`programs/${programNum}/run`);
}
cancelSectionRunById(id: number) {
@ -273,120 +269,40 @@ interface IRunSectionJSON { @@ -273,120 +269,40 @@ interface IRunSectionJSON {
duration: number;
}
class MqttSection extends Section {
class MqttSection extends s.Section {
onMessage(topic: string, payload: string) {
if (topic === "state") {
this.state = (payload === "true");
} else if (topic == null) {
const json = JSON.parse(payload) as ISectionJSON;
this.name = json.name;
}
}
}
interface ITimeOfDayJSON {
hour: number;
minute: number;
second: number;
millisecond: number;
}
function timeOfDayFromJSON(json: ITimeOfDayJSON): TimeOfDay {
return new TimeOfDay(json.hour, json.minute, json.second, json.millisecond);
this.updateFromJSON(JSON.parse(payload));
}
interface IScheduleJSON {
times: ITimeOfDayJSON[];
weekdays: number[];
from?: string;
to?: string;
}
function scheduleFromJSON(json: IScheduleJSON): Schedule {
const sched = new Schedule();
sched.times = json.times.map(timeOfDayFromJSON);
sched.weekdays = json.weekdays;
sched.from = json.from == null ? null : new Date(json.from);
sched.to = json.to == null ? null : new Date(json.to);
return sched;
}
interface IProgramItemJSON {
section: number;
duration: number;
updateFromJSON(json: any) {
update(schema.sectionSchema, this, json);
}
interface IProgramJSON {
name: string;
enabled: boolean;
sequence: IProgramItemJSON[];
sched: IScheduleJSON;
}
class MqttProgram extends Program {
class MqttProgram extends s.Program {
onMessage(topic: string, payload: string) {
if (topic === "running") {
this.running = (payload === "true");
} else if (topic == null) {
const json = JSON.parse(payload) as Partial<IProgramJSON>;
this.updateFromJSON(json);
}
}
updateFromJSON(json: Partial<IProgramJSON>) {
if (json.name != null) {
this.name = json.name;
}
if (json.enabled != null) {
this.enabled = json.enabled;
}
if (json.sequence != null) {
this.sequence = json.sequence.map((item) => (new ProgramItem(
item.section,
Duration.fromSeconds(item.duration),
)));
}
if (json.sched != null) {
this.schedule = scheduleFromJSON(json.sched);
}
this.updateFromJSON(JSON.parse(payload));
}
}
export interface ISectionRunJSON {
id: number;
section: number;
duration: number;
startTime?: number;
pauseTime?: number;
updateFromJSON(json: any) {
update(schema.programSchema, this, json);
}
function sectionRunFromJSON(json: ISectionRunJSON) {
const run = new SectionRun(json.id);
run.section = json.section;
run.duration = Duration.fromSeconds(json.duration);
run.startTime = json.startTime == null ? null : new Date(json.startTime);
run.pauseTime = json.pauseTime == null ? null : new Date(json.pauseTime);
return run;
}
interface ISectionRunnerJSON {
queue: ISectionRunJSON[];
current: ISectionRunJSON | null;
paused: boolean;
}
class MqttSectionRunner extends SectionRunner {
class MqttSectionRunner extends s.SectionRunner {
onMessage(payload: string) {
const json = JSON.parse(payload) as ISectionRunnerJSON;
this.updateFromJSON(json);
this.updateFromJSON(JSON.parse(payload));
}
updateFromJSON(json: ISectionRunnerJSON) {
this.queue.length = 0;
if (json.queue && json.queue.length) { // null means empty queue
this.queue.push.apply(this.queue, json.queue.map(sectionRunFromJSON));
}
this.current = json.current == null ? null : sectionRunFromJSON(json.current);
this.paused = json.paused;
updateFromJSON(json: any) {
update(schema.sectionRunnerSchema, this, json);
}
}

37
common/sprinklers/schedule.ts

@ -6,7 +6,7 @@ export class TimeOfDay { @@ -6,7 +6,7 @@ export class TimeOfDay {
readonly second: number;
readonly millisecond: number;
constructor(hour: number, minute: number = 0, second: number = 0, millisecond: number = 0) {
constructor(hour: number = 0, minute: number = 0, second: number = 0, millisecond: number = 0) {
this.hour = hour;
this.minute = minute;
this.second = second;
@ -22,9 +22,40 @@ export enum Weekday { @@ -22,9 +22,40 @@ export enum Weekday {
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday,
}
export enum Month {
January = 1,
February = 2,
March = 3,
April = 4,
May = 5,
June = 6,
July = 7,
August = 8,
September = 9,
October = 10,
November = 11,
December = 12,
}
export class DateOfYear {
readonly day: number;
readonly month: Month;
readonly year: number;
constructor(day: number = 0, month: Month = Month.January, year: number = 0) {
this.day = day;
this.month = month;
this.year = year;
}
toString() {
return `${Month[this.month]} ${this.day}, ${this.year}`;
}
}
export class Schedule {
@observable times: TimeOfDay[] = [];
@observable weekdays: Weekday[] = [];
@observable from: Date | null = null;
@observable to: Date | null = null;
@observable from: DateOfYear | null = null;
@observable to: DateOfYear | null = null;
}

3
package.json

@ -31,6 +31,7 @@ @@ -31,6 +31,7 @@
},
"homepage": "https://github.com/amikhalev/sprinklers3#readme",
"dependencies": {
"@types/async": "^2.0.43",
"@types/chalk": "^0.4.31",
"@types/classnames": "^2.2.0",
"@types/core-js": "^0.9.43",
@ -45,6 +46,7 @@ @@ -45,6 +46,7 @@
"@types/react-dom": "^15.5.0",
"@types/react-fontawesome": "^1.5.0",
"@types/react-hot-loader": "^3.0.4",
"async": "^2.5.0",
"autoprefixer": "^7.1.4",
"case-sensitive-paths-webpack-plugin": "^2.1.1",
"classnames": "^2.2.5",
@ -70,6 +72,7 @@ @@ -70,6 +72,7 @@
"react-fontawesome": "^1.6.1",
"semantic-ui-css": "^2.2.10",
"semantic-ui-react": "^0.73.0",
"serializr": "^1.1.13",
"tslint-loader": "^3.5.3",
"uglifyjs-webpack-plugin": "^0.4.6",
"url-loader": "^0.5.9"

3
server/configureLogger.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import log, { setLogger } from "@common/logger";
setLogger(log.child({
name: "sprinklers3/server",
level: "trace",
level: "debug",
}));

8
server/index.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
/* tslint:disable:ordered-imports */
import "./configureAlias";
import "env";
import "./configureLogger";
import log from "@common/logger";
import * as mqtt from "@common/sprinklers/mqtt";
@ -11,12 +12,13 @@ const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883"); @@ -11,12 +12,13 @@ const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883");
mqttClient.start();
import * as sjson from "@common/sprinklers/json";
import { sprinklersDeviceSchema } from "@common/sprinklers/json";
import { autorun } from "mobx";
import { serialize } from "serializr";
const device = mqttClient.getDevice("grinklers");
app.get("/api/grinklers", (req, res) => {
const j = sjson.sprinklersDeviceToJSON(device);
const j = serialize(sprinklersDeviceSchema, device);
log.trace(j);
res.send(j);
});

10
yarn.lock

@ -2,6 +2,10 @@ @@ -2,6 +2,10 @@
# yarn lockfile v1
"@types/async@^2.0.43":
version "2.0.43"
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.43.tgz#0d6fce7e11a582b4251a4bf5439399428c79e387"
"@types/chalk@^0.4.31":
version "0.4.31"
resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
@ -334,7 +338,7 @@ async@^1.5.2: @@ -334,7 +338,7 @@ async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.1.2, async@^2.4.1:
async@^2.1.2, async@^2.4.1, async@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
dependencies:
@ -4706,6 +4710,10 @@ send@0.15.4: @@ -4706,6 +4710,10 @@ send@0.15.4:
range-parser "~1.2.0"
statuses "~1.3.1"
serializr@^1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/serializr/-/serializr-1.1.13.tgz#21b20f220fcf94caaecd86eb09e438cde6532b25"
serve-index@^1.7.2:
version "1.9.0"
resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7"

Loading…
Cancel
Save