@andrewstart/av-encoder
Advanced tools
Comparing version 1.0.9 to 2.0.0
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -6,7 +9,6 @@ const ffmpeg = require("ffmpeg-cli"); | ||
const fs = require("fs-extra"); | ||
const fast_glob_1 = __importDefault(require("fast-glob")); | ||
const commander_1 = require("commander"); | ||
const JSON5 = require("json5"); | ||
const utils_1 = require("./utils"); | ||
const hasha = require("hasha"); | ||
const INPUT_TYPES = new Set(['.wav', '.aif', '.m4a', '.mp3', '.ogg', '.opus', '.flac']); | ||
const CACHE_FILE = '.aveaudiocache'; | ||
@@ -16,3 +18,3 @@ // opus - Opus (Opus Interactive Audio Codec) (decoders: opus libopus ) (encoders: opus libopus ) | ||
async function main() { | ||
var _a, _b; | ||
var _a; | ||
const program = new commander_1.Command(); | ||
@@ -38,33 +40,57 @@ program | ||
} | ||
const baseSrc = path.resolve(cwd, config.audio.baseSrc); | ||
const baseDest = path.resolve(cwd, config.audio.baseDest); | ||
const defaults = config.audio.default || { opusTargetBitrate: '32k', mp3Quality: '9' }; | ||
const cache = await utils_1.readCache(CACHE_FILE); | ||
const defaults = config.audio.default || { opusTargetBitrate: '32k', mp3Quality: '9', mono: false }; | ||
const cache = new utils_1.Cache(config.audio.cache || CACHE_FILE); | ||
await cache.load(); | ||
for (const group of config.audio.folders) { | ||
const srcFolder = path.resolve(baseSrc, group.src); | ||
if (!await fs.pathExists(srcFolder)) { | ||
console.log(`** Source folder does not exist: ${srcFolder} **`); | ||
continue; | ||
} | ||
const destFolder = path.resolve(baseDest, group.dest); | ||
const destFolder = path.resolve(cwd, group.dest); | ||
await fs.ensureDir(destFolder); | ||
const files = await fs.readdir(srcFolder); | ||
for (const file of files) { | ||
// skip files we don't consider input (primarily to ignore .DS_Store files and other garbage) | ||
if (!INPUT_TYPES.has(path.extname(file))) | ||
continue; | ||
const cacheId = path.join(group.src, file); | ||
const fileSrc = path.resolve(srcFolder, file); | ||
const hash = await hasha.fromFile(fileSrc, { algorithm: 'md5' }); | ||
let overwrite = false; | ||
if (!cache.has(cacheId) || hash !== cache.get(cacheId).hash) { | ||
overwrite = true; | ||
const changed = await utils_1.filterChanged(await fast_glob_1.default(group.src, { cwd }), async (file) => { | ||
var _a; | ||
const id = path.basename(file, path.extname(file)); | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[id]; | ||
const currentSettings = Object.assign({}, defaults, group, override); | ||
delete currentSettings.src; | ||
delete currentSettings.dest; | ||
delete currentSettings.overrides; | ||
const oldSettings = cache.getSettings(id) || currentSettings; | ||
const changed = { opus: false, mp3: false, caf: false }; | ||
if (currentSettings.mono != oldSettings.mono) { | ||
changed.opus = changed.caf = changed.mp3 = true; | ||
} | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[file]; | ||
if (currentSettings.opusTargetBitrate != oldSettings.opusTargetBitrate) { | ||
changed.opus = true; | ||
changed.caf = true; | ||
} | ||
if (currentSettings.mp3Quality != oldSettings.mp3Quality) { | ||
changed.mp3 = true; | ||
} | ||
if (await cache.isDifferent(file, cwd, currentSettings)) { | ||
changed.opus = changed.caf = changed.mp3 = true; | ||
} | ||
else { | ||
const targetBase = path.resolve(destFolder, id); | ||
const targetOpus = targetBase + '.opus'; | ||
const targetCaf = targetBase + '.caf'; | ||
const targetMp3 = targetBase + '.mp3'; | ||
if (!await fs.pathExists(targetOpus)) { | ||
changed.opus = true; | ||
} | ||
if (!await fs.pathExists(targetCaf)) { | ||
changed.caf = true; | ||
} | ||
if (!await fs.pathExists(targetMp3)) { | ||
changed.mp3 = true; | ||
} | ||
} | ||
return Object.keys(changed).filter(k => changed[k]); | ||
}); | ||
for (const file of changed) { | ||
const id = path.basename(file.item, path.extname(file.item)); | ||
const fileSrc = file.item; | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[id]; | ||
const settings = Object.assign({}, defaults, group, override); | ||
delete settings.src; | ||
delete settings.dest; | ||
const lastSettings = (_b = cache.get(cacheId)) === null || _b === void 0 ? void 0 : _b.settings; | ||
cache.set(cacheId, hash, settings); | ||
const targetBase = path.resolve(destFolder, file.slice(0, -4)); | ||
delete settings.overrides; | ||
const targetBase = path.resolve(destFolder, id); | ||
const targetOpus = targetBase + '.opus'; | ||
@@ -75,11 +101,11 @@ const targetCaf = targetBase + '.caf'; | ||
let encodes = []; | ||
if (overwrite || settings.opusTargetBitrate != lastSettings.opusTargetBitrate || !await fs.pathExists(targetOpus)) { | ||
if (file.modes.includes('opus')) { | ||
writes.push(`-c:a libopus -b:a ${settings.opusTargetBitrate} "${targetOpus}"`); | ||
encodes.push('opus'); | ||
} | ||
if (overwrite || settings.opusTargetBitrate != lastSettings.opusTargetBitrate || !await fs.pathExists(targetCaf)) { | ||
if (file.modes.includes('caf')) { | ||
writes.push(`-c:a libopus -b:a ${settings.opusTargetBitrate} "${targetCaf}"`); | ||
encodes.push('caf'); | ||
} | ||
if (overwrite || settings.mp3Quality != lastSettings.mp3Quality || !await fs.pathExists(targetMp3)) { | ||
if (file.modes.includes('mp3')) { | ||
writes.push(`-c:a libmp3lame -q:a ${settings.mp3Quality} "${targetMp3}"`); | ||
@@ -89,8 +115,8 @@ encodes.push('mp3'); | ||
if (!writes.length) { | ||
console.log(`${file} - skipped, already up to date`); | ||
console.log(`${file.item} - skipped, already up to date`); | ||
continue; | ||
} | ||
try { | ||
const result = await ffmpeg.run(`-y -i "${fileSrc}" ${writes.join(' ')}`); | ||
console.log(`${file} - encoded to ${encodes.join(',')}`); | ||
const result = await ffmpeg.run(`-y -i "${fileSrc}" ${settings.mono ? '-ac 1' : ''} ${writes.join(' ')}`); | ||
console.log(`${file.item} - encoded to ${encodes.join(',')}`); | ||
if (result) { | ||
@@ -105,4 +131,5 @@ console.log(result); | ||
} | ||
await utils_1.writeCache(cache, CACHE_FILE); | ||
cache.purgeUnseen(); | ||
await cache.save(); | ||
} | ||
main(); |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -6,10 +9,9 @@ const ffmpeg = require("ffmpeg-cli"); | ||
const fs = require("fs-extra"); | ||
const fast_glob_1 = __importDefault(require("fast-glob")); | ||
const commander_1 = require("commander"); | ||
const JSON5 = require("json5"); | ||
const utils_1 = require("./utils"); | ||
const hasha = require("hasha"); | ||
const INPUT_TYPES = new Set(['.mov', '.mp4']); | ||
const CACHE_FILE = '.avevideocache'; | ||
async function main() { | ||
var _a, _b, _c, _d, _e, _f; | ||
var _a, _b, _c, _d, _e; | ||
const program = new commander_1.Command(); | ||
@@ -40,71 +42,81 @@ program | ||
} | ||
const baseSrc = path.resolve(cwd, config.video.baseSrc); | ||
const baseDest = path.resolve(cwd, config.video.baseDest); | ||
const defaults = config.video.default || { audioOut: null, quality: 28, width: 1280 }; | ||
const cache = await utils_1.readCache(CACHE_FILE); | ||
const cache = new utils_1.Cache(config.video.cache || CACHE_FILE); | ||
await cache.load(); | ||
for (const group of config.video.folders) { | ||
const srcFolder = path.resolve(baseSrc, group.src); | ||
const destFolder = path.resolve(baseDest, group.dest); | ||
const destFolder = path.resolve(cwd, group.dest); | ||
await fs.ensureDir(destFolder); | ||
const files = await fs.readdir(srcFolder); | ||
for (const file of files) { | ||
// skip files we don't consider input (primarily to ignore .DS_Store files and other garbage) | ||
if (!INPUT_TYPES.has(path.extname(file))) | ||
continue; | ||
const cacheId = path.join(group.src, file); | ||
const fileSrc = path.resolve(srcFolder, file); | ||
const hash = await hasha.fromFile(fileSrc, { algorithm: 'md5' }); | ||
let overwrite = false; | ||
if (!cache.has(cacheId) || hash !== cache.get(cacheId).hash) { | ||
overwrite = true; | ||
const changed = await utils_1.filterChanged(await fast_glob_1.default(group.src, { cwd }), async (file) => { | ||
var _a; | ||
const id = path.basename(file, path.extname(file)); | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[id]; | ||
const currentSettings = Object.assign({}, defaults, group, override); | ||
delete currentSettings.src; | ||
delete currentSettings.dest; | ||
delete currentSettings.overrides; | ||
delete currentSettings.audioOut; | ||
const oldSettings = cache.getSettings(id) || currentSettings; | ||
let changed = false; | ||
if (currentSettings.quality != oldSettings.quality || currentSettings.width != oldSettings.width) { | ||
changed = true; | ||
} | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[file]; | ||
if (await cache.isDifferent(file, cwd, currentSettings)) { | ||
changed = true; | ||
} | ||
else if (!changed) { | ||
const targetBase = path.resolve(destFolder, id); | ||
const target = targetBase + '.mp4'; | ||
const targetCaf = targetBase + '.caf'; | ||
const targetMp3 = targetBase + '.mp3'; | ||
if (!await fs.pathExists(target)) { | ||
changed = true; | ||
} | ||
} | ||
return changed ? [''] : null; | ||
}); | ||
for (const file of changed) { | ||
const id = path.basename(file.item, path.extname(file.item)); | ||
const fileSrc = file.item; | ||
const override = (_a = group.overrides) === null || _a === void 0 ? void 0 : _a[id]; | ||
const settings = Object.assign({}, defaults, group, override); | ||
delete settings.audioOut; | ||
const audioOut = settings.audioOut; | ||
delete settings.src; | ||
delete settings.dest; | ||
const lastSettings = (_b = cache.get(cacheId)) === null || _b === void 0 ? void 0 : _b.settings; | ||
cache.set(cacheId, hash, settings); | ||
const target = path.resolve(destFolder, file.slice(0, -4) + '.mp4'); | ||
const audioOut = (override === null || override === void 0 ? void 0 : override.audioOut) || group.audioOut || defaults.audioOut; | ||
if (overwrite || settings.quality != lastSettings.quality || settings.width != lastSettings.width || !await fs.pathExists(target)) { | ||
try { | ||
// `-pix_fmt yuv420p` is for Quicktime compatibility (w/h must be divisible by 2) | ||
// `-profile:v baseline -level 3.0` is for Android compatibility - doesn't support higher profiles | ||
// `-movflags +faststart` allows play while downloading | ||
// `scale=1280:-2` scales down the video to 1280 wide, height as a multiple of 2 | ||
const result = await ffmpeg.run(`-y -i "${fileSrc}" -c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3.0 -crf ${(_d = (_c = override === null || override === void 0 ? void 0 : override.quality) !== null && _c !== void 0 ? _c : group.quality) !== null && _d !== void 0 ? _d : defaults.quality} -preset veryslow -vf scale=${(_f = (_e = override === null || override === void 0 ? void 0 : override.width) !== null && _e !== void 0 ? _e : group.width) !== null && _f !== void 0 ? _f : defaults.width}:-2 ${audioOut ? '-an' : 'c:a aac'} -strict experimental -movflags +faststart -threads 0 "${target}"`); | ||
console.log(`${file} - encoded to mp4`); | ||
if (result) { | ||
console.log(result); | ||
} | ||
delete settings.overrides; | ||
delete settings.audioOut; | ||
const target = path.resolve(destFolder, id + '.mp4'); | ||
try { | ||
// `-pix_fmt yuv420p` is for Quicktime compatibility (w/h must be divisible by 2) | ||
// `-profile:v baseline -level 3.0` is for Android compatibility - doesn't support higher profiles | ||
// `-movflags +faststart` allows play while downloading | ||
// `scale=1280:-2` scales down the video to 1280 wide, height as a multiple of 2 | ||
const result = await ffmpeg.run(`-y -i "${fileSrc}" -c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3.0 -crf ${(_c = (_b = override === null || override === void 0 ? void 0 : override.quality) !== null && _b !== void 0 ? _b : group.quality) !== null && _c !== void 0 ? _c : defaults.quality} -preset veryslow -vf scale=${(_e = (_d = override === null || override === void 0 ? void 0 : override.width) !== null && _d !== void 0 ? _d : group.width) !== null && _e !== void 0 ? _e : defaults.width}:-2 ${audioOut ? '-an' : 'c:a aac'} -strict experimental -movflags +faststart -threads 0 "${target}"`); | ||
console.log(`${file} - encoded to mp4`); | ||
if (result) { | ||
console.log(result); | ||
} | ||
catch (e) { | ||
console.log('Error:\n', e); | ||
} | ||
} | ||
else { | ||
console.log(`${file} - skipped, output exists`); | ||
catch (e) { | ||
console.log('Error:\n', e); | ||
} | ||
if (audioOut) { | ||
const audioDest = path.resolve(baseSrc, audioOut); | ||
const audioDest = path.resolve(cwd, audioOut); | ||
await fs.ensureDir(audioDest); | ||
const audioTarget = path.resolve(audioDest, file.slice(0, -4) + '.wav'); | ||
if (overwrite || !await fs.pathExists(audioTarget)) { | ||
try { | ||
const audioResult = await ffmpeg.run(`-y -i "${fileSrc}" -c:a pcm_f32le "${audioTarget}"`); | ||
console.log(`${file} - encoded to wav`); | ||
if (audioResult) { | ||
console.log(audioResult); | ||
} | ||
const audioTarget = path.resolve(audioDest, id + '.wav'); | ||
try { | ||
const audioResult = await ffmpeg.run(`-y -i "${fileSrc}" -c:a pcm_f32le "${audioTarget}"`); | ||
console.log(`${file} - encoded to wav`); | ||
if (audioResult) { | ||
console.log(audioResult); | ||
} | ||
catch (e) { | ||
console.log('Error:\n', e); | ||
} | ||
} | ||
catch (e) { | ||
console.log('Error:\n', e); | ||
} | ||
} | ||
} | ||
} | ||
await utils_1.writeCache(cache, CACHE_FILE); | ||
cache.purgeUnseen(); | ||
await cache.save(); | ||
} | ||
main(); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Cache = exports.writeCache = exports.readCache = void 0; | ||
exports.Cache = exports.filterChanged = void 0; | ||
const path = require("path"); | ||
const fs = require("fs-extra"); | ||
async function readCache(file) { | ||
file = path.resolve(process.cwd(), file); | ||
if (!await fs.pathExists(file)) { | ||
return new Cache(''); | ||
const hasha = require("hasha"); | ||
async function filterChanged(input, test) { | ||
const out = []; | ||
for (const item of input) { | ||
const result = await test(item); | ||
if (result && result.length) { | ||
out.push({ item, modes: result }); | ||
} | ||
} | ||
const text = await fs.readFile(file, 'utf8'); | ||
return new Cache(text); | ||
return out; | ||
} | ||
exports.readCache = readCache; | ||
async function writeCache(cache, file) { | ||
file = path.resolve(process.cwd(), file); | ||
await fs.writeFile(file, cache.toString()); | ||
} | ||
exports.writeCache = writeCache; | ||
exports.filterChanged = filterChanged; | ||
class Cache { | ||
constructor(text) { | ||
this.map = new Map(); | ||
const lines = text.split(/\r?\n/); | ||
constructor(cachePath) { | ||
this.hashes = new Map(); | ||
this.unseen = new Set(); | ||
this.cachePath = path.resolve(process.cwd(), cachePath); | ||
} | ||
getSettings(fileId) { | ||
if (!this.hashes.has(fileId)) { | ||
return null; | ||
} | ||
return this.hashes.get(fileId).settings; | ||
} | ||
/** Doesn't compare settings changes, just ingests them for saving. Only checks for src file changes. */ | ||
async isDifferent(filePath, rootDir, settings) { | ||
const absPath = path.resolve(rootDir, filePath); | ||
const hash = await hasha.fromFile(absPath, { algorithm: 'md5' }); | ||
const filename = path.basename(filePath, path.extname(filePath)); | ||
// if not present in cache, return true (add to hashes) | ||
// if present, remove from unseen, compare hash with hasha, and update hashes if changed | ||
let changed = true; | ||
if (this.hashes.has(filename)) { | ||
const data = this.hashes.get(filename); | ||
if (data.hash == hash) { | ||
changed = false; | ||
} | ||
} | ||
if (changed) { | ||
this.hashes.set(filename, { hash, settings }); | ||
} | ||
this.unseen.delete(filename); | ||
return changed; | ||
} | ||
async load() { | ||
if (!(await fs.pathExists(this.cachePath))) { | ||
return; | ||
} | ||
const file = await fs.readFile(this.cachePath, 'utf8'); | ||
const lines = file.split(/\r?\n/); | ||
for (let line of lines) { | ||
if (!line) | ||
continue; | ||
let file; | ||
let fileId; | ||
if (line[0] == '"') { | ||
file = line.substring(1, line.indexOf('"', 1)); | ||
fileId = line.substring(1, line.indexOf('"', 1)); | ||
line = line.substring(line.indexOf('"', 1) + 2); | ||
} | ||
else { | ||
file = line.substring(0, line.indexOf(' ', 1)); | ||
fileId = line.substring(0, line.indexOf(' ', 1)); | ||
line = line.substring(line.indexOf(' ') + 1); | ||
@@ -39,22 +71,22 @@ } | ||
const settings = JSON.parse(line); | ||
this.map.set(file, { hash, settings }); | ||
this.hashes.set(fileId, { hash, settings }); | ||
this.unseen.add(fileId); | ||
} | ||
} | ||
has(file) { | ||
return this.map.has(file); | ||
} | ||
set(file, hash, settings) { | ||
this.map.set(file, { hash, settings }); | ||
} | ||
get(file) { | ||
return this.map.get(file); | ||
} | ||
toString() { | ||
async save() { | ||
let text = ''; | ||
for (const [file, data] of this.map.entries()) { | ||
text += `${file.includes(' ') ? `"${file}"` : file} ${data.hash} ${JSON.stringify(data.settings)}\n`; | ||
for (const [fileId, data] of this.hashes.entries()) { | ||
text += `${fileId.includes(' ') ? `"${fileId}"` : fileId} ${data.hash} ${JSON.stringify(data.settings)}\n`; | ||
} | ||
return text; | ||
await fs.writeFile(this.cachePath, text); | ||
} | ||
purgeUnseen() { | ||
const missing = Array.from(this.unseen.values()); | ||
for (const id of missing) { | ||
this.hashes.delete(id); | ||
} | ||
this.unseen.clear(); | ||
return missing; | ||
} | ||
} | ||
exports.Cache = Cache; |
{ | ||
"name": "@andrewstart/av-encoder", | ||
"version": "1.0.9", | ||
"version": "2.0.0", | ||
"description": "Encodes and compresses audio/video for widely supported web formats", | ||
@@ -14,3 +14,4 @@ "bin": { | ||
"build": "tsc", | ||
"prepublishOnly": "npm run build" | ||
"prepublishOnly": "npm run build", | ||
"test": "node ./dist/encodeAudio.js -c test/test.json5" | ||
}, | ||
@@ -33,2 +34,3 @@ "repository": { | ||
"commander": "^8.1.0", | ||
"fast-glob": "^3.2.11", | ||
"ffmpeg-cli": "^2.7.5", | ||
@@ -35,0 +37,0 @@ "fs-extra": "^10.0.0", |
@@ -17,6 +17,4 @@ # av-encoder | ||
audio: { | ||
// root folder for all src paths | ||
baseSrc: 'src/audio', | ||
// root folder for all dest paths | ||
baseDest: 'assets/audio', | ||
// Optional, name a cache file to use instead of the default. This allows only changed files to be rerun in later runs. | ||
cache: "path/to/.cachefile", | ||
// default properties for audio encoding, if not specified | ||
@@ -28,13 +26,15 @@ default: { | ||
mp3Quality: '9', | ||
// True to force downmixing to mono. False or omit to leave as-is | ||
mono: true, | ||
}, | ||
// list of source folders and destinations. Source folders are not recursive, and destinations can be | ||
// list of source folders and destinations. Source folders are globs, and destinations can be | ||
// shared. | ||
folders: [ | ||
{ | ||
src: 'sfx', | ||
dest: 'sfx', | ||
src: 'src/sfx/*.wav', | ||
dest: 'assets/sfx', | ||
}, | ||
{ | ||
src: 'sfx/loops', | ||
dest: 'sfx/loops', | ||
src: ['src/sfx/loops/*.wav'], | ||
dest: 'assets/sfx/loops', | ||
// each folder can have specific encoding properties | ||
@@ -45,7 +45,7 @@ opusTargetBitrate: '48k', | ||
{ | ||
src: 'vo', | ||
dest: 'vo', | ||
src: 'src/vo/**/*.wav', | ||
dest: 'assets/vo', | ||
// you can also override settings for individual files if so desired | ||
overrides: { | ||
'intro.wav': { | ||
'intro': { | ||
opusTargetBitrate: '48k', | ||
@@ -60,4 +60,4 @@ mp3Quality: '7' | ||
video: { | ||
baseSrc: 'src/video', | ||
baseDest: 'assets/video', | ||
// Optional, name a cache file to use instead of the default. This allows only changed files to be rerun in later runs. | ||
cache: "path/to/.cachefile", | ||
default: { | ||
@@ -80,2 +80,2 @@ // MP4 quality: 0 is lossless, 23 is default, and 51 is worst possible. 18-28 is a sane range. | ||
### Project cache files | ||
Files for audio & video caches to track which files need to be encoded and which don't will be created in your project. `.aveaudiocache` and `.avevidiocache` will be created when encoding audio and video, respectively. If your output files are tracked with version control, then these cache files should be tracked as well. | ||
Files for audio & video caches to track which files need to be encoded and which don't will be created in your project. `.aveaudiocache` and `.avevidiocache` (or your project values) will be created when encoding audio and video, respectively. If your output files are tracked with version control, then these cache files should be tracked as well. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
19270
343
6
+ Addedfast-glob@^3.2.11
+ Added@nodelib/fs.scandir@2.1.5(transitive)
+ Added@nodelib/fs.stat@2.0.5(transitive)
+ Added@nodelib/fs.walk@1.2.8(transitive)
+ Addedbraces@3.0.3(transitive)
+ Addedfast-glob@3.3.2(transitive)
+ Addedfastq@1.17.1(transitive)
+ Addedfill-range@7.1.1(transitive)
+ Addedglob-parent@5.1.2(transitive)
+ Addedis-extglob@2.1.1(transitive)
+ Addedis-glob@4.0.3(transitive)
+ Addedis-number@7.0.0(transitive)
+ Addedmerge2@1.4.1(transitive)
+ Addedmicromatch@4.0.8(transitive)
+ Addedpicomatch@2.3.1(transitive)
+ Addedqueue-microtask@1.2.3(transitive)
+ Addedreusify@1.0.4(transitive)
+ Addedrun-parallel@1.2.0(transitive)
+ Addedto-regex-range@5.0.1(transitive)