large Venue update

master
rob 2 years ago
parent 5b99e5673c
commit d67ae3eaa5

@ -55,6 +55,7 @@ class AdminController extends SiteController {
router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/service-node', await this.loadChild(path.join(__dirname, 'admin', 'service-node')));
router.use('/site-link', await this.loadChild(path.join(__dirname, 'admin', 'site-link')));
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));
router.use('/venue', await this.loadChild(path.join(__dirname, 'admin', 'venue')));

@ -0,0 +1,109 @@
// admin/site-link.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class SiteLinkAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'venue';
return next();
});
router.param('linkId', this.populateLinkId.bind(this));
router.post('/:linkId', this.postUpdateLink.bind(this));
router.post('/', this.postCreateLink.bind(this));
router.get('/create', this.getLinkEditor.bind(this));
router.get('/:linkId', this.getLinkEditor.bind(this));
router.get('/', this.getHomeView.bind(this));
router.delete('/:linkId', this.deleteLink.bind(this));
return router;
}
async populateLinkId (req, res, next, linkId) {
const { siteLink: siteLinkService } = this.dtp.services;
try {
res.locals.link = await siteLinkService.getById(linkId);
return next();
} catch (error) {
this.log.error('failed to populate site link', { linkId, error });
return next(error);
}
}
async postUpdateLink (req, res, next) {
const { siteLink: siteLinkService } = this.dtp.services;
try {
await siteLinkService.update(res.locals.link, req.body);
res.redirect('/admin/site-link');
} catch (error) {
this.log.error('failed to update site link', { error });
return next(error);
}
}
async postCreateLink (req, res, next) {
const { siteLink: siteLinkService } = this.dtp.services;
try {
await siteLinkService.create(req.body);
res.redirect('/admin/site-link');
} catch (error) {
this.log.error('failed to create site link', { error });
return next(error);
}
}
async getLinkEditor (req, res) {
res.render('admin/site-link/editor');
}
async getHomeView (req, res, next) {
const { siteLink: siteLinkService } = this.dtp.services;
try {
res.locals.links = await siteLinkService.getLinks();
res.render('admin/site-link/index');
} catch (error) {
this.log.error('failed to present the Site Link Admin home view', { error });
return next(error);
}
}
async deleteLink (req, res) {
const { siteLink: siteLinkService } = this.dtp.services;
try {
const displayList = this.createDisplayList('delete-site-link');
await siteLinkService.remove(res.locals.link);
displayList.navigateTo('/admin/site-link');
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to delete site link', { error });
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {
name: 'adminSiteLink',
slug: 'admin-site-link',
create: async (dtp) => { return new SiteLinkAdminController(dtp); },
};

@ -23,12 +23,14 @@ class VenueAdminController extends SiteController {
});
router.param('channelId', this.populateChannelId.bind(this));
router.param('channelSlug', this.populateChannelSlug.bind(this));
router.post('/channel/:channelId', this.postUpdateChannel.bind(this));
router.post('/channel', this.postCreateChannel.bind(this));
router.get('/channel/create', this.getChannelEditor.bind(this));
router.get('/channel/:channelId', this.getChannelEditor.bind(this));
router.get('/channel/:channelSlug', this.getChannelEditor.bind(this));
router.get('/channel', this.getChannelHome.bind(this));
router.get('/', this.getHomeView.bind(this));
@ -40,7 +42,7 @@ class VenueAdminController extends SiteController {
async populateChannelId (req, res, next, channelId) {
const { venue: venueService } = this.dtp.services;
try {
res.locals.channel = await venueService.getChannelById(channelId);
res.locals.channel = await venueService.getChannelById(channelId, { withCredentials: true });
return next();
} catch (error) {
this.log.error('failed to populate Venue channel', { channelId, error });
@ -48,6 +50,17 @@ class VenueAdminController extends SiteController {
}
}
async populateChannelSlug (req, res, next, channelSlug) {
const { venue: venueService } = this.dtp.services;
try {
res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true });
return next();
} catch (error) {
this.log.error('failed to populate Venue channel by slug', { channelSlug, error });
return next(error);
}
}
async postUpdateChannel (req, res, next) {
const { venue: venueService } = this.dtp.services;
try {
@ -62,11 +75,11 @@ class VenueAdminController extends SiteController {
async postCreateChannel (req, res, next) {
const { user: userService, venue: venueService } = this.dtp.services;
try {
const owner = await userService.getUserAccount(req.body.ownerId);
const owner = await userService.lookup(req.body.owner);
if (!owner) {
throw new SiteError(400, 'Channel owner is empty or invalid');
}
await venueService.create(owner, req.body);
await venueService.createChannel(owner, req.body);
res.redirect('/admin/venue/channel');
} catch (error) {
this.log.error('failed to create Venue channel', { error });
@ -78,6 +91,18 @@ class VenueAdminController extends SiteController {
res.render('admin/venue/channel/editor');
}
async getChannelHome (req, res, next) {
const { venue: venueService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.channels = await venueService.getChannels(res.locals.pagination);
res.render('admin/venue/channel/index');
} catch (error) {
this.log.error('failed to present the Venue Admin home view', { error });
return next(error);
}
}
async getHomeView (req, res, next) {
const { venue: venueService } = this.dtp.services;
try {

@ -5,7 +5,7 @@
'use strict';
const express = require('express');
const { SiteController } = require('../../lib/site-lib');
const { SiteController, SiteError } = require('../../lib/site-lib');
class VenueController extends SiteController {
@ -25,18 +25,55 @@ class VenueController extends SiteController {
return next();
}, router);
router.param('channelSlug', this.populateChannelSlug.bind(this));
router.get(
'/',
'/:channelSlug',
limiterService.createMiddleware(limiterService.config.venue.getVenueEmbed),
this.getVenueEmbed.bind(this),
);
router.get(
'/',
this.getHome.bind(this),
);
return router;
}
async populateChannelSlug (req, res, next, channelSlug) {
const { venue: venueService } = this.dtp.services;
try {
res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true });
if (!res.locals.channel) {
throw new SiteError(404, `Channel "${channelSlug}" does not exists on ${this.dtp.config.site.name}. Please check the link/URL you used, and try again.`);
}
res.locals.channelCredentials = res.locals.channel.credentials;
delete res.locals.channel.credentials;
return next();
} catch (error) {
this.log.error('failed to populate Venue channel by slug', { channelSlug, error });
return next(error);
}
}
async getVenueEmbed (req, res) {
res.render('venue/embed');
}
async getHome (req, res, next) {
const { venue: venueService} = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.channels = await venueService.getChannels(res.locals.pagination);
res.render('venue/index');
} catch (error) {
this.log.error('failed to present the Venue home', { error });
return next(error);
}
}
}
module.exports = {

@ -0,0 +1,19 @@
// site-link.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const SiteLinkSchema = new Schema({
parent: { type: Schema.ObjectId, index: 1 },
label: { type: String, required: true },
url: { type: String, required: true },
iconUrl: { type: String, required: true },
target: { type: String },
});
module.exports = mongoose.model('SiteLink', SiteLinkSchema);

@ -18,7 +18,7 @@ const VenueChannelSchema = new Schema({
owner: { type: Schema.ObjectId, required: true, index: 1, refPath: 'ownerType' },
slug: { type: String, lowercase: true, unique: true, index: 1 },
lastStatus: { type: Schema.ObjectId, ref: 'VenueChannelStatus' },
credentials: { type: ChannelCredentialsSchema, select: false },
credentials: { type: ChannelCredentialsSchema, required: true, select: false },
});
module.exports = mongoose.model('VenueChannel', VenueChannelSchema);

@ -0,0 +1,89 @@
// announcement.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const SiteLink = mongoose.model('SiteLink');
const { URL } = require('url'); // jshint ignore:line
const favicon = require('favicon');
const { SiteService } = require('../../lib/site-lib');
class SiteLinkService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
middleware ( ) {
return async (req, res, next) => {
if (req.path.startsWith('/auth') || req.path.startsWith('/image') || req.path.startsWith('/manifest')) {
return next();
}
res.locals.links = await this.getLinks();
this.log.debug('site links', { count: res.locals.links.length, path: req.path });
return next();
};
}
async create (linkDefinition) {
const link = new SiteLink();
link.label = linkDefinition.label;
link.url = linkDefinition.url;
link.iconUrl = await this.getSiteFavicon(linkDefinition.url);
if (linkDefinition.targetBlank) {
link.target = '_blank';
}
await link.save();
return link.toObject();
}
async update (link, linkDefinition) {
const updateOp = { $set: { }, $unset: { } };
updateOp.$set.label = linkDefinition.label;
updateOp.$set.url = linkDefinition.url;
updateOp.$set.iconUrl = await this.getSiteFavicon(linkDefinition.url);
if (linkDefinition.targetBlank) {
updateOp.$set.target = '_blank';
} else {
updateOp.$unset.target = 1;
}
return SiteLink.findOneAndUpdate({ _id: link._id }, updateOp, { new: true });
}
async getLinks ( ) {
return SiteLink.find().sort({ label: 1 }).lean();
}
async getById (linkId) {
return SiteLink.findOne({ _id: linkId }).lean();
}
async remove (link) {
await SiteLink.deleteOne({ _id: link._id });
}
async getSiteFavicon (url) {
return new Promise((resolve, reject) => {
favicon(url, (err, iconUrl) => {
if (err) {
return reject(err);
}
return resolve(iconUrl);
});
});
}
}
module.exports = {
slug: 'site-link',
name: 'siteLink',
create: (dtp) => { return new SiteLinkService(dtp); },
};

@ -357,6 +357,61 @@ class UserService extends SiteService {
return user;
}
async lookup (account, options) {
options = Object.assign({ withEmail: false, withCredentials: false }, options);
this.log.debug('locating user record', { account });
const selects = [
'_id', 'created',
'username', 'username_lc',
'displayName', 'picture',
'flags', 'permissions',
];
if (options.withEmail) {
selects.push('email');
}
if (options.withCredentials) {
selects.push('passwordSalt');
selects.push('password');
}
const usernameRegex = new RegExp(`^${account}.*`);
/*
* First, check our local db
*/
let user = await User
.findOne({
$or: [
{ email: account.email },
{ username_lc: usernameRegex },
]
})
.select(selects.join(' '))
.lean();
if (user) {
// found, mark as 'User'
user.type = 'User';
} else {
// check for a matching CoreUser
user = await CoreUser
.findOne({
$or: [
{ username_lc: usernameRegex },
]
})
.select(selects.join(' '))
.lean();
if (user) {
// mark as CoreUser
user.type = 'CoreUser';
}
}
return user; // undefined means not found
}
registerPassportLocal ( ) {
const options = {
usernameField: 'username',

@ -13,6 +13,7 @@ const https = require('https');
const fetch = require('node-fetch'); // jshint ignore:line
const striptags = require('striptags');
const slug = require('slug');
const CACHE_DURATION = 60 * 5;
@ -26,21 +27,32 @@ class VenueService extends SiteService {
}
async start ( ) {
const { user: userService } = this.dtp.services;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
this.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
this.populateVenueChannel = [
{
path: 'owner',
select: userService.USER_SELECT,
},
{
path: 'lastStatus',
},
];
}
channelMiddleware ( ) {
return async (req, res, next) => {
try {
if (!res.locals.site || !res.locals.site.shingChannelSlug) {
return next();
}
res.locals.shingChannelFeed = await this.getChannelFeed(res.locals.site.shingChannelSlug, { allowCache: true });
res.locals.shingChannelStatus = await this.getChannelStatus(res.locals.site.shingChannelSlug, { allowCache: true });
this.log.debug('channel status', { status: res.locals.shingChannelStatus });
res.locals.venue = res.locals.venue || { };
res.locals.venue.channels = await VenueChannel
.find()
.populate(this.populateVenueChannel)
.lean();
return next();
} catch (error) {
this.log.error('failed to populate Soapbox channel feed', { error });
@ -51,25 +63,27 @@ class VenueService extends SiteService {
async createChannel (owner, channelDefinition) {
const channel = new VenueChannel();
channel.ownerType = owner.type;
channel.owner = owner._id;
if (!channelDefinition.slug || (channelDefinition.slug === '')) {
throw new SiteError(400, 'Must provide a channel URL slug');
}
channel.slug = striptags(channelDefinition.slug.trim());
channel.slug = this.getChannelSlug(channelDefinition.url);
if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
throw new SiteError(400, 'Must provide a stream key');
}
channel['credentials.streamKey'] = channelDefinition['credentials.streamKey'].trim();
if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) {
throw new SiteError(400, 'Must provide a widget key');
}
channel['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim();
channel.credentials = {
streamKey: channelDefinition['credentials.streamKey'].trim(),
widgetKey: channelDefinition['credentials.widgetKey'].trim(),
};
await channel.save();
await this.updateChannelStatus(channel);
return channel.toObject();
}
@ -77,10 +91,7 @@ class VenueService extends SiteService {
async updateChannel (channel, channelDefinition) {
const updateOp = { $set: { } };
if (!channelDefinition.slug || (channelDefinition.slug === '')) {
throw new SiteError(400, 'Must provide a channel URL slug');
}
updateOp.$set.slug = striptags(channelDefinition.slug.trim());
updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
throw new SiteError(400, 'Must provide a stream key');
@ -92,31 +103,44 @@ class VenueService extends SiteService {
}
updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim();
await VenueChannel.updateOne({ _id: channel._id }, updateOp);
channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true });
await this.updateChannelStatus(channel);
}
async getChannels (pagination) {
async getChannels (pagination, options) {
options = Object.assign({ withCredentials: false }, options);
pagination = Object.assign({ skip: 0, cpp: 10 }, pagination);
const search = { };
const channels = await VenueChannel
let q = VenueChannel
.find(search)
.sort({ slug: 1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateVenueChannel)
.lean();
.limit(pagination.cpp);
return channels;
if (options.withCredentials) {
q = q.select('+credentials');
}
return q.populate(this.populateVenueChannel).lean();
}
async getChannelById (channelId) {
const channel = await VenueChannel
.findOne({ _id: channelId })
.populate(this.populateVenueChannel)
.lean();
return channel;
async getChannelById (channelId, options) {
options = Object.assign({ withCredentials: false }, options);
let q = VenueChannel.findOne({ _id: channelId });
if (options.withCredentials) {
q = q.select('+credentials');
}
return q.populate(this.populateVenueChannel).lean();
}
async getChannelBySlug (channelSlug, options) {
options = Object.assign({ withCredentials: false }, options);
let q = VenueChannel.findOne({ slug: channelSlug.toLowerCase().trim() });
if (options.withCredentials) {
q = q.select('+credentials');
}
return q.populate(this.populateVenueChannel).lean();
}
async getChannelFeed (channelSlug, options) {
@ -145,23 +169,6 @@ class VenueService extends SiteService {
return json;
}
async getChannelStatus (channel, options) {
const { cache: cacheService } = this.dtp.services;
const cacheKey = `venue:ch:${channel.slug}:status`;
options = Object.assign({ allowCache: true }, options);
let json;
if (options.allowCache) {
json = await cacheService.getObject(cacheKey);
if (json) { return json; }
}
const channelStatus = await this.updateChannelStatus(channel);
await cacheService.setObjectEx(cacheKey, 30, channelStatus);
return channel;
}
async updateChannelStatus (channel) {
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`;
this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl });
@ -176,13 +183,30 @@ class VenueService extends SiteService {
throw new Error(`failed to fetch channel status: ${json.message}`);
}
const status = new VenueChannelStatus(json.channel);
let status = new VenueChannelStatus(json.channel);
status.created = new Date();
status.channel = channel._id;
await status.save();
await VenueChannel.updateOne({ _id: channel._id }, { $set: { lastStatus: status._id } });
return status.toObject();
}
getChannelSlug (channelUrl) {
const { URL } = require('url');
const url = new URL(channelUrl);
if (url.host !== this.soapboxDomain) {
throw new SiteError(400, 'This is not a valid DTP stream channel URL: Domain mismatch.');
}
const channelUrlParts = url.pathname.split('/').filter((part) => part.length > 0);
if (channelUrlParts[0] !== 'channel') {
throw new SiteError(400, 'This is not a valid DTP stream channel URL: Not on channel path.');
}
return slug(striptags(channelUrlParts[1].trim()));
}
}
module.exports = {

@ -30,6 +30,11 @@ ul(uk-nav).uk-nav-default
span.nav-item-icon
i.fas.fa-file
span.uk-margin-small-left Pages
li(class={ 'uk-active': (adminView === 'site-link') })
a(href="/admin/site-link")
span.nav-item-icon
i.fas.fa-link
span.uk-margin-small-left Links
li(class={ 'uk-active': (adminView === 'newsroom') })
a(href="/admin/newsroom")

@ -0,0 +1,42 @@
extends ../layouts/main
block content
- var formAction = link ? `/admin/site-link/${link._id}` : '/admin/site-link';
form(method="POST", action= formAction).uk-form
.uk-card.uk-card-secondary.uk-card-small
.uk-card-header
h1.uk-card-title= link ? 'Update Site Link' : 'Add Site Link'
.uk-card-body
.uk-margin
label(for="label").uk-form-label Label
input(id="label", name="label", type="text", placeholder="Enter menu label", value= link ? link.label : undefined).uk-input
.uk-margin
label(for="url").uk-form-label Target URL
input(id="url", name="url", type="url", placeholder="Enter URL", value= link ? link.url : undefined).uk-input
.uk-margin
.pretty.p-default
input(id="target-blank", name="targetBlank", type="checkbox", checked= link ? link.target === '_blank' : false)
.state
label Open in new window/tab
div(uk-grid).uk-card-footer
.uk-width-expand
+renderBackButton({ includeLabel: true, label: 'Cancel' })
if link
.uk-width-auto
button(
type="button",
data-link={ _id: link._id, label: link.label },
onclick="return dtp.adminApp.deleteSiteLink(event);",
).uk-button.dtp-button-danger.uk-border-rounded
span
i.fas.fa-trash
span.uk-margin-small-left DELETE LINK
.uk-width-auto
button(type="submit").uk-button.dtp-button-primary.uk-border-rounded
span
i.fas.fa-save
span.uk-margin-small-left= link ? 'Update Link' : 'Add Link'

@ -0,0 +1,16 @@
extends ../layouts/main
block content
div(uk-grid)
.uk-width-expand
h1 Link home
.uk-width-auto
a(href="/admin/site-link/create").uk-button.dtp-button-primary.uk-border-rounded
span
i.fas.fa-plus
span.uk-margin-small-left Add Link
ul.uk-list.uk-list-divider
each link of links
li
a(href=`/admin/site-link/${link._id}`)= link.label

@ -12,21 +12,22 @@ block content
.uk-card-body
.uk-margin
label(for="slug").uk-form-label Channel URL
input(type="url", name="url", placeholder="Paste Shing.tv channel URL").uk-input
input(type="url", name="url", placeholder="Paste Shing.tv channel URL", value= channel ? `https://${dtp.services.venue.soapboxDomain}/channel/${channel.slug}` : undefined).uk-input
.uk-text-small.uk-text-muted #{site.name} integrates #{dtp.services.venue.soapboxDomain} and wants the channel URL from there.
.uk-margin
label(for="owner").uk-form-label Owner
input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : undefined).uk-input
input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : 'rob').uk-input
.uk-text-small.uk-text-muted Enter the user's username here on #{site.name}, not from Shing.tv.
div(uk-grid)
div(class="uk-width-1-1 uk-width-2-3@m")
label(for="stream-key").uk-form-label Stream Key
input(id="stream-key", type="text", name="credentials.streamKey", placeholder="Paste Shing.tv stream key", value= channel ? channel.credentials.streamKey : undefined).uk-input
input(id="stream-key", name="credentials.streamKey", type="text", placeholder="Paste Shing.tv stream key", value= channel ? channel.credentials.streamKey : undefined).uk-input
div(class="uk-width-1-1 uk-width-1-3@m")
label(for="widget-key").uk-form-label Widget Key
input(id="widget-key", type="text", name="credentials.widgetKey", placeholder="Paste Shing.tv widget key", value= channel ? channel.credentials.widgetKey : undefined).uk-input
input(id="widget-key", name="credentials.widgetKey", type="text", placeholder="Paste Shing.tv widget key", value= channel ? channel.credentials.widgetKey : undefined).uk-input
.uk-card-footer.uk-flex.uk-flex-between
.uk-width-auto

@ -1,3 +1,17 @@
extends ../../layouts/main
block content
h1 VenueChannel Home
-
var onlineChannels = channels.filter((channel) => channel.lastUpdate && (channel.lastUpdate.status === 'live'))
var offlineChannels = channels.filter((channel) => !channel.lastUpdate || (channel.lastUpdate.status !== 'live'))
h1 Manage Your Venue Channels
a(href="/admin/venue/channel/create").uk-button.dtp-button-primary.uk-border-rounded Add Channel
if Array.isArray(offlineChannels) && (channels.length > 0)
uk.uk-list.uk-list-divider
each channel of offlineChannels
li
+renderVenueChannelListItem(channel, { baseUrl: '/admin/venue/channel' })
else
div There are no channels integrated with #{site.name}.

@ -1,5 +1,20 @@
extends ../layouts/main
block content
include ../../venue/components/channel-list-item
h1 Manage Your DTP Venue
a(href="/admin/venue/channel/create").uk-button.dtp-button-primary.uk-border-rounded Add channel
-
var onlineChannels = channels.filter((channel) => channel.lastUpdate && (channel.lastUpdate.status === 'live'))
var offlineChannels = channels.filter((channel) => !channel.lastUpdate || (channel.lastUpdate.status !== 'live'))
if Array.isArray(offlineChannels) && (channels.length > 0)
uk.uk-list.uk-list-divider
each channel of offlineChannels
li
+renderVenueChannelListItem(channel, { baseUrl: '/admin/venue/channel' })
else
div There are no channels integrated with #{site.name}.
pre= JSON.stringify(channels, null, 2)

@ -1,6 +1,6 @@
include ../user/components/profile-icon
- var isLive = shingChannelStatus && shingChannelStatus.isLive;
- var isLive = venue && !!venue.channels.find((channel) => channel.lastStatus && (channel.lastStatus.status === 'live'));
nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
.uk-navbar-left
@ -32,13 +32,31 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
else
span
i.fas.fa-tv
span(class="uk-visible@m").uk-margin-small-left= isLive ? 'Live Now' : 'Live'
span(class="uk-visible@m").uk-margin-small-left= isLive ? 'On Air' : 'Channels'
each menuItem in mainMenu
li(class={ 'uk-active': (pageSlug === menuItem.slug) })
a(href= menuItem.url, title= menuItem.label)
+renderButtonIcon(menuItem.icon || 'fa-file', menuItem.label)
if Array.isArray(links) && (links.length > 0)
li
a(href="")
+renderButtonIcon('fa-link', 'Links')
.uk-navbar-dropdown
ul.uk-nav.uk-navbar-dropdown-nav
each link in links
li
a(href= link.url, target= link.target)
div(uk-grid).uk-grid-collapse.uk-flex-middle
div(style="width: 24px;")
img(
src= link.iconUrl,
style="width: 16px; height: auto;",
onerror=`this.src= "/img/icon/${site.domainKey}/icon-16x16.png";`,
)
.uk-width-expand= link.label
div(class="uk-hidden@m").uk-navbar-center
//- Site name
ul.uk-navbar-nav

@ -1,6 +1,9 @@
include ../announcement/components/announcement
include ../newsroom/components/feed-entry-list-item
include ../venue/components/channel-card
include ../venue/components/channel-list-item
- var isLive = !!shingChannelStatus && shingChannelStatus.isLive && !!shingChannelStatus.liveEpisode;
mixin renderSidebarEpisode(episode)
@ -32,32 +35,28 @@ mixin renderPageSidebar ( )
//-
//- Shing.tv Channel Integration
//-
if isLive
.uk-margin-medium
+renderSectionTitle('Live Now!', {
label: 'Tune In',
title: shingChannelStatus.name,
url: '/venue',
})
.uk-card.uk-card-default.uk-card-small.uk-card-hover.uk-margin
if shingChannelStatus.liveThumbnail
.uk-card-media-top
a(href="/venue")
img(
src= shingChannelStatus.liveThumbnail.url,
onerror=`this.src = '${shingChannelStatus.thumbnailUrl}';`,
title="Tune in now",
)
if shingChannelStatus.liveEpisode && shingChannelStatus.liveEpisode.title
.uk-card-body
.uk-card-title.uk-margin-remove.uk-text-truncate
a(href="/venue", uk-tooltip= `Watch "${shingChannelStatus.liveEpisode.title}" now!`)= shingChannelStatus.liveEpisode.title
.uk-text-small
div(uk-grid).uk-grid-small.uk-flex-between
.uk-width-auto
div Started: #{moment(shingChannelStatus.liveEpisode.created).fromNow()}
.uk-width-auto #[i.fas.fa-eye] #{formatCount(shingChannelStatus.liveEpisode.stats.currentViewerCount)}
-
var onlineChannels = venue.channels.filter((channel) => channel.lastStatus && (channel.lastStatus.status === 'live'));
var offlineChannels = venue.channels.filter((channel) => !channel.lastStatus || (channel.lastStatus.status !== 'live'));
if Array.isArray(onlineChannels) && (onlineChannels.length > 0)
each channel of onlineChannels
.uk-margin-medium
+renderSectionTitle('Live Now!', {
label: 'Tune In',
title: channel.lastStatus.name,
url: '/venue',
})
+renderVenueChannelCard(channel)
if Array.isArray(offlineChannels) && (offlineChannels.length > 0)
.uk-margin-medium
+renderSectionTitle('Offline Channels')
ul.uk-list.uk-list-divider
each venueChannel of offlineChannels
li
+renderVenueChannelListItem(offlineChannels[0])
//-
//- Shing.tv Channel Feed

@ -0,0 +1,21 @@
mixin renderVenueChannelCard (channel)
.uk-card.uk-card-default.uk-card-small.uk-card-hover.uk-margin
if channel.lastStatus && channel.lastStatus.liveThumbnail
.uk-card-media-top
a(href=`/venue/${channel.slug}`)
img(
src= channel.lastStatus.liveThumbnail.url,
onerror=`this.src = '${channel.lastStatus.thumbnailUrl}';`,
title="Tune in now",
)
if channel.lastStatus && channel.lastStatus.liveEpisode && channel.lastStatus.liveEpisode.title
.uk-card-body
.uk-text-bold.uk-text-truncate
a(href="/venue", uk-tooltip= `Watch "${channel.lastStatus.liveEpisode.title}" now!`)= channel.lastStatus.liveEpisode.title
.uk-text-small
div(uk-grid).uk-grid-small.uk-flex-between
.uk-width-auto
div Started: #{moment(channel.lastStatus.liveEpisode.created).fromNow()}
.uk-width-auto #[i.fas.fa-eye] #{formatCount(channel.lastStatus.liveEpisode.stats.currentViewerCount)}

@ -0,0 +1,24 @@
mixin renderVenueChannelListItem (channel, options)
- options = Object.assign({ baseUrl: '/venue' }, options);
div(uk-grid).uk-grid-small
.uk-width-auto
a(href= getUserProfileUrl(channel.owner), uk-tooltip=`Visit ${channel.owner.displayName || channel.owner.username}'s profile`)
+renderProfileIcon(channel.owner)
.uk-width-expand
.uk-margin-small
if channel.lastStatus
.uk-text-bold
a(href=`${options.baseUrl}/${channel.slug}`, uk-tooltip=`Visit ${channel.lastStatus.name}`)= channel.lastStatus.name
else
div ...waiting for first channel status update to arrive
.uk-text-small.uk-text-meta
div(uk-grid).uk-grid-small.uk-grid-divider
.uk-width-expand.uk-text-truncate
+renderUserLink(channel.owner)
.uk-width-auto
if channel.lastStatus.status === 'live'
span.uk-text-success LIVE
else
span= moment(channel.lastStatus.lastLive).fromNow()

@ -2,4 +2,9 @@ extends ../layouts/main
block content
- var shingBaseUrl = `https://${dtp.services.venue.soapboxDomain}`;
iframe(src= `${shingBaseUrl}/channel/${site.shingChannelSlug}/embed/venue?k=${site.shingWidgetKey}`, style="width: 100%; height: 720px;", allowfullscreen)
iframe(src= `${shingBaseUrl}/channel/${channel.slug}/embed/venue?k=${channelCredentials.widgetKey}`, style="width: 100%; height: 720px;", allowfullscreen)
block viewjs
script.
window.dtp = window.dtp || { };
window.dtp.channel = !{JSON.stringify(channel)};

@ -0,0 +1,38 @@
extends ../layouts/main
block content
include ../components/pagination-bar
-
var onlineChannels = channels.filter((channel) => channel.lastStatus && (channel.lastStatus.status === 'live'))
var offlineChannels = channels.filter((channel) => !channel.lastStatus || (channel.lastStatus.status !== 'live'))
section.uk-section.uk-section-default.uk-section-small
.uk-container.uk-container-expand
.uk-margin-large
div(uk-grid)
.uk-width-2-3
+renderSectionTitle('Live Channels')
.uk-margin-small
if Array.isArray(onlineChannels) && (onlineChannels.length > 0)
div(uk-grid).uk-grid-small
each channel of onlineChannels
.uk-width-1-3
+renderVenueChannelCard(channel)
else
div There are no live channels. Please check back later!
.uk-width-1-3
+renderSectionTitle('Offline Channels')
.uk-margin-small
if Array.isArray(offlineChannels) && (offlineChannels.length > 0)
ul.uk-list.uk-list-divider
each channel of offlineChannels
li
+renderVenueChannelListItem(channel)
else
div There are no offline channels.
//- pre= JSON.stringify(onlineChannels, null, 2)
//- pre= JSON.stringify(offlineChannels, null, 2)

@ -0,0 +1,59 @@
// venue.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') });
const {
SiteLog,
SiteWorker,
} = require(path.join(__dirname, '..', '..', 'lib', 'site-lib'));
module.rootPath = path.resolve(__dirname, '..', '..');
module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json'));
module.config = {
environment: process.env.NODE_ENV,
root: module.rootPath,
component: { name: 'venue', slug: 'venue' },
};
module.config.site = require(path.join(module.rootPath, 'config', 'site'));
module.config.http = require(path.join(module.rootPath, 'config', 'http'));
class VenueWorker extends SiteWorker {
constructor (dtp) {
super(dtp, dtp.config.component);
}
async start ( ) {
await super.start();
await this.loadProcessor(path.join(__dirname, 'venue', 'cron', 'update-channel-status.js'));
await this.startProcessors();
}
async stop ( ) {
await super.stop();
}
}
(async ( ) => {
try {
module.log = new SiteLog(module, module.config.component);
module.worker = new VenueWorker(module);
await module.worker.start();
module.log.info(`${module.pkg.name} v${module.pkg.version} ${module.config.component.name} started`);
} catch (error) {
module.log.error('failed to start worker', { component: module.config.component, error });
process.exit(-1);
}
})();

@ -0,0 +1,70 @@
// venue/cron/update-channel-status.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const mongoose = require('mongoose');
const VenueChannel = mongoose.model('VenueChannel');
const { CronJob } = require('cron');
const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib'));
class UpdateChannelStatusCron extends SiteWorkerProcess {
static get COMPONENT ( ) {
return {
name: 'updateChannelStatus',
slug: 'update-channel-status',
};
}
constructor (worker) {
super(worker, UpdateChannelStatusCron.COMPONENT);
}
async start ( ) {
await super.start();
await this.updateChannelStatus(); // first-run the expirations
this.job = new CronJob(
'*/15 * * * * *',
this.updateChannelStatus.bind(this),
null,
true,
process.env.DTP_CRON_TIMEZONE || 'America/New_York',
);
}
async stop ( ) {
if (this.job) {
this.log.info('stopping channel update job');
this.job.stop();
delete this.job;
}
await super.stop();
}
async updateChannelStatus ( ) {
const { venue: venueService } = this.dtp.services;
try {
await VenueChannel
.find()
.lean()
.cursor()
.eachAsync(async (channel) => {
this.log.info('updating channel status', { channel: channel.slug });
const status = await venueService.updateChannelStatus(channel);
this.log.info('channel status updated', { channel: status.name, status: status.status });
});
} catch (error) {
this.log.error('failed to expire crashed hosts', { error });
}
}
}
module.exports = UpdateChannelStatusCron;

@ -442,6 +442,30 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
return false;
}
async deleteSiteLink (event) {
event.preventDefault();
event.stopPropagation();
const target = event.currentTarget || event.target;
const link = JSON.parse(target.getAttribute('data-link'));
try {
await UIkit.modal.confirm(`Are you sure you want to remove sit elink "${link.label}"?`);
} catch (error) {
// canceled
return false;
}
try {
const response = await fetch(`/admin/site-link/${link._id}`, { method: 'DELETE' });
await this.processResponse(response);
} catch (error) {
UIkit.modal.alert(`Failed to remove site link: ${error.message}`);
}
return false;
}
}
dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;

@ -25,8 +25,12 @@ module.config = {
module.log.info('registering Page service middleware');
const { page: pageService } = module.services;
app.use(pageService.menuMiddleware.bind(pageService));
const { siteLink: siteLinkService } = module.services;
app.use(
pageService.menuMiddleware.bind(pageService),
siteLinkService.middleware(),
);
},
};

@ -1302,7 +1302,7 @@ ajv-keywords@^3.5.2:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^6.12.5:
ajv@^6.12.3, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@ -1563,11 +1563,23 @@ asn1.js@^5.2.0:
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@~0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
dependencies:
safer-buffer "~2.1.0"
assert-never@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@ -1642,6 +1654,16 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
aws4@^1.8.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axios@0.21.4:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
@ -1762,6 +1784,13 @@ batch@0.6.1:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
dependencies:
tweetnacl "^0.14.3"
bellajs@^11.0.7:
version "11.1.1"
resolved "https://registry.yarnpkg.com/bellajs/-/bellajs-11.1.1.tgz#1828dae65e396bf6c199fa8e0e402597b387ce29"
@ -2165,6 +2194,11 @@ caniuse-lite@^1.0.30001280:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz"
integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ==
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
"chalk@4.1 - 4.1.2", chalk@^4.1.0, chalk@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -2430,7 +2464,7 @@ colors@^1.2.1:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
combined-stream@^1.0.8:
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@ -2631,6 +2665,11 @@ core-js-compat@^3.18.0, core-js-compat@^3.19.1:
browserslist "^4.18.1"
semver "7.0.0"
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@ -2757,6 +2796,13 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50"
type "^1.0.1"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
dependencies:
assert-plus "^1.0.0"
data-urls@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8"
@ -3138,6 +3184,14 @@ eazy-logger@3.1.0:
dependencies:
tfunk "^4.0.0"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -3613,7 +3667,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extend@^3.0.0:
extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@ -3643,6 +3697,16 @@ extract-zip@2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
extsprintf@^1.2.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
fancy-log@^1.3.2, fancy-log@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7"
@ -3687,6 +3751,13 @@ fast-xml-parser@^4.0.10:
dependencies:
strnum "^1.0.5"
favicon@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/favicon/-/favicon-0.0.2.tgz#5a650d1e6684d300822ea8fe1f1f2eefcb4d3f91"
integrity sha512-n7CzPjyg2DbbwAvMMpJ13Ek+I4Fl5fXl+jtdhpQoPKmMD7hsGVZVC41yWwdeS7TZa9WYoyhhhY1/VuTJT5cnNg==
dependencies:
request "2.x.x"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@ -3848,6 +3919,11 @@ foreach@^2.0.5:
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@ -3857,6 +3933,15 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@ -4022,6 +4107,13 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
dependencies:
assert-plus "^1.0.0"
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
@ -4262,6 +4354,19 @@ gulplog@^1.0.0:
dependencies:
glogg "^1.0.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
har-validator@~5.1.3:
version "5.1.5"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
dependencies:
ajv "^6.12.3"
har-schema "^2.0.0"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -4471,6 +4576,15 @@ http-proxy@^1.18.1:
follow-redirects "^1.0.0"
requires-port "^1.0.0"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
https-proxy-agent@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@ -4995,7 +5109,7 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.7:
foreach "^2.0.5"
has-tostringtag "^1.0.0"
is-typedarray@^1.0.0:
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
@ -5081,6 +5195,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
jake@^10.6.1:
version "10.8.2"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
@ -5133,6 +5252,11 @@ jsbn@1.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
jsdom@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a"
@ -5209,7 +5333,7 @@ json-schema-traverse@^1.0.0:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json-schema@^0.4.0:
json-schema@0.4.0, json-schema@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
@ -5224,6 +5348,11 @@ json-stream@^1.0.0:
resolved "https://registry.yarnpkg.com/json-stream/-/json-stream-1.0.0.tgz#1a3854e28d2bbeeab31cc7ddf683d2ddc5652708"
integrity sha1-GjhU4o0rvuqzHMfd9oPS3cVlJwg=
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json5@^2.1.2, json5@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
@ -5252,6 +5381,16 @@ jsonpointer@^5.0.0:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072"
integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==
jsprim@^1.2.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.4.0"
verror "1.10.0"
jstransformer@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
@ -5669,6 +5808,11 @@ mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.34"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
@ -5676,6 +5820,13 @@ mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.17,
dependencies:
mime-db "1.51.0"
mime-types@~2.1.19:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@ -6098,10 +6249,10 @@ o-stream@^0.3.0:
resolved "https://registry.yarnpkg.com/o-stream/-/o-stream-0.3.0.tgz#204d27bc3fb395164507d79b381e91752e8daedc"
integrity sha512-gbzl6qCJZ609x/M2t25HqCYQagFzWYCtQ84jcuObGr+V8D1Am4EVubkF4J+XFs6ukfiv96vNeiBb8FrbbMZYiQ==
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth2orize@^1.11.1:
version "1.11.1"
@ -6525,6 +6676,11 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picmo@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/picmo/-/picmo-5.4.0.tgz#d51c9258031b351217e2d165ed3781f4a192c938"
@ -6682,6 +6838,11 @@ prr@~1.0.1:
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
psl@^1.1.28:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
psl@^1.1.33:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@ -6881,6 +7042,11 @@ qs@6.9.7:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
querystring@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
@ -7213,6 +7379,32 @@ replace-homedir@^1.0.0:
is-absolute "^1.0.0"
remove-trailing-separator "^1.1.0"
request@2.x.x:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -7369,7 +7561,7 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -7911,6 +8103,21 @@ sprintf-js@1.1.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
sshpk@^1.7.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stack-trace@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@ -8386,6 +8593,14 @@ tough-cookie@^4.0.0:
punycode "^2.1.1"
universalify "^0.1.2"
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@ -8417,6 +8632,11 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@ -8711,6 +8931,11 @@ utils-merge@1.0.1, utils-merge@1.x.x:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
@ -8741,6 +8966,15 @@ vary@^1, vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
vinyl-fs@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7"

Loading…
Cancel
Save