Urgently fix API spam issues
This commit is contained in:
parent
05043505f6
commit
133f484e4e
6 changed files with 96 additions and 188 deletions
|
|
@ -3136,12 +3136,11 @@
|
|||
<div class="info">
|
||||
<span class="label">API Instances</span>
|
||||
<span class="description"
|
||||
>Manage and prioritize API instances. Automatically sorted by
|
||||
speed.</span
|
||||
>Manage and prioritize API instances.</span
|
||||
>
|
||||
</div>
|
||||
<button id="refresh-speed-test-btn" class="btn-secondary">
|
||||
Refresh Speed Test
|
||||
Refresh Instance List
|
||||
</button>
|
||||
</div>
|
||||
<ul id="api-instance-list"></ul>
|
||||
|
|
|
|||
98
js/api.js
98
js/api.js
|
|
@ -48,7 +48,7 @@ export class LosslessAPI {
|
|||
|
||||
const maxTotalAttempts = instances.length * 2; // Allow some retries across instances
|
||||
let lastError = null;
|
||||
let instanceIndex = 0;
|
||||
let instanceIndex = Math.floor(Math.random() * instances.length);
|
||||
|
||||
for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) {
|
||||
const baseUrl = instances[instanceIndex % instances.length];
|
||||
|
|
@ -174,7 +174,7 @@ export class LosslessAPI {
|
|||
return artist;
|
||||
}
|
||||
|
||||
async enrichTracksWithAlbumDates(tracks) {
|
||||
async enrichTracksWithAlbumDates(tracks, maxRequests = 20) {
|
||||
if (!trackDateSettings.useAlbumYear()) return tracks;
|
||||
|
||||
const albumIdsToFetch = [];
|
||||
|
|
@ -186,13 +186,26 @@ export class LosslessAPI {
|
|||
|
||||
if (albumIdsToFetch.length === 0) return tracks;
|
||||
|
||||
const albumDateMap = new Map();
|
||||
const results = await Promise.allSettled(albumIdsToFetch.map((id) => this.getAlbum(id)));
|
||||
// Limit the number of albums to fetch to prevent spamming
|
||||
const limitedIds = albumIdsToFetch.slice(0, maxRequests);
|
||||
if (albumIdsToFetch.length > maxRequests) {
|
||||
console.warn(`[Enrich] Too many albums to fetch (${albumIdsToFetch.length}). limiting to ${maxRequests}.`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.status === 'fulfilled' && result.value.album?.releaseDate) {
|
||||
albumDateMap.set(albumIdsToFetch[i], result.value.album.releaseDate);
|
||||
const albumDateMap = new Map();
|
||||
|
||||
// Chunk requests to avoid spamming
|
||||
const chunkSize = 5;
|
||||
for (let i = 0; i < limitedIds.length; i += chunkSize) {
|
||||
const chunk = limitedIds.slice(i, i + chunkSize);
|
||||
const results = await Promise.allSettled(chunk.map((id) => this.getAlbum(id)));
|
||||
|
||||
for (let j = 0; j < results.length; j++) {
|
||||
const result = results[j];
|
||||
const id = chunk[j];
|
||||
if (result.status === 'fulfilled' && result.value.album?.releaseDate) {
|
||||
albumDateMap.set(id, result.value.album.releaseDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,10 +317,11 @@ export class LosslessAPI {
|
|||
const data = await response.json();
|
||||
const normalized = this.normalizeSearchResponse(data, 'tracks');
|
||||
const preparedTracks = normalized.items.map((t) => this.prepareTrack(t));
|
||||
const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks);
|
||||
// Skip enrichment for search to be fast and lightweight
|
||||
// const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks);
|
||||
const result = {
|
||||
...normalized,
|
||||
items: enrichedTracks,
|
||||
items: preparedTracks,
|
||||
};
|
||||
|
||||
await this.cache.set('search_tracks', query, result);
|
||||
|
|
@ -616,7 +630,8 @@ export class LosslessAPI {
|
|||
}
|
||||
|
||||
// Enrich tracks with album release dates
|
||||
tracks = await this.enrichTracksWithAlbumDates(tracks);
|
||||
// Removed to reduce API load. Playlists can be very large.
|
||||
// tracks = await this.enrichTracksWithAlbumDates(tracks);
|
||||
|
||||
const result = { playlist, tracks };
|
||||
|
||||
|
|
@ -641,7 +656,8 @@ export class LosslessAPI {
|
|||
let tracks = items.map((i) => this.prepareTrack(i.item || i));
|
||||
|
||||
// Enrich tracks with album release dates
|
||||
tracks = await this.enrichTracksWithAlbumDates(tracks);
|
||||
// Limited to reduce API load
|
||||
tracks = await this.enrichTracksWithAlbumDates(tracks, 10);
|
||||
|
||||
const mix = {
|
||||
id: mixData.id,
|
||||
|
|
@ -657,8 +673,9 @@ export class LosslessAPI {
|
|||
return result;
|
||||
}
|
||||
|
||||
async getArtist(artistId) {
|
||||
const cached = await this.cache.get('artist', artistId);
|
||||
async getArtist(artistId, options = {}) {
|
||||
const cacheKey = options.lightweight ? `artist_${artistId}_light` : `artist_${artistId}`;
|
||||
const cached = await this.cache.get('artist', cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const [primaryResponse, contentResponse] = await Promise.all([
|
||||
|
|
@ -709,25 +726,27 @@ export class LosslessAPI {
|
|||
|
||||
entries.forEach((entry) => scan(entry));
|
||||
|
||||
// Attempt to find more albums/EPs via search since the direct feed might be limited
|
||||
try {
|
||||
const searchResults = await this.searchAlbums(artist.name);
|
||||
if (searchResults && searchResults.items) {
|
||||
const numericArtistId = Number(artistId);
|
||||
if (!options.lightweight) {
|
||||
// Attempt to find more albums/EPs via search since the direct feed might be limited
|
||||
try {
|
||||
const searchResults = await this.searchAlbums(artist.name);
|
||||
if (searchResults && searchResults.items) {
|
||||
const numericArtistId = Number(artistId);
|
||||
|
||||
for (const item of searchResults.items) {
|
||||
const itemArtistId = item.artist?.id;
|
||||
const matchesArtist =
|
||||
itemArtistId === numericArtistId ||
|
||||
(Array.isArray(item.artists) && item.artists.some((a) => a.id === numericArtistId));
|
||||
for (const item of searchResults.items) {
|
||||
const itemArtistId = item.artist?.id;
|
||||
const matchesArtist =
|
||||
itemArtistId === numericArtistId ||
|
||||
(Array.isArray(item.artists) && item.artists.some((a) => a.id === numericArtistId));
|
||||
|
||||
if (matchesArtist && !albumMap.has(item.id)) {
|
||||
albumMap.set(item.id, item);
|
||||
if (matchesArtist && !albumMap.has(item.id)) {
|
||||
albumMap.set(item.id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch additional albums via search:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch additional albums via search:', e);
|
||||
}
|
||||
|
||||
const rawReleases = Array.from(albumMap.values());
|
||||
|
|
@ -743,11 +762,11 @@ export class LosslessAPI {
|
|||
.slice(0, 15);
|
||||
|
||||
// Enrich tracks with album release dates
|
||||
const tracks = await this.enrichTracksWithAlbumDates(topTracks);
|
||||
const tracks = options.lightweight ? topTracks : await this.enrichTracksWithAlbumDates(topTracks);
|
||||
|
||||
const result = { ...artist, albums, eps, tracks };
|
||||
|
||||
await this.cache.set('artist', artistId, result);
|
||||
await this.cache.set('artist', cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -851,23 +870,32 @@ export class LosslessAPI {
|
|||
const artistsToProcess = artists.slice(0, Math.min(5, artists.length));
|
||||
console.log(`Processing ${artistsToProcess.length} artists for recommendations`);
|
||||
|
||||
for (const artist of artistsToProcess) {
|
||||
const artistPromises = artistsToProcess.map(async (artist) => {
|
||||
try {
|
||||
console.log(`Fetching tracks for artist: ${artist.name} (ID: ${artist.id})`);
|
||||
const artistData = await this.getArtist(artist.id);
|
||||
const artistData = await this.getArtist(artist.id, { lightweight: true });
|
||||
if (artistData && artistData.tracks && artistData.tracks.length > 0) {
|
||||
const newTracks = artistData.tracks.filter((track) => !seenTrackIds.has(track.id)).slice(0, 4);
|
||||
|
||||
console.log(`Found ${newTracks.length} new tracks from ${artist.name}`);
|
||||
recommendedTracks.push(...newTracks);
|
||||
seenTrackIds.add(...newTracks.map((t) => t.id));
|
||||
return newTracks;
|
||||
} else {
|
||||
console.warn(`No tracks found for artist ${artist.name}`);
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to get tracks for artist ${artist.name}:`, e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(artistPromises);
|
||||
results.forEach((tracks) => {
|
||||
if (tracks.length > 0) {
|
||||
recommendedTracks.push(...tracks);
|
||||
seenTrackIds.add(...tracks.map((t) => t.id));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Total recommended tracks found: ${recommendedTracks.length}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1054,7 +1054,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await api.settings.refreshSpeedTests();
|
||||
await api.settings.refreshInstances();
|
||||
ui.renderApiSettings();
|
||||
btn.textContent = 'Done!';
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
152
js/storage.js
152
js/storage.js
|
|
@ -2,8 +2,6 @@
|
|||
export const apiSettings = {
|
||||
STORAGE_KEY: 'monochrome-api-instances-v6',
|
||||
INSTANCES_URL: 'instances.json',
|
||||
SPEED_TEST_CACHE_KEY: 'monochrome-instance-speeds',
|
||||
SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60,
|
||||
defaultInstances: { api: [], streaming: [] },
|
||||
instancesLoaded: false,
|
||||
|
||||
|
|
@ -88,98 +86,6 @@ export const apiSettings = {
|
|||
}
|
||||
},
|
||||
|
||||
async speedTestInstance(url, type = 'api') {
|
||||
let testUrl;
|
||||
// API instances might not support /track/ endpoint (which checks for streamability)
|
||||
// So we test API instances with a lightweight metadata endpoint
|
||||
if (type === 'streaming') {
|
||||
testUrl = url.endsWith('/')
|
||||
? `${url}track/?id=204567804&quality=HIGH`
|
||||
: `${url}/track/?id=204567804&quality=HIGH`;
|
||||
} else {
|
||||
testUrl = url.endsWith('/')
|
||||
? `${url}artist/?id=3532302` // Daft Punk
|
||||
: `${url}/artist/?id=3532302`;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch(testUrl, {
|
||||
signal: controller.signal,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
return { url, type, speed: Infinity, error: `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const speed = endTime - startTime;
|
||||
|
||||
return { url, type, speed, error: null };
|
||||
} catch (error) {
|
||||
return { url, type, speed: Infinity, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
getCachedSpeedTests() {
|
||||
try {
|
||||
const cached = localStorage.getItem(this.SPEED_TEST_CACHE_KEY);
|
||||
if (!cached) return { speeds: {}, timestamp: Date.now() };
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
|
||||
if (Date.now() - data.timestamp > this.SPEED_TEST_CACHE_DURATION) {
|
||||
return { speeds: {}, timestamp: Date.now() };
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch {
|
||||
return { speeds: {}, timestamp: Date.now() };
|
||||
}
|
||||
},
|
||||
|
||||
updateSpeedCache(newResults) {
|
||||
const currentCache = this.getCachedSpeedTests();
|
||||
|
||||
newResults.forEach((r) => {
|
||||
// Use distinct keys for streaming tests to avoid overwriting API tests for same URL
|
||||
// API tests use raw URL as key (for backward compatibility with UI)
|
||||
const key = r.type === 'streaming' ? `${r.url}#streaming` : r.url;
|
||||
currentCache.speeds[key] = { speed: r.speed, error: r.error };
|
||||
});
|
||||
|
||||
currentCache.timestamp = Date.now();
|
||||
|
||||
try {
|
||||
localStorage.setItem(this.SPEED_TEST_CACHE_KEY, JSON.stringify(currentCache));
|
||||
} catch {
|
||||
console.warn('[SpeedTest] Failed to cache results');
|
||||
}
|
||||
|
||||
return currentCache;
|
||||
},
|
||||
|
||||
async testSpecificUrls(urls, type) {
|
||||
if (!urls || urls.length === 0) return [];
|
||||
console.log(`[SpeedTest] Testing ${urls.length} instances for ${type}...`);
|
||||
|
||||
const results = await Promise.all(urls.map((url) => this.speedTestInstance(url, type)));
|
||||
|
||||
const validResults = results.filter((r) => r.speed !== Infinity);
|
||||
console.log(
|
||||
`[SpeedTest] ${type} Results:`,
|
||||
validResults.map((r) => `${r.url}: ${r.speed.toFixed(0)}ms`)
|
||||
);
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
async getInstances(type = 'api', sortBySpeed = false) {
|
||||
let instancesObj;
|
||||
|
|
@ -207,60 +113,32 @@ export const apiSettings = {
|
|||
const targetUrls = instancesObj[type] || instancesObj.api || [];
|
||||
if (targetUrls.length === 0) return [];
|
||||
|
||||
const speedCache = this.getCachedSpeedTests();
|
||||
// Construct cache key based on type
|
||||
const getCacheKey = (u) => (type === 'streaming' ? `${u}#streaming` : u);
|
||||
|
||||
const urlsToTest = targetUrls.filter((url) => !speedCache.speeds[getCacheKey(url)]);
|
||||
|
||||
if (urlsToTest.length > 0) {
|
||||
const results = await this.testSpecificUrls(urlsToTest, type);
|
||||
this.updateSpeedCache(results);
|
||||
Object.assign(speedCache, this.getCachedSpeedTests());
|
||||
}
|
||||
|
||||
// Default: return instances in their stored/manual order (respects manual reordering)
|
||||
// Only sort by speed when explicitly requested (e.g., refresh speed test)
|
||||
if (!sortBySpeed) {
|
||||
return targetUrls;
|
||||
}
|
||||
|
||||
const sortList = (list) => {
|
||||
return [...list].sort((a, b) => {
|
||||
const speedA = speedCache.speeds[getCacheKey(a)]?.speed ?? Infinity;
|
||||
const speedB = speedCache.speeds[getCacheKey(b)]?.speed ?? Infinity;
|
||||
return speedA - speedB;
|
||||
});
|
||||
};
|
||||
|
||||
const sortedList = sortList(targetUrls);
|
||||
|
||||
// Persist the sorted order
|
||||
instancesObj[type] = sortedList;
|
||||
this.saveInstances(instancesObj);
|
||||
|
||||
return sortedList;
|
||||
return targetUrls;
|
||||
},
|
||||
|
||||
async refreshSpeedTests() {
|
||||
async refreshInstances() {
|
||||
const instances = await this.loadInstancesFromGitHub();
|
||||
const promises = [];
|
||||
|
||||
const shuffle = (array) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
if (instances.api && instances.api.length) {
|
||||
promises.push(this.testSpecificUrls(instances.api, 'api'));
|
||||
instances.api = shuffle([...instances.api]);
|
||||
}
|
||||
|
||||
|
||||
if (instances.streaming && instances.streaming.length) {
|
||||
promises.push(this.testSpecificUrls(instances.streaming, 'streaming'));
|
||||
instances.streaming = shuffle([...instances.streaming]);
|
||||
}
|
||||
|
||||
const resultsArray = await Promise.all(promises);
|
||||
const allResults = resultsArray.flat();
|
||||
|
||||
this.updateSpeedCache(allResults);
|
||||
this.saveInstances(instances);
|
||||
|
||||
// Return API instances for the UI to render (default view)
|
||||
return this.getInstances('api', true);
|
||||
return this.getInstances('api');
|
||||
},
|
||||
saveInstances(instances, type) {
|
||||
if (type) {
|
||||
|
|
|
|||
12
js/ui.js
12
js/ui.js
|
|
@ -3156,27 +3156,15 @@ export class UIRenderer {
|
|||
const container = document.getElementById('api-instance-list');
|
||||
Promise.all([this.api.settings.getInstances('api'), this.api.settings.getInstances('streaming')]).then(
|
||||
([apiInstances, streamingInstances]) => {
|
||||
const cachedData = this.api.settings.getCachedSpeedTests();
|
||||
const speeds = cachedData?.speeds || {};
|
||||
|
||||
const renderGroup = (instances, type) => {
|
||||
if (!instances || instances.length === 0) return '';
|
||||
|
||||
const listHtml = instances
|
||||
.map((url, index) => {
|
||||
const cacheKey = type === 'streaming' ? `${url}#streaming` : url;
|
||||
const speedInfo = speeds[cacheKey];
|
||||
const speedText = speedInfo
|
||||
? speedInfo.speed === Infinity || typeof speedInfo.speed !== 'number'
|
||||
? `<span style="color: var(--muted-foreground); font-size: 0.8rem;">Failed</span>`
|
||||
: `<span style="color: var(--muted-foreground); font-size: 0.8rem;">${speedInfo.speed.toFixed(0)}ms</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<li data-index="${index}" data-type="${type}">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div class="instance-url">${url}</div>
|
||||
${speedText}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="move-up" title="Move Up" ${index === 0 ? 'disabled' : ''}>
|
||||
|
|
|
|||
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -74,6 +74,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -1603,6 +1604,7 @@
|
|||
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
|
|
@ -1644,6 +1646,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -1687,6 +1690,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -3138,6 +3142,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
|
||||
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
|
|
@ -3186,6 +3191,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3209,6 +3215,7 @@
|
|||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -3496,6 +3503,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -4280,6 +4288,7 @@
|
|||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -6636,6 +6645,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -6719,6 +6729,7 @@
|
|||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
|
@ -7668,6 +7679,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
|
||||
|
|
@ -8082,6 +8094,7 @@
|
|||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
|
|
@ -8406,6 +8419,7 @@
|
|||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -8793,6 +8807,7 @@
|
|||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue