1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-08 06:55:26 +02:00
This commit is contained in:
Taly 2022-09-14 10:53:14 +00:00 committed by GitHub
commit f963f133f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 771 additions and 6 deletions

View file

@ -8,5 +8,5 @@
"watch": [ "watch": [
"**/*" "**/*"
], ],
"ext": "js,twig" "ext": "ts,js,twig"
} }

View file

@ -47,6 +47,7 @@
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
"@codexteam/misprints": "^1.0.0", "@codexteam/misprints": "^1.0.0",
"@codexteam/shortcuts": "^1.2.0",
"@editorjs/checklist": "^1.3.0", "@editorjs/checklist": "^1.3.0",
"@editorjs/code": "^2.7.0", "@editorjs/code": "^2.7.0",
"@editorjs/delimiter": "^1.2.0", "@editorjs/delimiter": "^1.2.0",
@ -83,6 +84,7 @@
"@types/sinon": "^10.0.2", "@types/sinon": "^10.0.2",
"@types/twig": "^1.12.6", "@types/twig": "^1.12.6",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^0.27.2",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",

View file

@ -0,0 +1,302 @@
import PageData from '../models/page.js';
import Pages from '../controllers/pages.js';
import urlify from '../utils/urlify.js';
import Page from '../models/page.js';
let globalWords: { [key: string]: {[key: string]: number} } = Object.create(null);
let globalPages: PageData[] = [];
class Search {
/**
* Initialize search
*/
public async init() {
if (globalWords && Object.keys(globalWords).length) {
return Promise.resolve();
}
await this.syncDB();
}
/**
* Load all pages from DB and update globalWords
* Use this method when any page was updated
*/
public async syncDB() {
globalWords = Object.create(null);
globalPages = await this.getPages();
/**
* Process all pages
*/
for await (const page of globalPages) {
/**
* Read content blocks from page
*/
for await (const block of page.body.blocks) {
const blockRatio = this.getBlockRatio(block);
const blockContent = this.getCleanTextFromBlock(block);
const blockWords: string[] = this.splitTextToWords(blockContent);
/**
* Process list of words in a block
*/
for await (const word of blockWords) {
if (!globalWords[word]) {
globalWords[word] = Object.create(null);
}
if (page._id) {
if (!globalWords[word][page._id]) {
globalWords[word][page._id] = 0;
}
/**
* Add page id to the list of pages with this word
*/
globalWords[word][page._id] += blockRatio;
}
}
}
}
console.log('Done');
}
/**
* Search for pages by given query
* @param searchString
*/
public async query(searchString: string) {
await this.init();
const searchWords = this.splitTextToWords(searchString);
const goodPages = (await this.getPagesByWords(searchWords))
.slice(0, 10);
const returnPages: {[key: string]: string|number, ratio: number}[] = [];
goodPages.forEach(({ pageId, ratio }) => {
const page = globalPages.filter(page => page._id === pageId).pop();
if (!page) {
return;
}
let section = '';
page.body.blocks.forEach((block: any) => {
let koef = 1;
let blockContent = this.getCleanTextFromBlock(block);
let shortBody = blockContent;
if (block.type === 'header') {
section = blockContent;
}
searchWords.forEach(word => {
if (blockContent.toLowerCase().indexOf(word) !== -1) {
koef *= 10;
}
})
shortBody = this.highlightSubstring(shortBody, searchWords);
if (koef > 0) {
returnPages.push({
...page,
shortBody,
anchor: urlify(section),
section,
ratio: ratio * koef,
})
}
});
});
return {
suggestions: ['description', 'about', 'contact'],
pages: returnPages
.sort((a, b) => b.ratio - a.ratio)
.slice(0, 15)
}
}
/**
*
* @private
*/
private async getPages(): Promise<Page[]> {
return await Pages.getAll();
}
/**
* Return list of pages with a given words
* @param words
* @private
*/
private async getPagesByWords(words: string[]) {
const pagesList: {[key: string]: number} = {};
/**
* Get list of words starting with a words from the search query
*/
const validWords = Object.keys(globalWords)
.filter(word => {
return !!words.filter(searchWord => word.indexOf(searchWord) !== -1).length
});
/**
* For each word get list of pages with this word
*/
validWords.forEach(word => {
Object.keys(globalWords[word])
.forEach(pageId => {
if (!pagesList[pageId]) {
pagesList[pageId] = 0;
}
pagesList[pageId] += globalWords[word][pageId]
})
})
/**
* Sort pages by frequency of given words
*/
const sortedPagesList = Object.keys(pagesList)
.map(pageId => {
return {
pageId,
ratio: pagesList[pageId]
}
})
.sort((a, b) => b.ratio - a.ratio);
return sortedPagesList;
}
/**
* Get block's ratio. It is used to calculate the weight of the words in the block
* @param block
* @private
*/
private getBlockRatio(block: any) {
switch (block.type) {
case 'header':
if (block.data.level === 1) {
return 16;
} else {
return 2;
}
case 'paragraph':
return 1.1;
case 'list':
return 1;
default:
return 0;
}
}
/**
* Return clear text content from block without HTML tags and special characters
* @param block
* @private
*/
private getCleanTextFromBlock(block: any): string {
let blockContent = '';
switch (block.type) {
case 'header':
blockContent = block.data.text;
break;
case 'paragraph':
blockContent = block.data.text
break;
case 'list':
blockContent = block.data.items.join(' ');
break;
default:
return blockContent;
}
blockContent = this.removeHTMLTags(blockContent);
blockContent = this.removeHTMLSpecialCharacters(blockContent);
return blockContent;
}
/**
* Remove HTML tags from string. Only content inside tags will be left
* @param text
* @private
*/
private removeHTMLTags(text: string) {
return text.replace(/<[^>]*>?/gm, '');
}
/**
* Remove special characters from text. For example: &nbsp; &amp; &quot; &lt; &gt;
* @param text
* @private
*/
private removeHTMLSpecialCharacters(text: string) {
return text.replace(/&[^;]*;?/gm, '');
}
/**
* Split text to words
* @param text
* @private
*/
private splitTextToWords(text: string): string[] {
return text
// lowercase all words
.toLowerCase()
// remove punctuation
.replace(/[.,;:]/gi, '')
// left only letters (+cyrillic) and numbers
.replace(/[^a-zа-я0-9]/gi, ' ')
// remove multiple spaces
.replace(/\s+/g, ' ')
// remove spaces at the beginning and at the end
.trim()
// split to words by spaces
.split(' ')
// ignore words shorter than 3 chars
.filter(word => word.length >= 3);
}
/**
* Highlight substring in string with a span wrapper
*/
private highlightSubstring(text: string, words: string|string[]) {
if (typeof words === 'string') {
words = [words];
}
const wordRegExp = new RegExp(words.join('|'), "ig");
const CLASS_STYLE = 'search-word';
return text.replace(wordRegExp, `<span class="${CLASS_STYLE}">$&</span>`);
}
}
/**
* Export initialized instance
*/
export default new Search();

View file

@ -1,12 +1,15 @@
import express from 'express'; import express from 'express';
import pagesAPI from './pages.js'; import pagesAPI from './pages.js';
import transportAPI from './transport.js'; import transportAPI from './transport.js';
import linksAPI from './links.js'; import linksAPI from './links.js';
import searchAPI from './search.js';
const router = express.Router(); const router = express.Router();
router.use('/', pagesAPI); router.use('/', pagesAPI);
router.use('/', transportAPI); router.use('/', transportAPI);
router.use('/', linksAPI); router.use('/', linksAPI);
router.use('/', searchAPI);
export default router; export default router;

View file

@ -2,6 +2,7 @@ import express, { Request, Response } from 'express';
import multerFunc from 'multer'; import multerFunc from 'multer';
import Pages from '../../controllers/pages.js'; import Pages from '../../controllers/pages.js';
import PagesOrder from '../../controllers/pagesOrder.js'; import PagesOrder from '../../controllers/pagesOrder.js';
import Search from '../../controllers/search.js';
const router = express.Router(); const router = express.Router();
const multer = multerFunc(); const multer = multerFunc();
@ -70,6 +71,9 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => {
/** push to the orders array */ /** push to the orders array */
await PagesOrder.push(parent, page._id); await PagesOrder.push(parent, page._id);
/** Update search index */
await Search.syncDB();
res.json({ res.json({
success: true, success: true,
result: page, result: page,
@ -127,6 +131,10 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
parent, parent,
uri, uri,
}); });
/** Update search index */
await Search.syncDB();
res.json({ res.json({
success: true, success: true,
result: page, result: page,
@ -206,6 +214,9 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
parentPageOrder.remove(req.params.id); parentPageOrder.remove(req.params.id);
await parentPageOrder.save(); await parentPageOrder.save();
/** Update search index */
await Search.syncDB();
res.json({ res.json({
success: true, success: true,
result: pageToRedirect, result: pageToRedirect,

View file

@ -0,0 +1,66 @@
import express, { Request, Response } from 'express';
import Search from '../../controllers/search.js';
const router = express.Router();
/**
* GET /search/:searchString
*
* Search given words in all documents
*/
router.get('/search', async (req: Request, res: Response) => {
try {
/**
* Get search string
*/
const searchString = req.query.text as string;
/**
* Get search results
*/
const searchResponse = await Search.query(searchString);
/**
* Compose response
*/
const compactedPages = searchResponse.pages.map(page => {
return {
/** Page id */
_id: page._id,
/** Page title */
title: page.title,
/** Page uri */
uri: page.uri,
/** Section heading name for the found fragment */
section: page.section,
/** Section's anchor */
anchor: page.anchor,
/** Page fragment with searched items */
shortBody: page.shortBody,
};
});
res.json({
success: true,
result: {
/** Found pages */
pages: compactedPages,
/** Typing suggestions */
suggestions: searchResponse.suggestions,
},
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
export default router;

View file

@ -15,7 +15,7 @@
hawkClientToken:"{{ config.hawkClientToken }}", hawkClientToken:"{{ config.hawkClientToken }}",
}; };
</script> </script>
<body> <body data-module="search">
{% include "components/header.twig" with res.locals.isAuthorized %} {% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs"> <div class="docs">

View file

@ -26,7 +26,7 @@
{% include 'components/button.twig' with {label: 'Edit', icon: 'pencil', size: 'small', url: '/page/edit/' ~ page._id, class: 'page__header-button'} %} {% include 'components/button.twig' with {label: 'Edit', icon: 'pencil', size: 'small', url: '/page/edit/' ~ page._id, class: 'page__header-button'} %}
{% endif %} {% endif %}
</header> </header>
<h1 class="page__title"> <h1 class="page__title" id="{{ page.title | urlify }}">
{{ page.title }} {{ page.title }}
</h1> </h1>
{% if (config.carbon and config.carbon.placement and config.carbon.serve) %} {% if (config.carbon and config.carbon.placement and config.carbon.serve) %}

View file

@ -17,6 +17,7 @@ import Writing from './modules/writing';
import Page from './modules/page'; import Page from './modules/page';
import Extensions from './modules/extensions'; import Extensions from './modules/extensions';
import Sidebar from './modules/sidebar'; import Sidebar from './modules/sidebar';
import Search from './modules/search';
import HawkCatcher from '@hawk.so/javascript'; import HawkCatcher from '@hawk.so/javascript';
/** /**
@ -31,6 +32,8 @@ class Docs {
this.page = new Page(); this.page = new Page();
this.extensions = new Extensions(); this.extensions = new Extensions();
this.sidebar = new Sidebar(); this.sidebar = new Sidebar();
this.search = new Search();
if (window.config.hawkClientToken) { if (window.config.hawkClientToken) {
this.hawk = new HawkCatcher(window.config.hawkClientToken); this.hawk = new HawkCatcher(window.config.hawkClientToken);
} }

View file

@ -17,6 +17,7 @@ export default class Page {
init() { init() {
this.codeStyler = this.createCodeStyling(); this.codeStyler = this.createCodeStyling();
this.tableOfContent = this.createTableOfContent(); this.tableOfContent = this.createTableOfContent();
this.search = this.createSearch();
} }
/** /**

View file

@ -0,0 +1,238 @@
import { debounce } from '../utils/decorators';
import Shortcut from '@codexteam/shortcuts';
import axios from 'axios';
export default class Search {
constructor() {
this.nodes = {
overlay: null,
searchWrapper: null,
searchInput: null,
searchResultWrapper: null
};
this.isVisible = false;
this.PLACEHOLDER = 'Search docs';
this.TOGGLER_SHORTCUT = 'CMD+SHIFT+F';
this.shortcut = null;
this.DEBOUNCE_TIME = 300;
this.debouncedSearch = null;
this.MIN_SEARCH_LENGTH = 1;
this.CSS = {
overlay: 'search-overlay',
overlayVisible: 'search-overlay--visible',
searchWrapper: 'search-wrapper',
searchInput: 'search-input',
searchResultWrapper: 'search-result-wrapper',
searchResultSuggestions: 'search-result-suggestions',
searchResultSuggestionItem: 'search-result-suggestions-item',
searchResultItem: 'search-result-item',
searchResultItemTitle: 'search-result-item__title',
searchResultItemSection: 'search-result-item__section',
searchResultItemDescription: 'search-result-item__description',
blurred: 'blurred',
noscroll: 'noscroll'
};
}
init(settings = {}, moduleEl) {
console.log('search init');
this.createSearchOverlay();
this.createDebouncedSearch();
this.enableShortcutListening();
/**
* Only for development needs:
*
* - force open overlay
* - type a search string
* - fire search
*/
// const testString = 'codex descri';
// this.toggleSearchOverlay(true);
// this.nodes.searchInput.value = testString;
// this.debouncedSearch(testString);
}
createSearchOverlay() {
this.nodes.overlay = document.createElement('div');
this.nodes.overlay.classList.add(this.CSS.overlay);
this.nodes.overlay.addEventListener('click', this.searchOverlayClickProcessor.bind(this));
this.nodes.searchWrapper = document.createElement('div');
this.nodes.searchWrapper.classList.add(this.CSS.searchWrapper);
this.nodes.searchInput = document.createElement('input');
this.nodes.searchInput.classList.add(this.CSS.searchInput);
this.nodes.searchInput.setAttribute('type', 'search');
this.nodes.searchInput.setAttribute('placeholder', this.PLACEHOLDER);
this.nodes.searchInput.setAttribute('autocomplete', 'off');
this.nodes.searchInput.addEventListener('input', this.searchInputOnchangeProcessor.bind(this));
this.nodes.searchWrapper.appendChild(this.nodes.searchInput);
this.nodes.searchResultWrapper = document.createElement('div');
this.nodes.searchResultWrapper.classList.add(this.CSS.searchResultWrapper);
this.nodes.searchWrapper.appendChild(this.nodes.searchResultWrapper);
this.nodes.overlay.appendChild(this.nodes.searchWrapper);
document.body.appendChild(this.nodes.overlay);
}
searchOverlayClickProcessor(event) {
if (event.target !== this.nodes.overlay) {
return;
}
this.toggleSearchOverlay(false);
}
searchInputOnchangeProcessor(event) {
const text = event.target.value;
if (text.length < this.MIN_SEARCH_LENGTH) {
this.clearSearchResults();
return;
}
this.debouncedSearch(text);
}
enableShortcutListening() {
this.shortcut = new Shortcut({
name : this.TOGGLER_SHORTCUT,
on : document.body,
callback: (event) => {
this.toggleSearchOverlay();
}
});
}
toggleSearchOverlay(force) {
this.isVisible = force || !this.isVisible;
this.nodes.overlay.classList.toggle(this.CSS.overlayVisible, this.isVisible);
document.body.classList.toggle(this.CSS.noscroll, this.isVisible);
// blur everything except search overlay
try {
document.getElementsByClassName('docs-header')[0].classList.toggle(this.CSS.blurred, this.isVisible);
document.getElementsByClassName('docs')[0].classList.toggle(this.CSS.blurred, this.isVisible);
} catch (e) {}
this.nodes.searchInput.focus();
this.nodes.searchInput.select();
}
createDebouncedSearch() {
this.debouncedSearch = debounce(this.getSearchResults, this.DEBOUNCE_TIME);
}
getSearchResults(text) {
axios.get('/api/search', {
params: {
text
}
})
.then(this.showSearchResult.bind(this));
}
clearSearchResults() {
this.nodes.searchResultWrapper.innerHTML = '';
}
showSearchResult({ data }) {
this.clearSearchResults();
// if (data.result.suggestions.length) {
// this.showSuggestedWordCompletion(data.result.suggestions[0]);
// }
// const suggestionsWrapper = this.generateSearchSuggestions(data.result.suggestions);
//
// this.nodes.searchResultWrapper.appendChild(suggestionsWrapper);
data.result.pages.forEach(page => {
const url = `/${page.uri}` + (page.section ? `#${page.anchor}` : '');
const result = document.createElement('a');
result.classList.add(this.CSS.searchResultItem);
result.setAttribute('href', url);
result.addEventListener('click', this.toggleSearchOverlay.bind(this, false));
const title = document.createElement('div');
title.classList.add(this.CSS.searchResultItemTitle);
title.innerHTML = page.title;
result.appendChild(title);
if (page.section && page.section !== page.title) {
const section = document.createElement('span');
section.classList.add(this.CSS.searchResultItemSection);
section.innerHTML = `${page.section}`;
title.appendChild(section);
}
const description = document.createElement('div');
description.classList.add(this.CSS.searchResultItemDescription);
description.innerHTML = `${page.shortBody}`;
result.appendChild(description);
this.nodes.searchResultWrapper.appendChild(result);
});
}
// showSuggestedWordCompletion(word) {
// const typedString = this.nodes.searchInput.value;
// const words = typedString.split(' ');
//
// words.pop();
// words.push(word);
//
// this.nodes.searchInput.value = words.join(' ');
//
// this.nodes.searchInput.select();
// this.nodes.searchInput.selectionStart = typedString.length;
// this.nodes.searchInput.selectionEnd = this.nodes.searchInput.value.length;
// }
// generateSearchSuggestions(suggestions = []) {
// const suggestionsWrapper = document.createElement('div');
//
// suggestionsWrapper.classList.add(this.CSS.searchResultSuggestions);
//
// suggestions.forEach(suggestion => {
// const suggestionItem = document.createElement('span');
//
// suggestionItem.classList.add(this.CSS.searchResultSuggestionItem);
// suggestionItem.innerHTML = suggestion;
// suggestionItem.addEventListener('click', this.searchSuggestionClickProcessor.bind(this));
//
// suggestionsWrapper.appendChild(suggestionItem);
// });
//
// return suggestionsWrapper;
// }
//
// searchSuggestionClickProcessor(event) {
// const word = event.target.innerHTML;
//
// const searchString = this.nodes.searchInput.value;
// const searchStringWords = searchString.split(' ');
//
// searchStringWords.pop();
// searchStringWords.push(word);
//
// this.nodes.searchInput.value = searchStringWords.join(' ');
//
// this.debouncedSearch(this.nodes.searchInput.value);
// }
}

View file

@ -0,0 +1,119 @@
.search {
&-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
overflow-y: scroll;
z-index: 10;
cursor: pointer;
&--visible {
display: block;
}
}
&-wrapper {
background: #fff;
margin: 30vh auto 20vh;
max-width: var(--layout-width-main-col);
width: 100%;
border-radius: 8px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
&-input {
width: 100%;
background: #bbb;
padding: 15px 25px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
outline: none;
background-color: transparent;
&::placeholder {
font-weight: 500;
}
}
&-result {
&-wrapper {
}
&-suggestions {
padding: 5px 25px;
overflow-x: scroll;
&-item {
margin-right: 12px;
padding: 2px 8px;
background: #ccedf6;
border-radius: 50px;
border: 1px solid #b5d8e2;
&:hover {
background: #aad4e0;
}
}
}
&-item {
padding: 15px 25px;
display: block;
&:nth-child(odd) {
background: #f5f5f5;
}
&:hover {
background: #ddd;
}
&__title {
font-weight: 500;
}
&__section {
opacity: 0.7;
&:before {
content: '•';
margin: 0 5px;
text-align: center;
}
}
&__description {
margin-top: 8px;
line-height: 1.5;
}
}
}
&-word {
color: var(--color-link-active);
}
}
.noscroll {
overflow: hidden;
}
.blurred {
filter: blur(3px);
}

View file

@ -13,6 +13,7 @@
@import './components/sidebar.pcss'; @import './components/sidebar.pcss';
@import './components/navigator.pcss'; @import './components/navigator.pcss';
@import './components/table-of-content.pcss'; @import './components/table-of-content.pcss';
@import './components/search.pcss';
body { body {
font-family: system-ui, Helvetica, Arial, Verdana; font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -1732,6 +1732,14 @@ axios@^0.21.1:
dependencies: dependencies:
follow-redirects "^1.14.0" follow-redirects "^1.14.0"
axios@^0.27.2:
version "0.27.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
dependencies:
follow-redirects "^1.14.9"
form-data "^4.0.0"
babel-eslint@^10.0.1: babel-eslint@^10.0.1:
version "10.1.0" version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@ -2080,7 +2088,8 @@ clone-response@^1.0.2:
clone@2.x: clone@2.x:
version "2.1.2" version "2.1.2"
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
code-point-at@^1.0.0: code-point-at@^1.0.0:
version "1.1.0" version "1.1.0"
@ -3162,7 +3171,7 @@ flatten@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
follow-redirects@^1.14.0: follow-redirects@^1.14.0, follow-redirects@^1.14.9:
version "1.15.1" version "1.15.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
@ -3193,6 +3202,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
formdata-polyfill@^4.0.10: formdata-polyfill@^4.0.10:
version "4.0.10" version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
@ -4310,7 +4328,8 @@ nise@^5.1.1:
node-cache@^5.1.2: node-cache@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies: dependencies:
clone "2.x" clone "2.x"