diff --git a/packages/backend/package.json b/packages/backend/package.json index a742991f3e..b206815641 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -105,7 +105,7 @@ "sanitize-html": "2.8.1", "seedrandom": "^3.0.5", "semver": "7.3.8", - "sharp": "0.29.3", + "sharp": "0.31.3", "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index ff52ad27d6..61cf811192 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { Cache } from '@/misc/cache.js'; -import { query } from '@/misc/prelude/url.js'; import type { Note } from '@/models/entities/Note.js'; import type { EmojisRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -27,9 +25,6 @@ export class CustomEmojiService { private cache: Cache<Emoji | null>; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.db) private db: DataSource, @@ -117,7 +112,7 @@ export class CustomEmojiService { const isLocal = emoji.host == null; const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため - const url = isLocal ? emojiUrl : `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; + const url = emojiUrl; return { name: emojiName, diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 9097bb08e0..62123246a7 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -33,7 +33,7 @@ export class DownloadService { @bindThis public async downloadUrl(url: string, path: string): Promise<void> { - this.logger.info(`Downloading ${chalk.cyan(url)} ...`); + this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); const timeout = 30 * 1000; const operationTimeout = 60 * 1000; diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 3a61873044..312189eea4 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -8,6 +8,16 @@ export type IImage = { ext: string | null; type: string; }; + +export const webpDefault: sharp.WebpOptions = { + quality: 85, + alphaQuality: 95, + lossless: false, + nearLossless: false, + smartSubsample: true, + mixed: true, +}; + import { bindThis } from '@/decorators.js'; @Injectable() @@ -53,21 +63,19 @@ export class ImageProcessingService { * with resize, remove metadata, resolve orientation, stop animation */ @bindThis - public async convertToWebp(path: string, width: number, height: number, quality = 85): Promise<IImage> { - return this.convertSharpToWebp(await sharp(path), width, height, quality); + public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { + return this.convertSharpToWebp(await sharp(path), width, height, options); } @bindThis - public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise<IImage> { + public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { const data = await sharp .resize(width, height, { fit: 'inside', withoutEnlargement: true, }) .rotate() - .webp({ - quality, - }) + .webp(options) .toBuffer(); return { diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 429977669e..7b8942e308 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -4,7 +4,7 @@ export function createTemp(): Promise<[string, () => void]> { return new Promise<[string, () => void]>((res, rej) => { tmp.file((e, path, fd, cleanup) => { if (e) return rej(e); - res([path, cleanup]); + res([path, process.env.NODE_ENV === 'production' ? cleanup : () => {}]); }); }); } @@ -17,7 +17,7 @@ export function createTempDir(): Promise<[string, () => void]> { }, (e, path, cleanup) => { if (e) return rej(e); - res([path, cleanup]); + res([path, process.env.NODE_ENV === 'production' ? cleanup : () => {}]); }, ); }); diff --git a/packages/backend/src/misc/prelude/url.ts b/packages/backend/src/misc/prelude/url.ts index a4f2f7f5a8..9b1dabc789 100644 --- a/packages/backend/src/misc/prelude/url.ts +++ b/packages/backend/src/misc/prelude/url.ts @@ -1,3 +1,8 @@ +/* objを検査して + * 1. 配列に何も入っていない時はクエリを付けない + * 2. プロパティがundefinedの時はクエリを付けない + * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) + */ export function query(obj: Record<string, unknown>): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts index 7355afcb98..4491a17545 100644 --- a/packages/backend/src/server/MediaProxyServerService.ts +++ b/packages/backend/src/server/MediaProxyServerService.ts @@ -9,7 +9,7 @@ import type { Config } from '@/config.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { createTemp } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { StatusError } from '@/misc/status-error.js'; @@ -81,8 +81,21 @@ export class MediaProxyServerService { const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); let image: IImage; - - if ('static' in request.query && isConvertibleImage) { + if ('emoji' in request.query && isConvertibleImage) { + const data = await sharp(path, { animated: !('static' in request.query) }) + .resize({ + height: 128, + withoutEnlargement: true, + }) + .webp(webpDefault) + .toBuffer(); + + image = { + data, + ext: 'webp', + type: 'image/webp', + }; + } else if ('static' in request.query && isConvertibleImage) { image = await this.imageProcessingService.convertToWebp(path, 498, 280); } else if ('preview' in request.query && isConvertibleImage) { image = await this.imageProcessingService.convertToWebp(path, 200, 200); @@ -91,7 +104,7 @@ export class MediaProxyServerService { // 画像でないなら404でお茶を濁す throw new StatusError('Unexpected mime', 404); } - + const mask = sharp(path) .resize(96, 96, { fit: 'inside', @@ -121,8 +134,8 @@ export class MediaProxyServerService { ext: 'png', type: 'image/png', }; - } else if (mime === 'image/svg+xml') { - image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, 1); + } else if (mime === 'image/svg+xml') { + image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault); } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); } else { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 84e8481d55..83a30dbe0b 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -220,7 +220,7 @@ export class ClientServerService { return reply.sendFile('/apple-touch-icon.png', staticAssets); }); - fastify.get<{ Params: { path: string } }>('/emoji/:path(.*)', async (request, reply) => { + fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { @@ -244,8 +244,15 @@ export class ClientServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - // ?? emoji.originalUrl してるのは後方互換性のため - return await reply.redirect(301, emoji.publicUrl ?? emoji.originalUrl); + const url = new URL("/proxy/emoji.webp", this.config.url); + url.searchParams.set('url', emoji.publicUrl ?? emoji.originalUrl); // ?? emoji.originalUrl してるのは後方互換性のため + url.searchParams.set('emoji', '1'); + if ('static' in request.query) url.searchParams.set('static', '1'); + + return await reply.redirect( + 301, + url.toString(), + ); }); fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 6b1b48e480..d150436fb2 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -16,12 +16,12 @@ </li> </ol> <ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> - <li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> - <span v-if="emoji.isCustomEmoji" class="emoji"><img :src="`/emoji/${emoji.name}.webp`" :alt="emoji.emoji"/></span> - <span v-else-if="defaultStore.state.emojiStyle != 'native'" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span> - <span v-else class="emoji">{{ emoji.emoji }}</span> + <li v-for="emoji in emojis" tabindex="-1" :key="emoji.emoji" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> + <div class="emoji"> + <MkEmoji :emoji="emoji.emoji" /> + </div> <!-- eslint-disable-next-line vue/no-v-html --> - <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> + <span class="name" v-html="emoji.name.replace(q ?? '', `<b>${q}</b>`)"></span> <span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> </li> </ol> @@ -37,7 +37,6 @@ import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import contains from '@/scripts/contains'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { acct } from '@/filters/user'; import * as os from '@/os'; import { MFM_TAGS } from '@/scripts/mfm-tags'; @@ -49,9 +48,13 @@ import { i18n } from '@/i18n'; type EmojiDef = { emoji: string; name: string; + url: string; aliasOf?: string; - url?: string; - isCustomEmoji?: boolean; +} | { + emoji: string; + name: string; + aliasOf?: string; + isCustomEmoji?: true; }; const lib = emojilist.filter(x => x.category !== 'flags'); @@ -87,7 +90,6 @@ for (const x of customEmojis) { emojiDefinitions.push({ name: x.name, emoji: `:${x.name}:`, - url: x.url, isCustomEmoji: true, }); @@ -97,7 +99,6 @@ for (const x of customEmojis) { name: alias, aliasOf: x.name, emoji: `:${x.name}:`, - url: x.url, isCustomEmoji: true, }); } @@ -452,14 +453,20 @@ onBeforeUnmount(() => { > .emojis > li { .emoji { - display: inline-block; + display: flex; margin: 0 4px 0 0; + height: 24px; width: 24px; + justify-content: center; + align-items: center; + font-size: 20px; > img { + height: 24px; width: 24px; - vertical-align: bottom; + object-fit: scale-down; } + } .alias { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index e9e265a916..c94da97747 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -81,7 +81,6 @@ import { ref, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import Ripple from '@/components/MkRipple.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 56570eaa05..9912faffe8 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -23,7 +23,7 @@ <script lang="ts" setup> import { watch } from 'vue'; import * as misskey from 'misskey-js'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +import { getStaticImageUrl } from '@/scripts/media-proxy'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store'; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 5f3e3c176d..60b8b3b1db 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -12,7 +12,7 @@ <script lang="ts" setup> import { onMounted, watch } from 'vue'; import * as misskey from 'misskey-js'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +import { getStaticImageUrl } from '@/scripts/media-proxy'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { acct, userPage } from '@/filters/user'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index bf6be7491d..67e9ef428a 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -7,7 +7,7 @@ <script lang="ts" setup> import { computed } from 'vue'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +import { getStaticImageUrl } from '@/scripts/media-proxy'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; import { defaultStore } from '@/store'; import { getEmojiName } from '@/scripts/emojilist'; diff --git a/packages/frontend/src/pages/user/index.photos.vue b/packages/frontend/src/pages/user/index.photos.vue index b33979a79d..fd975b52bb 100644 --- a/packages/frontend/src/pages/user/index.photos.vue +++ b/packages/frontend/src/pages/user/index.photos.vue @@ -21,7 +21,7 @@ <script lang="ts" setup> import { onMounted } from 'vue'; import * as misskey from 'misskey-js'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +import { getStaticImageUrl } from '@/scripts/media-proxy'; import { notePage } from '@/filters/note'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; diff --git a/packages/frontend/src/scripts/get-static-image-url.ts b/packages/frontend/src/scripts/get-static-image-url.ts deleted file mode 100644 index cbd1761983..0000000000 --- a/packages/frontend/src/scripts/get-static-image-url.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { url as instanceUrl } from '@/config'; -import * as url from '@/scripts/url'; - -export function getStaticImageUrl(baseUrl: string): string { - const u = new URL(baseUrl); - if (u.href.startsWith(`${instanceUrl}/proxy/`)) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; - } - - // 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する - const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; - - return `${instanceUrl}/proxy/${dummy}?${url.query({ - url: u.href, - static: '1', - })}`; -} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index aaf7f9e610..bea164e7c8 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -1,7 +1,15 @@ -import { query } from '@/scripts/url'; +import { query, appendQuery } from '@/scripts/url'; import { url } from '@/config'; export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { + if (imageUrl.startsWith(`${url}/proxy/`) || imageUrl.startsWith('/proxy/')) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + return appendQuery(imageUrl, query({ + fallback: '1', + ...(type ? { [type]: '1' } : {}), + })); + } + return `${url}/proxy/image.webp?${query({ url: imageUrl, fallback: '1', @@ -13,3 +21,27 @@ export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, if (imageUrl == null) return null; return getProxiedImageUrl(imageUrl, type); } + +export function getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); + + if (u.href.startsWith(`${url}/proxy/`)) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + if (u.href.startsWith(`${url}/emoji/`)) { + // もう既にemojiっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + // 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する + const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; + + return `${url}/proxy/${dummy}?${query({ + url: u.href, + static: '1', + })}`; +} diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts index 86735de9f0..b6a997449a 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -1,3 +1,8 @@ +/* objを検査して + * 1. 配列に何も入っていない時はクエリを付けない + * 2. プロパティがundefinedの時はクエリを付けない + * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) + */ export function query(obj: Record<string, any>): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/packages/frontend/src/widgets/photos.vue b/packages/frontend/src/widgets/photos.vue index 4ad5324053..65d1de1385 100644 --- a/packages/frontend/src/widgets/photos.vue +++ b/packages/frontend/src/widgets/photos.vue @@ -20,7 +20,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import { stream } from '@/stream'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +import { getStaticImageUrl } from '@/scripts/media-proxy'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; import { defaultStore } from '@/store'; diff --git a/packages/sw/src/scripts/url.ts b/packages/sw/src/scripts/url.ts index 1266bfedfd..5255076156 100644 --- a/packages/sw/src/scripts/url.ts +++ b/packages/sw/src/scripts/url.ts @@ -1,3 +1,8 @@ +/* objを検査して + * 1. 配列に何も入っていない時はクエリを付けない + * 2. プロパティがundefinedの時はクエリを付けない + * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) + */ export function query(obj: object): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/yarn.lock b/yarn.lock index 1b4b0fbb13..4f94a052e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4257,7 +4257,7 @@ __metadata: sanitize-html: 2.8.1 seedrandom: ^3.0.5 semver: 7.3.8 - sharp: 0.29.3 + sharp: 0.31.3 speakeasy: 2.0.0 strict-event-emitter-types: 2.0.0 stringz: 2.1.0 @@ -5367,7 +5367,7 @@ __metadata: languageName: node linkType: hard -"color@npm:^4.0.1": +"color@npm:^4.2.3": version: 4.2.3 resolution: "color@npm:4.2.3" dependencies: @@ -6161,16 +6161,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^1.0.3": - version: 1.0.3 - resolution: "detect-libc@npm:1.0.3" - bin: - detect-libc: ./bin/detect-libc.js - checksum: daaaed925ffa7889bd91d56e9624e6c8033911bb60f3a50a74a87500680652969dbaab9526d1e200a4c94acf80fc862a22131841145a0a8482d60a99c24f4a3e - languageName: node - linkType: hard - -"detect-libc@npm:^2.0.0": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": version: 2.0.1 resolution: "detect-libc@npm:2.0.1" checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 @@ -12167,12 +12158,12 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^4.2.0": - version: 4.3.0 - resolution: "node-addon-api@npm:4.3.0" +"node-addon-api@npm:^5.0.0": + version: 5.0.0 + resolution: "node-addon-api@npm:5.0.0" dependencies: node-gyp: latest - checksum: 3de396e23cc209f539c704583e8e99c148850226f6e389a641b92e8967953713228109f919765abc1f4355e801e8f41842f96210b8d61c7dcc10a477002dcf00 + checksum: 7c5e2043ac37f6108784d94ed73a44ae6d3e68eb968de60680922fc6bc3d17fa69448c0feb4e0c9d3f4c74a0324822e566a8340a56916d9d6f23cb3e85620334 languageName: node linkType: hard @@ -13672,7 +13663,7 @@ __metadata: languageName: node linkType: hard -"prebuild-install@npm:^7.0.0": +"prebuild-install@npm:^7.1.1": version: 7.1.1 resolution: "prebuild-install@npm:7.1.1" dependencies: @@ -15053,7 +15044,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.3.8, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7": +"semver@npm:7.3.8, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:^7.3.8": version: 7.3.8 resolution: "semver@npm:7.3.8" dependencies: @@ -15146,20 +15137,20 @@ __metadata: languageName: node linkType: hard -"sharp@npm:0.29.3": - version: 0.29.3 - resolution: "sharp@npm:0.29.3" +"sharp@npm:0.31.3": + version: 0.31.3 + resolution: "sharp@npm:0.31.3" dependencies: - color: ^4.0.1 - detect-libc: ^1.0.3 - node-addon-api: ^4.2.0 + color: ^4.2.3 + detect-libc: ^2.0.1 + node-addon-api: ^5.0.0 node-gyp: latest - prebuild-install: ^7.0.0 - semver: ^7.3.5 - simple-get: ^4.0.0 + prebuild-install: ^7.1.1 + semver: ^7.3.8 + simple-get: ^4.0.1 tar-fs: ^2.1.1 tunnel-agent: ^0.6.0 - checksum: d496cdd546c9abe743aebcee013731295f735687819a18c2bdcbba6f31a6b259f3da95af5c11260a8fedc9d4ab95697f5f8c4f3cd65232792b5cfb876bea7c9a + checksum: 29fd1dfbc616c6389f53f366cec342b4353d9f2a37e98952ca273db38dca57dfa0f336322d6d763f0fae876042ead22fd86ffe26d70c32ade2458d421db60d04 languageName: node linkType: hard @@ -15204,7 +15195,7 @@ __metadata: languageName: node linkType: hard -"simple-get@npm:^4.0.0": +"simple-get@npm:^4.0.0, simple-get@npm:^4.0.1": version: 4.0.1 resolution: "simple-get@npm:4.0.1" dependencies: