Made tests for search, all works

This commit is contained in:
Zechariah 2021-12-24 13:13:56 +08:00
parent fe73e1d5ab
commit 779035391c
20 changed files with 7860 additions and 182 deletions

10
.vscode/launch.json vendored
View File

@ -23,6 +23,16 @@
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "node", "runtimeExecutable": "node",
"runtimeArgs": ["--require", "ts-node/register/files"] "runtimeArgs": ["--require", "ts-node/register/files"]
},
{
"type": "node",
"request": "launch",
"name": "tests/testing.ts",
"program": "${workspaceFolder}/src/tests/testing.ts",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "node",
"runtimeArgs": ["--require", "ts-node/register/files"]
} }
] ]
} }

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@
"http-debug": "^0.1.2", "http-debug": "^0.1.2",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.4.3", "typescript": "^4.4.3",
"validate-any": "^1.1.1",
"youtube-music-api": "^1.0.6" "youtube-music-api": "^1.0.6"
}, },
"keywords": [ "keywords": [

View File

@ -1,8 +1,12 @@
import ArtistParser from "./utils/ArtistParser" import AlbumParser from "./parsers/AlbumParser"
import ArtistParser from "./parsers/ArtistParser"
import axios, { AxiosInstance } from "axios" import axios, { AxiosInstance } from "axios"
import fs from "fs" import fs from "fs"
import SearchParser from "./utils/SearchParser" import PlaylistParser from "./parsers/PlaylistParser"
import SearchParser from "./parsers/SearchParser"
import SongParser from "./parsers/SongParser"
import traverse from "./utils/traverse" import traverse from "./utils/traverse"
import VideoParser from "./parsers/VideoParser"
import { Cookie, CookieJar } from "tough-cookie" import { Cookie, CookieJar } from "tough-cookie"
export default class YTMusic { export default class YTMusic {
@ -223,7 +227,7 @@ export default class YTMusic {
public async search(query: string): Promise<YTMusic.SearchResult[]> public async search(query: string): Promise<YTMusic.SearchResult[]>
public async search(query: string, category?: string) { public async search(query: string, category?: string) {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query: query, query,
params: params:
{ {
SONG: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D", SONG: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D",
@ -234,16 +238,15 @@ export default class YTMusic {
}[category!] || null }[category!] || null
}) })
const searchParser = new SearchParser(searchData) return traverse(searchData, "musicResponsiveListItemRenderer").map(
return (
{ {
SONG: searchParser.parseSongs, SONG: SongParser.parseSearch,
VIDEO: searchParser.parseVideos, VIDEO: VideoParser.parseSearch,
ARTIST: searchParser.parseArtists, ARTIST: ArtistParser.parseSearch,
ALBUM: searchParser.parseAlbums, ALBUM: AlbumParser.parseSearch,
PLAYLIST: searchParser.parsePlaylists PLAYLIST: PlaylistParser.parseSearch
}[category!] || searchParser.parse }[category!] || SearchParser.parse
).call(searchParser) )
} }
public async getSong(videoId: string) { public async getSong(videoId: string) {
@ -301,17 +304,3 @@ export default class YTMusic {
fs.writeFileSync("data.json", JSON.stringify(data)) fs.writeFileSync("data.json", JSON.stringify(data))
} }
} }
const ytmusicapi = new YTMusic()
ytmusicapi.initialize().then(async () => {
console.log("Initialized")
const artistDetailed = (await ytmusicapi.search("Roundworm", "ARTIST"))[0]
const artistFull = await ytmusicapi.getArtist(artistDetailed.artistId)
console.log(JSON.stringify(artistFull, null, 4))
// const artistDetailed = (await ytmusicapi.search("IU Lilac", "ARTIST"))[0]
// const albumDetailed = (await ytmusicapi.getArtistAlbums(artistDetailed.artistId))[0]
// const albumFull = await ytmusicapi.getAlbum(albumDetailed.albumId)
// console.log(JSON.stringify(albumFull))
})

8
src/index.ts Normal file
View File

@ -0,0 +1,8 @@
import YTMusic from "./YTMusic"
const ytmusic = new YTMusic()
ytmusic.initialize().then(() => {
ytmusic.search("Yours Raiden", "ALBUM").then(res => {
console.log(JSON.stringify(res, null, 4))
})
})

View File

@ -0,0 +1,21 @@
import traverse from "../utils/traverse"
import fs from "fs"
export default class AlbumParser {
public static parseSearch(item: any): YTMusic.AlbumDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1),
playlistId: traverse(item, "overlay", "playlistId"),
artists: traverse(flexColumns[1], "runs")
.filter((run: any) => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
name: traverse(flexColumns[0], "runs", "text"),
year: +traverse(flexColumns[1], "runs", "text").at(-1),
thumbnails: [thumbnails].flat()
}
}
}

View File

@ -1,5 +1,5 @@
import Parse from "./Parser" import Parse from "./Parser"
import traverse from "./traverse" import traverse from "../utils/traverse"
export default class ArtistParser { export default class ArtistParser {
private data: any private data: any
@ -8,8 +8,20 @@ export default class ArtistParser {
this.data = data this.data = data
} }
public static parseSearch(item: any): YTMusic.ArtistDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "ARTIST",
artistId: traverse(item, "browseId"),
name: traverse(flexColumns[0], "runs", "text"),
thumbnails: [thumbnails].flat()
}
}
public parse(artistId: string): YTMusic.ArtistFull { public parse(artistId: string): YTMusic.ArtistFull {
const artistBasic: YTMusic.ArtistBasic = { const artistBasic = {
artistId, artistId,
name: traverse(this.data, "header", "title", "text").at(0) name: traverse(this.data, "header", "title", "text").at(0)
} }

View File

@ -0,0 +1,21 @@
import traverse from "../utils/traverse"
export default class PlaylistParser {
public static parseSearch(item: any, specific: boolean): YTMusic.PlaylistDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
const artistId = traverse(flexColumns[1], "browseId")
return {
type: "PLAYLIST",
playlistId: traverse(item, "overlay", "playlistId"),
name: traverse(flexColumns[0], "runs", "text"),
artist: {
artistId: artistId instanceof Array ? undefined : artistId,
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
},
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
thumbnails: [thumbnails].flat()
}
}
}

View File

@ -0,0 +1,30 @@
import AlbumParser from "./AlbumParser"
import ArtistParser from "./ArtistParser"
import PlaylistParser from "./PlaylistParser"
import SongParser from "./SongParser"
import traverse from "../utils/traverse"
import VideoParser from "./VideoParser"
export default class SearchParser {
public static parse(item: any): YTMusic.SearchResult {
const flexColumns = traverse(item, "flexColumns")
const type = traverse(flexColumns[1], "runs", "text").at(0) as
| "Song"
| "Video"
| "Artist"
| "EP"
| "Single"
| "Album"
| "Playlist"
return {
Song: () => SongParser.parseSearch(item),
Video: () => VideoParser.parseSearch(item, true),
Artist: () => ArtistParser.parseSearch(item),
EP: () => AlbumParser.parseSearch(item),
Single: () => AlbumParser.parseSearch(item),
Album: () => AlbumParser.parseSearch(item),
Playlist: () => PlaylistParser.parseSearch(item, true)
}[type]()
}
}

29
src/parsers/SongParser.ts Normal file
View File

@ -0,0 +1,29 @@
import Parser from "./Parser"
import traverse from "../utils/traverse"
export default class SongParser {
public static parseSearch(item: any): YTMusic.SongDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "SONG",
videoId: traverse(item, "playlistItemData", "videoId"),
name: traverse(flexColumns[0], "runs", "text"),
artists: traverse(flexColumns[1], "runs")
.map((run: any) =>
"navigationEndpoint" in run
? { name: run.text, artistId: traverse(run, "browseId") }
: null
)
.slice(0, -3)
.filter(Boolean),
album: {
albumId: traverse(item, "browseId").at(-1),
name: traverse(flexColumns[1], "runs", "text").at(-3)
},
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
thumbnails: thumbnails
}
}
}

View File

@ -0,0 +1,21 @@
import Parser from "./Parser"
import traverse from "../utils/traverse"
export default class VideoParser {
public static parseSearch(item: any, specific: boolean): YTMusic.VideoDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "VIDEO",
videoId: traverse(item, "playNavigationEndpoint", "videoId"),
name: traverse(flexColumns[0], "runs", "text"),
artists: traverse(flexColumns[1], "runs")
.filter((run: any) => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)),
duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)),
thumbnails: [thumbnails].flat()
}
}
}

96
src/tests/interfaces.ts Normal file
View File

@ -0,0 +1,96 @@
import ObjectValidator from "validate-any/build/validators/ObjectValidator"
import { LIST, NUMBER, OBJECT, OR, STRING, UNDEFINED } from "validate-any"
export const THUMBNAIL_FULL: ObjectValidator<YTMusic.ThumbnailFull> = OBJECT({
url: STRING(),
width: NUMBER(),
height: NUMBER()
})
export const ARTIST_BASIC: ObjectValidator<YTMusic.ArtistBasic> = OBJECT({
artistId: OR(STRING(), UNDEFINED()),
name: STRING()
})
export const ALBUM_BASIC: ObjectValidator<YTMusic.AlbumBasic> = OBJECT({
albumId: STRING(),
name: STRING()
})
export const SONG_DETAILED: ObjectValidator<YTMusic.SongDetailed> = OBJECT({
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const VIDEO_DETAILED: ObjectValidator<YTMusic.VideoDetailed> = OBJECT({
type: STRING("VIDEO"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
views: NUMBER(),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const ARTIST_DETAILED: ObjectValidator<YTMusic.ArtistDetailed> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const ALBUM_DETAILED: ObjectValidator<YTMusic.AlbumDetailed> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const ARTIST_FULL: ObjectValidator<YTMusic.ArtistFull> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
subscribers: NUMBER(),
topTracks: LIST(
OBJECT({
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
thumbnails: LIST(THUMBNAIL_FULL)
})
),
topAlbums: LIST(ALBUM_DETAILED)
})
export const ALBUM_FULL: ObjectValidator<YTMusic.AlbumFull> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
tracks: LIST(SONG_DETAILED)
})
export const PLAYLIST_DETAILED: ObjectValidator<YTMusic.PlaylistDetailed> = OBJECT({
type: STRING("PLAYLIST"),
playlistId: STRING(),
name: STRING(),
artist: ARTIST_BASIC,
trackCount: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})

40
src/tests/testing.ts Normal file
View File

@ -0,0 +1,40 @@
import Validator from "validate-any/build/classes/Validator"
import YTMusic from "../YTMusic"
import {
ALBUM_DETAILED,
ARTIST_DETAILED,
PLAYLIST_DETAILED,
SONG_DETAILED,
VIDEO_DETAILED
} from "./interfaces"
import { LIST, validate } from "validate-any"
const ytmusic = new YTMusic()
const tests: (query: string) => [() => Promise<any>, Validator<any>][] = query => [
[() => ytmusic.search(query, "SONG"), LIST(SONG_DETAILED)],
[() => ytmusic.search(query, "VIDEO"), LIST(VIDEO_DETAILED)],
[() => ytmusic.search(query, "ARTIST"), LIST(ARTIST_DETAILED)],
[() => ytmusic.search(query, "ALBUM"), LIST(ALBUM_DETAILED)],
[() => ytmusic.search(query, "PLAYLIST"), LIST(PLAYLIST_DETAILED)],
[
() => ytmusic.search(query),
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_DETAILED, SONG_DETAILED, VIDEO_DETAILED)
]
]
const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem"]
ytmusic.initialize().then(async () => {
queries.forEach(query => {
tests(query).forEach(async ([fetch, validator]) => {
const value = await fetch()
const result = validate(value, validator)
if (!result.success) {
console.log(JSON.stringify(value))
console.log(validator.formatSchema())
console.log(result.errors)
process.exit(0)
}
})
})
})

9
src/types.d.ts vendored
View File

@ -19,26 +19,27 @@ declare namespace YTMusic {
type: "VIDEO" type: "VIDEO"
videoId: string videoId: string
name: string name: string
artist: ArtistBasic artists: ArtistBasic[]
views: number views: number
duration: number duration: number
thumbnails: ThumbnailFull[] thumbnails: ThumbnailFull[]
} }
interface ArtistBasic { interface ArtistBasic {
artistId: string artistId?: string
name: string name: string
} }
interface ArtistDetailed extends ArtistBasic { interface ArtistDetailed extends ArtistBasic {
type: "ARTIST" type: "ARTIST"
artistId: string
thumbnails: ThumbnailFull[] thumbnails: ThumbnailFull[]
} }
interface ArtistFull extends ArtistDetailed { interface ArtistFull extends ArtistDetailed {
description: string description: string
subscribers: number subscribers: number
topTracks: (Omit<SongDetailed, "duration">)[] topTracks: Omit<SongDetailed, "duration">[]
topAlbums: AlbumDetailed[] topAlbums: AlbumDetailed[]
} }
@ -57,7 +58,7 @@ declare namespace YTMusic {
interface AlbumFull extends AlbumDetailed { interface AlbumFull extends AlbumDetailed {
description: string description: string
tracks: [] tracks: SongDetailed[]
} }
interface PlaylistDetailed { interface PlaylistDetailed {

View File

@ -1,148 +0,0 @@
import Parse from "./Parser"
import traverse from "./traverse"
export default class SearchParser {
private items: any[]
public constructor(data: any) {
this.items = [traverse(data, "musicResponsiveListItemRenderer")].flat()
}
public parseSongs(): YTMusic.SongDetailed[] {
return this.items.map(item => this.parseSong(item))
}
private parseSong(item: any): YTMusic.SongDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "SONG",
videoId: traverse(item, "playlistItemData", "videoId"),
name: traverse(flexColumns[0], "runs", "text"),
artists: traverse(flexColumns[1], "runs")
.map((run: any) =>
"navigationEndpoint" in run
? { name: run.text, artistId: traverse(run, "browseId") }
: null
)
.slice(0, -3)
.filter(Boolean),
album: {
albumId: traverse(item, "browseId").at(-1),
name: traverse(flexColumns[1], "runs", "text").at(-3)
},
duration: Parse.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
thumbnails: [thumbnails].flat()
}
}
public parseVideos(): YTMusic.VideoDetailed[] {
return this.items.map(item => this.parseVideo(item, true))
}
private parseVideo(item: any, specific: boolean): YTMusic.VideoDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "VIDEO",
videoId: traverse(item, "playNavigationEndpoint", "videoId"),
name: traverse(flexColumns[0], "runs", "text"),
artist: {
artistId: traverse(flexColumns[1], "browseId"),
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
},
views: Parse.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)),
duration: Parse.parseDuration(traverse(flexColumns[1], "text").at(-1)),
thumbnails: [thumbnails].flat()
}
}
public parseArtists(): YTMusic.ArtistDetailed[] {
return this.items.map(item => this.parseArtist(item))
}
private parseArtist(item: any): YTMusic.ArtistDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "ARTIST",
artistId: traverse(item, "browseId"),
name: traverse(flexColumns[0], "runs", "text"),
thumbnails: [thumbnails].flat()
}
}
public parseAlbums(): YTMusic.AlbumDetailed[] {
return this.items.map(item => this.parseAlbum(item))
}
private parseAlbum(item: any): YTMusic.AlbumDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "ALBUM",
albumId: traverse(item, "browseId").at(-1),
playlistId: traverse(item, "overlay", "playlistId"),
artists: traverse(flexColumns[1], "runs")
.map((run: any) =>
"navigationEndpoint" in run
? { name: run.text, artistId: traverse(run, "browseId") }
: null
)
.slice(0, -1)
.filter(Boolean),
name: traverse(flexColumns[0], "runs", "text"),
year: +traverse(flexColumns[1], "runs", "text").at(-1),
thumbnails: [thumbnails].flat()
}
}
public parsePlaylists(): YTMusic.PlaylistDetailed[] {
return this.items.map(item => this.parsePlaylist(item, true))
}
private parsePlaylist(item: any, specific: boolean): YTMusic.PlaylistDetailed {
const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails")
return {
type: "PLAYLIST",
playlistId: traverse(item, "overlay", "playlistId"),
name: traverse(flexColumns[0], "runs", "text"),
artist: {
artistId: traverse(flexColumns[1], "browseId"),
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
},
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
thumbnails: [thumbnails].flat()
}
}
public parse(): YTMusic.SearchResult[] {
return this.items.map(item => {
const flexColumns = traverse(item, "flexColumns")
const type = traverse(flexColumns[1], "runs", "text").at(0) as
| "Song"
| "Video"
| "Artist"
| "EP"
| "Single"
| "Album"
| "Playlist"
return {
Song: () => this.parseSong(item),
Video: () => this.parseVideo(item, true),
Artist: () => this.parseArtist(item),
EP: () => this.parseAlbum(item),
Single: () => this.parseAlbum(item),
Album: () => this.parseAlbum(item),
Playlist: () => this.parsePlaylist(item, true)
}[type]()
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
const traverse = (data: any, keys: string, single = false) => {
const again = (data: any, key: string): any => {
var res = []
data.hasOwnProperty(key) && res.push(data[key])
if (single && data.hasOwnProperty(key)) {
return res.shift()
}
if (data instanceof Array) {
for (let i = 0; i < data.length; i++) {
res = res.concat(again(data[i], key))
}
} else if (data instanceof Object) {
const c = Object.keys(data)
if (c.length > 0) {
for (let i = 0; i < c.length; i++) {
res = res.concat(again(data[c[i]], key))
}
}
}
return res.length == 1 ? res.shift() : res
}
let z = keys.split(":"),
value = data
for (let i = 0; i < z.length; i++) {
value = again(value, z[i])
}
console.log(value)
}
traverse(require("./data.json"), "playNavigationEndpoint:videoId")
export default {}

View File

@ -0,0 +1,33 @@
const traverse = (data: any, keys: string[], single: boolean = false) => {
const again = (data: any, key: string): any => {
let res = []
if (data instanceof Object && key in data) {
if (single) return data[key]
res.push(data[key])
}
if (data instanceof Array) {
res.push(...data.map(v => again(v, key)).flat())
} else if (data instanceof Object) {
res.push(
...Object.keys(data)
.map(k => again(data[k], key))
.flat()
)
}
return res
}
let value = data
for (const key of keys) {
value = again(value, key)
}
return value
}
traverse(require("./data.json")[0], ["playNavigationEndpoint", "videoId"], true)
export default {}

162
yarn.lock
View File

@ -44,6 +44,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40"
integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg== integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
acorn-walk@^8.1.1: acorn-walk@^8.1.1:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
@ -73,6 +78,37 @@ axios@^0.24.0:
dependencies: dependencies:
follow-redirects "^1.14.4" follow-redirects "^1.14.4"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
commander@^2.19.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
config-chain@^1.1.12:
version "1.1.13"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==
dependencies:
ini "^1.3.4"
proto-list "~1.2.1"
create-require@^1.1.0: create-require@^1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
@ -90,6 +126,16 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
editorconfig@^0.15.3:
version "0.15.3"
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==
dependencies:
commander "^2.19.0"
lru-cache "^4.1.5"
semver "^5.6.0"
sigmund "^1.0.1"
follow-redirects@1.5.10: follow-redirects@1.5.10:
version "1.5.10" version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
@ -102,26 +148,115 @@ follow-redirects@^1.14.4:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
http-debug@^0.1.2: http-debug@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/http-debug/-/http-debug-0.1.2.tgz#aadbfef99bc39c206439ece14b99040c5a4b4d6e" resolved "https://registry.yarnpkg.com/http-debug/-/http-debug-0.1.2.tgz#aadbfef99bc39c206439ece14b99040c5a4b4d6e"
integrity sha1-qtv++ZvDnCBkOezhS5kEDFpLTW4= integrity sha1-qtv++ZvDnCBkOezhS5kEDFpLTW4=
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@^1.3.4:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
js-beautify@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d"
integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==
dependencies:
config-chain "^1.1.12"
editorconfig "^0.15.3"
glob "^7.1.3"
nopt "^5.0.0"
lodash@^4.17.15: lodash@^4.17.15:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lru-cache@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
make-error@^1.1.1: make-error@^1.1.1:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
psl@^1.1.33: psl@^1.1.33:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@ -132,6 +267,16 @@ 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==
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
sigmund@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
tough-cookie@^4.0.0: tough-cookie@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
@ -169,6 +314,23 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
validate-any@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/validate-any/-/validate-any-1.1.1.tgz#0845d9e8f2b44e342633e6a73ffa21973b71507f"
integrity sha512-L3bCSEn/eH/mdPp+hFQg4nTBeMr19V4kbKMxMSOwRlbMA6N6LWi52Duz0gMQK9gJkz8VbMcvx5Yka2u9J8epjQ==
dependencies:
js-beautify "^1.14.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yn@3.1.1: yn@3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"