Merge branch 'ansible' of github.com:matchu/impress-2020 into ansible

This commit is contained in:
Emi Matchu 2021-11-23 12:42:59 -08:00
commit d4b115e805
12 changed files with 340 additions and 550 deletions

View file

@ -160,7 +160,7 @@
# process. They'll be able to manage it without `sudo`, including during # process. They'll be able to manage it without `sudo`, including during
# normal deploys, and run `pm2 monit` from their shell to see status. # normal deploys, and run `pm2 monit` from their shell to see status.
become: yes become: yes
command: "pm2 startup systemd {{ ansible_user_id }} --hp /home/{{ ansible_user_id }}" command: "pm2 startup systemd -u {{ ansible_user_id }} --hp /home/{{ ansible_user_id }}"
- name: Create pm2 ecosystem file - name: Create pm2 ecosystem file
copy: copy:
@ -267,6 +267,62 @@
- libgif-dev - libgif-dev
- librsvg2-dev - librsvg2-dev
- name: Install Playwright system dependencies
# NOTE: I copied the package list from the source list for
# `npx playwright install-deps`, which I couldn't get running in
# Ansible as root, and besides, I prefer manually managing the
# package list over running an npm script as root!
# TODO: We're using Puppeteer now, should this list change in some way?
become: yes
apt:
update_cache: yes
name:
# Tools
- xvfb
- fonts-noto-color-emoji
- ttf-unifont
- libfontconfig
- libfreetype6
- xfonts-cyrillic
- xfonts-scalable
- fonts-liberation
- fonts-ipafont-gothic
- fonts-wqy-zenhei
- fonts-tlwg-loma-otf
- ttf-ubuntu-font-family
# Chromium
- fonts-liberation
- libasound2
- libatk-bridge2.0-0
- libatk1.0-0
- libatspi2.0-0
- libcairo2
- libcups2
- libdbus-1-3
- libdrm2
- libegl1
- libgbm1
- libglib2.0-0
- libgtk-3-0
- libnspr4
- libnss3
- libpango-1.0-0
- libx11-6
- libx11-xcb1
- libxcb1
- libxcomposite1
- libxdamage1
- libxext6
- libxfixes3
- libxrandr2
- libxshmfence1
- name: Enable user namespace cloning for Chromium sandboxing
become: yes
ansible.posix.sysctl:
name: kernel.unprivileged_userns_clone
value: "1"
handlers: handlers:
- name: Restart nginx - name: Restart nginx
become: yes become: yes

View file

@ -33,6 +33,7 @@
"easeljs": "^1.0.2", "easeljs": "^1.0.2",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"framer-motion": "^4.1.11", "framer-motion": "^4.1.11",
"generic-pool": "^3.8.2",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"honeycomb-beeline": "^2.7.4", "honeycomb-beeline": "^2.7.4",
"immer": "^9.0.6", "immer": "^9.0.6",
@ -43,7 +44,7 @@
"mysql2": "^2.1.0", "mysql2": "^2.1.0",
"next": "12.0.2", "next": "12.0.2",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"playwright-core": "^1.14.0", "puppeteer": "^11.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-autosuggest": "^10.0.2", "react-autosuggest": "^10.0.2",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
@ -112,7 +113,6 @@
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"jest-image-snapshot": "^4.3.0", "jest-image-snapshot": "^4.3.0",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",
"playwright": "^1.14.0",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"react-is": "^16.13.1", "react-is": "^16.13.1",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",

View file

@ -8,127 +8,15 @@ const beeline = require("honeycomb-beeline")({
disableInstrumentationOnLoad: true, disableInstrumentationOnLoad: true,
}); });
import fetch from "node-fetch";
import connectToDb from "../../src/server/db";
async function handle(req, res) { async function handle(req, res) {
const allNcItemNamesAndIdsPromise = loadAllNcItemNamesAndIds(); res.setHeader("Content-Type", "text/plain; charset=utf8");
res
let itemValuesByIdOrName; .status(410)
try { .send(
itemValuesByIdOrName = await loadWakaValuesByIdOrName(); "WakaGuide.com is no longer updating its values, so we no longer " +
} catch (e) { "serve them from this endpoint. The most recent set of values is " +
console.error(e); "archived here: https://docs.google.com/spreadsheets/d/1DRMrniTSZP0sgZK6OAFFYqpmbT6xY_Ve_i480zghOX0"
res.setHeader("Content-Type", "text/plain"); );
res.status(500).send("Error loading Waka data from Google Sheets API");
return;
}
// Restructure the value data to use IDs as keys, instead of names.
const allNcItemNamesAndIds = await allNcItemNamesAndIdsPromise;
const itemValues = {};
for (const { name, id } of allNcItemNamesAndIds) {
if (id in itemValuesByIdOrName) {
itemValues[id] = itemValuesByIdOrName[id];
} else if (name in itemValuesByIdOrName) {
itemValues[id] = itemValuesByIdOrName[name];
}
}
// Cache for 1 minute, and immediately serve stale data for a day after.
// This should keep it fast and responsive, and stay well within our API key
// limits. (This will cause the client to send more requests than necessary,
// but the CDN cache should generally respond quickly with a small 304 Not
// Modified, unless the data really did change.)
res.setHeader(
"Cache-Control",
"public, max-age=3600, stale-while-revalidate=86400"
);
return res.send(itemValues);
}
async function loadAllNcItemNamesAndIds() {
const db = await connectToDb();
const [rows] = await db.query(`
SELECT items.id, item_translations.name FROM items
INNER JOIN item_translations ON item_translations.item_id = items.id
WHERE
(items.rarity_index IN (0, 500) OR is_manually_nc = 1)
AND item_translations.locale = "en"
`);
return rows.map(({ id, name }) => ({ id, name: normalizeItemName(name) }));
}
/**
* Load all Waka values from the spreadsheet. Returns an object keyed by ID or
* name - that is, if the item ID is provided in the sheet, we use that as the
* key; or if not, we use the name as the key.
*/
async function loadWakaValuesByIdOrName() {
if (!process.env["GOOGLE_API_KEY"]) {
throw new Error(`GOOGLE_API_KEY environment variable must be provided`);
}
const res = await fetch(
`https://sheets.googleapis.com/v4/spreadsheets/` +
`1DRMrniTSZP0sgZK6OAFFYqpmbT6xY_Ve_i480zghOX0/values/NC%20Values` +
`?fields=values&key=${encodeURIComponent(process.env["GOOGLE_API_KEY"])}`
);
const json = await res.json();
if (!res.ok) {
if (json.error) {
const { code, status, message } = json.error;
throw new Error(
`Google Sheets API returned error ${code} ${status}: ${message}`
);
} else {
throw new Error(
`Google Sheets API returned unexpected error: ${res.status} ${res.statusText}`
);
}
}
// Get the rows from the JSON response - skipping the first-row headers.
const rows = json.values.slice(1);
// Reformat the rows as a map from item name to value. We offer the item data
// as an object with a single field `value` for extensibility, but we omit
// the spreadsheet columns that we don't use on DTI, like Notes.
//
// NOTE: The Sheets API only returns the first non-empty cells of the row.
// That's why we set `""` as the defaults, in case the value/notes/etc
// aren't provided.
const itemValuesByIdOrName = {};
for (const [
itemName,
value = "",
unusedNotes = "",
unusedMarks = "",
itemId = "",
] of rows) {
const normalizedItemName = normalizeItemName(itemName);
itemValuesByIdOrName[itemId || normalizedItemName] = { value };
}
return itemValuesByIdOrName;
}
function normalizeItemName(name) {
return (
name
// Remove all spaces, they're a common source of inconsistency
.replace(/\s+/g, "")
// Lower case, because capitalization is another common source
.toLowerCase()
// Remove diacritics: https://stackoverflow.com/a/37511463/107415
// Waka has some stray ones in item names, not sure why!
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
);
} }
async function handleWithBeeline(req, res) { async function handleWithBeeline(req, res) {

View file

@ -22,34 +22,37 @@ const beeline = require("honeycomb-beeline")({
disableInstrumentationOnLoad: true, disableInstrumentationOnLoad: true,
}); });
// To render the image, we load the /internal/assetImage page in the web app, const puppeteer = require("puppeteer");
// a simple page specifically designed for this API endpoint! const genericPool = require("generic-pool");
const ASSET_IMAGE_PAGE_BASE_URL = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/internal/assetImage`
: process.env.NODE_ENV === "development"
? "http://localhost:3000/internal/assetImage"
: "https://impress-2020.openneo.net/internal/assetImage";
// TODO: We used to share a browser instamce, but we couldn't get it to reload console.info(`Creating new browser instance`);
// correctly after accidental closes, so we're just gonna always load a const browserPromise = puppeteer.launch({ headless: true });
// new one now. What are the perf implications of this? Does it slow down
// response time substantially? // We maintain a small pool of browser pages, to manage memory usage. If all
async function getBrowser() { // the pages are already in use, a request will wait for one of them to become
if (process.env["NODE_ENV"] === "production") { // available.
// In production, we use a special chrome-aws-lambda Chromium. //
const chromium = require("chrome-aws-lambda"); // NOTE: 4 pages is about where our 1-cpu prod environment maxes out. We might
const playwright = require("playwright-core"); // want to upgrade to the 2-cpu box as we add more pressure though, and
return await playwright.chromium.launch({ // then maybe we can afford more pages in the pool?
args: chromium.args,
executablePath: await chromium.executablePath, const PAGE_POOL = genericPool.createPool(
headless: true, {
}); create: async () => {
} else { console.debug(`Creating a browser page`);
// In development, we use the standard playwright Chromium. const browser = await browserPromise;
const playwright = require("playwright"); return await browser.newPage();
return await playwright.chromium.launch({ headless: true }); },
} destroy: (page) => {
} console.debug(`Closing a browser page`);
page.close();
},
validate: (page) => page.browser().isConnected(),
},
{ min: 4, max: 4, testOnBorrow: true, acquireTimeoutMillis: 15000 }
);
PAGE_POOL.on("factoryCreateError", (error) => console.error(error));
PAGE_POOL.on("factoryDestroyError", (error) => console.error(error));
async function handle(req, res) { async function handle(req, res) {
const { libraryUrl, size } = req.query; const { libraryUrl, size } = req.query;
@ -73,6 +76,9 @@ async function handle(req, res) {
imageBuffer = await loadAndScreenshotImage(libraryUrl, size); imageBuffer = await loadAndScreenshotImage(libraryUrl, size);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e.name === "TimeoutError") {
return reject(res, `Server under heavy load: ${e.message}`, 503);
}
return reject(res, `Could not load image: ${e.message}`, 500); return reject(res, `Could not load image: ${e.message}`, 500);
} }
@ -86,18 +92,24 @@ async function handle(req, res) {
} }
async function loadAndScreenshotImage(libraryUrl, size) { async function loadAndScreenshotImage(libraryUrl, size) {
const assetImagePageUrl = new URL(ASSET_IMAGE_PAGE_BASE_URL); // To render the image, we load the /internal/assetImage page in the web app,
// a simple page specifically designed for this API endpoint!
//
// NOTE: If we deploy to a host where localhost:3000 won't work, make this
// configurable with an env var, e.g. process.env.LOCAL_APP_HOST
const assetImagePageUrl = new URL(
"http://localhost:3000/internal/assetImage"
);
assetImagePageUrl.search = new URLSearchParams({ assetImagePageUrl.search = new URLSearchParams({
libraryUrl, libraryUrl,
size, size,
}).toString(); }).toString();
console.debug("Opening browser page"); console.debug("Getting browser page");
const browser = await getBrowser(); const page = await PAGE_POOL.acquire();
const page = await browser.newPage();
console.debug("Page opened, navigating to: " + assetImagePageUrl.toString());
try { try {
console.debug("Page ready, navigating to: " + assetImagePageUrl.toString());
await page.goto(assetImagePageUrl.toString()); await page.goto(assetImagePageUrl.toString());
console.debug("Page loaded, awaiting image"); console.debug("Page loaded, awaiting image");
@ -106,10 +118,20 @@ async function loadAndScreenshotImage(libraryUrl, size) {
// present, or raising the error if present. // present, or raising the error if present.
const imageBufferPromise = screenshotImageFromPage(page); const imageBufferPromise = screenshotImageFromPage(page);
const errorMessagePromise = readErrorMessageFromPage(page); const errorMessagePromise = readErrorMessageFromPage(page);
const firstResultFromPage = await Promise.any([ let firstResultFromPage;
imageBufferPromise.then((imageBuffer) => ({ imageBuffer })), try {
errorMessagePromise.then((errorMessage) => ({ errorMessage })), firstResultFromPage = await Promise.any([
]); imageBufferPromise.then((imageBuffer) => ({ imageBuffer })),
errorMessagePromise.then((errorMessage) => ({ errorMessage })),
]);
} catch (error) {
if (error.errors) {
// If both promises failed, show all error messages.
throw new Error(error.errors.map((e) => e.message).join(", "));
} else {
throw error;
}
}
if (firstResultFromPage.errorMessage) { if (firstResultFromPage.errorMessage) {
throw new Error(firstResultFromPage.errorMessage); throw new Error(firstResultFromPage.errorMessage);
@ -122,18 +144,9 @@ async function loadAndScreenshotImage(libraryUrl, size) {
); );
} }
} finally { } finally {
// Tear down our resources when we're done! If it fails, log the error, but // To avoid memory leaks, we destroy the page when we're done with it.
// don't block the success of the image. // The pool will replace it with a fresh one!
try { PAGE_POOL.destroy(page);
await page.close();
} catch (e) {
console.warn("Error closing page after image finished", e);
}
try {
await browser.close();
} catch (e) {
console.warn("Error closing browser after image finished", e);
}
} }
} }
@ -173,7 +186,7 @@ function isNeopetsUrl(urlString) {
} }
function reject(res, message, status = 400) { function reject(res, message, status = 400) {
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain; charset=utf8");
return res.status(status).send(message); return res.status(status).send(message);
} }

View file

@ -35,11 +35,12 @@ const beeline = require("honeycomb-beeline")({
sampleRate: 10, sampleRate: 10,
}); });
import fetch from "node-fetch";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { print as graphqlPrint } from "graphql/language/printer"; import { ApolloServer } from "apollo-server";
import { createTestClient } from "apollo-server-testing";
import connectToDb from "../../src/server/db"; import connectToDb from "../../src/server/db";
import { config as graphqlConfig } from "../../src/server";
import { renderOutfitImage } from "../../src/server/outfit-images"; import { renderOutfitImage } from "../../src/server/outfit-images";
import getVisibleLayers, { import getVisibleLayers, {
petAppearanceFragmentForGetVisibleLayers, petAppearanceFragmentForGetVisibleLayers,
@ -143,50 +144,35 @@ async function handle(req, res) {
return res.send(image); return res.send(image);
} }
const GRAPHQL_ENDPOINT = process.env.VERCEL_URL // Check out this scrappy way of making a query against server code ^_^`
? `https://${process.env.VERCEL_URL}/api/graphql` const graphqlClient = createTestClient(new ApolloServer(graphqlConfig));
: process.env.NODE_ENV === "development"
? "http://localhost:3000/api/graphql"
: "https://impress-2020.openneo.net/api/graphql";
// NOTE: Unlike in-app views, we only load PNGs here. We expect this to
// generally perform better, and be pretty reliable now that TNT is
// generating canonical PNGs for every layer!
const GRAPHQL_QUERY = gql`
query ApiOutfitImage($outfitId: ID!, $size: LayerImageSize) {
outfit(id: $outfitId) {
petAppearance {
layers {
id
imageUrl(size: $size)
}
...PetAppearanceForGetVisibleLayers
}
itemAppearances {
layers {
id
imageUrl(size: $size)
}
...ItemAppearanceForGetVisibleLayers
}
}
}
${petAppearanceFragmentForGetVisibleLayers}
${itemAppearanceFragmentForGetVisibleLayers}
`;
const GRAPHQL_QUERY_STRING = graphqlPrint(GRAPHQL_QUERY);
async function loadLayerUrlsForSavedOutfit(outfitId, size) { async function loadLayerUrlsForSavedOutfit(outfitId, size) {
const { errors, data } = await fetch(GRAPHQL_ENDPOINT, { const { errors, data } = await graphqlClient.query({
method: "POST", query: gql`
headers: { query ApiOutfitImage($outfitId: ID!, $size: LayerImageSize) {
"Content-Type": "application/json", outfit(id: $outfitId) {
}, petAppearance {
body: JSON.stringify({ layers {
query: GRAPHQL_QUERY_STRING, id
variables: { outfitId, size: `SIZE_${size}` }, imageUrl(size: $size)
}), }
}).then((res) => res.json()); ...PetAppearanceForGetVisibleLayers
}
itemAppearances {
layers {
id
imageUrl(size: $size)
}
...ItemAppearanceForGetVisibleLayers
}
}
}
${petAppearanceFragmentForGetVisibleLayers}
${itemAppearanceFragmentForGetVisibleLayers}
`,
variables: { outfitId, size: `SIZE_${size}` },
});
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
throw new Error( throw new Error(
@ -225,7 +211,7 @@ async function loadUpdatedAtForSavedOutfit(outfitId) {
} }
function reject(res, message, status = 400) { function reject(res, message, status = 400) {
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain; charset=utf8");
return res.status(status).send(message); return res.status(status).send(message);
} }

View file

@ -1,151 +0,0 @@
/**
* /api/outfitPageSSR also serves the initial request for /outfits/:id, to
* add title and meta tags. This primarily for sharing, like on Discord or
* Twitter or Facebook!
*
* The route is configured in vercel.json, at the project root.
*
* To be honest, we probably should have built Impress 2020 on Next.js, and
* then we'd be getting realistic server-side rendering across practically the
* whole app very cheaply. But this is a good hack for what we have!
*
* TODO: We could add the basic outfit page layout and image preview, to use
* SSR to decrease time-to-first-content for the end-user, too
*/
const beeline = require("honeycomb-beeline")({
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
dataset:
process.env["NODE_ENV"] === "production"
? "Dress to Impress (2020)"
: "Dress to Impress (2020, dev)",
serviceName: "impress-2020-gql-server",
disableInstrumentationOnLoad: true,
});
import escapeHtml from "escape-html";
import fetch from "node-fetch";
import connectToDb from "../../src/server/db";
import { normalizeRow } from "../../src/server/util";
async function handle(req, res) {
// Load index.html as our initial page content. If this fails, it probably
// means something is misconfigured in a big way; we don't have a great way
// to recover, and we'll just show an error message.
let initialHtml;
try {
initialHtml = await loadIndexPageHtml();
} catch (e) {
console.error("Error loading index.html:", e);
return reject(res, "Sorry, there was an error loading this outfit page!");
}
// Load the given outfit by ID. If this fails, it's possible that it's just a
// problem with the SSR, and the client will be able to handle it better
// anyway, so just show the standard index.html and let the app load
// normally, as if there was no error. (We'll just log it.)
let outfit;
try {
outfit = await loadOutfitData(req.query.id);
} catch (e) {
console.error("Error loading outfit data:", e);
return sendHtml(res, initialHtml, 200);
}
// Similarly, if the outfit isn't found, we just show index.html - but with a
// 404 and a gentler log message.
if (outfit == null) {
console.info(`Outfit not found: ${req.query.id}`);
return sendHtml(res, initialHtml, 404);
}
const outfitName = outfit.name || "Untitled outfit";
// Okay, now let's rewrite the HTML to include some outfit data!
//
// WARNING!!!
// Be sure to always use `escapeHtml` when inserting user data!!
// WARNING!!!
//
let html = initialHtml;
// Add the outfit name to the title.
html = html.replace(
/<title>(.*)<\/title>/,
`<title>${escapeHtml(outfitName)} | Dress to Impress</title>`
);
// Add sharing meta tags just before the </head> tag.
const updatedAtTimestamp = Math.floor(
new Date(outfit.updatedAt).getTime() / 1000
);
const outfitUrl =
`https://impress-2020.openneo.net/outfits` +
`/${encodeURIComponent(outfit.id)}`;
const imageUrl =
`https://impress-outfit-images.openneo.net/outfits` +
`/${encodeURIComponent(outfit.id)}` +
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
`/600.png`;
const metaTags = `
<meta property="og:title" content="${escapeHtml(outfitName)}">
<meta property="og:type" content="website">
<meta property="og:image" content="${escapeHtml(imageUrl)}">
<meta property="og:url" content="${escapeHtml(outfitUrl)}">
<meta property="og:site_name" content="Dress to Impress">
<meta property="og:description" content="A custom Neopets outfit, designed on Dress to Impress!">
`;
html = html.replace(/<\/head>/, `${metaTags}</head>`);
console.info(`Successfully SSR'd outfit ${outfit.id}`);
return sendHtml(res, html);
}
async function loadOutfitData(id) {
const db = await connectToDb();
const [rows] = await db.query(`SELECT * FROM outfits WHERE id = ?;`, [id]);
if (rows.length === 0) {
return null;
}
return normalizeRow(rows[0]);
}
let cachedIndexPageHtml = null;
async function loadIndexPageHtml() {
if (cachedIndexPageHtml == null) {
// Request the same built copy of index.html that we're already serving at
// our homepage.
const homepageUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/`
: process.env.NODE_ENV === "development"
? "http://localhost:3000/"
: "https://impress-2020.openneo.net/";
const liveIndexPageHtml = await fetch(homepageUrl).then((res) =>
res.text()
);
cachedIndexPageHtml = liveIndexPageHtml;
}
return cachedIndexPageHtml;
}
function reject(res, message, status = 400) {
res.setHeader("Content-Type", "text/plain");
return res.status(status).send(message);
}
function sendHtml(res, html, status = 200) {
res.setHeader("Content-Type", "text/html");
return res.status(status).send(html);
}
async function handleWithBeeline(req, res) {
beeline.withTrace(
{ name: "api/outfitPageSSR", operation_name: "api/outfitPageSSR" },
() => handle(req, res)
);
}
export default handleWithBeeline;

View file

@ -2,6 +2,7 @@ import React from "react";
import { ClassNames } from "@emotion/react"; import { ClassNames } from "@emotion/react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { import {
Alert,
Box, Box,
Button, Button,
Center, Center,
@ -56,6 +57,23 @@ function HomePage() {
return ( return (
<Flex direction="column" align="center" textAlign="center" marginTop="8"> <Flex direction="column" align="center" textAlign="center" marginTop="8">
<Alert status="warning" maxWidth="600px">
<Box>
<strong>
The Neopets Metaverse team is no longer licensed to use this
software.
</strong>{" "}
<Box
as="a"
href="https://twitter.com/NeopetsDTI/status/1460386400839168001?s=20"
textDecoration="underline"
>
More information available here.
</Box>{" "}
Thanks for understanding!
</Box>
</Alert>
<Box height="4" />
<Box <Box
width="200px" width="200px"
height="200px" height="200px"

View file

@ -299,13 +299,24 @@ function computeOverallCachePolicy(
// If maxAge is 0, then we consider it uncacheable so it doesn't matter what // If maxAge is 0, then we consider it uncacheable so it doesn't matter what
// the scope was. // the scope was.
return lowestMaxAge && lowestMaxAgePlusSWR // FORK if (lowestMaxAge && lowestMaxAgePlusSWR) {
? { return {
maxAge: lowestMaxAge, maxAge: lowestMaxAge,
staleWhileRevalidate: lowestMaxAgePlusSWR - lowestMaxAge, // FORK staleWhileRevalidate: lowestMaxAgePlusSWR - lowestMaxAge, // FORK
scope, scope,
} };
: undefined; } else if (scope !== CacheScope.Public) {
// TODO: It'd probably be a bit better to leave the ages unspecified if
// the hints didn't specify them, but I don't wanna mess with the
// header-writing code right now.
return {
maxAge: 0,
staleWhileRevalidate: 0,
scope,
};
} else {
return undefined;
}
} }
function addHint( function addHint(

View file

@ -1,5 +1,4 @@
import DataLoader from "dataloader"; import DataLoader from "dataloader";
import fetch from "node-fetch";
import { normalizeRow } from "./util"; import { normalizeRow } from "./util";
const buildClosetListLoader = (db) => const buildClosetListLoader = (db) =>
@ -795,30 +794,6 @@ const buildItemTradesLoader = (db, loaders) =>
{ cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` } { cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` }
); );
const buildItemWakaValueLoader = () =>
new DataLoader(async (itemIds) => {
// This loader calls our /api/allWakaValues endpoint, to take advantage of
// the CDN caching. This helps us respond a bit faster than Google Sheets
// API would, and avoid putting pressure on our Google Sheets API quotas.
// (Some kind of internal memcache or process-level cache would be a more
// idiomatic solution in a monolith server environment!)
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/allWakaValues`
: process.env.NODE_ENV === "production"
? "https://impress-2020.openneo.net/api/allWakaValues"
: "http://localhost:3000/api/allWakaValues";
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Error loading /api/allWakaValues: ${res.status} ${res.statusText}`
);
}
const allWakaValues = await res.json();
return itemIds.map((itemId) => allWakaValues[itemId]);
});
const buildPetTypeLoader = (db, loaders) => const buildPetTypeLoader = (db, loaders) =>
new DataLoader(async (petTypeIds) => { new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(","); const qs = petTypeIds.map((_) => "?").join(",");
@ -1470,7 +1445,6 @@ function buildLoaders(db) {
db db
); );
loaders.itemTradesLoader = buildItemTradesLoader(db, loaders); loaders.itemTradesLoader = buildItemTradesLoader(db, loaders);
loaders.itemWakaValueLoader = buildItemWakaValueLoader();
loaders.petTypeLoader = buildPetTypeLoader(db, loaders); loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader( loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
db, db,

View file

@ -28,14 +28,13 @@ const typeDefs = gql`
createdAt: String createdAt: String
""" """
This item's capsule trade value as text, according to wakaguide.com, as a Deprecated: This item's capsule trade value as text, according to
human-readable string. Will be null if the value is not known, or if wakaguide.com, as a human-readable string. **This now always returns null.**
there's an error connecting to the data source.
""" """
wakaValueText: String @cacheControl(maxAge: ${oneHour}) wakaValueText: String @cacheControl(maxAge: ${oneHour})
currentUserOwnsThis: Boolean! @cacheControl(scope: PRIVATE) currentUserOwnsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE)
currentUserWantsThis: Boolean! @cacheControl(scope: PRIVATE) currentUserWantsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE)
""" """
How many users are offering/seeking this in their public trade lists. How many users are offering/seeking this in their public trade lists.
@ -315,17 +314,9 @@ const resolvers = {
const item = await itemLoader.load(id); const item = await itemLoader.load(id);
return item.createdAt && item.createdAt.toISOString(); return item.createdAt && item.createdAt.toISOString();
}, },
wakaValueText: async ({ id }, _, { itemWakaValueLoader }) => { wakaValueText: () => {
let wakaValue; // This feature is deprecated, so now we just always return unknown value.
try { return null;
wakaValue = await itemWakaValueLoader.load(id);
} catch (e) {
console.error(`Error loading wakaValueText for item ${id}, skipping:`);
console.error(e);
wakaValue = null;
}
return wakaValue ? wakaValue.value : null;
}, },
currentUserOwnsThis: async ( currentUserOwnsThis: async (
@ -665,8 +656,13 @@ const resolvers = {
itemSearchItemsLoader, itemSearchItemsLoader,
petTypeBySpeciesAndColorLoader, petTypeBySpeciesAndColorLoader,
currentUserId, currentUserId,
} },
{ cacheControl }
) => { ) => {
if (currentUserOwnsOrWants != null) {
cacheControl.setCacheHint({ scope: "PRIVATE" });
}
let bodyId = null; let bodyId = null;
if (fitsPet) { if (fitsPet) {
const petType = await petTypeBySpeciesAndColorLoader.load({ const petType = await petTypeBySpeciesAndColorLoader.load({
@ -799,8 +795,12 @@ const resolvers = {
numTotalItems: async ( numTotalItems: async (
{ query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds },
{ offset, limit }, { offset, limit },
{ currentUserId, itemSearchNumTotalItemsLoader } { currentUserId, itemSearchNumTotalItemsLoader },
{ cacheControl }
) => { ) => {
if (currentUserOwnsOrWants != null) {
cacheControl.setCacheHint({ scope: "PRIVATE" });
}
const numTotalItems = await itemSearchNumTotalItemsLoader.load({ const numTotalItems = await itemSearchNumTotalItemsLoader.load({
query: query.trim(), query: query.trim(),
bodyId, bodyId,
@ -816,8 +816,12 @@ const resolvers = {
items: async ( items: async (
{ query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds },
{ offset, limit }, { offset, limit },
{ currentUserId, itemSearchItemsLoader } { currentUserId, itemSearchItemsLoader },
{ cacheControl }
) => { ) => {
if (currentUserOwnsOrWants != null) {
cacheControl.setCacheHint({ scope: "PRIVATE" });
}
const items = await itemSearchItemsLoader.load({ const items = await itemSearchItemsLoader.load({
query: query.trim(), query: query.trim(),
bodyId, bodyId,

View file

@ -47,7 +47,20 @@ const typeDefs = gql`
user(id: ID!): User user(id: ID!): User
userByName(name: String!): User userByName(name: String!): User
userByEmail(email: String!, supportSecret: String!): User userByEmail(email: String!, supportSecret: String!): User
currentUser: User
"""
The currently logged-in user.
"""
# Don't allow caching of *anything* nested inside currentUser, because we
# want logins/logouts always reset user data properly.
#
# TODO: If we wanted to privately cache a currentUser field, we could
# remove the maxAge condition here, and attach user ID to the GraphQL
# request URL when sending auth headers. That way, changing user
# would send different requests and avoid the old cache hits. (But we
# should leave the scope, to emphasize that the CDN cache shouldn't
# cache it.)
currentUser: User @cacheControl(maxAge: 0, scope: PRIVATE)
} }
`; `;

236
yarn.lock
View file

@ -4564,7 +4564,7 @@ buffer@5.6.0, buffer@^5.2.0:
base64-js "^1.0.2" base64-js "^1.0.2"
ieee754 "^1.1.4" ieee754 "^1.1.4"
buffer@^5.5.0: buffer@^5.2.1, buffer@^5.5.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@ -4961,7 +4961,7 @@ commander@^5.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^6.1.0, commander@^6.2.0: commander@^6.2.0:
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
@ -5560,6 +5560,11 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
devtools-protocol@0.0.901419:
version "0.0.901419"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
dicer@0.3.0: dicer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
@ -5930,11 +5935,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escape-string-regexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escape-string-regexp@^4.0.0: escape-string-regexp@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@ -6361,17 +6361,7 @@ extglob@^2.0.4:
snapdragon "^0.8.1" snapdragon "^0.8.1"
to-regex "^3.0.1" to-regex "^3.0.1"
extract-zip@^1.7.0: extract-zip@2.0.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extract-zip@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@ -6382,6 +6372,16 @@ extract-zip@^2.0.1:
optionalDependencies: optionalDependencies:
"@types/yauzl" "^2.9.1" "@types/yauzl" "^2.9.1"
extract-zip@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extsprintf@1.3.0: extsprintf@1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -6771,6 +6771,11 @@ generate-function@^2.3.1:
dependencies: dependencies:
is-property "^1.0.2" is-property "^1.0.2"
generic-pool@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9"
integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==
gensync@^1.0.0-beta.1: gensync@^1.0.0-beta.1:
version "1.0.0-beta.1" version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@ -6979,11 +6984,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
graceful-fs@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graphql-extensions@^0.11.1: graphql-extensions@^0.11.1:
version "0.11.1" version "0.11.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.11.1.tgz#f543f544a047a7a4dd930123f662dfcc01527416" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.11.1.tgz#f543f544a047a7a4dd930123f662dfcc01527416"
@ -7055,9 +7055,9 @@ graphql@^14.5.3:
iterall "^1.2.2" iterall "^1.2.2"
graphql@^15.5.0: graphql@^15.5.0:
version "15.5.0" version "15.7.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
gud@^1.0.0: gud@^1.0.0:
version "1.0.0" version "1.0.0"
@ -7290,6 +7290,14 @@ https-browserify@1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^3.0.0: https-proxy-agent@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
@ -7298,14 +7306,6 @@ https-proxy-agent@^3.0.0:
agent-base "^4.3.0" agent-base "^4.3.0"
debug "^3.1.0" debug "^3.1.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1: human-signals@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@ -7988,11 +7988,6 @@ jpeg-js@^0.4.0:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7"
integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw== integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw==
jpeg-js@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
js-base64@^2.5.1: js-base64@^2.5.1:
version "2.6.4" version "2.6.4"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
@ -9007,6 +9002,13 @@ node-fetch@2.6.1:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0: node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0:
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
@ -9649,6 +9651,13 @@ pixelmatch@^5.1.0:
dependencies: dependencies:
pngjs "^4.0.1" pngjs "^4.0.1"
pkg-dir@4.2.0, pkg-dir@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
pkg-dir@^2.0.0: pkg-dir@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
@ -9663,58 +9672,11 @@ pkg-dir@^3.0.0:
dependencies: dependencies:
find-up "^3.0.0" find-up "^3.0.0"
pkg-dir@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
platform@1.3.6: platform@1.3.6:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
playwright-core@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.14.0.tgz#af51da7b201c11eeda780e2db3f05c8bca74c8be"
integrity sha512-n6NdknezSfRgB6LkLwcrbm5orRQZSpbd8LZmlc4YrIXV0VEvJr5tzP3xlHXpiFBfTr3yoFuagldI3T7bD/8H3w==
dependencies:
commander "^6.1.0"
debug "^4.1.1"
extract-zip "^2.0.1"
https-proxy-agent "^5.0.0"
jpeg-js "^0.4.2"
mime "^2.4.6"
pngjs "^5.0.0"
progress "^2.0.3"
proper-lockfile "^4.1.1"
proxy-from-env "^1.1.0"
rimraf "^3.0.2"
stack-utils "^2.0.3"
ws "^7.4.6"
yazl "^2.5.1"
playwright@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.14.0.tgz#18301b11f5278a446d36b5cf96f67db36ce2cd20"
integrity sha512-aR5oZ1iVsjQkGfYCjgYAmyMAVu0MQ0i8MgdnfdqDu9EVLfbnpuuFmTv/Rb7/Yjno1kOrDUP9+RyNC+zfG3wozA==
dependencies:
commander "^6.1.0"
debug "^4.1.1"
extract-zip "^2.0.1"
https-proxy-agent "^5.0.0"
jpeg-js "^0.4.2"
mime "^2.4.6"
pngjs "^5.0.0"
progress "^2.0.3"
proper-lockfile "^4.1.1"
proxy-from-env "^1.1.0"
rimraf "^3.0.2"
stack-utils "^2.0.3"
ws "^7.4.6"
yazl "^2.5.1"
please-upgrade-node@^3.2.0: please-upgrade-node@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
@ -9732,11 +9694,6 @@ pngjs@^4.0.1:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe"
integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
popmotion@9.3.5: popmotion@9.3.5:
version "9.3.5" version "9.3.5"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.5.tgz#e821aff3424a021b0f2c93922db31c55cfe64149" resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.5.tgz#e821aff3424a021b0f2c93922db31c55cfe64149"
@ -9845,7 +9802,7 @@ process@~0.5.1:
resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
progress@^2.0.0, progress@^2.0.3: progress@2.0.3, progress@^2.0.0:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@ -9864,15 +9821,6 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
proper-lockfile@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==
dependencies:
graceful-fs "^4.2.4"
retry "^0.12.0"
signal-exit "^3.0.2"
proxy-addr@~2.0.5: proxy-addr@~2.0.5:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -9895,7 +9843,7 @@ proxy-agent@3:
proxy-from-env "^1.0.0" proxy-from-env "^1.0.0"
socks-proxy-agent "^4.0.1" socks-proxy-agent "^4.0.1"
proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: proxy-from-env@1.1.0, proxy-from-env@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@ -9940,6 +9888,24 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f"
integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg==
dependencies:
debug "4.3.2"
devtools-protocol "0.0.901419"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
node-fetch "2.6.5"
pkg-dir "4.2.0"
progress "2.0.3"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.2.3"
qs@6.7.0: qs@6.7.0:
version "6.7.0" version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@ -10484,6 +10450,13 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1" version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -10491,13 +10464,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
rimraf@~2.6.2: rimraf@~2.6.2:
version "2.6.3" version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@ -11019,13 +10985,6 @@ ssim.js@^3.1.1:
resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df" resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df"
integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g== integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==
stack-utils@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==
dependencies:
escape-string-regexp "^2.0.0"
stacktrace-parser@0.1.10: stacktrace-parser@0.1.10:
version "0.1.10" version "0.1.10"
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
@ -11400,7 +11359,7 @@ table@^6.0.9:
string-width "^4.2.3" string-width "^4.2.3"
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
tar-fs@^2.0.0, tar-fs@^2.1.1: tar-fs@2.1.1, tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@ -11582,6 +11541,11 @@ tr46@^1.0.1:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
truncate-utf8-bytes@^1.0.0: truncate-utf8-bytes@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
@ -11745,6 +11709,14 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2" has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2" which-boxed-primitive "^1.0.2"
unbzip2-stream@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
dependencies:
buffer "^5.2.1"
through "^2.3.8"
unfetch@^4.1.0: unfetch@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
@ -12008,6 +11980,11 @@ watchpack@2.1.1:
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@ -12018,6 +11995,14 @@ whatwg-fetch@^3.0.0:
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^7.0.0: whatwg-url@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@ -12109,6 +12094,11 @@ write-file-atomic@^2.3.0:
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
signal-exit "^3.0.2" signal-exit "^3.0.2"
ws@8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
ws@^5.2.0: ws@^5.2.0:
version "5.2.2" version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
@ -12123,11 +12113,6 @@ ws@^6.0.0:
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"
ws@^7.4.6:
version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
ws@~7.4.2: ws@~7.4.2:
version "7.4.4" version "7.4.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
@ -12245,13 +12230,6 @@ yauzl@^2.10.0:
buffer-crc32 "~0.2.3" buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0" fd-slicer "~1.1.0"
yazl@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35"
integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==
dependencies:
buffer-crc32 "~0.2.3"
yeast@0.1.2: yeast@0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"