@@ -108,3 +108,4 @@ dist | |||||
vendor | vendor | ||||
test/*.webm | test/*.webm | ||||
.idea/ | .idea/ | ||||
scenarios/ |
@@ -13,11 +13,14 @@ | |||||
"pridepack" | "pridepack" | ||||
], | ], | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/dotenv": "^8.2.0", | |||||
"@types/node": "^18.14.1", | "@types/node": "^18.14.1", | ||||
"dotenv": "^16.0.3", | |||||
"eslint": "^8.35.0", | "eslint": "^8.35.0", | ||||
"eslint-config-lxsmnsyc": "^0.5.0", | "eslint-config-lxsmnsyc": "^0.5.0", | ||||
"pridepack": "2.4.2", | "pridepack": "2.4.2", | ||||
"tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
"tsx": "^3.12.6", | |||||
"typescript": "^4.9.5", | "typescript": "^4.9.5", | ||||
"vitest": "^0.28.1" | "vitest": "^0.28.1" | ||||
}, | }, | ||||
@@ -30,7 +33,8 @@ | |||||
"watch": "pridepack watch", | "watch": "pridepack watch", | ||||
"start": "pridepack start", | "start": "pridepack start", | ||||
"dev": "pridepack dev", | "dev": "pridepack dev", | ||||
"test": "vitest" | |||||
"test": "vitest", | |||||
"tsx": "tsx" | |||||
}, | }, | ||||
"private": false, | "private": false, | ||||
"description": "Clip YouTube videos.", | "description": "Clip YouTube videos.", | ||||
@@ -1,53 +1,173 @@ | |||||
import { spawnSync } from 'child_process'; | import { spawnSync } from 'child_process'; | ||||
import { unlinkSync, readFileSync } from 'fs'; | import { unlinkSync, readFileSync } from 'fs'; | ||||
import { EventEmitter } from 'events'; | |||||
export enum VideoType { | export enum VideoType { | ||||
YOUTUBE = 'youtube', | YOUTUBE = 'youtube', | ||||
} | } | ||||
interface ClipVideoBaseParams { | interface ClipVideoBaseParams { | ||||
url: string, | |||||
start: number | string, | |||||
end: number | string, | |||||
url: string; | |||||
start?: number | string; | |||||
end?: number | string; | |||||
} | } | ||||
interface ClipYouTubeVideoParams extends ClipVideoBaseParams { | interface ClipYouTubeVideoParams extends ClipVideoBaseParams { | ||||
downloaderExecutablePath?: string; | downloaderExecutablePath?: string; | ||||
postprocessorExecutablePath?: string; | |||||
} | } | ||||
interface ClipVideoParams extends ClipYouTubeVideoParams { | interface ClipVideoParams extends ClipYouTubeVideoParams { | ||||
type: VideoType, | type: VideoType, | ||||
} | } | ||||
const clipYouTubeVideo = (params: ClipYouTubeVideoParams) => { | |||||
if (!params.downloaderExecutablePath) { | |||||
throw new Error('Downloader not found.'); | |||||
const convertSecondsToDuration = (s: number) => { | |||||
const milliseconds = (s - Math.floor(s)) * 1000; | |||||
const seconds = s % 60; | |||||
const minutes = Math.floor(s / 60) % 60; | |||||
const hours = Math.floor(s / 3600); | |||||
const sss = milliseconds.toString().padStart(3, '0'); | |||||
const ss = seconds.toString().padStart(2, '0'); | |||||
const mm = minutes.toString().padStart(2, '0'); | |||||
const hh = hours.toString().padStart(2, '0'); | |||||
return `${hh}:${mm}:${ss}.${sss}`; | |||||
}; | |||||
const convertDurationToSeconds = (d: string) => { | |||||
const [hh, mm, ss] = d.split(':'); | |||||
return (Number(hh) * 3600) + (Number(mm) * 60) + Number(ss); | |||||
}; | |||||
export interface ClipEventEmitter extends EventEmitter { | |||||
process(): void; | |||||
} | |||||
class YouTubeVideoClipEventEmitter extends EventEmitter implements ClipEventEmitter { | |||||
constructor(private readonly params: ClipYouTubeVideoParams) { | |||||
super(); | |||||
} | } | ||||
spawnSync( | |||||
params.downloaderExecutablePath, | |||||
[ | |||||
'--postprocessor-args', | |||||
`-ss ${params.start.toString()} -to ${params.end.toString()}`, | |||||
'-o', | |||||
process() { | |||||
if (!this.params.downloaderExecutablePath) { | |||||
this.emit('error', new Error('Downloader not found.')); | |||||
this.emit('end', null); | |||||
return; | |||||
} | |||||
if (!this.params.postprocessorExecutablePath) { | |||||
this.emit('error', new Error('ffmpeg not found.')); | |||||
this.emit('end', null); | |||||
return; | |||||
} | |||||
const downloadArgs = [ | |||||
'--youtube-skip-dash-manifest', | |||||
'-f', 'bestvideo+bestaudio', | |||||
'-g', this.params.url, | |||||
]; | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'start', | |||||
command: `${this.params.downloaderExecutablePath} ${downloadArgs.join(' ')}`, | |||||
}); | |||||
const downloaderProcess = spawnSync(this.params.downloaderExecutablePath, downloadArgs); | |||||
if (downloaderProcess.error) { | |||||
this.emit('error', new Error('Downloader error.', { cause: downloaderProcess.error })); | |||||
this.emit('end', null); | |||||
return; | |||||
} | |||||
const [videoUrlStream, audioUrlStream] = downloaderProcess.stdout.toString('utf-8').split('\n'); | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'success', | |||||
streamUrls: { | |||||
video: [videoUrlStream], | |||||
audio: [audioUrlStream], | |||||
}, | |||||
}); | |||||
let startSeconds: number; | |||||
if (typeof this.params.start === 'undefined') { | |||||
startSeconds = 0; | |||||
} else if (typeof this.params.start === 'string') { | |||||
startSeconds = convertDurationToSeconds(this.params.start); | |||||
} else { | |||||
startSeconds = this.params.start; | |||||
} | |||||
const ffmpegProcessVideoStreamArgs = [ | |||||
'-ss', (this.params.start ?? '00:00:00').toString(), | |||||
'-i', videoUrlStream, | |||||
]; | |||||
const ffmpegProcessAudioStreamArgs = [ | |||||
'-ss', (this.params.start ?? '00:00:00').toString(), | |||||
'-i', audioUrlStream, | |||||
]; | |||||
if (typeof this.params.end !== 'undefined') { | |||||
// -to flag is broken on video stream in ffmpeg..... | |||||
const endSeconds = ( | |||||
typeof this.params.end === 'string' | |||||
? convertDurationToSeconds(this.params.end) | |||||
: this.params.end | |||||
); | |||||
const difference = endSeconds - startSeconds; | |||||
const clipDuration = convertSecondsToDuration(difference); | |||||
ffmpegProcessVideoStreamArgs.push('-t'); | |||||
ffmpegProcessVideoStreamArgs.push(clipDuration); | |||||
ffmpegProcessAudioStreamArgs.push('-t'); | |||||
ffmpegProcessAudioStreamArgs.push(clipDuration); | |||||
} | |||||
const ffmpegProcessArgs = [ | |||||
...ffmpegProcessVideoStreamArgs, | |||||
...ffmpegProcessAudioStreamArgs, | |||||
'-map', '0:v', | |||||
'-map', '1:a', | |||||
'output.webm', | 'output.webm', | ||||
params.url, | |||||
], | |||||
); | |||||
const output = readFileSync('output.webm'); | |||||
unlinkSync('output.webm'); | |||||
return output; | |||||
}; | |||||
]; | |||||
this.emit('process', { | |||||
type: 'postprocess', | |||||
phase: 'start', | |||||
command: `${this.params.postprocessorExecutablePath} ${ffmpegProcessArgs.join(' ')}`, | |||||
}); | |||||
const ffmpegProcess = spawnSync(this.params.postprocessorExecutablePath, ffmpegProcessArgs); | |||||
if (ffmpegProcess.error) { | |||||
this.emit('error', new Error('ffmpeg error.', { cause: ffmpegProcess.error })); | |||||
this.emit('end'); | |||||
return; | |||||
} | |||||
const output = readFileSync('output.webm'); | |||||
unlinkSync('output.webm'); | |||||
this.emit('process', { | |||||
type: 'postprocess', | |||||
phase: 'success', | |||||
output, | |||||
}); | |||||
this.emit('end'); | |||||
} | |||||
} | |||||
export const clipVideo = (params: ClipVideoParams) => { | |||||
export const createVideoClipper = (params: ClipVideoParams) => { | |||||
const { | const { | ||||
type: videoType, url, start, end, | |||||
type: videoType, | |||||
url, | |||||
start, | |||||
end, | |||||
downloaderExecutablePath, | downloaderExecutablePath, | ||||
postprocessorExecutablePath, | |||||
} = params; | } = params; | ||||
switch (videoType as string) { | switch (videoType as string) { | ||||
case VideoType.YOUTUBE: | case VideoType.YOUTUBE: | ||||
return clipYouTubeVideo({ | |||||
return new YouTubeVideoClipEventEmitter({ | |||||
downloaderExecutablePath, | downloaderExecutablePath, | ||||
postprocessorExecutablePath, | |||||
url, | url, | ||||
start, | start, | ||||
end, | end, | ||||
@@ -1,9 +1,10 @@ | |||||
import { | import { | ||||
describe, it, expect, vi, beforeEach, Mock, | |||||
describe, it, expect, vi, beforeEach, Mock, afterEach, | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import { spawnSync } from 'child_process'; | import { spawnSync } from 'child_process'; | ||||
import { readFileSync, unlinkSync } from 'fs'; | import { readFileSync, unlinkSync } from 'fs'; | ||||
import * as youtubeClipCore from '../src'; | |||||
import * as webVideoClipCore from '../src'; | |||||
import { ClipEventEmitter } from '../src'; | |||||
vi.mock('child_process', () => ({ | vi.mock('child_process', () => ({ | ||||
spawnSync: vi.fn(), | spawnSync: vi.fn(), | ||||
@@ -14,44 +15,92 @@ vi.mock('fs', () => ({ | |||||
unlinkSync: vi.fn(), | unlinkSync: vi.fn(), | ||||
})); | })); | ||||
describe('clipVideo', () => { | |||||
describe('createVideoClipper', () => { | |||||
beforeEach(() => { | beforeEach(() => { | ||||
(readFileSync as Mock).mockReturnValueOnce(Buffer.from('')); | (readFileSync as Mock).mockReturnValueOnce(Buffer.from('')); | ||||
}); | }); | ||||
it('calls the downloader function', () => { | |||||
youtubeClipCore.clipVideo({ | |||||
let clipper: ClipEventEmitter; | |||||
beforeEach(() => { | |||||
clipper = webVideoClipCore.createVideoClipper({ | |||||
downloaderExecutablePath: 'yt-dlp', | downloaderExecutablePath: 'yt-dlp', | ||||
type: youtubeClipCore.VideoType.YOUTUBE, | |||||
postprocessorExecutablePath: 'ffmpeg', | |||||
type: webVideoClipCore.VideoType.YOUTUBE, | |||||
url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | ||||
start: 0, | start: 0, | ||||
end: 0, | end: 0, | ||||
}); | }); | ||||
}); | |||||
expect(spawnSync).toBeCalledWith('yt-dlp', expect.anything()); | |||||
afterEach(() => { | |||||
(spawnSync as Mock).mockReset(); | |||||
}); | }); | ||||
it('calls the buffer extract function', () => { | |||||
youtubeClipCore.clipVideo({ | |||||
downloaderExecutablePath: 'yt-dlp', | |||||
type: youtubeClipCore.VideoType.YOUTUBE, | |||||
url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | |||||
start: 0, | |||||
end: 0, | |||||
it('calls the downloader function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(spawnSync).nthCalledWith(1, 'yt-dlp', expect.anything()); | |||||
})); | |||||
it('emits downloader errors', () => new Promise((done) => { | |||||
const causeError = new Error('generic downloader message'); | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ error: causeError }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('error', (err: Error) => { | |||||
expect(err.cause).toHaveProperty('message', causeError.message); | |||||
}); | }); | ||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
})); | |||||
expect(readFileSync).toBeCalled(); | |||||
}); | |||||
it('calls the postprocess function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
it('calls the cleanup function', () => { | |||||
youtubeClipCore.clipVideo({ | |||||
downloaderExecutablePath: 'yt-dlp', | |||||
type: youtubeClipCore.VideoType.YOUTUBE, | |||||
url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | |||||
start: 0, | |||||
end: 0, | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(spawnSync).nthCalledWith(2, 'ffmpeg', expect.anything()); | |||||
})); | |||||
it('emits postprocess errors', () => new Promise((done) => { | |||||
const causeError = new Error('generic postprocessor message'); | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({ error: causeError }); | |||||
clipper.on('error', (err: Error) => { | |||||
expect(err.cause).toHaveProperty('message', causeError.message); | |||||
}); | }); | ||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
})); | |||||
it('calls the buffer extract function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(readFileSync).toBeCalled(); | |||||
})); | |||||
it('calls the cleanup function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(unlinkSync).toBeCalled(); | expect(unlinkSync).toBeCalled(); | ||||
}); | |||||
})); | |||||
}); | }); |
@@ -1,15 +0,0 @@ | |||||
const { clipVideo, VideoType } = require('../dist/cjs/development'); | |||||
const fs = require('fs'); | |||||
const dotenv = require('dotenv'); | |||||
dotenv.config(); | |||||
const clippedVideoBuffer = clipVideo({ | |||||
downloaderExecutablePath: process.env.YOUTUBE_DOWNLOADER_EXECUTABLE_PATH, | |||||
type: VideoType.YOUTUBE, | |||||
url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | |||||
start: 0, | |||||
end: 1, | |||||
}); | |||||
fs.writeFileSync('output.webm', clippedVideoBuffer); |
@@ -233,6 +233,30 @@ | |||||
"@babel/helper-validator-identifier" "^7.19.1" | "@babel/helper-validator-identifier" "^7.19.1" | ||||
to-fast-properties "^2.0.0" | to-fast-properties "^2.0.0" | ||||
"@esbuild-kit/cjs-loader@^2.4.2": | |||||
version "2.4.2" | |||||
resolved "https://registry.yarnpkg.com/@esbuild-kit/cjs-loader/-/cjs-loader-2.4.2.tgz#cb4dde00fbf744a68c4f20162ea15a8242d0fa54" | |||||
integrity sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg== | |||||
dependencies: | |||||
"@esbuild-kit/core-utils" "^3.0.0" | |||||
get-tsconfig "^4.4.0" | |||||
"@esbuild-kit/core-utils@^3.0.0": | |||||
version "3.1.0" | |||||
resolved "https://registry.yarnpkg.com/@esbuild-kit/core-utils/-/core-utils-3.1.0.tgz#49945d533dbd5e1b7620aa0fc522c15e6ec089c5" | |||||
integrity sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw== | |||||
dependencies: | |||||
esbuild "~0.17.6" | |||||
source-map-support "^0.5.21" | |||||
"@esbuild-kit/esm-loader@^2.5.5": | |||||
version "2.5.5" | |||||
resolved "https://registry.yarnpkg.com/@esbuild-kit/esm-loader/-/esm-loader-2.5.5.tgz#b82da14fcee3fc1d219869756c06f43f67d1ca71" | |||||
integrity sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw== | |||||
dependencies: | |||||
"@esbuild-kit/core-utils" "^3.0.0" | |||||
get-tsconfig "^4.4.0" | |||||
"@esbuild/android-arm64@0.17.12": | "@esbuild/android-arm64@0.17.12": | ||||
version "0.17.12" | version "0.17.12" | ||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz#15a8e2b407d03989b899e325151dc2e96d19c620" | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz#15a8e2b407d03989b899e325151dc2e96d19c620" | ||||
@@ -508,6 +532,13 @@ | |||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" | ||||
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== | integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== | ||||
"@types/dotenv@^8.2.0": | |||||
version "8.2.0" | |||||
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053" | |||||
integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw== | |||||
dependencies: | |||||
dotenv "*" | |||||
"@types/json-schema@^7.0.9": | "@types/json-schema@^7.0.9": | ||||
version "7.0.11" | version "7.0.11" | ||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" | ||||
@@ -1180,7 +1211,7 @@ dot-prop@^5.2.0: | |||||
dependencies: | dependencies: | ||||
is-obj "^2.0.0" | is-obj "^2.0.0" | ||||
dotenv@^16.0.3: | |||||
dotenv@*, dotenv@^16.0.3: | |||||
version "16.0.3" | version "16.0.3" | ||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" | ||||
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== | integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== | ||||
@@ -1293,7 +1324,7 @@ es-to-primitive@^1.2.1: | |||||
is-date-object "^1.0.1" | is-date-object "^1.0.1" | ||||
is-symbol "^1.0.2" | is-symbol "^1.0.2" | ||||
esbuild@^0.17.4, esbuild@^0.17.5: | |||||
esbuild@^0.17.4, esbuild@^0.17.5, esbuild@~0.17.6: | |||||
version "0.17.12" | version "0.17.12" | ||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.12.tgz#2ad7523bf1bc01881e9d904bc04e693bd3bdcf2f" | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.12.tgz#2ad7523bf1bc01881e9d904bc04e693bd3bdcf2f" | ||||
integrity sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ== | integrity sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ== | ||||
@@ -1807,7 +1838,7 @@ get-symbol-description@^1.0.0: | |||||
call-bind "^1.0.2" | call-bind "^1.0.2" | ||||
get-intrinsic "^1.1.1" | get-intrinsic "^1.1.1" | ||||
get-tsconfig@^4.2.0: | |||||
get-tsconfig@^4.2.0, get-tsconfig@^4.4.0: | |||||
version "4.4.0" | version "4.4.0" | ||||
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.4.0.tgz#64eee64596668a81b8fce18403f94f245ee0d4e5" | resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.4.0.tgz#64eee64596668a81b8fce18403f94f245ee0d4e5" | ||||
integrity sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ== | integrity sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ== | ||||
@@ -3240,6 +3271,17 @@ tsutils@^3.21.0: | |||||
dependencies: | dependencies: | ||||
tslib "^1.8.1" | tslib "^1.8.1" | ||||
tsx@^3.12.6: | |||||
version "3.12.6" | |||||
resolved "https://registry.yarnpkg.com/tsx/-/tsx-3.12.6.tgz#36b3693e48b8392da374487190972c7b80e433b4" | |||||
integrity sha512-q93WgS3lBdHlPgS0h1i+87Pt6n9K/qULIMNYZo07nSeu2z5QE2CellcAZfofVXBo2tQg9av2ZcRMQ2S2i5oadQ== | |||||
dependencies: | |||||
"@esbuild-kit/cjs-loader" "^2.4.2" | |||||
"@esbuild-kit/core-utils" "^3.0.0" | |||||
"@esbuild-kit/esm-loader" "^2.5.5" | |||||
optionalDependencies: | |||||
fsevents "~2.3.2" | |||||
type-check@^0.4.0, type-check@~0.4.0: | type-check@^0.4.0, type-check@~0.4.0: | ||||
version "0.4.0" | version "0.4.0" | ||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" | ||||