Initial work for postgen

This commit is contained in:
Eric Bailey
2024-08-28 18:23:28 -05:00
parent 6a4c56dfa9
commit e74bd093ee
38 changed files with 2271 additions and 24 deletions
+6 -4
View File
@@ -4,13 +4,13 @@
"type": "module",
"main": "src/index.ts",
"scripts": {
"start": "node --loader ts-node/esm ./src/bin.ts",
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
"start": " NODE_ENV=production node --loader ts-node/esm ./src/bin.ts",
"dev": "LOG_ENABLED=true NODE_ENV=development node --watch-path ./src --loader ts-node/esm ./src/bin.ts | pino-pretty",
"build": "tsc && cp -r src/assets dist/",
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
},
"dependencies": {
"@atproto/api": "0.12.19-next.0",
"@atproto/api": "^0.13.4",
"@atproto/common": "^0.4.0",
"@resvg/resvg-js": "^2.6.2",
"express": "^4.19.2",
@@ -18,10 +18,12 @@
"pino": "^9.2.0",
"react": "^18.3.1",
"satori": "^0.10.13",
"twemoji": "^14.0.2"
"twemoji": "^14.0.2",
"whatwg-mimetype": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.14.3",
"pino-pretty": "^11.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import {atoms as a, style as s} from '../theme/index.js'
export function Box({
children,
cx,
}: {
children?: React.ReactNode
cx?: Record<string, any>[]
}) {
return <div style={s([a.flex, a.flex_col, ...(cx ?? [])])}>{children}</div>
}
+55
View File
@@ -0,0 +1,55 @@
import React from 'react'
import {atoms as a} from '../theme/index.js'
import {Box} from './Box.js'
export function Row({
children,
gutter = 0,
cx,
}: {
children: React.ReactNode
gutter?: number
cx?: Record<string, any>[]
}) {
return (
<Box
cx={[
a.flex_row,
{
marginLeft: -gutter,
marginRight: -gutter,
},
...(cx || []),
]}>
{children}
</Box>
)
}
export function Column({
children,
gutter = 0,
cx,
width = 1,
}: {
children: React.ReactNode
gutter?: number
cx?: Record<string, any>[]
width?: number
}) {
return (
<Box
cx={[
a.flex_col,
{
paddingLeft: gutter,
paddingRight: gutter,
width: `${width * 100}%`,
},
...(cx || []),
]}>
{children}
</Box>
)
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react'
import {style as s} from '../theme/index.js'
import {Image as ImageSource} from '../util/resolvePostData.js'
export type ImageProps = Omit<
React.ImgHTMLAttributes<HTMLImageElement>,
'src' | 'style'
> & {
image: ImageSource
cx?: Record<string, any>[]
}
export function Image({image, cx, ...rest}: ImageProps) {
const {image: src, mime = 'image/jpeg'} = image
return (
<img
{...rest}
src={`data:${mime};base64,${src.toString('base64')}`}
style={cx ? s(cx) : undefined}
/>
)
}
+62
View File
@@ -0,0 +1,62 @@
/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
import React from 'react'
import {atoms as a, theme as t} from '../theme/index.js'
import {Image as ImageSource} from '../util/resolvePostData.js'
import {toShortUrl} from '../util/toShortUrl.js'
import {Box} from './Box.js'
import {Image} from './Image.js'
import {Text} from './Text.js'
export function LinkCard({
image,
uri,
title,
description,
}: {
image?: ImageSource
uri?: string
title: string
description: string
}) {
return (
<Box
cx={[
a.w_full,
a.rounded_sm,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{image && (
<Box
cx={[
a.relative,
a.w_full,
t.atoms.bg_contrast_25,
{paddingTop: (630 / 1200) * 100 + '%'},
]}>
<Image
image={image}
cx={[
a.absolute,
a.inset_0,
{
objectFit: 'cover',
},
]}
/>
</Box>
)}
<Box cx={[a.p_md, t.atoms.bg]}>
{uri && (
<Text cx={[a.text_xs, a.pb_sm, t.atoms.text_contrast_medium]}>
{toShortUrl(uri)}
</Text>
)}
<Text cx={[a.text_md, a.font_bold, a.pb_xs]}>{title}</Text>
<Text cx={[a.text_sm, a.leading_snug]}>{description}</Text>
</Box>
</Box>
)
}
+484
View File
@@ -0,0 +1,484 @@
/* eslint-disable bsky-internal/avoid-unwrapped-text, react-native-a11y/has-valid-accessibility-ignores-invert-colors */
import React from 'react'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AppBskyGraphStarterpack,
} from '@atproto/api'
import {atoms as a, gradient, theme as t} from '../theme/index.js'
import {formatCount} from '../util/formatCount.js'
import {formatDate} from '../util/formatDate.js'
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
import {Image as ImageSource, PostData} from '../util/resolvePostData.js'
import {Box} from './Box.js'
import * as Grid from './Grid.js'
import {CircleInfo} from './icons/CircleInfo.js'
import {Heart} from './icons/Heart.js'
import {Logomark} from './icons/Logomark.js'
import {Logotype} from './icons/Logotype.js'
import {Repost} from './icons/Repost.js'
import {Image} from './Image.js'
import {LinkCard} from './LinkCard.js'
import {RichText} from './RichText.js'
import {Text} from './Text.js'
export function Post({
post,
data,
}: {
post: AppBskyFeedDefs.PostView
data: PostData
}) {
if (AppBskyFeedPost.isRecord(post.record)) {
const avatar = data.images.get(post.author.avatar)
const text = post.record.text
const rt = data.texts.get(text)
const hasInteractions = post.likeCount > 0 || post.repostCount > 0
return (
<Box
cx={[
a.flex,
a.align_center,
a.w_full,
a.h_full,
a.p_3xl,
{
backgroundImage: `linear-gradient(to bottom, ${gradient('sky')})`,
},
]}>
<Box
cx={[a.flex, a.flex_col, a.w_full, a.p_xl, a.rounded_md, t.atoms.bg]}>
<Box cx={[a.flex_row, a.align_center, a.gap_sm, a.pb_sm]}>
<Box
cx={[
a.rounded_full,
a.overflow_hidden,
t.atoms.bg_contrast_25,
{
width: '48px',
height: '48px',
},
]}>
{avatar && <Image height="100%" width="100%" image={avatar} />}
</Box>
<Box cx={[a.flex_col]}>
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>
{post.author.displayName || post.author.handle}
</Text>
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
@{post.author.handle}
</Text>
</Box>
</Box>
{rt && <RichText value={rt} />}
{post.embed && (
<Box cx={[a.pt_md]}>
<Embeds embed={post.embed} data={data} />
</Box>
)}
<Box cx={[a.flex_row, a.align_center, a.justify_between, a.pt_lg]}>
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
{formatDate(post.record.createdAt)}
</Text>
{!hasInteractions && <Logo />}
</Box>
{hasInteractions && (
<Box cx={[a.pt_md]}>
<Box
cx={[
a.w_full,
a.pb_md,
a.border_t,
t.atoms.border_contrast_low,
]}
/>
<Box cx={[a.flex_row, a.align_center, a.justify_between]}>
<Box cx={[a.flex_row, a.align_center, a.gap_2xl]}>
{post.likeCount > 0 && (
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
<Heart size={22} fill={t.palette.red_400} />
<Box
cx={[
a.text_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
{formatCount(post.likeCount)}
</Box>
</Box>
)}
{post.repostCount > 0 && (
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
<Repost size={22} fill={t.palette.green_600} />
<Box
cx={[
a.text_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
{formatCount(post.repostCount)}
</Box>
</Box>
)}
</Box>
<Logo />
</Box>
</Box>
)}
</Box>
</Box>
)
}
return null
}
export function Logo() {
return (
<Box cx={[a.flex_row, a.align_center]}>
<Logomark size={20} fill={t.palette.primary_500} />
<Box
cx={[
{
paddingTop: '3px',
paddingLeft: '6px',
},
]}>
<Logotype size={64} fill={t.palette.contrast_800} />
</Box>
</Box>
)
}
export function Embeds({
embed,
data,
hideNestedEmbeds,
}: {
embed: AppBskyFeedDefs.PostView['embed']
data: PostData
hideNestedEmbeds?: boolean
}) {
if (AppBskyEmbedExternal.isView(embed)) {
const {title, description, uri, thumb} = embed.external
const image = data.images.get(thumb)
return (
<LinkCard
image={image}
title={title}
description={description}
uri={uri}
/>
)
}
if (AppBskyEmbedImages.isView(embed)) {
const {images} = embed
if (images.length > 0) {
const imgs = images.map(({fullsize}) => data.images.get(fullsize))
if (imgs.length === 1) {
return (
<Box cx={[a.rounded_sm, a.w_full, a.overflow_hidden]}>
<Image image={imgs[0]} />
</Box>
)
} else if (imgs.length === 2) {
return (
<Grid.Row gutter={a.p_xs.padding}>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
<SquareImage image={imgs[0]} />
</Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
<SquareImage image={imgs[1]} />
</Grid.Column>
</Grid.Row>
)
} else if (imgs.length === 3) {
return (
<Grid.Row gutter={a.p_xs.padding}>
<Grid.Column gutter={a.p_xs.padding} width={2 / 3}>
<SquareImage image={imgs[0]} />
</Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 3} cx={[a.gap_sm]}>
<SquareImage image={imgs[1]} />
<SquareImage image={imgs[2]} />
</Grid.Column>
</Grid.Row>
)
} else {
return (
<Grid.Row gutter={a.p_xs.padding}>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
<SquareImage image={imgs[0]} />
<SquareImage image={imgs[1]} />
</Grid.Column>
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
<SquareImage image={imgs[2]} />
<SquareImage image={imgs[3]} />
</Grid.Column>
</Grid.Row>
)
}
}
}
if (AppBskyEmbedRecord.isView(embed)) {
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
return <FeedCard embed={embed.record} data={data} />
}
if (AppBskyGraphDefs.isListView(embed.record)) {
return <ListCard embed={embed.record} data={data} />
}
if (
AppBskyGraphDefs.isStarterPackViewBasic(embed.record) &&
AppBskyGraphStarterpack.isRecord(embed.record.record)
) {
const uri = getStarterPackImageUri(embed.record)
const {name, description} = embed.record.record
const image = data.images.get(uri)
return <LinkCard image={image} title={name} description={description} />
}
return <QuoteEmbed embed={embed} data={data} />
}
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
return (
<Box cx={[a.gap_md]}>
<Embeds embed={embed.media} data={data} />
{!hideNestedEmbeds && <QuoteEmbed embed={embed.record} data={data} />}
</Box>
)
}
return null
}
export function FeedCard({
embed,
data,
}: {
embed: AppBskyFeedDefs.GeneratorView
data: PostData
}) {
const {avatar, displayName, likeCount, creator} = embed
const image = data.images.get(avatar)
return (
<Box
cx={[
a.w_full,
a.gap_sm,
a.rounded_sm,
a.p_md,
a.border,
t.atoms.border_contrast_low,
]}>
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
{image && (
<Image
image={image}
cx={[
a.rounded_sm,
{
width: '40px',
},
]}
/>
)}
<Box cx={[a.pt_2xs]}>
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{displayName}</Text>
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
</Box>
</Box>
{likeCount > 1 && (
<Text cx={[a.text_sm, a.leading_snug]}>
Liked by {formatCount(likeCount)} users
</Text>
)}
</Box>
)
}
export function ListCard({
embed,
data,
}: {
embed: AppBskyGraphDefs.ListView
data: PostData
}) {
const {avatar, name, creator} = embed
const image = data.images.get(avatar)
return (
<Box
cx={[
a.w_full,
a.gap_sm,
a.rounded_sm,
a.p_md,
a.border,
t.atoms.border_contrast_low,
]}>
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
{image && (
<Image
image={image}
cx={[
a.rounded_sm,
{
width: '40px',
},
]}
/>
)}
<Box cx={[a.pt_2xs]}>
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{name}</Text>
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
</Box>
</Box>
</Box>
)
}
export function SquareImage({image}: {image: ImageSource}) {
return (
<Box
cx={[
a.relative,
a.rounded_sm,
a.overflow_hidden,
a.w_full,
t.atoms.bg_contrast_25,
{paddingTop: '100%'},
]}>
<Image
image={image}
cx={[
a.absolute,
a.inset_0,
{
objectFit: 'cover',
},
]}
/>
</Box>
)
}
export function QuoteEmbed({
embed,
data,
}: {
embed: AppBskyEmbedRecord.View
data: PostData
}) {
if (
AppBskyEmbedRecord.isViewRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.value) &&
AppBskyFeedPost.validateRecord(embed.record.value).success
) {
const {author, value: post, embeds} = embed.record
const avatar = data.images.get(author.avatar)
const rt = data.texts.get(post.text)
const notPublic = author.labels.some(l => l.val === `!no-unauthenticated`)
if (notPublic) {
return (
<NotQuoteEmbed>
The author of the quoted post has requested their posts not be
displayed on external sites
</NotQuoteEmbed>
)
}
return (
<Box
cx={[
a.w_full,
a.p_md,
a.rounded_sm,
a.border,
t.atoms.border_contrast_low,
]}>
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.pb_sm]}>
<Box
cx={[
a.rounded_full,
a.overflow_hidden,
t.atoms.bg_contrast_25,
{
width: '20px',
height: '20px',
},
]}>
{avatar && <Image height="100%" width="100%" image={avatar} />}
</Box>
<Box cx={[a.flex_row, a.align_center, a.gap_xs]}>
<Text cx={[a.text_sm, a.font_bold]}>
{author.displayName || author.handle}
</Text>
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
@{author.handle}
</Text>
</Box>
</Box>
{rt && (
<Box>
<RichText value={rt} cx={[a.text_sm]} />
</Box>
)}
{Boolean(embeds && embeds.length) && (
<Box cx={[a.pt_sm]}>
<Embeds embed={embeds[0]} data={data} hideNestedEmbeds />
</Box>
)}
</Box>
)
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
return <NotQuoteEmbed>Quoted post is blocked</NotQuoteEmbed>
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
return (
<NotQuoteEmbed>
Quoted post not found, it may have been deleted.
</NotQuoteEmbed>
)
} else if (AppBskyEmbedRecord.isViewDetached(embed.record)) {
return <NotQuoteEmbed>Quoted post detached by author</NotQuoteEmbed>
}
return null
}
export function NotQuoteEmbed({children}: {children: React.ReactNode}) {
return (
<Box
cx={[
a.flex_row,
a.align_center,
a.gap_sm,
a.rounded_sm,
a.p_md,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size={20} fill={t.atoms.text_contrast_low.color} />
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
{children}
</Text>
</Box>
)
}
+91
View File
@@ -0,0 +1,91 @@
import React from 'react'
import {AppBskyRichtextFacet, RichText as RichTextApi} from '@atproto/api'
import {atoms as a, style as s, theme as t} from '../theme/index.js'
import {toShortUrl} from '../util/toShortUrl.js'
import {Text} from './Text.js'
export function RichText({
value,
disableLinks,
cx,
}: {
value: RichTextApi
disableLinks?: boolean
cx?: Record<string, any>[]
}) {
const {facets} = value
const baseStyles = [
a.leading_snug,
{
whiteSpace: 'pre-wrap',
},
cx ? s(cx) : {},
]
const linkStyles = [
{
color: t.palette.primary_500,
},
]
if (!facets?.length) {
// if (isOnlyEmoji(text)) {
// const fontSize =
// (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
// return (
// <Text
// selectable={selectable}
// testID={testID}
// style={[baseStyles, {fontSize}]}
// // @ts-ignore web only -prf
// dataSet={WORD_WRAP}>
// {text}
// </Text>
// )
// }
}
const els = []
let key = 0
// N.B. must access segments via `richText.segments`, not via destructuring
for (const segment of value.segments()) {
const {text, link, mention, tag} = segment
if (
mention &&
AppBskyRichtextFacet.validateMention(mention).success &&
!disableLinks
) {
els.push(
<span key={key} style={s(linkStyles)}>
{text}
</span>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
const url = toShortUrl(text)
if (disableLinks) {
els.push(url)
} else {
els.push(
<span key={key} style={s(linkStyles)}>
{url}
</span>,
)
}
} else if (
!disableLinks &&
tag &&
AppBskyRichtextFacet.validateTag(tag).success
) {
els.push(
<span key={key} style={s(linkStyles)}>
{segment.text}
</span>,
)
} else {
els.push(segment.text)
}
key++
}
return <Text cx={baseStyles}>{els}</Text>
}
@@ -106,6 +106,7 @@ export function StarterPack(props: {
color: 'white',
padding: 60,
fontSize: 40,
fontWeight: '700',
}}>
JOIN THE CONVERSATION
</div>
@@ -134,6 +135,7 @@ export function StarterPack(props: {
fontSize: isLongTitle ? 55 : 65,
display: 'flex',
textAlign: 'center',
fontWeight: '700',
}}>
{record?.name || 'Starter Pack'}
</div>
+26
View File
@@ -0,0 +1,26 @@
import React from 'react'
import {atoms as a, style as s, theme as t} from '../theme/index.js'
export function Text({
children,
cx,
}: {
children?: React.ReactNode
cx?: Record<string, any>[]
}) {
return (
<div
style={s([
a.flex,
a.flex_col,
a.font_normal,
a.leading_tight,
a.tracking_wide,
t.atoms.text,
...(cx ?? []),
])}>
{children}
</div>
)
}
@@ -0,0 +1,14 @@
import React from 'react'
export function CircleInfo({size, fill}: {size: number; fill?: string}) {
return (
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
<path
fill={fill}
fillRule="evenodd"
clipRule="evenodd"
d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react'
export function Heart({size, fill}: {size: number; fill?: string}) {
return (
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
<path
fill={fill}
fillRule="evenodd"
clipRule="evenodd"
d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"
/>
</svg>
)
}
@@ -0,0 +1,12 @@
import React from 'react'
export function Logomark({size, fill}: {size: number; fill?: string}) {
return (
<svg fill="none" viewBox="0 0 500 441" width={size}>
<path
fill={fill}
d="M108.382 29.634C165.705 72.668 227.36 159.93 250 206.754c22.64-46.824 84.295-134.084 141.618-177.12C432.98-1.42 500-25.447 500 51.008c0 15.269-8.755 128.269-13.889 146.615-17.848 63.779-82.883 80.047-140.735 70.2 101.122 17.211 126.846 74.218 71.291 131.225-105.511 108.268-151.649-27.165-163.471-61.867-2.167-6.361-3.181-9.338-3.196-6.807-.015-2.531-1.029.446-3.196 6.807-11.822 34.702-57.96 170.135-163.47 61.867-55.556-57.007-29.832-114.014 71.29-131.225-57.852 9.847-122.887-6.421-140.735-70.2C8.755 179.278 0 66.278 0 51.009 0-25.446 67.02-1.419 108.382 29.634Z"
/>
</svg>
)
}
@@ -0,0 +1,12 @@
import React from 'react'
export function Logotype({size, fill}: {size: number; fill?: string}) {
return (
<svg fill="none" viewBox="0 0 398 108" width={size}>
<path
fill={fill}
d="M53.139 40.276c9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.766 23.886-28.494 23.886H.632V.818h36.996c17.809 0 26.426 9.25 26.426 21.544 0 8.196-3.677 14.167-10.915 17.914Zm-16.66-26.462H16.143v21.779h20.336c7.928 0 12.179-4.215 12.179-11.24 0-6.44-4.366-10.539-12.179-10.539ZM16.143 71.07h21.945c8.732 0 13.442-4.098 13.442-11.474 0-7.728-4.48-11.592-13.442-11.592H16.143V71.07ZM88.892 84.184H74.415V.818h14.477v83.366ZM136.892 57.605V23.767h14.477v60.417h-14.017v-8.782c-4.481 6.791-10.686 10.187-18.613 10.187-12.524 0-20.681-7.728-20.681-21.778V23.767h14.476v37.585c0 7.61 3.677 11.474 11.145 11.474 7.009 0 13.213-5.268 13.213-15.22ZM217.264 55.03v3.512h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.664 0 11.145-2.927 13.558-8.664h13.902c-3.102 12.294-13.443 20.139-27.575 20.139-8.847 0-15.97-2.927-21.37-8.665-5.4-5.737-8.158-13.347-8.158-22.949 0-9.484 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664 8.961 0 16.085 3.044 21.37 9.016 5.285 5.971 7.928 13.933 7.928 23.651Zm-29.413-21.194c-7.928 0-13.443 4.684-14.822 14.402h29.758c-1.264-8.781-6.549-14.402-14.936-14.402ZM249.35 85.823c-17.234 0-26.311-6.908-27.115-20.841h14.132c.804 7.493 4.481 10.303 13.213 10.303 7.813 0 11.719-2.459 11.719-7.26 0-4.331-2.757-6.439-11.604-7.961l-6.779-1.17c-12.983-2.226-19.417-8.314-19.417-18.267 0-11.357 8.847-18.265 24.587-18.265 16.89 0 25.622 6.79 26.196 20.49H260.61c-.345-7.376-4.596-9.952-12.524-9.952-6.894 0-10.34 2.342-10.34 7.025 0 4.215 2.987 6.089 9.881 7.376l7.468 1.171c14.362 2.693 20.566 8.08 20.566 18.383 0 12.177-9.651 18.968-26.311 18.968ZM339.201 84.184h-16.545l-17.235-28.101-8.961 9.133v18.968h-14.247V.818h14.247v48.006l24.128-25.057h17.234l-22.405 22.832 23.784 37.585ZM378.008 38.988l4.826-15.221H398L375.136 89.1c-2.413 6.674-5.4 11.475-9.192 14.168-3.791 2.693-9.191 3.981-16.315 3.981-2.413 0-4.481-.117-6.319-.351V95.307h5.515c6.549 0 9.766-4.098 9.766-9.718 0-2.81-.919-6.908-2.758-12.177l-17.234-49.645h15.626l4.825 15.104c3.562 11.358 6.664 22.598 9.422 33.721 2.528-9.6 5.745-20.841 9.536-33.604Z"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
import React from 'react'
export function Repost({size, fill}: {size: number; fill?: string}) {
return (
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
<path
fill={fill}
fillRule="evenodd"
clipRule="evenodd"
d="M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z"
/>
</svg>
)
}
+4 -1
View File
@@ -31,9 +31,12 @@ export class AppContext {
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
const fontFiles = readdirSync(fontDirectory)
const fonts = fontFiles.map(file => {
const basename = path.basename(file, path.extname(file))
const [weight, name] = basename.split('-')
return {
name: path.basename(file, path.extname(file)),
name,
data: readFileSync(path.join(fontDirectory, file)),
weight: parseInt(weight),
}
})
return new AppContext({
+2
View File
@@ -2,6 +2,7 @@ import {Express} from 'express'
import {AppContext} from '../context.js'
import {default as health} from './health.js'
import {default as post} from './post.js'
import {default as starterPack} from './starter-pack.js'
export * from './util.js'
@@ -9,5 +10,6 @@ export * from './util.js'
export default function (ctx: AppContext, app: Express) {
app = health(ctx, app) // GET /_health
app = starterPack(ctx, app) // GET /start/:actor/:rkey
app = post(ctx, app) // POST /post
return app
}
+77
View File
@@ -0,0 +1,77 @@
import React from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import resvg from '@resvg/resvg-js'
import {Express} from 'express'
import satori from 'satori'
import {Post} from '../components/Post.js'
import {AppContext} from '../context.js'
import {httpLogger} from '../logger.js'
import {loadEmojiAsSvg} from '../util.js'
import {resolvePostData} from '../util/resolvePostData.js'
import {handler, originVerifyMiddleware} from './util.js'
const WIDTH = 600
export default function (ctx: AppContext, app: Express) {
return app.get(
'/:actor/post/:rkey',
originVerifyMiddleware(ctx),
handler(async (req, res) => {
let {actor, rkey} = req.params
let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
let post: AppBskyFeedDefs.PostView
try {
if (!actor.startsWith('did:')) {
const res = await ctx.appviewAgent.resolveHandle({
handle: actor,
})
actor = res.data.did
}
uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
const {data} = await ctx.appviewAgent.getPosts({
uris: [uri.toString()],
})
post = data.posts.at(0)
const notPublic = post.author.labels.some(
l => l.val === `!no-unauthenticated`,
)
if (notPublic) {
return res.status(404).end('not found')
}
} catch (err) {
httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post')
return res.status(404).end('not found')
}
if (!AppBskyFeedPost.isRecord(post.record)) {
return res.status(404).end('not found')
}
const data = await resolvePostData(post, ctx.appviewAgent)
const svg = await satori(<Post post={post} data={data} />, {
fonts: ctx.fonts,
width: WIDTH,
loadAdditionalAsset: async (code, text) => {
if (code === 'emoji') {
return await loadEmojiAsSvg(text)
}
},
})
const output = await resvg.renderAsync(svg, {
fitTo: {
mode: 'width',
value: WIDTH * 2,
},
logLevel: 'trace',
})
res.statusCode = 200
res.setHeader('content-type', 'image/png')
res.setHeader('cdn-tag', [...data.images.keys()].join(','))
return res.end(output.asPng())
}),
)
}
+566
View File
@@ -0,0 +1,566 @@
import * as tokens from './tokens.js'
export const atoms = {
block: {
display: 'block',
},
inline_block: {
display: 'inline-block',
},
inline: {
display: 'inline',
},
/*
* Positioning
*/
fixed: {
position: 'absolute',
},
absolute: {
position: 'absolute',
},
relative: {
position: 'relative',
},
inset_0: {
top: 0,
left: 0,
right: 0,
bottom: 0,
},
overflow_hidden: {
overflow: 'hidden',
},
/*
* Width
*/
w_full: {
width: '100%',
},
h_full: {
height: '100%',
},
h_full_vh: {
height: '100vh',
},
/*
* Theme-independent bg colors
*/
bg_transparent: {
backgroundColor: 'transparent',
},
/*
* Border radius
*/
rounded_2xs: {
borderRadius: tokens.borderRadius._2xs,
},
rounded_xs: {
borderRadius: tokens.borderRadius.xs,
},
rounded_sm: {
borderRadius: tokens.borderRadius.sm,
},
rounded_md: {
borderRadius: tokens.borderRadius.md,
},
rounded_full: {
borderRadius: tokens.borderRadius.full,
},
/*
* Flex
*/
gap_2xs: {
gap: tokens.space._2xs,
},
gap_xs: {
gap: tokens.space.xs,
},
gap_sm: {
gap: tokens.space.sm,
},
gap_md: {
gap: tokens.space.md,
},
gap_lg: {
gap: tokens.space.lg,
},
gap_xl: {
gap: tokens.space.xl,
},
gap_2xl: {
gap: tokens.space._2xl,
},
gap_3xl: {
gap: tokens.space._3xl,
},
gap_4xl: {
gap: tokens.space._4xl,
},
gap_5xl: {
gap: tokens.space._5xl,
},
flex: {
display: 'flex',
},
flex_col: {
flexDirection: 'column',
},
flex_row: {
flexDirection: 'row',
},
flex_col_reverse: {
flexDirection: 'column-reverse',
},
flex_row_reverse: {
flexDirection: 'row-reverse',
},
flex_wrap: {
flexWrap: 'wrap',
},
flex_0: {
flex: '0 0 auto',
},
flex_1: {
flex: 1,
},
flex_grow: {
flexGrow: 1,
},
flex_shrink: {
flexShrink: 1,
},
flex_shrink_0: {
flexShrink: 0,
},
justify_start: {
justifyContent: 'flex-start',
},
justify_center: {
justifyContent: 'center',
},
justify_between: {
justifyContent: 'space-between',
},
justify_end: {
justifyContent: 'flex-end',
},
align_center: {
alignItems: 'center',
},
align_start: {
alignItems: 'flex-start',
},
align_end: {
alignItems: 'flex-end',
},
align_baseline: {
alignItems: 'baseline',
},
align_stretch: {
alignItems: 'stretch',
},
self_auto: {
alignSelf: 'auto',
},
self_start: {
alignSelf: 'flex-start',
},
self_end: {
alignSelf: 'flex-end',
},
self_center: {
alignSelf: 'center',
},
self_stretch: {
alignSelf: 'stretch',
},
self_baseline: {
alignSelf: 'baseline',
},
/*
* Text
*/
text_left: {
textAlign: 'left',
},
text_center: {
textAlign: 'center',
},
text_right: {
textAlign: 'right',
},
text_2xs: {
fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25,
},
text_xs: {
fontSize: tokens.fontSize.xs,
letterSpacing: 0.25,
},
text_sm: {
fontSize: tokens.fontSize.sm,
letterSpacing: 0.25,
},
text_md: {
fontSize: tokens.fontSize.md,
letterSpacing: 0.25,
},
text_lg: {
fontSize: tokens.fontSize.lg,
letterSpacing: 0.25,
},
text_xl: {
fontSize: tokens.fontSize.xl,
letterSpacing: 0.25,
},
text_2xl: {
fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25,
},
text_3xl: {
fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25,
},
text_4xl: {
fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25,
},
text_5xl: {
fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25,
},
leading_tight: {
lineHeight: '1.15',
},
leading_snug: {
lineHeight: '1.3',
},
leading_normal: {
lineHeight: '1.5',
},
tracking_normal: {
letterSpacing: 0,
},
tracking_wide: {
letterSpacing: 0.25,
},
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
font_heavy: {
fontWeight: tokens.fontWeight.heavy,
},
italic: {
fontStyle: 'italic',
},
mono: {
fontFamily: 'monospace',
},
/*
* Border
*/
border_0: {
borderWidth: '0px',
},
border: {
borderWidth: '1px',
borderStyle: 'solid',
},
border_t: {
borderTopWidth: '1px',
borderStyle: 'solid',
},
border_b: {
borderBottomWidth: '1px',
borderStyle: 'solid',
},
border_l: {
borderLeftWidth: '1px',
borderStyle: 'solid',
},
border_r: {
borderRightWidth: '1px',
borderStyle: 'solid',
},
/*
* Padding
*/
p_0: {
padding: 0,
},
p_2xs: {
padding: tokens.space._2xs,
},
p_xs: {
padding: tokens.space.xs,
},
p_sm: {
padding: tokens.space.sm,
},
p_md: {
padding: tokens.space.md,
},
p_lg: {
padding: tokens.space.lg,
},
p_xl: {
padding: tokens.space.xl,
},
p_2xl: {
padding: tokens.space._2xl,
},
p_3xl: {
padding: tokens.space._3xl,
},
p_4xl: {
padding: tokens.space._4xl,
},
p_5xl: {
padding: tokens.space._5xl,
},
px_0: {
paddingLeft: 0,
paddingRight: 0,
},
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
},
px_xs: {
paddingLeft: tokens.space.xs,
paddingRight: tokens.space.xs,
},
px_sm: {
paddingLeft: tokens.space.sm,
paddingRight: tokens.space.sm,
},
px_md: {
paddingLeft: tokens.space.md,
paddingRight: tokens.space.md,
},
px_lg: {
paddingLeft: tokens.space.lg,
paddingRight: tokens.space.lg,
},
px_xl: {
paddingLeft: tokens.space.xl,
paddingRight: tokens.space.xl,
},
px_2xl: {
paddingLeft: tokens.space._2xl,
paddingRight: tokens.space._2xl,
},
px_3xl: {
paddingLeft: tokens.space._3xl,
paddingRight: tokens.space._3xl,
},
px_4xl: {
paddingLeft: tokens.space._4xl,
paddingRight: tokens.space._4xl,
},
px_5xl: {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
py_0: {
paddingTop: 0,
paddingBottom: 0,
},
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
},
py_xs: {
paddingTop: tokens.space.xs,
paddingBottom: tokens.space.xs,
},
py_sm: {
paddingTop: tokens.space.sm,
paddingBottom: tokens.space.sm,
},
py_md: {
paddingTop: tokens.space.md,
paddingBottom: tokens.space.md,
},
py_lg: {
paddingTop: tokens.space.lg,
paddingBottom: tokens.space.lg,
},
py_xl: {
paddingTop: tokens.space.xl,
paddingBottom: tokens.space.xl,
},
py_2xl: {
paddingTop: tokens.space._2xl,
paddingBottom: tokens.space._2xl,
},
py_3xl: {
paddingTop: tokens.space._3xl,
paddingBottom: tokens.space._3xl,
},
py_4xl: {
paddingTop: tokens.space._4xl,
paddingBottom: tokens.space._4xl,
},
py_5xl: {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
pt_0: {
paddingTop: 0,
},
pt_2xs: {
paddingTop: tokens.space._2xs,
},
pt_xs: {
paddingTop: tokens.space.xs,
},
pt_sm: {
paddingTop: tokens.space.sm,
},
pt_md: {
paddingTop: tokens.space.md,
},
pt_lg: {
paddingTop: tokens.space.lg,
},
pt_xl: {
paddingTop: tokens.space.xl,
},
pt_2xl: {
paddingTop: tokens.space._2xl,
},
pt_3xl: {
paddingTop: tokens.space._3xl,
},
pt_4xl: {
paddingTop: tokens.space._4xl,
},
pt_5xl: {
paddingTop: tokens.space._5xl,
},
pb_0: {
paddingBottom: 0,
},
pb_2xs: {
paddingBottom: tokens.space._2xs,
},
pb_xs: {
paddingBottom: tokens.space.xs,
},
pb_sm: {
paddingBottom: tokens.space.sm,
},
pb_md: {
paddingBottom: tokens.space.md,
},
pb_lg: {
paddingBottom: tokens.space.lg,
},
pb_xl: {
paddingBottom: tokens.space.xl,
},
pb_2xl: {
paddingBottom: tokens.space._2xl,
},
pb_3xl: {
paddingBottom: tokens.space._3xl,
},
pb_4xl: {
paddingBottom: tokens.space._4xl,
},
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
pl_0: {
paddingLeft: 0,
},
pl_2xs: {
paddingLeft: tokens.space._2xs,
},
pl_xs: {
paddingLeft: tokens.space.xs,
},
pl_sm: {
paddingLeft: tokens.space.sm,
},
pl_md: {
paddingLeft: tokens.space.md,
},
pl_lg: {
paddingLeft: tokens.space.lg,
},
pl_xl: {
paddingLeft: tokens.space.xl,
},
pl_2xl: {
paddingLeft: tokens.space._2xl,
},
pl_3xl: {
paddingLeft: tokens.space._3xl,
},
pl_4xl: {
paddingLeft: tokens.space._4xl,
},
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
pr_0: {
paddingRight: 0,
},
pr_2xs: {
paddingRight: tokens.space._2xs,
},
pr_xs: {
paddingRight: tokens.space.xs,
},
pr_sm: {
paddingRight: tokens.space.sm,
},
pr_md: {
paddingRight: tokens.space.md,
},
pr_lg: {
paddingRight: tokens.space.lg,
},
pr_xl: {
paddingRight: tokens.space.xl,
},
pr_2xl: {
paddingRight: tokens.space._2xl,
},
pr_3xl: {
paddingRight: tokens.space._3xl,
},
pr_4xl: {
paddingRight: tokens.space._4xl,
},
pr_5xl: {
paddingRight: tokens.space._5xl,
},
/*
* Text decoration
*/
underline: {
textDecorationLine: 'underline',
},
strike_through: {
textDecorationLine: 'line-through',
},
} as const
+84
View File
@@ -0,0 +1,84 @@
export const BLUE_HUE = 211
export const RED_HUE = 346
export const GREEN_HUE = 152
/**
* Smooth progression of lightness "stops" for generating HSL colors.
*/
export const COLOR_STOPS = [
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
]
export function generateScale(start: number, end: number) {
const range = end - start
return COLOR_STOPS.map(stop => {
return start + range * stop
})
}
export const defaultScale = generateScale(6, 100)
// dim shifted 6% lighter
export const dimScale = generateScale(12, 100)
export const colors = {
white: '#FFFFFF',
black: '#000000',
contrast_0: `hsl(${BLUE_HUE}, 20%, ${defaultScale[14]}%)`,
contrast_25: `hsl(${BLUE_HUE}, 20%, ${defaultScale[13]}%)`,
contrast_50: `hsl(${BLUE_HUE}, 20%, ${defaultScale[12]}%)`,
contrast_100: `hsl(${BLUE_HUE}, 20%, ${defaultScale[11]}%)`,
contrast_200: `hsl(${BLUE_HUE}, 20%, ${defaultScale[10]}%)`,
contrast_300: `hsl(${BLUE_HUE}, 20%, ${defaultScale[9]}%)`,
contrast_400: `hsl(${BLUE_HUE}, 20%, ${defaultScale[8]}%)`,
contrast_500: `hsl(${BLUE_HUE}, 20%, ${defaultScale[7]}%)`,
contrast_600: `hsl(${BLUE_HUE}, 24%, ${defaultScale[6]}%)`,
contrast_700: `hsl(${BLUE_HUE}, 24%, ${defaultScale[5]}%)`,
contrast_800: `hsl(${BLUE_HUE}, 28%, ${defaultScale[4]}%)`,
contrast_900: `hsl(${BLUE_HUE}, 28%, ${defaultScale[3]}%)`,
contrast_950: `hsl(${BLUE_HUE}, 28%, ${defaultScale[2]}%)`,
contrast_975: `hsl(${BLUE_HUE}, 28%, ${defaultScale[1]}%)`,
contrast_1000: `hsl(${BLUE_HUE}, 28%, ${defaultScale[0]}%)`,
primary_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
primary_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
primary_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
primary_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
primary_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
primary_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
primary_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
primary_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
primary_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
primary_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
primary_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
primary_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
primary_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
red_25: `hsl(${RED_HUE}, 91%, 97%)`,
red_50: `hsl(${RED_HUE}, 91%, 95%)`,
red_100: `hsl(${RED_HUE}, 91%, 90%)`,
red_200: `hsl(${RED_HUE}, 91%, 80%)`,
red_300: `hsl(${RED_HUE}, 91%, 70%)`,
red_400: `hsl(${RED_HUE}, 91%, 60%)`,
red_500: `hsl(${RED_HUE}, 91%, 50%)`,
red_600: `hsl(${RED_HUE}, 91%, 42%)`,
red_700: `hsl(${RED_HUE}, 91%, 34%)`,
red_800: `hsl(${RED_HUE}, 91%, 26%)`,
red_900: `hsl(${RED_HUE}, 91%, 18%)`,
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
} as const
+100
View File
@@ -0,0 +1,100 @@
import {colors} from './colors.js'
import * as tokens from './tokens.js'
export {atoms} from './atoms.js'
export * as tokens from './tokens.js'
const fontFamily = 'Inter'
export const theme = {
palette: colors,
atoms: {
text: {
color: colors.black,
fontFamily,
},
text_contrast_low: {
color: colors.contrast_500,
fontFamily,
},
text_contrast_medium: {
color: colors.contrast_700,
fontFamily,
},
text_contrast_high: {
color: colors.contrast_900,
fontFamily,
},
text_inverted: {
color: colors.white,
fontFamily,
},
bg: {
backgroundColor: colors.white,
},
bg_contrast_25: {
backgroundColor: colors.contrast_25,
},
bg_contrast_50: {
backgroundColor: colors.contrast_50,
},
bg_contrast_100: {
backgroundColor: colors.contrast_100,
},
bg_contrast_200: {
backgroundColor: colors.contrast_200,
},
bg_contrast_300: {
backgroundColor: colors.contrast_300,
},
bg_contrast_400: {
backgroundColor: colors.contrast_400,
},
bg_contrast_500: {
backgroundColor: colors.contrast_500,
},
bg_contrast_600: {
backgroundColor: colors.contrast_600,
},
bg_contrast_700: {
backgroundColor: colors.contrast_700,
},
bg_contrast_800: {
backgroundColor: colors.contrast_800,
},
bg_contrast_900: {
backgroundColor: colors.contrast_900,
},
bg_contrast_950: {
backgroundColor: colors.contrast_950,
},
bg_contrast_975: {
backgroundColor: colors.contrast_975,
},
bg_primary_500: {
backgroundColor: colors.primary_500,
},
border_contrast_low: {
borderColor: colors.contrast_100,
},
border_contrast_medium: {
borderColor: colors.contrast_200,
},
border_contrast_high: {
borderColor: colors.contrast_300,
},
},
}
export function style(styleObjects: Record<string, any>[]) {
return Object.assign({}, ...styleObjects)
}
export function gradient(color: keyof typeof tokens.gradients) {
const {values} = tokens.gradients[color]
return values
.map(([pos, color]) => {
return `${color} ${pos * 100}%`
})
.join(', ')
}
+104
View File
@@ -0,0 +1,104 @@
export const space = {
_2xs: 2,
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
_2xl: 24,
_3xl: 28,
_4xl: 32,
_5xl: 40,
} as const
export const fontSize = {
_2xs: 10,
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
_2xl: 22,
_3xl: 26,
_4xl: 32,
_5xl: 40,
} as const
export const lineHeight = {
none: 1,
normal: 1.5,
relaxed: 1.625,
} as const
export const borderRadius = {
_2xs: 2,
xs: 4,
sm: 8,
md: 12,
full: 999,
} as const
export const fontWeight = {
normal: '400',
semibold: '500',
bold: '600',
heavy: '700',
} as const
export const gradients = {
sky: {
values: [
[0, '#0A7AFF'],
[1, '#59B9FF'],
],
hover_value: '#0A7AFF',
},
midnight: {
values: [
[0, '#022C5E'],
[1, '#4079BC'],
],
hover_value: '#022C5E',
},
sunrise: {
values: [
[0, '#4E90AE'],
[0.4, '#AEA3AB'],
[0.8, '#E6A98F'],
[1, '#F3A84C'],
],
hover_value: '#AEA3AB',
},
sunset: {
values: [
[0, '#6772AF'],
[0.6, '#B88BB6'],
[1, '#FFA6AC'],
],
hover_value: '#B88BB6',
},
summer: {
values: [
[0, '#FF6A56'],
[0.3, '#FF9156'],
[1, '#FFDD87'],
],
hover_value: '#FF9156',
},
nordic: {
values: [
[0, '#083367'],
[1, '#9EE8C1'],
],
hover_value: '#3A7085',
},
bonfire: {
values: [
[0, '#203E4E'],
[0.4, '#755B62'],
[0.8, '#CD7765'],
[1, '#EF956E'],
],
hover_value: '#755B62',
},
} as const
+10
View File
@@ -0,0 +1,10 @@
export function formatCount(count: number) {
return Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
// @ts-ignore
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
roundingMode: 'trunc',
}).format(count)
}
+11
View File
@@ -0,0 +1,11 @@
export function formatDate(date: number | string | Date) {
const d = new Date(date)
return `${d.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
})} at ${d.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
})}`
}
+12
View File
@@ -0,0 +1,12 @@
import MIMEType from 'whatwg-mimetype'
export async function getImage(url: string) {
const response = await fetch(url)
const mimeType = new MIMEType(response.headers.get('content-type') ?? '')
const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
if (response.status !== 200) return null
return {
mime: mimeType.essence as string | undefined,
image: Buffer.from(arrayBuf),
}
}
@@ -0,0 +1,13 @@
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
const DEV = process.env.NODE_ENV === 'development'
const HOST = DEV
? `http://localhost:3000/start`
: `https://ogcard.cdn.bsky.app/start`
export function getStarterPackImageUri(
record: AppBskyGraphDefs.StarterPackViewBasic,
) {
const urip = new AtUri(record.uri)
return `${HOST}/${record.creator.did}/${urip.rkey}`
}
+322
View File
@@ -0,0 +1,322 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AtpAgent,
RichText,
} from '@atproto/api'
import {httpLogger} from '../logger.js'
import {getImage} from './getImage.js'
import {getStarterPackImageUri} from './getStarterPackImageUri.js'
export type Metadata = {
aspectRatio: {
width: number
height: number
}
}
export type Image = Metadata & {
mime: 'image/jpeg' | 'image/png' | string | undefined
image: Buffer
}
export type PostData = {
images: Map<string, Image>
texts: Map<string, RichText>
}
function normalizeAspectRatio(aspectRatio?: {
width: number
height: number
}): Metadata['aspectRatio'] {
if (!aspectRatio)
return {
width: 1000,
height: 1000,
}
return aspectRatio
}
export async function resolvePostData(
post: AppBskyFeedDefs.PostView,
agent: AtpAgent,
): Promise<PostData> {
const images: Map<string, Metadata> = new Map()
const texts: Map<string, RichText> = new Map()
// console.log(JSON.stringify(post, null, 2))
if (post.author.avatar) {
images.set(post.author.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
if (AppBskyFeedPost.isRecord(post.record) && post.record.text) {
texts.set(post.record.text, new RichText({text: post.record.text}))
}
if (post.embed) {
if (AppBskyEmbedImages.isView(post.embed)) {
// get OPs media
for (const image of post.embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(post.embed)) {
if (post.embed.external.thumb) {
images.set(post.embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecord.isView(post.embed)) {
if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
if (post.embed.record.author.avatar) {
images.set(post.embed.record.author.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
for (const embed of post.embed.record.embeds) {
if (AppBskyEmbedImages.isView(embed)) {
for (const image of embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(embed)) {
if (embed.external.thumb) {
images.set(embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedImages.isView(embed.media)) {
for (const image of embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
} else if (AppBskyEmbedExternal.isView(embed.media)) {
if (embed.media.external.thumb) {
images.set(embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
}
if (AppBskyGraphDefs.isListView(embed.record)) {
if (embed.record.avatar) {
images.set(embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
if (embed.record.avatar) {
images.set(embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) {
const uri = getStarterPackImageUri(embed.record)
images.set(uri, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (
AppBskyFeedPost.isRecord(post.embed.record.value) &&
post.embed.record.value.text
) {
texts.set(
post.embed.record.value.text,
new RichText({text: post.embed.record.value.text}),
)
}
}
if (AppBskyGraphDefs.isListView(post.embed.record)) {
if (post.embed.record.avatar) {
images.set(post.embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyFeedDefs.isGeneratorView(post.embed.record)) {
if (post.embed.record.avatar) {
images.set(post.embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyGraphDefs.isStarterPackViewBasic(post.embed.record)) {
const uri = getStarterPackImageUri(post.embed.record)
images.set(uri, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
// get OPs media
if (AppBskyEmbedImages.isView(post.embed.media)) {
for (const image of post.embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(post.embed.media)) {
if (post.embed.media.external.thumb) {
images.set(post.embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
// get media from embedded post
if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
for (const embed of post.embed.record.record.embeds) {
if (AppBskyEmbedImages.isView(embed)) {
for (const image of embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(embed)) {
if (embed.external.thumb) {
images.set(embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedImages.isView(embed.media)) {
for (const image of embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
} else if (AppBskyEmbedExternal.isView(embed.media)) {
if (embed.media.external.thumb) {
images.set(embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
}
}
if (
AppBskyFeedPost.isRecord(post.embed.record.record.value) &&
post.embed.record.record.value.text
) {
texts.set(
post.embed.record.record.value.text,
new RichText({text: post.embed.record.record.value.text}),
)
}
}
}
}
const deduped = Array.from(images.entries())
const resolved = await Promise.all(
deduped.map(async ([url, meta]) => {
try {
const image = await getImage(url)
return [
url,
{
...meta,
...image,
},
] as const
} catch (err) {
httpLogger.warn({err, uri: url}, 'could not fetch image')
return [
url,
{
...meta,
image: null,
},
] as const
}
}),
)
await Promise.all(Array.from(texts.values()).map(r => r.detectFacets(agent)))
const extracted = resolved.filter(([, i]) => i.image !== null) as [
string,
Image,
][]
return {
images: new Map(extracted),
texts,
}
}
+16
View File
@@ -0,0 +1,16 @@
export function toShortUrl(url: string): string {
try {
const urlp = new URL(url)
if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') {
return url
}
const path =
(urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash
if (path.length > 15) {
return urlp.host + path.slice(0, 13) + '...'
}
return urlp.host + path
} catch (e) {
return url
}
}
+122 -19
View File
@@ -2,15 +2,16 @@
# yarn lockfile v1
"@atproto/api@0.12.19-next.0":
version "0.12.19-next.0"
resolved "https://registry.npmjs.org/@atproto/api/-/api-0.12.19-next.0.tgz"
integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg==
"@atproto/api@^0.13.4":
version "0.13.4"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.4.tgz#47631e622760b2a18e4aee75b728e9cf8952e221"
integrity sha512-Fwn37hP+Xr9YjA/hadvn7ZKbUPQhJiUus1+govgNKF3/jNdyk2ICoEe0z+hxaO3xX8LCU5yARbgt3SRoXbIwrg==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
"@atproto/lexicon" "^0.4.1"
"@atproto/syntax" "^0.3.0"
"@atproto/xrpc" "^0.5.0"
"@atproto/xrpc" "^0.6.1"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
@@ -36,29 +37,29 @@
multiformats "^9.9.0"
pino "^8.15.0"
"@atproto/lexicon@^0.4.0":
version "0.4.0"
resolved "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz"
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
"@atproto/lexicon@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0"
integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/syntax" "^0.3.0"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.21.4"
zod "^3.23.8"
"@atproto/syntax@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz"
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
"@atproto/xrpc@^0.5.0":
version "0.5.0"
resolved "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz"
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
"@atproto/xrpc@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5"
integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A==
dependencies:
"@atproto/lexicon" "^0.4.0"
zod "^3.21.4"
"@atproto/lexicon" "^0.4.1"
zod "^3.23.8"
"@cbor-extract/cbor-extract-darwin-arm64@2.2.0":
version "2.2.0"
@@ -278,6 +279,11 @@ atomic-sleep@^1.0.0:
resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
await-lock@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
base64-js@0.0.8:
version "0.0.8"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz"
@@ -371,6 +377,11 @@ color-name@^1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^2.0.7:
version "2.0.20"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
@@ -422,6 +433,11 @@ css-to-react-native@^3.0.0:
css-color-keywords "^1.0.0"
postcss-value-parser "^4.0.2"
dateformat@^4.6.3:
version "4.6.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
@@ -478,6 +494,13 @@ encodeurl@~1.0.2:
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz"
@@ -547,6 +570,11 @@ express@^4.19.2:
utils-merge "1.0.1"
vary "~1.1.2"
fast-copy@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
fast-printf@^1.6.9:
version "1.6.9"
resolved "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz"
@@ -559,6 +587,11 @@ fast-redact@^3.1.1:
resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz"
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
fast-safe-stringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fflate@^0.7.3:
version "0.7.4"
resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz"
@@ -653,6 +686,11 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
help-me@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
hex-rgb@^4.1.0:
version "4.3.0"
resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz"
@@ -706,6 +744,11 @@ iso-datestring-validator@^2.2.2:
resolved "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz"
integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
joycon@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
@@ -779,6 +822,11 @@ mime@1.6.0:
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@@ -823,6 +871,13 @@ on-finished@2.4.1:
dependencies:
ee-first "1.1.1"
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz"
@@ -865,7 +920,7 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
pino-abstract-transport@^1.2.0:
pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz"
integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
@@ -873,6 +928,26 @@ pino-abstract-transport@^1.2.0:
readable-stream "^4.0.0"
split2 "^4.0.0"
pino-pretty@^11.2.2:
version "11.2.2"
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.2.tgz#5e8ec69b31e90eb187715af07b1d29a544e60d39"
integrity sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==
dependencies:
colorette "^2.0.7"
dateformat "^4.6.3"
fast-copy "^3.0.2"
fast-safe-stringify "^2.1.1"
help-me "^5.0.0"
joycon "^3.1.1"
minimist "^1.2.6"
on-exit-leak-free "^2.1.0"
pino-abstract-transport "^1.0.0"
pump "^3.0.0"
readable-stream "^4.0.0"
secure-json-parse "^2.4.0"
sonic-boom "^4.0.1"
strip-json-comments "^3.1.1"
pino-std-serializers@^6.0.0:
version "6.2.2"
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz"
@@ -940,6 +1015,14 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
qs@6.11.0:
version "6.11.0"
resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz"
@@ -1030,6 +1113,11 @@ satori@^0.10.13:
postcss-value-parser "^4.2.0"
yoga-wasm-web "^0.3.3"
secure-json-parse@^2.4.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
@@ -1127,6 +1215,11 @@ string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
thread-stream@^2.6.0:
version "2.7.0"
resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
@@ -1253,6 +1346,16 @@ vary@~1.1.2:
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
whatwg-mimetype@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
@@ -1263,7 +1366,7 @@ yoga-wasm-web@^0.3.3:
resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz"
integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==
zod@^3.21.4:
zod@^3.21.4, zod@^3.23.8:
version "3.23.8"
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==