mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: add ogImagePlugin to build and upload og images (#213)
This commit is contained in:
2
apps/ssr/next-env.d.ts
vendored
2
apps/ssr/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import './.next/dev/types/routes.d.ts'
|
||||
import './.next/types/routes.d.ts'
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"linkedom": "0.18.12",
|
||||
"pg": "8.16.3",
|
||||
"postgres": "3.4.7",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-responsive-masonry": "2.7.1",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@afilmory/db": "workspace:*",
|
||||
"@afilmory/env": "workspace:*",
|
||||
"@afilmory/framework": "workspace:*",
|
||||
"@afilmory/og-renderer": "workspace:*",
|
||||
"@afilmory/redis": "workspace:*",
|
||||
"@afilmory/sdk": "workspace:*",
|
||||
"@afilmory/task-queue": "workspace:*",
|
||||
@@ -45,7 +46,7 @@
|
||||
"picocolors": "1.1.1",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"resend": "6.6.0",
|
||||
"satori": "0.18.3",
|
||||
"satori": "catalog:",
|
||||
"tsyringe": "4.10.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import { resolve } from 'node:path'
|
||||
|
||||
import type { PhotoManifestItem } from '@afilmory/builder'
|
||||
import type { OnModuleDestroy } from '@afilmory/framework'
|
||||
import type { ExifInfo, HomepageOgTemplateProps, PhotoDimensions } from '@afilmory/og-renderer'
|
||||
import { renderHomepageOgImage, renderOgImage } from '@afilmory/og-renderer'
|
||||
import { BizException, ErrorCode } from 'core/errors'
|
||||
import { SiteSettingService } from 'core/modules/configuration/site-setting/site-setting.service'
|
||||
import type { Context } from 'hono'
|
||||
@@ -12,8 +14,6 @@ import { injectable } from 'tsyringe'
|
||||
import { ManifestService } from '../manifest/manifest.service'
|
||||
import geistFontUrl from './assets/Geist-Medium.ttf?url'
|
||||
import harmonySansScMediumFontUrl from './assets/HarmonyOS_Sans_SC_Medium.ttf?url'
|
||||
import { renderHomepageOgImage, renderOgImage } from './og.renderer'
|
||||
import type { ExifInfo, HomepageOgTemplateProps, PhotoDimensions } from './og.template'
|
||||
|
||||
const CACHE_CONTROL = 'public, max-age=31536000, stale-while-revalidate=31536000'
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ src/core/
|
||||
│ └── exif.ts # EXIF 数据提取
|
||||
├── photo/ # 照片处理
|
||||
│ ├── info-extractor.ts # 照片信息提取
|
||||
│ ├── processor.ts # 照片处理主逻辑
|
||||
│ └── processor.ts # 照片处理主逻辑
|
||||
├── plugins/ # 插件系统
|
||||
│ ├── og-image-storage/ # OG 图片存储插件
|
||||
│ │ └── index.ts # 生成并上传 OG 图片
|
||||
│ │ └── README.md # 使用说明与集成示例
|
||||
│ └── geocoding.ts # 反向地理编码
|
||||
├── manifest/ # Manifest 管理
|
||||
│ └── manager.ts # Manifest 读写和管理
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@afilmory/og-renderer": "workspace:*",
|
||||
"@resvg/resvg-js": "2.6.2",
|
||||
"@vingle/bmp-js": "^0.2.5",
|
||||
"blurhash": "2.0.5",
|
||||
"c12": "^3.3.3",
|
||||
@@ -23,6 +25,7 @@
|
||||
"fast-xml-parser": "5.3.3",
|
||||
"heic-convert": "2.1.0",
|
||||
"heic-to": "1.3.0",
|
||||
"satori": "catalog:",
|
||||
"sharp": "0.34.5",
|
||||
"thumbhash": "0.1.1"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,8 @@ export type { GeocodingPluginOptions } from './plugins/geocoding.js'
|
||||
export { default as geocodingPlugin } from './plugins/geocoding.js'
|
||||
export type { GitHubRepoSyncPluginOptions } from './plugins/github-repo-sync.js'
|
||||
export { createGitHubRepoSyncPlugin, default as githubRepoSyncPlugin } from './plugins/github-repo-sync.js'
|
||||
export type { OgImagePluginOptions } from './plugins/og-image-storage/index.js'
|
||||
export { default as ogImagePlugin } from './plugins/og-image-storage/index.js'
|
||||
export type { B2StoragePluginOptions } from './plugins/storage/b2.js'
|
||||
export { default as b2StoragePlugin } from './plugins/storage/b2.js'
|
||||
export type { EagleStoragePluginOptions } from './plugins/storage/eagle.js'
|
||||
|
||||
109
packages/builder/src/plugins/og-image-storage/README.md
Normal file
109
packages/builder/src/plugins/og-image-storage/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# OG Image Storage Plugin
|
||||
|
||||
This plugin renders Open Graph (OG) images for each processed photo and uploads them to remote storage.
|
||||
|
||||
## Examples
|
||||
|
||||
### Plugin usage
|
||||
```ts
|
||||
import { defineBuilderConfig, thumbnailStoragePlugin, ogImagePlugin } from '@afilmory/builder'
|
||||
|
||||
export default defineBuilderConfig(() => ({
|
||||
storage: {
|
||||
provider: 's3',
|
||||
bucket: process.env.S3_BUCKET_NAME!,
|
||||
region: process.env.S3_REGION!,
|
||||
endpoint: process.env.S3_ENDPOINT, // Optional, defaults to AWS S3
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
||||
prefix: process.env.S3_PREFIX || 'photos/',
|
||||
customDomain: process.env.S3_CUSTOM_DOMAIN, // Optional CDN domain
|
||||
excludeRegex: process.env.S3_EXCLUDE_REGEX,
|
||||
downloadConcurrency: 16, // Adjust based on network
|
||||
},
|
||||
plugins: [
|
||||
thumbnailStoragePlugin(),
|
||||
ogImagePlugin({
|
||||
siteName: "YOUR_SITE_NAME",
|
||||
accentColor: '#fb7185',
|
||||
}),
|
||||
],
|
||||
}))
|
||||
```
|
||||
|
||||
### Example Cloudflare Pages Middleware
|
||||
|
||||
If you are using Cloudflare Pages to host your Afilmory, you can use the following minimal middleware to rewrite the OG image meta tags on photo pages to point to the generated OG images stored in your storage.
|
||||
You only need to change the `SITE_ORIGIN` and `OG_PATH` constants to match your site, and put the file in your `functions/_middleware.js` path.
|
||||
|
||||
```js
|
||||
// Minimal middleware to rewrite OG image meta tags for photo pages on Cloudflare Pages.
|
||||
// Assumes OG images are stored at https://your.afilmory.site/.afilmory/og-images/{slug}.png
|
||||
|
||||
const SITE_ORIGIN = 'https://your.afilmory.site'
|
||||
const OG_PATH = '/.afilmory/og-images'
|
||||
const OG_PATTERN = /^https?:\/\/your\.afilmory\.site\/+og-image.*\.png$/i
|
||||
|
||||
const normalizeSlug = (slug) => {
|
||||
try {
|
||||
return encodeURIComponent(decodeURIComponent(slug))
|
||||
} catch {
|
||||
return encodeURIComponent(slug)
|
||||
}
|
||||
}
|
||||
|
||||
const buildOgUrl = (slug) => `${SITE_ORIGIN}${OG_PATH}/${normalizeSlug(slug)}.png`
|
||||
const stripPhotosPrefix = (pathname) => pathname.replace(/^\/?photos\//, '').replace(/\/$/, '')
|
||||
const isHtml = (response) => (response.headers.get('content-type') || '').includes('text/html')
|
||||
const shouldRewrite = (content) => Boolean(content?.trim() && OG_PATTERN.test(content.trim()))
|
||||
|
||||
export const onRequest = async ({ request, next }) => {
|
||||
const url = new URL(request.url)
|
||||
if (!url.pathname.startsWith('/photos/')) return next()
|
||||
|
||||
const slug = stripPhotosPrefix(url.pathname)
|
||||
if (!slug) return next()
|
||||
|
||||
const response = await next()
|
||||
if (!isHtml(response)) return response
|
||||
|
||||
const ogUrl = buildOgUrl(slug)
|
||||
const handler = {
|
||||
element(element) {
|
||||
const content = element.getAttribute('content')
|
||||
if (shouldRewrite(content)) element.setAttribute('content', ogUrl)
|
||||
},
|
||||
}
|
||||
|
||||
return new HTMLRewriter()
|
||||
.on('meta[property="og:image"]', handler)
|
||||
.on('meta[property="twitter:image"]', handler)
|
||||
.transform(response)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## How it works
|
||||
- Hooks into the builder after each photo is processed.
|
||||
- Loads site branding from `config.json` (or provided path) and falls back to simple defaults.
|
||||
- Reuses the generated thumbnail (buffer or URL) as the image source, avoiding extra reads of the original file.
|
||||
- Injects light EXIF/context (title, date, focal length, aperture, ISO, shutter speed, camera) into the card.
|
||||
- Renders the card with `@afilmory/og-renderer` (Satori + resvg) using bundled fonts, then uploads the PNG to storage.
|
||||
- Caches uploads and public URLs within a single run so repeated work is skipped.
|
||||
|
||||
## Configuration
|
||||
- `enable` (boolean): turn the plugin on/off. Defaults to `true`.
|
||||
- `directory` (string): remote path prefix. Defaults to `.afilmory/og-images`.
|
||||
- `storageConfig` (storage config): optional override; otherwise uses the builder's current storage.
|
||||
- `contentType` (string): MIME type for uploads. Defaults to `image/png`.
|
||||
- `siteName` / `accentColor` (strings): optional overrides for branding.
|
||||
- `siteConfigPath` (string): path to a site config JSON; defaults to `config.json` in `process.cwd()`.
|
||||
|
||||
## Dependencies
|
||||
- Uses fonts from `be/apps/core/src/modules/content/og/assets` (falls back to other repo paths). If fonts are missing, the plugin skips rendering for that run.
|
||||
- Relies on the thumbnail storage plugin to provide in-memory thumbnail data when available; otherwise it will read a local/public or remote thumbnail URL.
|
||||
|
||||
## Notes
|
||||
- Unsupported storage provider: `eagle` (the plugin disables itself in this case).
|
||||
- Remote keys are cached; forcing (`isForceMode` or `isForceManifest`) re-renders and re-uploads.
|
||||
- OG URLs are attached to `item.ogImageUrl` on the manifest entries.
|
||||
456
packages/builder/src/plugins/og-image-storage/index.ts
Normal file
456
packages/builder/src/plugins/og-image-storage/index.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { ExifInfo } from '@afilmory/og-renderer'
|
||||
import { renderOgImage } from '@afilmory/og-renderer'
|
||||
import type { SatoriOptions } from 'satori'
|
||||
|
||||
import type { Logger } from '../../logger/index.js'
|
||||
import { workdir } from '../../path.js'
|
||||
import { StorageManager } from '../../storage/index.js'
|
||||
import type { S3CompatibleConfig, StorageConfig } from '../../storage/interfaces.js'
|
||||
import type { PhotoManifestItem } from '../../types/photo.js'
|
||||
import type { ThumbnailPluginData } from '../thumbnail-storage/shared.js'
|
||||
import { THUMBNAIL_PLUGIN_DATA_KEY } from '../thumbnail-storage/shared.js'
|
||||
import type { BuilderPlugin } from '../types.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '../../../../..')
|
||||
const ogAssetsDir = path.join(repoRoot, 'be/apps/core/src/modules/content/og/assets')
|
||||
|
||||
const PLUGIN_NAME = 'afilmory:og-image'
|
||||
const RUN_STATE_KEY = 'state'
|
||||
const DEFAULT_DIRECTORY = '.afilmory/og-images'
|
||||
const DEFAULT_CONTENT_TYPE = 'image/png'
|
||||
const DEFAULT_SITE_NAME = 'Photo Gallery'
|
||||
const DEFAULT_ACCENT_COLOR = '#007bff'
|
||||
|
||||
type UploadableStorageConfig = Exclude<StorageConfig, { provider: 'eagle' }>
|
||||
|
||||
interface OgImagePluginOptions {
|
||||
enable?: boolean
|
||||
directory?: string
|
||||
storageConfig?: UploadableStorageConfig
|
||||
contentType?: string
|
||||
siteName?: string
|
||||
accentColor?: string
|
||||
siteConfigPath?: string
|
||||
}
|
||||
|
||||
interface ResolvedPluginConfig {
|
||||
directory: string
|
||||
remotePrefix: string
|
||||
contentType: string
|
||||
useDefaultStorage: boolean
|
||||
storageConfig: UploadableStorageConfig | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface SiteMeta {
|
||||
siteName: string
|
||||
accentColor?: string
|
||||
}
|
||||
|
||||
interface PluginRunState {
|
||||
uploaded: Set<string>
|
||||
urlCache: Map<string, string>
|
||||
fonts?: SatoriOptions['fonts'] | null
|
||||
siteMeta?: SiteMeta
|
||||
}
|
||||
|
||||
function normalizeDirectory(directory: string | undefined): string {
|
||||
const value = directory?.trim() || DEFAULT_DIRECTORY
|
||||
const normalized = value.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
return normalized || DEFAULT_DIRECTORY
|
||||
}
|
||||
|
||||
function trimSlashes(value: string | undefined | null): string | null {
|
||||
if (!value) return null
|
||||
const normalized = value.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
return normalized.length > 0 ? normalized : null
|
||||
}
|
||||
|
||||
function joinSegments(...segments: Array<string | null | undefined>): string {
|
||||
const filtered = segments
|
||||
.map((segment) => (segment ?? '').replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, ''))
|
||||
.filter((segment) => segment.length > 0)
|
||||
return filtered.join('/')
|
||||
}
|
||||
|
||||
function resolveRemotePrefix(config: UploadableStorageConfig, directory: string): string {
|
||||
switch (config.provider) {
|
||||
case 's3':
|
||||
case 'oss':
|
||||
case 'cos': {
|
||||
const base = trimSlashes((config as S3CompatibleConfig).prefix)
|
||||
return joinSegments(base, directory)
|
||||
}
|
||||
default: {
|
||||
return joinSegments(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or initialize per-run caches to dedupe uploads and URL lookups.
|
||||
*/
|
||||
function getOrCreateRunState(container: Map<string, unknown>): PluginRunState {
|
||||
let state = container.get(RUN_STATE_KEY) as PluginRunState | undefined
|
||||
if (!state) {
|
||||
state = {
|
||||
uploaded: new Set<string>(),
|
||||
urlCache: new Map<string, string>(),
|
||||
fonts: null,
|
||||
}
|
||||
container.set(RUN_STATE_KEY, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
async function loadFontFile(fileName: string): Promise<Buffer | null> {
|
||||
const candidates = [
|
||||
path.join(ogAssetsDir, fileName),
|
||||
path.join(repoRoot, 'apps/core/src/modules/content/og/assets', fileName),
|
||||
path.join(repoRoot, 'core/src/modules/content/og/assets', fileName),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const stats = await stat(candidate).catch(() => null)
|
||||
if (stats?.isFile()) {
|
||||
return await readFile(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Load required fonts for Satori/resvg. Missing fonts cause the plugin to skip rendering.
|
||||
*/
|
||||
async function loadFonts(logger: Logger): Promise<SatoriOptions['fonts'] | null> {
|
||||
const geist = await loadFontFile('Geist-Medium.ttf')
|
||||
const harmony = await loadFontFile('HarmonyOS_Sans_SC_Medium.ttf')
|
||||
|
||||
if (!geist || !harmony) {
|
||||
logger.main.warn('OG image plugin: fonts not found, skip rendering for this run.')
|
||||
return null
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Geist',
|
||||
data: geist,
|
||||
style: 'normal',
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: 'HarmonyOS Sans SC',
|
||||
data: harmony,
|
||||
style: 'normal',
|
||||
weight: 400,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve site branding from a JSON config file, with sane fallbacks when the file is absent.
|
||||
*/
|
||||
async function loadSiteMeta(options: OgImagePluginOptions, logger: Logger): Promise<SiteMeta> {
|
||||
const fallback: SiteMeta = {
|
||||
siteName: options.siteName?.trim() || DEFAULT_SITE_NAME,
|
||||
accentColor: options.accentColor?.trim() || DEFAULT_ACCENT_COLOR,
|
||||
}
|
||||
|
||||
const siteConfigPath = options.siteConfigPath
|
||||
? path.resolve(process.cwd(), options.siteConfigPath)
|
||||
: path.resolve(process.cwd(), 'config.json')
|
||||
|
||||
try {
|
||||
const raw = await readFile(siteConfigPath, 'utf8')
|
||||
const parsed = JSON.parse(raw) as Partial<{ name: string; title: string; accentColor: string }>
|
||||
|
||||
return {
|
||||
siteName: parsed.name?.trim() || parsed.title?.trim() || fallback.siteName,
|
||||
accentColor: parsed.accentColor?.trim() || fallback.accentColor,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.main.info(`OG image plugin: using fallback site meta (${siteConfigPath} not readable: ${message}).`)
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
function bufferToDataUrl(buffer: Buffer, contentType: string): string {
|
||||
const base64 = buffer.toString('base64')
|
||||
return `data:${contentType};base64,${base64}`
|
||||
}
|
||||
|
||||
function guessContentType(thumbnailUrl: string): string {
|
||||
const lowered = thumbnailUrl.toLowerCase()
|
||||
if (lowered.endsWith('.png')) return 'image/png'
|
||||
if (lowered.endsWith('.webp')) return 'image/webp'
|
||||
return 'image/jpeg'
|
||||
}
|
||||
|
||||
async function resolveThumbnailDataUrl(
|
||||
item: PhotoManifestItem,
|
||||
pluginData: ThumbnailPluginData | undefined,
|
||||
logger: Logger,
|
||||
): Promise<string | null> {
|
||||
// Prefer the in-memory thumbnail to avoid extra reads; fall back to URLs when needed.
|
||||
if (pluginData?.buffer) {
|
||||
return bufferToDataUrl(pluginData.buffer, 'image/jpeg')
|
||||
}
|
||||
|
||||
const thumbnailUrl = pluginData?.localUrl || item.thumbnailUrl
|
||||
if (!thumbnailUrl) return null
|
||||
|
||||
const contentType = guessContentType(thumbnailUrl)
|
||||
|
||||
if (/^https?:\/\//i.test(thumbnailUrl)) {
|
||||
try {
|
||||
const response = await fetch(thumbnailUrl)
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
return bufferToDataUrl(Buffer.from(arrayBuffer), response.headers.get('content-type') ?? contentType)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.thumbnail?.warn?.(`OG image plugin: failed to fetch remote thumbnail ${thumbnailUrl}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = thumbnailUrl.replace(/^\/+/, '')
|
||||
const localPath = path.join(workdir, 'public', normalized)
|
||||
|
||||
try {
|
||||
const localBuffer = await readFile(localPath)
|
||||
return bufferToDataUrl(localBuffer, contentType)
|
||||
} catch (error) {
|
||||
logger.thumbnail?.debug?.(`OG image plugin: could not read local thumbnail ${localPath}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(input?: string | null): string | undefined {
|
||||
if (!input) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(input)
|
||||
if (Number.isNaN(timestamp)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lightweight EXIF summary for display; returns null when nothing meaningful is present.
|
||||
*/
|
||||
function buildExifInfo(photo: PhotoManifestItem): ExifInfo | null {
|
||||
const { exif } = photo
|
||||
if (!exif) {
|
||||
return null
|
||||
}
|
||||
|
||||
const focalLength = exif.FocalLengthIn35mmFormat || exif.FocalLength
|
||||
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null
|
||||
const iso = exif.ISO ?? null
|
||||
const shutterSpeed = exif.ExposureTime ? `${exif.ExposureTime}s` : null
|
||||
const camera =
|
||||
exif.Make && exif.Model ? `${exif.Make.trim()} ${exif.Model.trim()}`.trim() : (exif.Model ?? exif.Make ?? null)
|
||||
|
||||
if (!focalLength && !aperture && !iso && !shutterSpeed && !camera) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
focalLength: focalLength ?? null,
|
||||
aperture,
|
||||
iso,
|
||||
shutterSpeed,
|
||||
camera,
|
||||
}
|
||||
}
|
||||
|
||||
function getPhotoDimensions(photo: PhotoManifestItem) {
|
||||
return {
|
||||
width: photo.width || 1,
|
||||
height: photo.height || 1,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Open Graph images for processed photos and upload them to remote storage.
|
||||
*
|
||||
* The plugin reuses generated thumbnails as the image source, injects light EXIF
|
||||
* metadata, and caches uploads/URLs during a single builder run to reduce storage
|
||||
* churn.
|
||||
*/
|
||||
export default function ogImagePlugin(options: OgImagePluginOptions = {}): BuilderPlugin {
|
||||
let resolved: ResolvedPluginConfig | null = null
|
||||
let externalStorageManager: StorageManager | null = null
|
||||
|
||||
return {
|
||||
name: PLUGIN_NAME,
|
||||
hooks: {
|
||||
onInit: ({ builder, config, logger }) => {
|
||||
const enable = options.enable ?? true
|
||||
const directory = normalizeDirectory(options.directory)
|
||||
const contentType = options.contentType ?? DEFAULT_CONTENT_TYPE
|
||||
|
||||
if (!enable) {
|
||||
resolved = {
|
||||
directory,
|
||||
remotePrefix: '',
|
||||
contentType,
|
||||
useDefaultStorage: true,
|
||||
storageConfig: null,
|
||||
enabled: false,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const fallbackStorage = config.user?.storage ?? builder.getStorageConfig()
|
||||
const storageConfig = (options.storageConfig ?? fallbackStorage) as StorageConfig
|
||||
|
||||
if (storageConfig.provider === 'eagle') {
|
||||
logger.main.warn('OG image plugin does not support Eagle storage provider; plugin disabled.')
|
||||
resolved = {
|
||||
directory,
|
||||
remotePrefix: '',
|
||||
contentType,
|
||||
useDefaultStorage: !options.storageConfig,
|
||||
storageConfig: null,
|
||||
enabled: false,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const uploadableConfig = storageConfig as UploadableStorageConfig
|
||||
const remotePrefix = resolveRemotePrefix(uploadableConfig, directory)
|
||||
|
||||
resolved = {
|
||||
directory,
|
||||
remotePrefix,
|
||||
contentType,
|
||||
useDefaultStorage: !options.storageConfig,
|
||||
storageConfig: uploadableConfig,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
if (!options.storageConfig) {
|
||||
builder.getStorageManager().addExcludePrefix(remotePrefix)
|
||||
} else {
|
||||
externalStorageManager = new StorageManager(uploadableConfig)
|
||||
}
|
||||
},
|
||||
afterPhotoProcess: async ({ builder, payload, runShared, logger }) => {
|
||||
if (!resolved || !resolved.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const { item, type } = payload.result
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
const storageManager = resolved.useDefaultStorage ? builder.getStorageManager() : externalStorageManager
|
||||
if (!storageManager) {
|
||||
logger.main.warn('OG image plugin could not resolve storage manager. Skipping upload.')
|
||||
return
|
||||
}
|
||||
|
||||
const state = getOrCreateRunState(runShared)
|
||||
|
||||
if (!state.siteMeta) {
|
||||
state.siteMeta = await loadSiteMeta(options, logger)
|
||||
}
|
||||
|
||||
if (!state.fonts) {
|
||||
state.fonts = await loadFonts(logger)
|
||||
}
|
||||
|
||||
const { fonts } = state
|
||||
if (!fonts) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldRender = type !== 'skipped' || payload.options.isForceMode || payload.options.isForceManifest
|
||||
|
||||
const remoteKey = joinSegments(resolved.remotePrefix, `${item.id}.png`)
|
||||
|
||||
if (!shouldRender) {
|
||||
try {
|
||||
const remoteUrl = await storageManager.generatePublicUrl(remoteKey)
|
||||
state.urlCache.set(remoteKey, remoteUrl)
|
||||
item.ogImageUrl = remoteUrl
|
||||
} catch (error) {
|
||||
logger.main.info(`OG image plugin: skipped rendering and could not resolve URL for ${remoteKey}.`, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const thumbnailData = payload.context.pluginData[THUMBNAIL_PLUGIN_DATA_KEY] as ThumbnailPluginData | undefined
|
||||
const thumbnailSrc = await resolveThumbnailDataUrl(item, thumbnailData, logger)
|
||||
const exifInfo = buildExifInfo(item)
|
||||
const formattedDate = formatDate(item.exif?.DateTimeOriginal ?? item.lastModified)
|
||||
|
||||
try {
|
||||
const png = await renderOgImage({
|
||||
template: {
|
||||
photoTitle: item.title || item.id || 'Untitled Photo',
|
||||
siteName: state.siteMeta.siteName || DEFAULT_SITE_NAME,
|
||||
tags: (item.tags ?? []).slice(0, 3),
|
||||
formattedDate,
|
||||
exifInfo,
|
||||
thumbnailSrc,
|
||||
photoDimensions: getPhotoDimensions(item),
|
||||
accentColor: state.siteMeta.accentColor ?? DEFAULT_ACCENT_COLOR,
|
||||
},
|
||||
fonts,
|
||||
})
|
||||
|
||||
const stateForUpload = state
|
||||
if (
|
||||
!stateForUpload.uploaded.has(remoteKey) ||
|
||||
payload.options.isForceMode ||
|
||||
payload.options.isForceManifest
|
||||
) {
|
||||
try {
|
||||
await storageManager.uploadFile(remoteKey, Buffer.from(png), {
|
||||
contentType: resolved.contentType,
|
||||
})
|
||||
stateForUpload.uploaded.add(remoteKey)
|
||||
} catch (error) {
|
||||
logger.main.error(`OG image plugin: failed to upload ${remoteKey}`, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let remoteUrl = stateForUpload.urlCache.get(remoteKey)
|
||||
if (!remoteUrl) {
|
||||
try {
|
||||
remoteUrl = await storageManager.generatePublicUrl(remoteKey)
|
||||
stateForUpload.urlCache.set(remoteKey, remoteUrl)
|
||||
} catch (error) {
|
||||
logger.main.error(`OG image plugin: failed to generate URL for ${remoteKey}`, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
item.ogImageUrl = remoteUrl
|
||||
} catch (error) {
|
||||
logger.main.error(`OG image plugin: failed to render OG image for ${item.id}`, error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type { OgImagePluginOptions }
|
||||
@@ -60,6 +60,7 @@ export interface PhotoManifestItem extends PhotoInfo {
|
||||
originalUrl: string
|
||||
format: string
|
||||
thumbnailUrl: string
|
||||
ogImageUrl?: string | null
|
||||
thumbHash: string | null
|
||||
width: number
|
||||
height: number
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
@@ -15,12 +19,22 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"paths": {
|
||||
"@afilmory/builder/*": ["./src/*"],
|
||||
"@pkg": ["./package.json"],
|
||||
"@env": ["../../env.ts"]
|
||||
"@afilmory/builder/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@pkg": [
|
||||
"./package.json"
|
||||
],
|
||||
"@env": [
|
||||
"../../env.ts"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*", "./scripts/**/*"]
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./scripts/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
15
packages/renderer/package.json
Normal file
15
packages/renderer/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@afilmory/og-renderer",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./renderer": "./src/og.renderer.tsx",
|
||||
"./template": "./src/og.template.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@resvg/resvg-js": "2.6.2",
|
||||
"hono": "4.11.1",
|
||||
"satori": "catalog:"
|
||||
}
|
||||
}
|
||||
3
packages/renderer/src/index.ts
Normal file
3
packages/renderer/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { renderHomepageOgImage, renderOgImage } from './og/og.renderer'
|
||||
export type { ExifInfo, HomepageOgTemplateProps, OgTemplateProps, PhotoDimensions } from './og/og.template'
|
||||
export { HomepageOgTemplate, OgTemplate } from './og/og.template'
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @jsxImportSource hono/jsx */
|
||||
/** @jsxRuntime automatic */
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
import { Resvg } from '@resvg/resvg-js'
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @jsxImportSource hono/jsx */
|
||||
/** @jsxRuntime automatic */
|
||||
|
||||
export interface PhotoDimensions {
|
||||
width: number
|
||||
44
packages/renderer/tmp-og.js
Normal file
44
packages/renderer/tmp-og.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
import { Resvg } from '@resvg/resvg-js'
|
||||
import { jsx } from 'hono/jsx/jsx-runtime'
|
||||
import satori from 'satori'
|
||||
|
||||
import { HomepageOgTemplate, OgTemplate } from './og.template'
|
||||
import { get_icon_code, load_emoji } from './tweemoji'
|
||||
|
||||
export async function renderOgImage({ template, fonts }) {
|
||||
const svg = await satori(/* @__PURE__ */ jsx(OgTemplate, { ...template }), {
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
embedFont: true,
|
||||
})
|
||||
const svgInput = typeof svg === 'string' ? svg : Buffer.from(svg)
|
||||
const renderer = new Resvg(svgInput, {
|
||||
fitTo: { mode: 'width', value: 1200 },
|
||||
background: 'rgba(0,0,0,0)',
|
||||
})
|
||||
return renderer.render().asPng()
|
||||
}
|
||||
export async function renderHomepageOgImage({ template, fonts }) {
|
||||
const svg = await satori(/* @__PURE__ */ jsx(HomepageOgTemplate, { ...template }), {
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
embedFont: true,
|
||||
async loadAdditionalAsset(code, segment) {
|
||||
if (code === 'emoji' && segment) {
|
||||
return `data:image/svg+xml;base64,${btoa(await load_emoji(get_icon_code(segment)))}`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
})
|
||||
const svgInput = typeof svg === 'string' ? svg : Buffer.from(svg)
|
||||
const renderer = new Resvg(svgInput, {
|
||||
fitTo: { mode: 'width', value: 1200 },
|
||||
background: 'rgba(0,0,0,0)',
|
||||
})
|
||||
return renderer.render().asPng()
|
||||
}
|
||||
28
packages/renderer/tsconfig.json
Normal file
28
packages/renderer/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
122
pnpm-lock.yaml
generated
122
pnpm-lock.yaml
generated
@@ -24,6 +24,9 @@ catalogs:
|
||||
dotenv-expand:
|
||||
specifier: 12.0.3
|
||||
version: 12.0.3
|
||||
satori:
|
||||
specifier: 0.18.3
|
||||
version: 0.18.3
|
||||
tailwind-scrollbar:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2
|
||||
@@ -521,6 +524,9 @@ importers:
|
||||
postgres:
|
||||
specifier: 3.4.7
|
||||
version: 3.4.7
|
||||
prop-types:
|
||||
specifier: 15.8.1
|
||||
version: 15.8.1
|
||||
react:
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3
|
||||
@@ -929,6 +935,9 @@ importers:
|
||||
'@afilmory/framework':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/framework
|
||||
'@afilmory/og-renderer':
|
||||
specifier: workspace:*
|
||||
version: link:../../../packages/renderer
|
||||
'@afilmory/redis':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/redis
|
||||
@@ -1002,7 +1011,7 @@ importers:
|
||||
specifier: 6.6.0
|
||||
version: 6.6.0
|
||||
satori:
|
||||
specifier: 0.18.3
|
||||
specifier: 'catalog:'
|
||||
version: 0.18.3
|
||||
tsyringe:
|
||||
specifier: 4.10.0
|
||||
@@ -1428,6 +1437,12 @@ importers:
|
||||
|
||||
packages/builder:
|
||||
dependencies:
|
||||
'@afilmory/og-renderer':
|
||||
specifier: workspace:*
|
||||
version: link:../renderer
|
||||
'@resvg/resvg-js':
|
||||
specifier: 2.6.2
|
||||
version: 2.6.2
|
||||
'@vingle/bmp-js':
|
||||
specifier: ^0.2.5
|
||||
version: 0.2.5
|
||||
@@ -1458,6 +1473,9 @@ importers:
|
||||
heic-to:
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
satori:
|
||||
specifier: 'catalog:'
|
||||
version: 0.18.3
|
||||
sharp:
|
||||
specifier: 0.34.5
|
||||
version: 0.34.5
|
||||
@@ -1500,6 +1518,18 @@ importers:
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3(react@19.2.3)
|
||||
|
||||
packages/renderer:
|
||||
devDependencies:
|
||||
'@resvg/resvg-js':
|
||||
specifier: 2.6.2
|
||||
version: 2.6.2
|
||||
hono:
|
||||
specifier: 4.11.1
|
||||
version: 4.11.1
|
||||
satori:
|
||||
specifier: 'catalog:'
|
||||
version: 0.18.3
|
||||
|
||||
packages/sdk:
|
||||
dependencies:
|
||||
zod:
|
||||
@@ -3413,89 +3443,105 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -3680,24 +3726,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.10':
|
||||
resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.10':
|
||||
resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
|
||||
@@ -3767,36 +3817,42 @@ 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==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -4775,24 +4831,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
|
||||
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
|
||||
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@resvg/resvg-js-linux-x64-musl@2.6.2':
|
||||
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
|
||||
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
|
||||
@@ -4855,24 +4915,28 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
|
||||
@@ -4987,56 +5051,67 @@ packages:
|
||||
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
|
||||
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.3':
|
||||
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.3':
|
||||
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.3':
|
||||
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
|
||||
@@ -5385,24 +5460,28 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.15.5':
|
||||
resolution: {integrity: sha512-Of+wmVh5h47tTpN9ghHVjfL0CJrgn99XmaJjmzWFW7agPdVY6gTDgkk6zQ6q4hcDQ7hXb0BGw6YFpuanBzNPow==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.15.5':
|
||||
resolution: {integrity: sha512-98kuPS0lZVgjmc/2uTm39r1/OfwKM0PM13ZllOAWi5avJVjRd/j1xA9rKeUzHDWt+ocH9mTCQsAT1jjKSq45bg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.15.5':
|
||||
resolution: {integrity: sha512-Rk+OtNQP3W/dZExL74LlaakXAQn6/vbrgatmjFqJPO4RZkq+nLo5g7eDUVjyojuERh7R2yhqNvZ/ZZQe8JQqqA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.15.5':
|
||||
resolution: {integrity: sha512-e3RTdJ769+PrN25iCAlxmsljEVu6iIWS7sE21zmlSiipftBQvSAOWuCDv2A8cH9lm5pSbZtwk8AUpIYCNsj2oQ==}
|
||||
@@ -5520,24 +5599,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@@ -6051,41 +6134,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -6377,12 +6468,14 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
autocorrect-node-linux-x64-musl@2.14.0:
|
||||
resolution: {integrity: sha512-3LifiLG61VIXBrOITpD/REcg/ZEUuLz/P2XUWMCfGqhzoUb8oFgM3o0A6extvIH2NefiHMndB9kNbfQjXCl3zA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
autocorrect-node-win32-x64-msvc@2.14.0:
|
||||
resolution: {integrity: sha512-OM6TeGUW0+4R9KtUYTYNEwlInd+b2kumw/B1HCbCzb1F7HpfaNxnZiMk1li0bdPPBDzD5YuzwM30VJZMCWIi5A==}
|
||||
@@ -8666,24 +8759,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@@ -8762,6 +8859,10 @@ packages:
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
lower-case@2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
|
||||
@@ -9239,6 +9340,10 @@ packages:
|
||||
engines: {node: ^14.16.0 || >=16.10.0}
|
||||
hasBin: true
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9664,6 +9769,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
@@ -19808,6 +19916,10 @@ snapshots:
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lower-case@2.0.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -20664,6 +20776,8 @@ snapshots:
|
||||
pkg-types: 2.3.0
|
||||
tinyexec: 1.0.1
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
@@ -21037,6 +21151,12 @@ snapshots:
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
protocol-buffers-schema@3.6.0: {}
|
||||
|
||||
@@ -13,6 +13,7 @@ catalog:
|
||||
'@tailwindcss/postcss': 4.1.18
|
||||
'@tailwindcss/typography': 0.5.19
|
||||
dotenv-expand: 12.0.3
|
||||
satori: 0.18.3
|
||||
tailwind-scrollbar: 4.0.2
|
||||
tailwind-variants: 3.2.2
|
||||
tailwindcss: 4.1.18
|
||||
|
||||
Reference in New Issue
Block a user