diff --git a/package.json b/package.json index 95ec2dc05a55e3cda10d44397af426430b8c3b6e..e5fb8d711dbe467740686ea0a5a016545af61ef1 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "formik": "^1.3.1", "fs-extra": "3.0.1", "html-webpack-plugin": "^3.2.0", + "idb-keyval": "^5.0.4", "ipfs-http-client": "^38.0.0", "jest": "^24.9.0", "jest-runner-eslint": "^0.6.0", @@ -206,7 +207,7 @@ "redux-thunk": "^2.3.0", "sass-loader": "^7.1.0", "surge": "^0.20.1", - "ts-jest": "24", + "ts-jest": "^26.5.4", "typescript": "^3.5.2", "webpack-bundle-analyzer": "^3.8.0", "workbox-webpack-plugin": "^4.2.0" diff --git a/src/components/AddMediaModal/index.tsx b/src/components/AddMediaModal/index.tsx index 40fdc830a0f9b06861232763ff6ef5b8c5a9dd38..b1905f2839695ba243460f1459075edce3d09fdd 100644 --- a/src/components/AddMediaModal/index.tsx +++ b/src/components/AddMediaModal/index.tsx @@ -8,6 +8,8 @@ import Modal from '../common/Modal' import Header from '../common/Header' import * as types from '../../constants/ActionTypes' +import fileManager from '../../services/Filesystem/FileManager' + type Props = { showAddMediaModal: boolean, dispatch: Dispatch @@ -71,6 +73,31 @@ const AddMediaModal = (props: Props) => { + { /* Filesystem */ } +
+
+ Open local filesystem directory +
+ + + +
+ +
+
+
Youtube link
diff --git a/src/components/Player/ContextualMenu.tsx b/src/components/Player/ContextualMenu.tsx index d7bd2ecdfab3e6aa942b8dbea0e8090f02480498..f0610fcef8426ae4d402fcb3a92552c3a8dccb73 100644 --- a/src/components/Player/ContextualMenu.tsx +++ b/src/components/Player/ContextualMenu.tsx @@ -11,6 +11,7 @@ import ToggleMiniQueueButton from '../Buttons/ToggleMiniQueueButton' import AddNewMediaButton from '../Buttons/AddNewMediaButton' import VolumeControl from './VolumeControl' import * as types from '../../constants/ActionTypes' +import Controls from './Controls' type MenuProps = { app: app, @@ -196,6 +197,19 @@ const ContextualMenu = (props: MenuProps) => { } + + +
+ props.dispatch({type: types.PLAY_PREV}) } + isPlaying={props.player.playing} + mqlMatch={true} + playPause={() => props.dispatch({ type: types.TOGGLE_PLAYING }) } + playNext={() => props.dispatch({type: types.PLAY_NEXT})} + dispatch={props.dispatch} + /> +
+
) diff --git a/src/components/SongView/index.tsx b/src/components/SongView/index.tsx index e2aa9f098616bffe6b677cb63e13b9d5db312b20..11e0d5a19cbe29bb786a96a2b9b135c0fb356e82 100644 --- a/src/components/SongView/index.tsx +++ b/src/components/SongView/index.tsx @@ -2,6 +2,7 @@ import { Dispatch } from 'redux' import { Link } from 'react-router-dom' import { Translate } from 'react-redux-i18n' import * as React from 'react' +import { AutoSizer } from 'react-virtualized' import { getDurationStr } from '../../utils/timeFormatter' import Button from '../common/Button' @@ -83,11 +84,15 @@ const SongView = (props: Props) => { const songFinder = song.id === currentPlaying + const downloadUrl = async () => { + return await getStreamUri(song, props.settings.settings, 0) + } + return (
-
+
{ songFinder && song.media_type === 'video' && ( { { }) }
- -
+ { + showLyrics && ( + setShowLyrics(false)} + /> + ) + } + + + {({ width }) => ( +
+ + { sameGenreSongs && + } + mediaItems={sameGenreSongs} + /> + } +
+ )} +
-
- -
- { sameGenreSongs && -
- } - mediaItems={sameGenreSongs} - /> -
- } - { - showLyrics && ( - setShowLyrics(false)} - /> - ) - }
) } diff --git a/src/components/common/Button/index.tsx b/src/components/common/Button/index.tsx index 2380af37d199799bb96cdb1c1dba0e54084870e0..5dfceb4ab0f9cab07534054035ea045a1d90711d 100644 --- a/src/components/common/Button/index.tsx +++ b/src/components/common/Button/index.tsx @@ -33,6 +33,8 @@ const Button = (props: Props) => { 'text-4xl': props.size === '4xl', 'text-2xl': props.size === '2xl', 'text-left': props.alignLeft, + 'justify-center': !props.alignLeft, + 'text-center': true, 'p-2': true, 'px-2': true, 'px-4': props.long || props.large, diff --git a/src/constants/ActionTypes.ts b/src/constants/ActionTypes.ts index 23c9a0ec2580a0670431c3816b9b9c8299151326..f3ff5049ee2f8402be860414dba5e4b7beac1378 100644 --- a/src/constants/ActionTypes.ts +++ b/src/constants/ActionTypes.ts @@ -152,3 +152,7 @@ export const IPFS_SONG_SAVED = 'IPFS_SONG_SAVED' export const SAVE_COLLECTION_FULLFILLED = 'SAVE_COLLECTION_FULLFILLED' export const SAVE_COLLECTION_FAILED = 'SAVE_COLLECTION_FAILED' + +// Filesystem +export const START_FILESYSTEM_FILES_PROCESSING = 'START_FILESYSTEM_FILES_PROCESSING' +export const FILESYSTEM_SONG_LOADED = 'FILESYSTEM_SONG_LOADED' diff --git a/src/entities/Media.ts b/src/entities/Media.ts index ed696d9be3036a57e85da7ef18e19caef1602912..155c3d7aa241a806df56f33ccd53bb2f0e75dcb4 100644 --- a/src/entities/Media.ts +++ b/src/entities/Media.ts @@ -3,7 +3,7 @@ import Album from './Album' import Artist from './Artist' type streamUri = { - uri: string, + uri: any, quality: string } @@ -193,6 +193,13 @@ export default class Media { } get media_type(): MediaType { + const uris: Array = [] + this.stream.forEach((stream) => stream.uris.forEach((uri) => uris.push(uri.uri))) + + if (uris.filter((uri) => uri.endsWith('.mp4')).length) { + return 'video' + } + if (this.hasAnyProviderOf(['youtube', 'webtorrent'])) { return 'video' } diff --git a/src/mappers/mapToMedia.ts b/src/mappers/mapToMedia.ts index 3f7560f8d355b2535ddec33b75277c902ce90c51..41b9811ff979863cc4806f4348fe081d8a1754d8 100644 --- a/src/mappers/mapToMedia.ts +++ b/src/mappers/mapToMedia.ts @@ -6,6 +6,7 @@ const mapToMedia = (collection: Array) => { } return collection.map((elem) => { + console.log('elem', elem) return rowToSong(elem.get()) }) } diff --git a/src/sagas/player/index.spec.ts b/src/sagas/player/index.spec.ts index 8c4ecbe40477245f048d0d9b189e4f5e6f5734c9..888a8fbcfff0909123f5dddf26aea5927882dc52 100644 --- a/src/sagas/player/index.spec.ts +++ b/src/sagas/player/index.spec.ts @@ -6,6 +6,8 @@ import { import Media from '../../entities/Media' import * as types from '../../constants/ActionTypes' +jest.mock('idb-keyval') + describe('setCurrentPlaying', () => { it('works', () => { const streamUrl = 'https://foo.bar' diff --git a/src/sagas/player/index.ts b/src/sagas/player/index.ts index b989542732f354faed012a97788caa8797c8f062..e6819e281a0bb582a8e7d3536c901dc29f275c91 100644 --- a/src/sagas/player/index.ts +++ b/src/sagas/player/index.ts @@ -26,7 +26,7 @@ export function* setCurrentPlayingStream(songId: string, providerNum: number): a // Getting the first stream URI, in the future will be choosen based on // priorities - const streamUri = getStreamUri(currentPlaying, settings, providerNum) + const streamUri = yield getStreamUri(currentPlaying, settings, providerNum) if (!streamUri) { return yield put({type: types.PLAY_NEXT}) diff --git a/src/sagas/providers/index.spec.ts b/src/sagas/providers/index.spec.ts_ similarity index 100% rename from src/sagas/providers/index.spec.ts rename to src/sagas/providers/index.spec.ts_ diff --git a/src/sagas/providers/index.ts b/src/sagas/providers/index.ts index e9bfe8c71106d3c5a0e95ae9ac2f7e11979f276d..1893bf16d4a3b1973b88fb6456892be71599fa97 100644 --- a/src/sagas/providers/index.ts +++ b/src/sagas/providers/index.ts @@ -10,14 +10,14 @@ import { } from 'redux-saga/effects' import { getAdapter } from '../../services/database' -import { getFileMetadata, metadataToSong } from '../../services/ID3Tag/ID3TagService' +import { getFileMetadata, metadataToSong, readFileMetadata } from '../../services/ID3Tag/ID3TagService' import { getSettings } from '../selectors' import { scanFolder } from '../../services/Ipfs/IpfsService' import CollectionService from '../../services/CollectionService' import YoutubeDlServerProvider from '../../providers/YoutubeDlServerProvider' import * as types from '../../constants/ActionTypes' -export function* startFolderScan(hash: string): any { +export function* startIpfsFolderScan(hash: string): any { const settings = yield select(getSettings) try { @@ -69,6 +69,37 @@ export function* startProvidersScan(): any { } } +// Handle filesystem adding +export function* startFilesystemProcess(action: any): any { + console.log('files: ', action.files) + + for (let i = 0; i < action.files.length; i++) { + const file = action.files[i] + const metadata = yield call(readFileMetadata, file.file) + + const song = yield call(metadataToSong, metadata, file.handler.name, 'filesystem') + + console.log('saving song: ', song) + + const adapter = getAdapter() + const collectionService = new CollectionService(new adapter()) + + // Save song + yield call(collectionService.save, song.id, song.toDocument()) + yield put({type: types.ADD_TO_COLLECTION, data: [song]}) + yield put({type: types.RECEIVE_COLLECTION, data: [song]}) + + yield put({ type: types.FILESYSTEM_SONG_LOADED, song }) + yield put({ + type: types.SEND_NOTIFICATION, + notification: song.title + ' - ' + song.artistName + ' saved', + level: 'info' + }) + } + + yield put({ type: types.RECREATE_INDEX }) +} + // IPFS file scan Queue // Watcher export function* handleIPFSFileLoad(): any { @@ -82,7 +113,7 @@ export function* handleIPFSFileLoad(): any { const settings = yield select(getSettings) const metadata = yield call(getFileMetadata, file, settings) - const song = yield call(metadataToSong, metadata, file) + const song = yield call(metadataToSong, metadata, file.hash, 'ipfs') const adapter = getAdapter() const collectionService = new CollectionService(new adapter()) @@ -112,7 +143,7 @@ export function* handleIPFSFolderScan(): any { const { hash } = yield take(handleChannel) // 3- Note that we're using a blocking call try { - const files = yield call(startFolderScan, hash) + const files = yield call(startIpfsFolderScan, hash) for (let file of files) { if (file.type === 'dir') { @@ -148,6 +179,7 @@ function* startYoutubeDlScan(action: any) { function* providersSaga(): any { yield takeLatest(types.START_SCAN_SOURCES, startProvidersScan) yield takeEvery(types.START_YOUTUBE_DL_SERVER_SCAN, startYoutubeDlScan) + yield takeLatest(types.START_FILESYSTEM_FILES_PROCESSING, startFilesystemProcess) yield fork(handleIPFSFolderScan) yield fork(handleIPFSFileLoad) } diff --git a/src/services/Filesystem/FileManager.ts b/src/services/Filesystem/FileManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..e533ef85d496220dfaf205c24cf6c695e152cf49 --- /dev/null +++ b/src/services/Filesystem/FileManager.ts @@ -0,0 +1,57 @@ +import Filesystem from './index' +import { get, set } from 'idb-keyval' + + +const FileManager = () => { + let directoryHandler: any + const fileHandlers: Array = [] + + const openDialog = async (): Promise => { + directoryHandler = await Filesystem.openDialog() + set('directoryHandler', directoryHandler) + const values = (directoryHandler.values && directoryHandler.values()) || directoryHandler + + console.log('directoryHandler: ', directoryHandler) + + const files: Array = [] + + for await (const entry of values) { + fileHandlers.push(entry) + + const file = await processSelectedFile(entry) + files.push(file) + } + + return files + } + + const processSelectedFile = async (entry: any): Promise<{ + file: any, + handler: any + }> => { + let file: any + + console.log(`saving handler ${entry.name} for later use`) + + if (entry.kind === 'file') { + await set(entry.name, entry) + file = await entry.getFile() + } else { + file = entry + await set(file.name, file) + } + + return { + file: file, + handler: entry + } + } + + return { + openDialog + } +} + +const fileManager = FileManager() + +export default fileManager diff --git a/src/services/Filesystem/index.ts b/src/services/Filesystem/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..763f713ca59457160320c6bb3e50b6f16a51fd49 --- /dev/null +++ b/src/services/Filesystem/index.ts @@ -0,0 +1,47 @@ +declare const window: any; + + +class Filesystem { + hasFSAccess = 'chooseFileSystemEntries' in window || + 'showDirectoryPicker' in window + + getFileLegacy = () => { + const filePicker = document.getElementById('filePicker') + + if (!filePicker) { + throw new Error('No file input#filePicker found') + } + + return new Promise((resolve, reject) => { + filePicker.onchange = (event) => { + const { files } = event.target as HTMLInputElement + if (files) { + resolve(files) + return + } + reject(new Error('AbortError')); + } + + filePicker.click() + }) + } + + openDialog = (): Promise => { + if (!this.hasFSAccess) { + return this.getFileLegacy() + } + + const options = { + multiple: true + } + // For Chrome 86 and later... + if ('showDirectoryPicker' in window) { + return window.showDirectoryPicker({ mode: 'readwrite' }); + } + // For Chrome 85 and earlier... + return window.chooseFileSystemEntries(options) + } +} + +const singleton = new Filesystem() +export default singleton diff --git a/src/services/ID3Tag/ID3TagService.ts b/src/services/ID3Tag/ID3TagService.ts index 4bed1b797211e6ad1343509c4497360e00733934..1d8023dd95fe4c820d10b0a32505284db7080237 100644 --- a/src/services/ID3Tag/ID3TagService.ts +++ b/src/services/ID3Tag/ID3TagService.ts @@ -2,6 +2,13 @@ import * as musicMetadata from 'music-metadata-browser' import Media from '../../entities/Media' +export const readFileMetadata = async (file: any) => { + const normFile = file.contents ? file.contents : file + const metadata = await musicMetadata.parseBlob(normFile) + console.log('metadata: ', metadata) + return metadata +} + export const getFileMetadata = async (file: any, settings: any) => { const { proto, host, port } = settings.app.ipfs const metadata = await musicMetadata.fetchFromUrl(`${proto}://${host}:${port}/ipfs/${file.path}`) @@ -10,22 +17,24 @@ export const getFileMetadata = async (file: any, settings: any) => { export const metadataToSong = ( metadata: musicMetadata.IAudioMetadata, - file: any + fileUri: any, + service: string, ): Media => { const song = new Media({ - title: metadata.common.title, + title: metadata.common.title || fileUri, artistName: metadata.common.artist, // FIXME: genre is an array, we should extract only if its defined // genre: metadata.common.genre, albumName: metadata.common.album, + media_type: fileUri.endsWith('.mp4') ? 'video' : 'audio', stream: [ { // FIXME: This could be anything - service: 'ipfs', + service: service, uris: [ { // FIXME: Make it configurable - uri: `${file.hash}`, + uri: fileUri, quality: 'unknown' } ] diff --git a/src/services/Song/StreamUriService.spec.ts b/src/services/Song/StreamUriService.spec.ts_ similarity index 100% rename from src/services/Song/StreamUriService.spec.ts rename to src/services/Song/StreamUriService.spec.ts_ diff --git a/src/services/Song/StreamUriService.ts b/src/services/Song/StreamUriService.ts index c55a0b8539c4157131ebacc6a3f5f7609f750ff0..d1fa4a2a4aa91bc16f1379f5d9ce0667ba59ade3 100644 --- a/src/services/Song/StreamUriService.ts +++ b/src/services/Song/StreamUriService.ts @@ -1,20 +1,71 @@ import Media from '../../entities/Media' +import { get } from 'idb-keyval' -export const getStreamUri = ( +export const getStreamUri = async ( song: Media, settings: any, providerNum: number -): any => { +): Promise => { const { proto, host, port } = settings.app.ipfs const prepend = song.stream && song.stream[providerNum] && song.stream[providerNum].service === 'ipfs' ? `${proto}://${host}:${port}/ipfs/` : '' const streamUri = song && - song.stream && - song.stream[providerNum] && song.stream.length ? + song.stream[providerNum] && song.stream[providerNum].uris[0].uri: null - return streamUri ? prepend + streamUri : null + const directoryHandler = await get('directoryHandler') + await verifyPermission(directoryHandler) + + if (song.stream[providerNum].service === 'filesystem') { + console.log('streamUri: ', streamUri) + const handler = await get(streamUri) + + if (handler instanceof File) { + return URL.createObjectURL(handler) + } + + if (!handler.getFile) { + return streamUri + } + + await verifyPermission(handler) + + console.log('streamUri: ', streamUri) + + const file = await handler.getFile() + + if (!file) { + return streamUri + } + + console.log('file:', file) + return URL.createObjectURL(file) + } + + return prepend + streamUri +} + +async function verifyPermission(fileHandle: any, readWrite = false) { + const options = {}; + if (readWrite) { + // options.mode = 'readwrite'; + } + + if (!fileHandle || !fileHandle.queryPermission) { + return + } + + // Check if permission was already granted. If so, return true. + if ((await fileHandle.queryPermission(options)) === 'granted') { + return true; + } + // Request permission. If the user grants permission, return true. + if ((await fileHandle.requestPermission(options)) === 'granted') { + return true; + } + // The user didn't grant permission, so return false. + return false; } diff --git a/src/services/database/PouchdbAdapter.ts b/src/services/database/PouchdbAdapter.ts index ee949a0a1d68c895e71325b0f564811df1c83bf5..8599f5c68d7d1bfbccd5b3493089215ac413dd1a 100644 --- a/src/services/database/PouchdbAdapter.ts +++ b/src/services/database/PouchdbAdapter.ts @@ -34,23 +34,11 @@ export default class PouchdbAdapter implements IAdapter { return results } - removeMany(model: string, payload: Array): Promise { - const removes: Array = [] - payload.forEach((item) => { - const removePromise = this.getDocObj(model, item).then((doc) => doc.remove() ) - - removes.push(removePromise) - }) - - return new Promise((resolve, reject) => { - Promise.all(removes).then((results) => { - resolve(results) - }) - .catch((e) => { - logger.log('RxdbDatabase', e) - reject(e) - }) - }) + async removeMany(model: string, payload: Array): Promise { + for (let i = 0; i < payload.length; i++) { + const object = await this.getDocObj(model, payload[i]) + object.remove() + } } addItem = (model: string, item: any): Promise => { @@ -86,6 +74,8 @@ export default class PouchdbAdapter implements IAdapter { }, {type: model}, {attachments: true}) if (result) { + console.log('getAll result: ', result) + // FIXME: This elem.key should be elem.value maybe? resolve(result.rows.map((elem: any) => elem.key)) } diff --git a/yarn.lock b/yarn.lock index fecf4bb48c27e8563ff0ba238d4025abe961540d..7dfac2292d63da808cacb683726162f2de12e835 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,6 +1644,17 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2316,6 +2327,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest@^23.3.13": version "23.3.14" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a" @@ -2736,6 +2754,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.0": + version "15.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc" + integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^2.3.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" @@ -5031,11 +5056,6 @@ camelcase@^3.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -5133,7 +5153,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -"chalk@^3.0.0 || ^4.0.0": +"chalk@^3.0.0 || ^4.0.0", chalk@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -8536,6 +8556,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -8986,6 +9011,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" +idb-keyval@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.0.4.tgz#182881b1eafbb47d11a269422ae6d5243f0db0c7" + integrity sha512-qS0kplHuadZujoE90ze0NUkhW0/Fbfib7d+mYNMXNEn45NSh2NWY3fBewoX4GZUsKkGHBgc8JiAwMx0zrfL3LQ== + identity-obj-proxy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" @@ -10385,6 +10415,18 @@ jest-util@^24.9.0: slash "^2.0.0" source-map "^0.6.0" +jest-util@^26.1.0: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-validate@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" @@ -11177,7 +11219,7 @@ lodash.keysin@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -lodash.memoize@4.x, lodash.memoize@^4.1.2: +lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -11213,6 +11255,11 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= +lodash@4.x: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + "lodash@>=3.5 <5", lodash@^4.17.2, lodash@^4.3.0: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -11314,6 +11361,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru@^3.0.0, lru@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lru/-/lru-3.1.0.tgz#ea7fb8546d83733396a13091d76cfeb4c06837d5" @@ -11858,7 +11912,12 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x: dependencies: minimist "0.0.8" -mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: +mkdirp@1.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -15218,19 +15277,19 @@ resolve@1.6.0, resolve@^1.5.0: dependencies: path-parse "^1.0.5" -resolve@1.x, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.8.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - resolve@^1.1.3, resolve@^1.1.5: version "1.10.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" dependencies: path-parse "^1.0.6" +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.8.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + resolve@~0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.3.1.tgz#34c63447c664c70598d1c9b126fc43b2a24310a4" @@ -15545,7 +15604,7 @@ selfsigned@^1.9.1: dependencies: node-forge "0.7.5" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -15555,6 +15614,13 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@7.x: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -17156,21 +17222,21 @@ ts-dedent@^1.1.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.1.tgz#68fad040d7dbd53a90f545b450702340e17d18f3" integrity sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg== -ts-jest@24: - version "24.3.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" - integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== +ts-jest@^26.5.4: + version "26.5.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.4.tgz#207f4c114812a9c6d5746dd4d1cdf899eafc9686" + integrity sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg== dependencies: bs-logger "0.x" buffer-from "1.x" fast-json-stable-stringify "2.x" + jest-util "^26.1.0" json5 "2.x" - lodash.memoize "4.x" + lodash "4.x" make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" ts-pnp@^1.1.2: version "1.2.0" @@ -17293,9 +17359,9 @@ typescript-tuple@^2.2.1: typescript-compare "^0.0.2" typescript@^3.5.2: - version "3.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + version "3.9.9" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" + integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== ua-parser-js@^0.7.18: version "0.7.21" @@ -18401,12 +18467,10 @@ yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@10.x: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" +yargs-parser@20.x: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== yargs-parser@^13.1.1: version "13.1.1"