diff --git a/infrastructure/api/api-stack.ts b/infrastructure/api/api-stack.ts index a8cdbc5..45a1687 100644 --- a/infrastructure/api/api-stack.ts +++ b/infrastructure/api/api-stack.ts @@ -39,6 +39,9 @@ export class ApiStack extends Stack { ], }), ], + bundling: { + nodeModules: ['@takumi-rs/wasm'], + }, }); const fnUrl = lambdaFunction.addFunctionUrl({ @@ -50,6 +53,16 @@ export class ApiStack extends Stack { this.apiDomain = originDomain; const lambdaOrigin = new HttpOrigin(originDomain); + const ArtworkCachePolicy = new CachePolicy(this, 'ArtworkCachePolicy', { + cachePolicyName: 'ArtworkCaching', + defaultTtl: Duration.days(7), + maxTtl: Duration.days(30), + minTtl: Duration.minutes(15), + headerBehavior: CacheHeaderBehavior.none(), + queryStringBehavior: CacheQueryStringBehavior.none(), + cookieBehavior: CacheCookieBehavior.none(), + }); + const distribution = new Distribution(this, 'ApiDistribution', { defaultBehavior: { origin: lambdaOrigin, @@ -61,15 +74,13 @@ export class ApiStack extends Stack { '/plex/artwork/*': { origin: lambdaOrigin, allowedMethods: AllowedMethods.ALLOW_GET_HEAD, - cachePolicy: new CachePolicy(this, 'ArtworkCachePolicy', { - cachePolicyName: 'ArtworkCaching', - defaultTtl: Duration.days(7), - maxTtl: Duration.days(30), - minTtl: Duration.seconds(0), - headerBehavior: CacheHeaderBehavior.none(), - queryStringBehavior: CacheQueryStringBehavior.none(), - cookieBehavior: CacheCookieBehavior.none(), - }), + cachePolicy: ArtworkCachePolicy, + originRequestPolicy: OriginRequestPolicy.CORS_S3_ORIGIN, + }, + '/og-image/*': { + origin: lambdaOrigin, + allowedMethods: AllowedMethods.ALLOW_GET_HEAD, + cachePolicy: ArtworkCachePolicy, originRequestPolicy: OriginRequestPolicy.CORS_S3_ORIGIN, }, }, diff --git a/packages/api/package.json b/packages/api/package.json index f773014..12004cd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -15,7 +15,9 @@ "hono": "4.10.1", "neverthrow": "8.2.0", "web-shared": "workspace:*", - "zod": "4.1.5" + "zod": "4.1.5", + "@takumi-rs/image-response": "1.2.1", + "@takumi-rs/wasm": "1.2.1" }, "devDependencies": { "@hono/node-server": "1.19.1" diff --git a/packages/api/src/lib/handleResult.ts b/packages/api/src/lib/handleResult.ts index e10aa18..5e186e6 100644 --- a/packages/api/src/lib/handleResult.ts +++ b/packages/api/src/lib/handleResult.ts @@ -3,6 +3,26 @@ import { Result } from 'neverthrow'; import { ErrorCodes, ErrorWithCode } from 'web-shared/src/types/errors'; import { logger } from 'web-shared/src/lib/logger'; +const serializeErrors = (value: unknown): unknown => { + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack, + cause: value.cause !== undefined ? serializeErrors(value.cause) : undefined, + }; + } + if (Array.isArray(value)) { + return value.map(serializeErrors); + } + if (value !== null && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, serializeErrors(v)]), + ); + } + return value; +}; + /** * Maps a NeverThrow Result to a Hono response * @param c - Hono context @@ -19,8 +39,11 @@ export const handleResult = >( ) => { if (result.isErr()) { const { context, code, message } = result.error; + const serializedContext = serializeErrors(context) as + | Record + | undefined; - logger.error({ ...(context || {}), code }, message); + logger.error({ ...(serializedContext || {}), code }, message); const statusCode = (errorStatusMap?.[code as E['code']] || 500) as number; return c.json( @@ -28,7 +51,7 @@ export const handleResult = >( error: { message: result.error.message, code: result.error.code, - context, + context: serializedContext, }, }, statusCode as any, diff --git a/packages/api/src/router.ts b/packages/api/src/router.ts index 0c70271..e08227c 100644 --- a/packages/api/src/router.ts +++ b/packages/api/src/router.ts @@ -5,6 +5,7 @@ import { logger } from 'web-shared/src/lib/logger'; import { lastfmRouter } from './routes/lastfm'; import { wellKnownRouter } from './routes/wellknown'; import { goatCounterRouter } from './routes/goatcounter'; +import { ogImageRouter } from './routes/og-image'; export const router = new Hono(); router.use('*', async (c, next) => { @@ -20,3 +21,4 @@ router.route('/plex', plexRouter); router.route('/lastfm', lastfmRouter); router.route('/.well-known', wellKnownRouter); router.route('/goatcounter', goatCounterRouter); +router.route('/og-image', ogImageRouter); diff --git a/packages/api/src/routes/og-image.ts b/packages/api/src/routes/og-image.ts new file mode 100644 index 0000000..0215753 --- /dev/null +++ b/packages/api/src/routes/og-image.ts @@ -0,0 +1,18 @@ +import { Hono } from 'hono'; +import { StatusCode } from 'hono/utils/http-status'; +import { getResponse } from '../services/og-image/getResponse'; +import { handleResult } from '../lib/handleResult'; + +export const ogImageRouter = new Hono(); + +ogImageRouter.get('/:title/:subtitle?', async (c) => { + const { title, subtitle } = c.req.param(); + const ogImage = await getResponse(title, subtitle); + + return handleResult(c, ogImage, ({ status, headers, body }) => { + c.status(status as StatusCode); + headers.forEach((value, key) => c.header(key, value)); + + return c.body(body); + }); +}); diff --git a/packages/api/src/services/og-image/getResponse.tsx b/packages/api/src/services/og-image/getResponse.tsx new file mode 100644 index 0000000..ed56a0e --- /dev/null +++ b/packages/api/src/services/og-image/getResponse.tsx @@ -0,0 +1,62 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { err, ok, Result } from 'neverthrow'; +import { ErrorWithCode, newError, OGErrors } from 'web-shared/src/types/errors'; +import { ImageResponse } from '@takumi-rs/image-response/wasm'; +import { jsxAsString as jsx } from 'web-shared/src/lib/jsx'; +import { stylesheet } from './styles'; + +const wasmBytes = readFileSync( + createRequire(import.meta.url).resolve( + '@takumi-rs/wasm/takumi_wasm_bg.wasm', + ), +); + +const lazyEscape = (string: string) => + string + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + +export const getResponse = async ( + title: string, + subtitle?: string, +): Promise>> => { + try { + const response = new ImageResponse( + ( +
+
+

{lazyEscape(title)}

+

{lazyEscape(subtitle || '')}

+
+
+

A blog post on pfy.ch

+ +
+
+ ), + { + width: 1200, + height: 630, + format: 'webp', + stylesheets: [stylesheet], + module: wasmBytes, + onError: () => {}, + }, + ); + + await response.ready; + + return ok(response); + } catch (error) { + return err( + newError( + 'ImageGenerationFailed', + 'an unexpected error occurred when attempting to generate an image', + { error }, + ), + ); + } +}; diff --git a/packages/api/src/services/og-image/styles.ts b/packages/api/src/services/og-image/styles.ts new file mode 100644 index 0000000..0d22e96 --- /dev/null +++ b/packages/api/src/services/og-image/styles.ts @@ -0,0 +1,53 @@ +// language=CSS +export const stylesheet = ` + h1, + p { + margin: 0; + font-family: "Geist", monospace; + text-wrap: pretty; + } + + h1 { + margin-bottom: 0.5em; + text-overflow: ellipsis; + line-clamp: 2; + } + + p { + font-size: 1.5rem; + line-height: 2rem; + text-overflow: ellipsis; + line-clamp: 3; + } + + .container { + padding: 16px 24px; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + background: #fff; + + .details { + flex: 1 1 auto; + } + + .info { + flex: 0 0 auto; + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 8px; + + img { + width: 32px; + height: 32px; + } + + p { + font-weight: 300; + font-size: 18px; + } + } + } +`; diff --git a/packages/site/assets/scripts/lib/api.ts b/packages/shared/src/lib/api.ts similarity index 100% rename from packages/site/assets/scripts/lib/api.ts rename to packages/shared/src/lib/api.ts diff --git a/packages/site/lib/jsx.ts b/packages/shared/src/lib/jsx.ts similarity index 94% rename from packages/site/lib/jsx.ts rename to packages/shared/src/lib/jsx.ts index fdb563a..02c71e9 100644 --- a/packages/site/lib/jsx.ts +++ b/packages/shared/src/lib/jsx.ts @@ -1,4 +1,9 @@ -import { Attributes } from '../assets/scripts/old/jsx'; +export type Attributes = Omit< + Partial, + 'style' +> & { + style?: string; +}; export const jsxAsString = ( tag: T, diff --git a/packages/shared/src/types/errors.ts b/packages/shared/src/types/errors.ts index 22bf316..d7ecef0 100644 --- a/packages/shared/src/types/errors.ts +++ b/packages/shared/src/types/errors.ts @@ -19,12 +19,14 @@ export type LastFmScrobbleErrors = | 'LastFMScrobblerTokenError' | InternalErrors; export type GoatCounterErrors = 'GoatCounterQueryError' | InternalErrors; +export type OGErrors = 'ImageGenerationFailed' | InternalErrors; export type ErrorCodes = | PlexErrors | LastFmErrors | LastFmScrobbleErrors - | GoatCounterErrors; + | GoatCounterErrors + | OGErrors; export interface ErrorWithCode { code: T; diff --git a/packages/site/assets/scripts/components/album.tsx b/packages/site/assets/scripts/components/album.tsx index 50f6d80..e9b9210 100644 --- a/packages/site/assets/scripts/components/album.tsx +++ b/packages/site/assets/scripts/components/album.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { TypedAttributes, TypedElement } from '../htmlElementTyped'; import { addHoverEffect } from '../old/utils'; diff --git a/packages/site/assets/scripts/components/bmsDifficulties.tsx b/packages/site/assets/scripts/components/bmsDifficulties.tsx index a4cef1e..ce1bf72 100644 --- a/packages/site/assets/scripts/components/bmsDifficulties.tsx +++ b/packages/site/assets/scripts/components/bmsDifficulties.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { TypedAttributes, TypedElement } from '../htmlElementTyped'; import { addHoverEffect } from '../old/utils'; diff --git a/packages/site/assets/scripts/components/bsky-comments.tsx b/packages/site/assets/scripts/components/bsky-comments.tsx index 3bfe761..5a1138e 100644 --- a/packages/site/assets/scripts/components/bsky-comments.tsx +++ b/packages/site/assets/scripts/components/bsky-comments.tsx @@ -1,5 +1,5 @@ import { Like, Reply } from 'web-shared/src/types/bsky'; -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { skeleton } from '../old/utils'; export class BskyComments extends HTMLElement { diff --git a/packages/site/assets/scripts/components/catCount.tsx b/packages/site/assets/scripts/components/catCount.tsx index 3cf0dfc..7b3f48e 100644 --- a/packages/site/assets/scripts/components/catCount.tsx +++ b/packages/site/assets/scripts/components/catCount.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { makeApiRequest, skeleton } from '../old/utils'; export class CatCount extends HTMLElement { diff --git a/packages/site/assets/scripts/components/estrogen/estrogen-levels.tsx b/packages/site/assets/scripts/components/estrogen/estrogen-levels.tsx index c2591f2..a06b3f3 100644 --- a/packages/site/assets/scripts/components/estrogen/estrogen-levels.tsx +++ b/packages/site/assets/scripts/components/estrogen/estrogen-levels.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { dataPointsEachSide, getTimeSeries } from './estrogen'; import { max, maxBy } from 'lodash'; diff --git a/packages/site/assets/scripts/components/nowListening.tsx b/packages/site/assets/scripts/components/nowListening.tsx index a84b675..6784222 100644 --- a/packages/site/assets/scripts/components/nowListening.tsx +++ b/packages/site/assets/scripts/components/nowListening.tsx @@ -1,5 +1,5 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; -import { apiUrl } from '../lib/api'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; +import { apiUrl } from 'web-shared/src/lib/api'; import { getNowListening } from '../lib/plex'; interface PlexNowListening { diff --git a/packages/site/assets/scripts/components/recentListens.tsx b/packages/site/assets/scripts/components/recentListens.tsx index f11c324..90f6c3b 100644 --- a/packages/site/assets/scripts/components/recentListens.tsx +++ b/packages/site/assets/scripts/components/recentListens.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { getRecentListens } from '../lib/lastfm'; import { getAlbumImage } from 'web-shared/src/types/lastfm'; diff --git a/packages/site/assets/scripts/components/scrobbler.tsx b/packages/site/assets/scripts/components/scrobbler.tsx index 428b73a..823aad7 100644 --- a/packages/site/assets/scripts/components/scrobbler.tsx +++ b/packages/site/assets/scripts/components/scrobbler.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { API_KEY, handleLastFmToken, diff --git a/packages/site/assets/scripts/components/skeleton.tsx b/packages/site/assets/scripts/components/skeleton.tsx index f980642..c3a15f2 100644 --- a/packages/site/assets/scripts/components/skeleton.tsx +++ b/packages/site/assets/scripts/components/skeleton.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { TypedAttributes, TypedElement } from '../htmlElementTyped'; export interface SkeletonAttributes extends TypedAttributes { diff --git a/packages/site/assets/scripts/components/updatedAt.tsx b/packages/site/assets/scripts/components/updatedAt.tsx index 6420af7..29a6a2f 100644 --- a/packages/site/assets/scripts/components/updatedAt.tsx +++ b/packages/site/assets/scripts/components/updatedAt.tsx @@ -1,4 +1,4 @@ -import { jsxAsElement as jsx } from '../../../lib/jsx'; +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; import { TypedAttributes, TypedElement } from '../htmlElementTyped'; export interface UpdatedAtAttributes extends TypedAttributes { diff --git a/packages/site/assets/scripts/lib/lastfm.ts b/packages/site/assets/scripts/lib/lastfm.ts index af4fd71..afea7f6 100644 --- a/packages/site/assets/scripts/lib/lastfm.ts +++ b/packages/site/assets/scripts/lib/lastfm.ts @@ -2,7 +2,7 @@ import { err, ok, Result } from 'neverthrow'; import { TopAlbumResponse } from 'web-shared/src/types/lastfm'; import { ErrorWithCode, LastFmErrors } from 'web-shared/src/types/errors'; import { safeFetch } from 'web-shared/src/lib/fetch'; -import { apiUrl } from './api'; +import { apiUrl } from 'web-shared/src/lib/api'; export const getRecentListens = async (): Promise< Result> diff --git a/packages/site/assets/scripts/lib/plex.ts b/packages/site/assets/scripts/lib/plex.ts index 7465bca..cfb524d 100644 --- a/packages/site/assets/scripts/lib/plex.ts +++ b/packages/site/assets/scripts/lib/plex.ts @@ -1,7 +1,7 @@ import { err, ok, Result } from 'neverthrow'; import { PlexNowListening } from 'web-shared/src/types/plex'; import { ErrorWithCode, PlexErrors } from 'web-shared/src/types/errors'; -import { apiUrl } from './api'; +import { apiUrl } from 'web-shared/src/lib/api'; import { safeFetch } from 'web-shared/src/lib/fetch'; export const getNowListening = async (): Promise< diff --git a/packages/site/build/steps/posts/create-post-object.ts b/packages/site/build/steps/posts/create-post-object.ts index 804fd56..62a3883 100644 --- a/packages/site/build/steps/posts/create-post-object.ts +++ b/packages/site/build/steps/posts/create-post-object.ts @@ -7,6 +7,7 @@ import { dateDiffInDays, formatDate } from '../../utils/dates'; import { path2emoji } from '../../utils/path2emoji'; import { getFormattedNow, setLastBuild } from '../../utils/set-last-build'; import { mdToHtml } from './marked'; +import { apiUrl } from 'web-shared/src/lib/api'; export interface KnownFrontMatter { updatedAt?: string; @@ -35,8 +36,10 @@ export interface Post { interface TemplateValues { title: string; + titleEncoded: string; hidden?: boolean; summary?: string; + summaryEncoded: string; image?: string; content: string; lastBuildDate: string; @@ -52,6 +55,7 @@ interface TemplateValues { headScript?: string[]; stylesheet?: string[]; footerScript?: string[]; + apiUrl: string; } const rawTemplate = await readFile(resolve('build', 'templates', 'post.hbs'), { @@ -83,6 +87,8 @@ export const renderTemplate = async (args: { template({ title: title, summary: summary, + titleEncoded: encodeURIComponent(title), + summaryEncoded: encodeURIComponent(summary || ' '), image: image, hidden: hidden, content: htmlContent, @@ -96,6 +102,7 @@ export const renderTemplate = async (args: { string: formatDate(updated), } : undefined, + apiUrl: apiUrl, }), ), { diff --git a/packages/site/build/templates/post.hbs b/packages/site/build/templates/post.hbs index e6015b8..cfb00d6 100644 --- a/packages/site/build/templates/post.hbs +++ b/packages/site/build/templates/post.hbs @@ -32,6 +32,8 @@ {{/if}} {{#if image}} + {{else}} + {{/if}} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f594ddf..f5a6149 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: '@aws-sdk/client-ssm': specifier: 3.879.0 version: 3.879.0 + '@takumi-rs/image-response': + specifier: ^1.1.2 + version: 1.2.1 + '@takumi-rs/wasm': + specifier: ^1.1.2 + version: 1.2.1 hono: specifier: 4.10.1 version: 4.10.1 @@ -613,24 +619,28 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} @@ -643,6 +653,7 @@ packages: engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -670,6 +681,7 @@ packages: resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] + libc: [glibc] '@smithy/abort-controller@4.0.5': resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==} @@ -843,6 +855,79 @@ packages: resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==} engines: {node: '>=18.0.0'} + '@takumi-rs/core-darwin-arm64@1.2.1': + resolution: {integrity: sha512-XO2nY+qcC4fKTATJMrmzjqacB6WI6ocHNIbP5xsXAFvi5r87qGRNfD3s8wSNtp+DX/DeGmwMT2THve0d0XSHqw==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [arm64] + os: [darwin] + + '@takumi-rs/core-darwin-x64@1.2.1': + resolution: {integrity: sha512-OuKeyjvtW4k2DXnGrg+GQRzCregtG48o61CPUGbOcu6QRRgmUQxptA93zBPQJL6i2qlqj0W3trK5lFC4JTLchQ==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [x64] + os: [darwin] + + '@takumi-rs/core-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-JqhIXKUmR4Dq7DSHaVY/O7GHrKOrh0LhJjtzZLs8x2MKPNFGmzBQ9vtiOgBIbnomM5xhYXU617TG3gTpykwPjQ==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@takumi-rs/core-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-eGREMxS+7MKJ470g9RpCVtxj0SRrdW8rJvpSemoKyIDipc+YKVA7uaRt9yG+5hTEaY6wxLAhmoLjr7fDud5jlA==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@takumi-rs/core-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-KIf4WdMmOJPLeKvJQzFtLII8hn2J6zGf+8EKBx+Hs6E/5ZClXlLO9jIRs3pPTJd2Uzidaz0pogSDikKp+agxjA==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@takumi-rs/core-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-NXxoJ3qdaLyh21/z27iSUYABiPMHtBowZivHd4ZRfcvRhdG1lvFszpVIlzMXUnGP3sNYG2Ky9+1a9JF+dZe7xg==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@takumi-rs/core-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-FVxGPPyKPrjuybGv2QlODRHL5QkcaZi5T9DygNGNqcgT6pcG1M6mQTpUHR0Wt44ODMNCJABpdD9YEQvJS53SJA==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [arm64] + os: [win32] + + '@takumi-rs/core-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Wgb+uyiNxIDk74Zt+K3vxyZZqPAycZ+CL132qUkY2uYWMaHPQAqC3Z9Rm3OPFFap9Lmh6I06HWC+f9VLLhnoHA==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + cpu: [x64] + os: [win32] + + '@takumi-rs/core@1.2.1': + resolution: {integrity: sha512-+Rt7vZWH2mBv1RXUqFBt4gnr3rSJoByUSTpyYjoI/q478XON77NDeNZIrkIaXh0M7R7OA126cX9PCFnD/NebwA==} + engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} + + '@takumi-rs/helpers@1.2.1': + resolution: {integrity: sha512-LEb0KzydyBXH/gQQVH/d5uF48IYHsIpjA3CcvpyhkMQrG6U+A4BtuXDb14U02CRR/s3IWLZ+U5pAOqbDCrHzMA==} + peerDependencies: + react: ^19.2.5 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@takumi-rs/image-response@1.2.1': + resolution: {integrity: sha512-s/yrnCoK5U52q0hdXebJ54VwMSkr6QU2dj9Px31r0scN5HQ8I6r/q6s1/l/lYFAUsani2fxNKXF2k6Q2aJUNXA==} + + '@takumi-rs/wasm@1.2.1': + resolution: {integrity: sha512-lBTMgN0KYYCIShZe/VHQBoTTxJuU3qbHn8Z4qE08nkeCBX5t54BV2FMRsD3W4mLtf066xv8nBc7LZUzZh0dEXA==} + '@types/finalhandler@1.2.4': resolution: {integrity: sha512-ojpQ5ywnKZko/+tw8lR4xvUN5Uvfnar4ZtfpoLG1TdxlmiIqcQGUbXmc9iV5ud7J6rRtacNbwTkCE98NmsBPYw==} @@ -2640,6 +2725,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + takumi-js@1.2.1: + resolution: {integrity: sha512-veh7yZ+uJ0gkzHCZBEC1Hiefg7QiFep0EUP6nCGoqEgIVyUWSbWMScQYqSWWfWlliWjUUwruroRkc7g8PFbHtQ==} + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} @@ -3751,6 +3839,62 @@ snapshots: '@smithy/types': 4.3.2 tslib: 2.8.1 + '@takumi-rs/core-darwin-arm64@1.2.1': + optional: true + + '@takumi-rs/core-darwin-x64@1.2.1': + optional: true + + '@takumi-rs/core-linux-arm64-gnu@1.2.1': + optional: true + + '@takumi-rs/core-linux-arm64-musl@1.2.1': + optional: true + + '@takumi-rs/core-linux-x64-gnu@1.2.1': + optional: true + + '@takumi-rs/core-linux-x64-musl@1.2.1': + optional: true + + '@takumi-rs/core-win32-arm64-msvc@1.2.1': + optional: true + + '@takumi-rs/core-win32-x64-msvc@1.2.1': + optional: true + + '@takumi-rs/core@1.2.1': + dependencies: + '@takumi-rs/helpers': 1.2.1 + optionalDependencies: + '@takumi-rs/core-darwin-arm64': 1.2.1 + '@takumi-rs/core-darwin-x64': 1.2.1 + '@takumi-rs/core-linux-arm64-gnu': 1.2.1 + '@takumi-rs/core-linux-arm64-musl': 1.2.1 + '@takumi-rs/core-linux-x64-gnu': 1.2.1 + '@takumi-rs/core-linux-x64-musl': 1.2.1 + '@takumi-rs/core-win32-arm64-msvc': 1.2.1 + '@takumi-rs/core-win32-x64-msvc': 1.2.1 + transitivePeerDependencies: + - react + - react-dom + + '@takumi-rs/helpers@1.2.1': {} + + '@takumi-rs/image-response@1.2.1': + dependencies: + takumi-js: 1.2.1 + transitivePeerDependencies: + - react + - react-dom + + '@takumi-rs/wasm@1.2.1': + dependencies: + '@takumi-rs/helpers': 1.2.1 + transitivePeerDependencies: + - react + - react-dom + '@types/finalhandler@1.2.4': dependencies: '@types/node': 24.3.0 @@ -5619,6 +5763,15 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + takumi-js@1.2.1: + dependencies: + '@takumi-rs/core': 1.2.1 + '@takumi-rs/helpers': 1.2.1 + '@takumi-rs/wasm': 1.2.1 + transitivePeerDependencies: + - react + - react-dom + thread-stream@3.1.0: dependencies: real-require: 0.2.0