Browse Source

multi-image support, and UIkit added for UI/UX

- UIkit CSS framework added
- First bits of UIkit theme built (far from done)
- multiple image support works (backed by Redis and local drive storage
for images)
develop
Rob Colbert 2 years ago
parent
commit
b4acb5c5d7
  1. 5
      app/views/canvas/components/multiplayer-canvas.pug
  2. 31
      app/views/canvas/editor.pug
  3. 37
      app/views/canvas/view.pug
  4. 17
      app/views/error.pug
  5. 26
      app/views/home.pug
  6. 12
      app/views/layouts/base.pug
  7. 3
      app/views/layouts/components/navbar.pug
  8. 5
      app/views/layouts/components/page-footer.pug
  9. 11
      app/views/layouts/main.pug
  10. 85
      canvas-io-server.js
  11. 84
      client/js/canvas-app.js
  12. 15
      client/less/lib/canvas-metabar.less
  13. 11
      client/less/lib/color-palette.less
  14. 25
      client/less/lib/main.less
  15. 16
      client/less/lib/multiplayer-canvas.less
  16. 23
      client/less/lib/variables.less
  17. 4
      client/less/style.less
  18. 2
      dist/canvas-app.bundle.js
  19. 2
      dist/canvas-app.bundle.js.LICENSE.txt
  20. 11557
      dist/canvas-app.css
  21. 23
      lib/canvas-image.js
  22. 5
      package.json
  23. 108
      webapp.js
  24. 2
      webpack.config.js
  25. 171
      yarn.lock

5
app/views/canvas/components/multiplayer-canvas.pug

@ -2,6 +2,7 @@ mixin renderMultiplayerCanvas (image)
canvas(
id="multiplayer-canvas",
data-image-id= image.id,
width= image.image.width,
height= image.image.height,
data-view-mode= viewMode,
width= image.width,
height= image.height,
).multiplayer-canvas

31
app/views/canvas/editor.pug

@ -0,0 +1,31 @@
extends ../layouts/main
block content
- var formActionUrl = image ? `/${image.id}` : '/';
section.uk-section.uk-section-default
.uk-container.uk-width-xlarge
form(method="POST", action= formActionUrl, enctype="multipart/form-data").uk-form
.uk-card.uk-card-default.uk-card-small
.uk-card-header.uk-flex.uk-flex-center
h1.uk-card-title= image ? "Update Battle Canvas" : "Create Battle Canvas"
.uk-card-body
p You are creating a new Battle Canvas. It will receive it's own URL, and it will be listed on the home page. Anyone will be able to draw on the image.
.uk-margin
label(for="name").uk-form-label Canvas name
input(id="name", name="name", type="text", maxlength=80, placeholder="Enter image name", value= image ? image.name : undefined, required).uk-input
div(uk-grid)
.uk-width-1-2
.uk-margin
label(for="canvas-width").uk-form-label Canvas width
input(id="canvas-width", name="width", type="number", step="8", min="16", max="128", value= image ? image.width : 32, required).uk-input
.uk-width-1-2
.uk-margin
label(for="canvas-height").uk-form-label Canvas height
input(id="canvas-height", name="height", type="number", step="8", min="16", max="128", value= image ? image.height : 32, required).uk-input
.uk-margin
label(for="limit-pixels").uk-form-label Pixels per minute
input(id="limit-pixels", name="limit.pixels", type="number", step="5", min="10", max="60", value= image ? image.limitPixels : 10, required).uk-input
.uk-card-footer.uk-flex.uk-flex-center
button(type="submit").uk-button.uk-button-primary= image ? "Update canvas" : "Create canvas"

37
app/views/canvas/view.pug

@ -3,17 +3,36 @@ block content
include components/multiplayer-canvas.pug
+renderMultiplayerCanvas(image)
.uk-card.uk-card-secondary.uk-card-small
.uk-card-body
div(uk-grid).uk-flex-between.uk-text-small
.uk-width-auto
div Remaining: #[span#moves-remaining-display 10]
.uk-width-auto
div Consumed: #[span#moves-consumed-display 0]
.uk-width-auto
div Reset #[span#resets-at-display N/A]
.uk-width-auto
div Now #[span#current-time-display N/A]
.canvas-metabar
.canvas-meta-status Remaining: #[span#moves-remaining-display 10]
.canvas-meta-status Consumed: #[span#moves-consumed-display 0]
.canvas-meta-timer Reset #[span#resets-at-display N/A]
.canvas-meta-timer Now #[span#current-time-display N/A]
.color-palette
section.uk-section.uk-section-muted.uk-section-small
.uk-container
div(uk-grid).uk-grid-match.uk-grid-small
div(class="uk-width-1-1 uk-width-expand@s")
div
+renderMultiplayerCanvas(image)
div(class="uk-width-1-1 uk-width-auto@s")
.color-palette
block viewjs
script.
window.dtp = window.dtp || { };
window.dtp.imageId = !{JSON.stringify(imageId || 'test-image')};
window.dtp.connectToken = !{JSON.stringify(connectToken)};
if connectToken
script.
window.dtp.connectToken = !{JSON.stringify(connectToken)};
if image
script.
window.dtp.imageId = !{JSON.stringify(image ? image.id : 'test-image')};

17
app/views/error.pug

@ -0,0 +1,17 @@
extends layouts/main
block content
section.uk-section.uk-section-default.uk-section-small
.uk-container
.uk-margin
.uk-text-large= message
if error && error.status
div.uk-text-small.uk-text-muted status:#{error.status}
a(href="/").uk-button.uk-button-default.uk-border-rounded
span.uk-margin-small-right
i.fas.fa-home
span Home
if error.stack
pre= error.stack

26
app/views/home.pug

@ -1,9 +1,25 @@
extends layouts/main
block content
h1 Massively Multiplayer Online HTML5 Canvas
p Select an image to start contributing
section.uk-section.uk-section-default
.uk-container
each image in images
a(href=`/${image.id}`, style="display: block; width: 128px;")
img(src=`/${image.id}/image`).canvas-preview
.uk-margin.uk-text-center
h1.uk-margin-remove Pixel
div A demonstration of the Nice Game SDK
.uk-text-center
a(href="/create").uk-button.uk-button-primary.uk-button-large New Canvas
section.uk-section.uk-section-muted
.uk-container
.uk-margin.uk-text-center
h1.uk-margin-remove Live Images
.uk-text-bold Current ongoing battles for pixel domaination
div(uk-grid).uk-flex-center
each image in images
.uk-width-auto
a(href=`/${image.id}`, style="display: block; width: 128px;").uk-link-text
img(src=`/${image.id}/image`).canvas-preview
.uk-text-small= image.name || 'Unnamed'

12
app/views/layouts/base.pug

@ -7,18 +7,16 @@ html(lang="en")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Multiplayer Canvas
title DTP Multiplayer Canvas
link(rel="stylesheet", href="/dist/canvas-app.css")
link(rel="icon", type="image/x-icon", href="/dist/assets/icon/favicon.ico")
body(data-dtp-env= process.env.NODE_ENV)
body(data-dtp-env= process.env.NODE_ENV, data-view-mode= viewMode)
.container
block layout-content
block layout-content
if appModuleUrl
script(src='/socket.io/socket.io.js')
script(async, src=`${appModuleUrl}?v=${pkg.version}`, type="module")
script(src='/socket.io/socket.io.js')
script(async, src=`/dist/canvas-app.bundle.js?v=${pkg.version}`, type="module")
block viewjs

3
app/views/layouts/components/navbar.pug

@ -0,0 +1,3 @@
nav(uk-navbar).uk-navbar-container
.uk-navbar-center
a(href="/").uk-navbar-item.uk-logo Nice Pixel

5
app/views/layouts/components/page-footer.pug

@ -0,0 +1,5 @@
section.uk-section.uk-section-secondary.uk-section-small
.uk-container
div(uk-grid).uk-flex-center
.uk-width-auto
a(href="https://git.digitaltelepresence.com/rob/multiplayer-canvas", target="_blank") get the source

11
app/views/layouts/main.pug

@ -1,10 +1,9 @@
extends base
block layout-content
.navbar
.brand-link
a(href="https://nicecrew.digital", target="_blank")
img(src="/dist/assets/img/nicecrew-banner.png", alt="NiceCrew Banner").brand-header
.container
include components/navbar
block content
block content
block page-footer
include components/page-footer

85
canvas-io-server.js

@ -4,12 +4,11 @@
'use strict';
import fs from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
import fs from 'fs';
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
@ -32,7 +31,10 @@ export default class CanvasIoServer {
}
async start ( ) {
await this.createImage('test-image', 32, 32);
const images = await CanvasImage.getImages(this.app.redis);
for (const image of images) {
await this.loadImage(image);
}
const pubClient = this.app.redis.duplicate();
pubClient.on('error', this.onRedisError.bind(this));
@ -42,6 +44,10 @@ export default class CanvasIoServer {
const transports = ['websocket'/*, 'polling'*/];
const adapter = createAdapter(pubClient, subClient);
await subClient.subscribe('mpcvs:admin');
subClient.on('message', this.onPubSubMessage.bind(this));
this.io = new Server(this.app.httpServer, { adapter, transports });
this.io.on('connection', this.onSocketConnect.bind(this));
@ -57,18 +63,6 @@ export default class CanvasIoServer {
console.log('CanvasIoServer ready');
}
async createImage (imageId, width, height) {
console.log('creating image', { imageId, width, height });
this.images[imageId] = new CanvasImage(imageId);
try {
await this.images[imageId].load();
} catch (error) {
this.images[imageId].create(width, height);
this.images[imageId].setPixel({ x: 5, y: 5, r: 255, g: 0, b: 0 });
}
this.app.redis.hset(this.images[imageId].key, 'width', width, 'height', height);
}
async onRedisError (error) {
console.log('Redis error', error);
}
@ -85,7 +79,7 @@ export default class CanvasIoServer {
sid: socket.sid,
handshake: socket.handshake,
});
socket.disconnect(close);
socket.disconnect(true);
return;
}
@ -180,6 +174,7 @@ export default class CanvasIoServer {
console.log('onSetPixel', IMAGE_ID, message);
image.setPixel(message);
this.io.in(IMAGE_ID).emit('canvas-setpixel', {
x: message.x,
y: message.y,
@ -197,4 +192,62 @@ export default class CanvasIoServer {
}
}
}
async loadImage (imageMetadata) {
const { id, name, width, height } = imageMetadata;
console.log('loading image', imageMetadata);
const image = new CanvasImage(id, name || 'Test Image');
try {
await image.load();
} catch (error) {
console.log('image load failed', error);
console.log('creating new image', { id, name, width, height });
image.create(width, height);
await image.save();
}
if (image.width !== width || image.height !== height) {
throw new Error('Image file geometry mismatch');
}
this.images[image.id] = image;
}
/**
* Receives Redis pub/sub messages for this process. Determines if the message
* is for a channel we care about and, if so, calls the processor for that
* message type.
* @param {Object} message The message received
*/
async onPubSubMessage (channel, message) {
if (channel !== 'mpcvs:admin') {
return; // not one of our messages
}
message = JSON.parse(message);
switch (message.command) {
case 'image-create':
return this.onImageCreate(message);
default:
break;
}
throw new Error(`Invalid mpcsv:admin command: ${message.command}`);
}
async onImageCreate (message) {
const { id, name, width, height } = message.image;
console.log('creating image', { id, width, height });
const image = new CanvasImage(id, name);
image.create(width, height);
const IMAGE_KEY = image.key;
console.log('registering image in Redis', { id, key: IMAGE_KEY });
await this.app.redis.hset(IMAGE_KEY, 'name', image.name, 'width', image.width, 'height', image.height);
await image.save();
this.images[image.id] = image;
}
}

84
client/js/canvas-app.js

@ -6,9 +6,6 @@
window.dtp = window.dtp || { };
const IMAGE_W = 32;
const IMAGE_H = 32;
const DTP_COMPONENT_NAME = 'canvas-app';
import '../less/style.less';
@ -19,6 +16,8 @@ import Color from 'color';
import ColorConvert from 'color-convert';
import moment from 'moment';
import UIkit from 'uikit';
export default class CanvasApp {
constructor ( ) {
@ -49,7 +48,7 @@ export default class CanvasApp {
socket.on('canvas-setpixel', this.onCanvasSetPixel.bind(this));
socket.on('canvas-limiter-update', this.onCanvasLimiterUpdate.bind(this));
socket.emit('getimagedata');
socket.emit('getimagedata', { id: window.dtp.imageId });
}
async onCanvasError (message) {
@ -58,12 +57,18 @@ export default class CanvasApp {
async onCanvasImageData (message) {
this.log.info('onCanvasImageData', 'full image update received', { message });
this.imageData = new ImageData( // jshint ignore:line
this.image = {
width: message.width,
height: message.height,
};
this.image.data = new ImageData( // jshint ignore:line
new Uint8ClampedArray(message.data),
message.width,
message.height,
this.image.width,
this.image.height,
);
this.ctx.putImageData(this.imageData, 0, 0);
this.ctx.putImageData(this.image.data, 0, 0);
//TODO: Flush any queued updates received while waiting for this to happen
@ -75,7 +80,7 @@ export default class CanvasApp {
async onCanvasSetPixel (message) {
this.log.info('onCanvasSetPixel', 'pixel update received', message);
this.setPixel(message);
this.ctx.putImageData(this.imageData, 0, 0);
this.ctx.putImageData(this.image.data, 0, 0);
}
async onCanvasLimiterUpdate (message) {
@ -92,6 +97,11 @@ export default class CanvasApp {
initCanvas ( ) {
this.canvas = document.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');
this.inputTarget = {
active: false,
x: -1,
y: -1,
};
}
enableLocalInput ( ) {
@ -128,9 +138,9 @@ export default class CanvasApp {
stepSize = 360 / 8;
for (let h = 0; h < 360; h += stepSize) {
for (let v = 25; v <= 75; v += 25) {
for (let v = 25; v <= 100; v += 25) {
for (let s = 25; s <= 100; s += 25) {
colors.push('#' + ColorConvert.hsl.hex(h, s, v));
colors.push('#' + ColorConvert.hsv.hex(h, s, v));
}
}
}
@ -170,61 +180,72 @@ export default class CanvasApp {
updatePixels (message) {
for (const pixel of message.pixels) {
if ((pixel.x < 0) ||
(pixel.x > (IMAGE_W - 1)) ||
(pixel.x > (this.image.width - 1)) ||
(pixel.y < 0) ||
(pixel.y > (IMAGE_H - 1))) {
(pixel.y > (this.image.height - 1))) {
this.log.info('updatePixels', 'rejecting invalid pixel update', pixel);
return;
}
}
this.ctx.putImageData(this.imageData, 0, 0);
this.ctx.putImageData(this.image.data, 0, 0);
}
setPixel (pixel) {
let pixelIdx = ((pixel.y * IMAGE_W ) + pixel.x) * 4;
let pixelIdx = ((pixel.y * this.image.width) + pixel.x) * 4;
this.log.info('updatePixels', 'updating pixel', { pixelIdx, pixel });
this.imageData.data[pixelIdx++] = pixel.r;
this.imageData.data[pixelIdx++] = pixel.g;
this.imageData.data[pixelIdx++] = pixel.b;
this.imageData.data[pixelIdx++] = 255;
this.image.data.data[pixelIdx++] = pixel.r;
this.image.data.data[pixelIdx++] = pixel.g;
this.image.data.data[pixelIdx++] = pixel.b;
this.image.data.data[pixelIdx++] = 255;
}
async onCanvasMouseMove (event) {
const cellPixels = this.canvas.clientWidth / 32;
const cellPixels = this.canvas.clientWidth / this.image.width;
if (cellPixels === 0) {
return; // prevent a divide by zero
}
const x = Math.floor(event.offsetX / cellPixels);
const y = Math.floor(event.offsetY / cellPixels);
this.log.debug('onCanvasMouseMove', 'mouse move event', { event });
this.inputTarget.active = true;
this.inputTarget.x = Math.floor(event.offsetX / cellPixels);
this.inputTarget.y = Math.floor(event.offsetY / cellPixels);
this.ctx.putImageData(this.imageData, 0, 0);
this.ctx.putImageData(this.image.data, 0, 0);
const oldFillStyle = this.ctx.fillStyle;
this.ctx.fillStyle = this.currentColor.hex();
this.ctx.fillRect(x, y, 1, 1);
this.ctx.fillRect(this.inputTarget.x, this.inputTarget.y, 1, 1);
this.ctx.fillStyle = oldFillStyle;
}
async onCanvasMouseLeave ( ) {
this.ctx.putImageData(this.imageData, 0, 0);
this.inputTarget.active = false;
this.inputTarget.x = -1;
this.inputTarget.y = -1;
this.ctx.putImageData(this.image.data, 0, 0);
}
async onCanvasClick (event) {
const cellPixels = this.canvas.clientWidth / 32;
this.log.info('onCanvasClick', 'generating setpixel request', { image: this.image });
const cellPixels = this.canvas.clientWidth / this.image.width;
if (cellPixels === 0) {
return; // prevent a divide by zero
}
const x = Math.floor(event.offsetX / cellPixels);
const y = Math.floor(event.offsetY / cellPixels);
this.inputTarget.active = true;
this.inputTarget.x = Math.floor(event.offsetX / cellPixels);
this.inputTarget.y = Math.floor(event.offsetY / cellPixels);
const pixel = {
x, y,
x: this.inputTarget.x,
y: this.inputTarget.y,
r: this.currentColor.red(),
g: this.currentColor.green(),
b: this.currentColor.blue(),
};
this.socket.socket.emit('setpixel', pixel);
}
@ -234,5 +255,8 @@ export default class CanvasApp {
}
window.addEventListener('load', async ( ) => {
window.dtp.app = new CanvasApp();
const canvas = document.querySelector('canvas');
if (canvas) {
window.dtp.app = new CanvasApp();
}
});

15
client/less/lib/canvas-metabar.less

@ -1,15 +0,0 @@
.canvas-metabar {
display: flex;
.canvas-meta-status {
flex-grow: 1;
padding: 8px;
background-color: var(--metabar-background-light);
}
.canvas-meta-timer {
flex-basis: auto;
padding: 8px;
background-color: var(--metabar-background-dark);
}
}

11
client/less/lib/color-palette.less

@ -5,10 +5,15 @@
}
.color-palette {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
@media screen and (min-width: 960px) {
max-width: 40px * 8;
}
@media screen and (min-width: 1200px) {
max-width: 40px * 16;
}
button.color-select-btn {
display: inline-block;

25
client/less/lib/main.less

@ -5,26 +5,19 @@
*/
html, body {
margin: 0;
padding: 0;
font-size: 14px;
background-color: var(--background-color);
color: var(--text-color);
}
body {
display: block;
body[data-view-mode="obs"] {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
width: 100%;
height: 100%;
overflow: auto;
}
.margin {
display: block;
margin: var(--default-margin) 0;
}
right: 100%;
margin: 0;
padding: 0;
.margin-right {
margin-right: var(--default-margin);
display: flex;
flex-direction: column;
justify-content: center;
}

16
client/less/lib/multiplayer-canvas.less

@ -6,13 +6,25 @@
canvas.multiplayer-canvas,
img.canvas-preview {
// object-fit: contain;
// object-position: center;
display: block;
box-sizing: border-box;
width: 100%;
max-width: 640px;
height: auto;
border: solid 1px white;
border: solid 1px var(--outline-color);
image-rendering: pixelated;
background-color: #333333;
&[data-view-mode="obs"] {
max-width: initial;
}
}
canvas.multiplayer-canvas {
cursor: crosshair;
}

23
client/less/lib/variables.less

@ -4,13 +4,26 @@
* License: Apache-2.0
*/
/*
* UIkit variable overrides control the vast majority of "color" in the app.
* These changes were done to implement a "dark mode" for UIkit.
*/
@global-background: #2a2a2a;
@global-color: #e8e8e8;
@global-muted-background: #4a4a4a;
@global-muted-color: #e8e8e8;
@global-emphasis-color: #f0f0f0;
@logo-color: #8a8a8a;
* {
--background-color : #2a2a2a;
--text-color : #e8e8e8;
--brand-color : rgb(4, 130, 216);
--background-color : @global-background;
--outline-color : @global-color;
--metabar-background-light : #4a4a4a;
--metabar-background-dark : #2a2a2a;
--text-color : @global-color;
--brand-color : rgb(4, 130, 216);
--default-margin : 30px;
}

4
client/less/style.less

@ -4,12 +4,12 @@
* License: Apache-2.0
*/
@import 'node_modules/uikit/src/less/uikit.less';
@import 'lib/variables.less';
@import 'lib/main.less';
@import 'lib/brand-link.less';
@import 'lib/canvas-metabar.less';
@import 'lib/color-palette.less';
@import 'lib/container.less';
@import 'lib/multiplayer-canvas.less';
@import 'lib/navbar.less';

2
dist/canvas-app.bundle.js

File diff suppressed because one or more lines are too long

2
dist/canvas-app.bundle.js.LICENSE.txt

@ -1,3 +1,5 @@
/*! UIkit 3.13.7 | https://www.getuikit.com | (c) 2014 - 2022 YOOtheme | MIT License */
//! moment.js
//! moment.js locale configuration

11557
dist/canvas-app.css

File diff suppressed because it is too large

23
lib/canvas-image.js

@ -14,8 +14,25 @@ import Jimp from 'jimp';
export default class CanvasImage {
constructor (id) {
static async getImages (redis) {
const images = [ ];
for (const key of await redis.keys('mpcvs:images:*')) {
const image = await redis.hgetall(key);
image.width = parseInt(image.width, 10);
image.height = parseInt(image.height, 10);
const tokens = key.split(':');
const id = tokens[tokens.length - 1];
images.push({ id, ...image });
}
return images;
}
constructor (id, name) {
this.id = id;
this.name = name;
this.idPrefix = this.id.slice(0, 4);
this.imagePath = path.resolve(__dirname, '..', 'images', this.idPrefix);
this._dirty = false;
@ -27,6 +44,10 @@ export default class CanvasImage {
get filename ( ) { return path.join(this.imagePath, `${this.id}.png`); }
get width ( ) { return this.image.bitmap.width; }
get height ( ) { return this.image.bitmap.height; }
create (width, height) {
this.image = new Jimp(width, height, 0xffffffff);
this._dirty = false;

5
package.json

@ -17,6 +17,7 @@
"@socket.io/redis-adapter": "^7.1.0",
"color": "^4.2.1",
"color-convert": "^2.0.1",
"compression": "^1.7.4",
"connect-redis": "^6.1.3",
"cron": "^1.8.2",
"dotenv": "^16.0.0",
@ -27,11 +28,15 @@
"ioredis": "^5.0.3",
"jimp": "^0.16.1",
"marked": "^4.0.12",
"method-override": "^3.0.0",
"moment": "^2.29.2",
"multer": "^1.4.4",
"numeral": "^2.0.6",
"pug": "^3.0.2",
"rate-limiter-flexible": "^2.3.6",
"socket.io-emitter": "^3.2.0",
"striptags": "^3.2.0",
"uikit": "^3.13.7",
"uuid": "^8.3.2",
"winston": "^3.7.1"
},

108
webapp.js

@ -16,6 +16,7 @@ import { createRequire } from 'module';
const require = createRequire(import.meta.url); // jshint ignore:line
import express from 'express';
import multer from 'multer';
import session from 'express-session';
import ConnectRedis from 'connect-redis';
@ -29,7 +30,11 @@ import ioEmitter from 'socket.io-emitter';
import CanvasIoServer from './canvas-io-server.js';
import CanvasImage from './lib/canvas-image.js';
import compress from 'compression';
import methodOverride from 'method-override';
import { marked } from 'marked';
import striptags from 'striptags';
import winston from 'winston';
import expressWinston from 'express-winston';
@ -47,8 +52,10 @@ class MultiplayerCanvasApp {
constructor ( ) {
this.app = express();
this.app.set('view engine', 'pug');
this.app.set('views', path.join(__dirname, 'app', 'views'));
this.app.set('x-powered-by', false);
this.app.locals.config = APP_CONFIG;
this.app.locals.pkg = APP_CONFIG.pkg;
@ -114,6 +121,7 @@ class MultiplayerCanvasApp {
await this.createIoServer();
await this.createExpress();
await this.registerRoutes();
await this.registerDefaultErrorHandler();
/*
* Start the HTTP server.
@ -173,6 +181,15 @@ class MultiplayerCanvasApp {
this.app.use(this.webpackDevMiddleware);
}
/*
* ExpressJS middleware
*/
this.app.use(express.json({ }));
this.app.use(express.urlencoded({ extended: true }));
this.app.use(compress());
this.app.use(methodOverride());
/*
* ExpressJS session support
*/
@ -202,21 +219,48 @@ class MultiplayerCanvasApp {
this.app.use(session(this.sessionConfig));
}
/**
* Register the ExpressJS app request routes.
*/
async registerRoutes ( ) {
const upload = multer({ dest: path.join('/tmp', 'dtp', 'mpcvs', 'canvas') });
this.app.param('imageId', this.populateImageId.bind(this));
this.app.post('/', upload.none(), this.postCreateCanvas.bind(this));
this.app.get('/create', this.getCanvasEditor.bind(this));
this.app.get('/:imageId/image', this.getCanvasImage.bind(this));
this.app.get('/:imageId/obs', this.getObsWidget.bind(this));
this.app.get('/:imageId/edit', this.getCanvasEditor.bind(this));
this.app.get('/:imageId', this.getCanvasView.bind(this));
this.app.get('/', this.getHomeView.bind(this));
}
async registerDefaultErrorHandler ( ) {
/*
* Default error handler
*/
console.log('registering default error handler');
this.app.use((error, req, res, next) => { // jshint ignore:line
res.locals.errorCode = error.statusCode || error.status || 500;
console.log('App error', { url: req.url, error });
res.status(res.locals.errorCode).render('error', {
message: error.message,
error,
errorCode: res.locals.errorCode,
});
});
}
async populateImageId (req, res, next, imageId) {
try {
const image = new CanvasImage(imageId);
const imageMetadata = await this.redis.hgetall(image.key);
res.locals.image = {
id: imageId,
image: await this.redis.hgetall(image.key),
...imageMetadata,
};
if (!res.locals.image) {
throw new Error('Image not found');
@ -227,9 +271,57 @@ class MultiplayerCanvasApp {
}
}
async getObsWidget (req, res, next) {
async postCreateCanvas (req, res, next) {
try {
const image = { id: uuidv4() };
if (!req.body.name || !req.body.name.length) {
return next(new Error('Must include a name for your battle canvas'));
}
image.name = striptags(req.body.name.trim());
if (!image.name.length) {
return next(new Error('Must include a name for your battle canvas'));
}
if (!req.body.width) {
return next(new Error('Invalid canvas width'));
}
image.width = parseInt(req.body.width, 10);
if (image.width < 32 || image.width > 128) {
return next(new Error('Invalid canvas width'));
}
if (!req.body.height) {
return next(new Error('Invalid canvas height'));
}
image.height = parseInt(req.body.height, 10);
if (image.height < 32 || image.height > 128) {
return next(new Error('Invalid canvas height'));
}
if (!req.body['limit.pixels']) {
return next(new Error('Invalid pixel limit'));
}
image.limit = {
pixels: parseInt(req.body['limit.pixels'], 10),
};
if (image.limit.pixels < 10 || image.limit.pixels > 60) {
return next(new Error('Invalid pixel limit'));
}
this.redis.publish('mpcvs:admin', JSON.stringify({
command: 'image-create',
image,
}));
} catch (error) {
return next(error);
}
}
async getObsWidget (req, res) {
this.createConnectToken(req, res);
res.locals.viewMode = 'obs';
res.locals.appModuleUrl = '/dist/canvas-app.bundle.js';
res.render('canvas/obs');
}
@ -255,19 +347,13 @@ class MultiplayerCanvasApp {
res.render('canvas/view');
}
async getImageCreateForm (req, res) {
async getCanvasEditor (req, res) {
res.render('canvas/editor');
}
async getHomeView (req, res, next) {
try {
res.locals.images = [ ];
for (const key of await this.redis.keys('mpcvs:images:*')) {
const image = await this.redis.hgetall(key);
const tokens = key.split(':');
const id = tokens[tokens.length - 1];
res.locals.images.push({ id, image });
}
res.locals.images = await CanvasImage.getImages(this.redis);
res.render('home');
} catch (error) {
return next(error);
@ -277,7 +363,7 @@ class MultiplayerCanvasApp {
async createConnectToken (req, res) {
res.locals.connectToken = uuidv4();
await this.redis.setex(`connect-token:${res.locals.connectToken}`, 30, JSON.stringify({
imageId: 'test-image',
imageId: res.locals.image.id,
ip: req.ip,
}));
return res.locals.connectToken;

2
webpack.config.js

@ -86,7 +86,7 @@ export default {
options: {
sourceMap: true,
lessOptions: {
strictMath: true,
math: "always",
},
},
},

171
yarn.lock

@ -560,7 +560,7 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
accepts@~1.3.4, accepts@~1.3.8:
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
@ -664,6 +664,11 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
append-field@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
[email protected]:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@ -878,6 +883,19 @@ buffer@^5.2.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
busboy@^0.2.11:
version "0.2.14"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
dependencies:
dicer "0.2.5"
readable-stream "1.1.x"
[email protected]:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
[email protected]:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@ -1055,11 +1073,41 @@ component-emitter@~1.3.0:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
dependencies:
mime-db ">= 1.43.0 < 2"
compression@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
dependencies:
accepts "~1.3.5"
bytes "3.0.0"
compressible "~2.0.16"
debug "2.6.9"
on-headers "~1.0.2"
safe-buffer "5.1.2"
vary "~1.1.2"
[email protected]:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concat-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
connect-history-api-fallback@^1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
@ -1189,6 +1237,13 @@ [email protected], debug@^2.2.0, debug@~2.6.4:
dependencies:
ms "2.0.0"
[email protected]:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
[email protected]:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
@ -1242,6 +1297,14 @@ dev-ip@^1.0.1:
resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0"
integrity sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=
[email protected]:
version "0.2.5"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
dependencies:
readable-stream "1.1.x"
streamsearch "0.1.2"
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
@ -1890,7 +1953,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, [email protected], inherits@^2.0.3, inherits@~2.0.1:
inherits@2, [email protected], inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -2031,6 +2094,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@ -2267,6 +2335,16 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
method-override@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2"
integrity sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==
dependencies:
debug "3.1.0"
methods "~1.1.2"
parseurl "~1.3.2"
vary "~1.1.2"
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@ -2280,7 +2358,7 @@ micromatch@^4.0.2:
braces "^3.0.2"
picomatch "^2.3.1"
[email protected]:
[email protected], "mime-db@>= 1.43.0 < 2":
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
@ -2345,7 +2423,7 @@ mitt@^1.1.3:
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d"
integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==
mkdirp@^0.5.1:
mkdirp@^0.5.1, mkdirp@^0.5.4:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
@ -2379,6 +2457,20 @@ [email protected], ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
multer@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
dependencies:
append-field "^1.0.0"
busboy "^0.2.11"
concat-stream "^1.5.2"
mkdirp "^0.5.4"
object-assign "^4.1.1"
on-finished "^2.3.0"
type-is "^1.6.4"
xtend "^4.0.0"
nanoid@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557"
@ -2445,6 +2537,13 @@ omggif@^1.0.10, omggif@^1.0.9:
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==
on-finished@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -2679,6 +2778,11 @@ postcss@^8.4.7:
picocolors "^1.0.0"
source-map-js "^1.0.2"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@ -2874,6 +2978,29 @@ [email protected]:
isarray "0.0.1"
string_decoder "~0.10.x"
[email protected]:
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^2.2.2:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
@ -2989,6 +3116,11 @@ rxjs@^5.5.6:
dependencies:
symbol-observable "1.0.1"
[email protected], safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
[email protected], safe-buffer@^5.1.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -3291,6 +3423,11 @@ stream-throttle@^0.1.3:
commander "^2.2.0"
limiter "^1.0.5"
[email protected]:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@ -3312,6 +3449,13 @@ string_decoder@~0.10.x:
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -3336,6 +3480,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
striptags@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
style-loader@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
@ -3451,7 +3600,7 @@ tslib@^2.3.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
type-is@~1.6.18:
type-is@^1.6.4, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@ -3459,6 +3608,11 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
[email protected]:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
@ -3476,6 +3630,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
uikit@^3.13.7:
version "3.13.7"
resolved "https://registry.yarnpkg.com/uikit/-/uikit-3.13.7.tgz#14804f113d3e7b3d8b3adf435610090a3bd8d0ac"
integrity sha512-yQrwf5TThdOgBzODL9TBT4tl5b608qS49FaXD1mKcpjVicCmnRMdtQ9zHcmmMJ2zuayDeg6h3S5+L1BDYOX8EA==
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@ -3500,7 +3659,7 @@ utif@^2.0.1:
dependencies:
pako "^1.0.5"
util-deprecate@^1.0.1, util-deprecate@^1.0.2:
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=

Loading…
Cancel
Save