diff --git a/app/controllers/favicons_controller.ts b/app/controllers/favicons_controller.ts index 0285f80..ed48ffd 100644 --- a/app/controllers/favicons_controller.ts +++ b/app/controllers/favicons_controller.ts @@ -1,10 +1,8 @@ +import FaviconNotFoundException from '#exceptions/favicon_not_found_exception'; import type { HttpContext } from '@adonisjs/core/http'; import logger from '@adonisjs/core/services/logger'; import { parse } from 'node-html-parser'; -import { createReadStream } from 'node:fs'; -import { resolve } from 'node:path'; - -const LOG_PREFIX = '[Favicon]'; +import { cache } from '../lib/cache.js'; interface Favicon { buffer: Buffer; @@ -33,165 +31,123 @@ export default class FaviconsController { throw new Error('Missing URL'); } - const faviconRequestUrl = this.buildFaviconUrl(url, '/favicon.ico'); + const cacheNs = cache.namespace('favicon'); + const favicon = await cacheNs.getOrSet({ + key: url, + ttl: '1h', + factory: () => this.tryGetFavicon(url), + }); + return this.sendImage(ctx, favicon); + } + + private async tryGetFavicon(url: string): Promise { + const faviconUrl = this.buildFaviconUrl(url, '/favicon.ico'); try { - const favicon = await this.getFavicon(faviconRequestUrl); - return this.sendImage(ctx, favicon); - } catch (error) { - logger.debug( - `${LOG_PREFIX} [first: ${faviconRequestUrl}] unable to retrieve favicon from favicon.ico url` - ); + return await this.fetchFavicon(faviconUrl); + } catch { + logger.debug(`Unable to retrieve favicon from ${faviconUrl}`); } - const requestDocument = await this.makeRequestWithUserAgent(url); - const documentAsText = await requestDocument.text(); + const documentText = await this.fetchDocumentText(url); + const faviconPath = this.extractFaviconPath(documentText); - const faviconPath = this.findFaviconPath(documentAsText); if (!faviconPath) { - logger.debug( - `${LOG_PREFIX} [first: ${faviconRequestUrl}] no link/href attribute found` - ); - return this.sendDefaultImage(ctx); + throw new FaviconNotFoundException(`No favicon path found in ${url}`); } - const finalUrl = this.buildFaviconUrl(requestDocument.url, faviconPath); - try { - if (!faviconPath) { - throw new Error('Unable to find favicon path'); - } - - if (this.isBase64Image(faviconPath)) { - logger.debug( - `${LOG_PREFIX} [second: ${faviconRequestUrl}] info: base64, convert it to buffer` - ); - const buffer = this.convertBase64ToBuffer(faviconPath); - return this.sendImage(ctx, { - buffer, - type: 'image/x-icon', - size: buffer.length, - url: faviconPath, - }); - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - const finalUrl = faviconPath.startsWith('http') - ? faviconPath - : this.buildFaviconUrl(requestDocument.url, faviconPath); - const favicon = await this.downloadImageFromUrl(finalUrl); - if (!this.isImage(favicon.type)) { - throw new Error('Favicon path does not return an image'); - } - - logger.debug(`${LOG_PREFIX} [second: ${finalUrl}] success: image found`); - return this.sendImage(ctx, favicon); - } catch (error) { - const errorMessage = error?.message || 'Unable to retrieve favicon'; - logger.debug(`${LOG_PREFIX} [second: ${finalUrl}] error`, { - errorMessage, - }); - return this.sendDefaultImage(ctx); - } + return this.fetchFaviconFromPath(url, faviconPath); } - private buildFaviconUrl(url: string, faviconPath: string) { - const { origin } = new URL(url); - if (faviconPath.startsWith('/')) { - // https://example.com + /favicon.ico - return origin + faviconPath; - } - // https://example.com/a/b?c=d -> https://example.com/a/b - const slimUrl = this.urlWithoutSearchParams(url); - - // https://example.com/a/b/ -> https://example.com/a/b - const newUrl = slimUrl.endsWith('/') ? slimUrl.slice(0, -1) : slimUrl; - if (newUrl === origin) { - return `${newUrl}/${faviconPath}`; + private async fetchFavicon(url: string): Promise { + const response = await this.fetchWithUserAgent(url); + if (!response.ok) { + throw new FaviconNotFoundException(`Request to ${url} failed`); } - // https://example.com/a/b or https://example.com/a/b/cdef -> https://example.com/a/ - const relativeUrl = this.removeLastSectionUrl(newUrl) + '/'; - if (relativeUrl.endsWith('/')) { - return relativeUrl + faviconPath; + const blob = await response.blob(); + if (!this.isImage(blob.type) || blob.size === 0) { + throw new FaviconNotFoundException(`Invalid image at ${url}`); } - // https://example.com/a -> https://example.com/a/favicon.ico - return `${relativeUrl}/${faviconPath}`; - } - - private urlWithoutSearchParams(url: string) { - const newUrl = new URL(url); - return newUrl.protocol + '//' + newUrl.host + newUrl.pathname; - } - - private removeLastSectionUrl(url: string) { - const urlArr = url.split('/'); - urlArr.pop(); - return urlArr.join('/'); - } - - private findFaviconPath(text: string) { - const document = parse(text); - const favicon = Array.from(document.getElementsByTagName('link')).find( - (element) => - element && - this.relList.includes(element.getAttribute('rel')!) && - element.getAttribute('href') - ); - - return favicon?.getAttribute('href') || undefined; - } - - private async getFavicon(url: string): Promise { - if (!url) throw new Error('Missing URL'); - - const favicon = await this.downloadImageFromUrl(url); - if (!this.isImage(favicon.type) || favicon.size === 0) { - throw new Error('Favicon path does not return an image'); - } - - return favicon; - } - - private async makeRequestWithUserAgent(url: string) { - const headers = new Headers(); - headers.set('User-Agent', this.userAgent); - - return await fetch(url, { headers }); - } - - private async downloadImageFromUrl(url: string): Promise { - const request = await this.makeRequestWithUserAgent(url); - if (!request.ok) { - throw new Error('Request failed'); - } - - const blob = await request.blob(); return { buffer: Buffer.from(await blob.arrayBuffer()), - url: request.url, + url: response.url, type: blob.type, size: blob.size, }; } - private isImage = (type: string) => type.includes('image'); + private async fetchDocumentText(url: string): Promise { + const response = await this.fetchWithUserAgent(url); + if (!response.ok) { + throw new FaviconNotFoundException(`Request to ${url} failed`); + } - private isBase64Image = (data: string) => data.startsWith('data:image/'); + return await response.text(); + } - private convertBase64ToBuffer = (base64: string) => - Buffer.from(base64, 'base64'); + private extractFaviconPath(html: string): string | undefined { + const document = parse(html); + const link = document + .getElementsByTagName('link') + .find((element) => this.relList.includes(element.getAttribute('rel')!)); + return link?.getAttribute('href'); + } + + private async fetchFaviconFromPath( + baseUrl: string, + path: string + ): Promise { + if (this.isBase64Image(path)) { + const buffer = this.convertBase64ToBuffer(path); + return { + buffer, + type: 'image/x-icon', + size: buffer.length, + url: path, + }; + } + + const faviconUrl = this.buildFaviconUrl(baseUrl, path); + return this.fetchFavicon(faviconUrl); + } + + private buildFaviconUrl(base: string, path: string): string { + const { origin } = new URL(base); + if (path.startsWith('/')) { + return origin + path; + } + + const basePath = this.urlWithoutSearchParams(base); + const baseUrl = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + return `${baseUrl}/${path}`; + } + + private urlWithoutSearchParams(url: string): string { + const { protocol, host, pathname } = new URL(url); + return `${protocol}//${host}${pathname}`; + } + + private isImage(type: string): boolean { + return type.startsWith('image/'); + } + + private isBase64Image(data: string): boolean { + return data.startsWith('data:image/'); + } + + private convertBase64ToBuffer(base64: string): Buffer { + return Buffer.from(base64.split(',')[1], 'base64'); + } + + private async fetchWithUserAgent(url: string): Promise { + const headers = new Headers({ 'User-Agent': this.userAgent }); + return fetch(url, { headers }); + } private sendImage(ctx: HttpContext, { buffer, type, size }: Favicon) { ctx.response.header('Content-Type', type); - ctx.response.header('Content-Length', size); + ctx.response.header('Content-Length', size.toString()); ctx.response.send(buffer, true); } - - private sendDefaultImage(ctx: HttpContext) { - const readStream = createReadStream( - resolve(process.cwd(), './public/empty-image.png') - ); - ctx.response.writeHead(206); - ctx.response.stream(readStream); - } } diff --git a/app/exceptions/favicon_not_found_exception.ts b/app/exceptions/favicon_not_found_exception.ts new file mode 100644 index 0000000..5dadd52 --- /dev/null +++ b/app/exceptions/favicon_not_found_exception.ts @@ -0,0 +1,20 @@ +import { Exception } from '@adonisjs/core/exceptions'; +import { HttpContext } from '@adonisjs/core/http'; +import logger from '@adonisjs/core/services/logger'; +import { createReadStream } from 'node:fs'; +import { resolve } from 'node:path'; + +export default class FaviconNotFoundException extends Exception { + static status = 404; + static code = 'E_FAVICON_NOT_FOUND'; + + async handle(error: this, ctx: HttpContext) { + const readStream = createReadStream( + resolve(process.cwd(), './public/empty-image.png') + ); + + ctx.response.header('Content-Type', 'image/png'); + ctx.response.stream(readStream); + logger.debug(error.message); + } +} diff --git a/app/lib/cache.ts b/app/lib/cache.ts new file mode 100644 index 0000000..f74035f --- /dev/null +++ b/app/lib/cache.ts @@ -0,0 +1,10 @@ +import { BentoCache, bentostore } from 'bentocache'; +import { memoryDriver } from 'bentocache/drivers/memory'; + +export const cache = new BentoCache({ + default: 'cache', + + stores: { + cache: bentostore().useL1Layer(memoryDriver({ maxSize: 10_000 })), + }, +}); diff --git a/inertia/components/dashboard/search/search_result_item.tsx b/inertia/components/dashboard/search/search_result_item.tsx index 469ddae..ef8ac08 100644 --- a/inertia/components/dashboard/search/search_result_item.tsx +++ b/inertia/components/dashboard/search/search_result_item.tsx @@ -28,10 +28,10 @@ const ItemLegeng = styled.span(({ theme }) => ({ })); interface CommonResultProps { - isActive: boolean; innerRef: RefObject; - onHoverEnter: () => void; - onHoverLeave: () => void; + isActive: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; } export default function SearchResultItem({ @@ -46,14 +46,13 @@ export default function SearchResultItem({ const itemRef = useRef(null); const [isHovering, setHovering] = useState(false); - const handleHoverEnter = () => { + const onMouseEnter = () => { if (!isHovering) { onHover(result); setHovering(true); } }; - - const handleHoverLeave = () => setHovering(false); + const onMouseLeave = () => setHovering(false); useEffect(() => { if (isActive && !isHovering) { @@ -64,31 +63,22 @@ export default function SearchResultItem({ } }, [itemRef, isActive]); + const commonProps = { + onMouseEnter, + onMouseLeave, + isActive, + }; return result.type === 'collection' ? ( - + ) : ( - + ); } function ResultLink({ result, - isActive, innerRef, - onHoverEnter, - onHoverLeave, + ...props }: { result: SearchResultLink; } & CommonResultProps) { @@ -100,11 +90,9 @@ function ResultLink({ return ( ( =4" } }, - "node_modules/@adonisjs/core/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@adonisjs/core/node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, "node_modules/@adonisjs/core/node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -2798,6 +2780,27 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/@boringnode/bus": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@boringnode/bus/-/bus-0.6.0.tgz", + "integrity": "sha512-i6AKHvFqRUC5KZTX7WXYcWIV8Wfnem6t5Li1Bn5oU+YXAq0zw1y09bZk9HxB2smugfcFlDgzRwpXuwC9fLTE9g==", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "@poppinss/utils": "^6.7.3", + "object-hash": "^3.0.0" + }, + "engines": { + "node": ">=20.11.1" + }, + "peerDependencies": { + "ioredis": "^5.0.0" + }, + "peerDependenciesMeta": { + "ioredis": { + "optional": true + } + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3768,6 +3771,25 @@ "node": ">=8" } }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@poppinss/chokidar-ts": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@poppinss/chokidar-ts/-/chokidar-ts-4.1.4.tgz", @@ -7427,6 +7449,14 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -7683,6 +7713,45 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bentocache": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/bentocache/-/bentocache-1.0.0-beta.9.tgz", + "integrity": "sha512-KykJ/8q20DO9FHQ7Yhqv8RlgI/jo7kgoE0EJ65w30sziNdaDSsy8gVRCOTf06m+TcoVdBu2luKH9V4xaavBAbA==", + "dependencies": { + "@boringnode/bus": "^0.6.0", + "@poppinss/utils": "^6.7.3", + "async-mutex": "^0.5.0", + "chunkify": "^5.0.0", + "hexoid": "^1.0.0", + "lru-cache": "^10.2.2", + "p-timeout": "^6.1.2", + "typescript-log": "^2.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.438.0", + "ioredis": "^5.3.2", + "knex": "^3.0.1", + "kysely": "^0.27.3", + "orchid-orm": "^1.24.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "orchid-orm": { + "optional": true + } + } + }, "node_modules/camelcase": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", @@ -7936,6 +8005,17 @@ "node": ">=8.0" } }, + "node_modules/chunkify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chunkify/-/chunkify-5.0.0.tgz", + "integrity": "sha512-G8dj/3/Gm+1yL4oWSdwIxihZWFlgC4V2zYtIApacI0iPIRKBHlBGOGAiDUBZgrj4H8MBA8g8fPFwnJrWF3wl7Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -8163,18 +8243,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cpy/node_modules/p-timeout": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.2.tgz", - "integrity": "sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==", - "devOptional": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cpy/node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9113,12 +9181,6 @@ "node": ">=12.20" } }, - "node_modules/eslint-plugin-jsonc/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -9195,12 +9257,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/eslint-plugin-prettier/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/eslint-plugin-unicorn": { "version": "47.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz", @@ -11282,6 +11338,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -11975,6 +12039,14 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -12082,6 +12154,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -12090,6 +12170,17 @@ "node": ">=14.0.0" } }, + "node_modules/p-timeout": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.2.tgz", + "integrity": "sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -12906,15 +12997,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, - "node_modules/read-package-up/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/read-package-up/node_modules/normalize-package-data": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", @@ -13903,6 +13985,11 @@ } } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -13916,6 +14003,14 @@ "node": ">=14.17" } }, + "node_modules/typescript-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typescript-log/-/typescript-log-2.0.0.tgz", + "integrity": "sha512-TyW8lmURJSo0yjBovEhESpah3haDYBgsnQRocBF4MQSDJSYs/DOmhjo2cpSrGyvD9OaX++dbbonq9TkIQeA+Bw==", + "peerDependencies": { + "tslib": "^2.0.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index c6af40c..a7919d8 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@inertiajs/react": "^1.0.16", "@izzyjs/route": "^1.1.0-0", "@vinejs/vine": "^2.0.0", + "bentocache": "^1.0.0-beta.9", "edge.js": "^6.0.2", "hex-rgb": "^5.0.0", "i18next": "^23.11.4",