feat: update database connection logic and enhance upsert functionality

- Refactored the database connection logic to utilize `neondatabase`, ensuring correct imports and type safety.
- Implemented atomic upsert operations in the views API, allowing for efficient insertion and updating of records using PostgreSQL's `ON CONFLICT` clause.
- Added new dependencies `ast-kit` and `unplugin-ast` for improved AST manipulation in the project.
- Enhanced the UI components with toast notifications for user feedback on reactions and improved styling with Tailwind CSS.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-06-27 01:19:08 +08:00
parent 49ac494998
commit 5bd5947ec2
15 changed files with 403 additions and 45 deletions

View File

@@ -0,0 +1,122 @@
<!-- Generated by SpecStory -->
# 改成 neondatabase (2025-06-27 00:50:50)
_**User**_
改成 neondatabase
---
_**Assistant**_
---
Okay, I see that the current database connection logic in `apps/ssr/src/lib/db.ts` is not quite right and has some errors. I'll fix it to correctly use `neondatabase`.
Here's what I'll do:
1. I'll correct the import to use `neon` from `@neondatabase/serverless` and also import the `NeonQueryFunction` type for type safety.
2. I will fix the `createDrizzle` helper function to accept the database client as an argument.
3. I'll update the `DbManager` class to properly manage the database connection state, including the client and drizzle instances.
4. The `connect` method will be adjusted to correctly establish a connection using `neon` and initialize the drizzle instance.
5. Finally, `getDb` and `getPool` methods will be updated to ensure the database is connected and ready before returning the instance or pool.
I will now apply these changes.
---
```diff
import { env } from '@env'
- import { drizzle } from 'drizzle-orm/neon-http';
- import { neon } from '@neondatabase/serverless';
+ import { drizzle } from 'drizzle-orm/neon-http'
+ import { neon, type NeonQueryFunction } from '@neondatabase/serverless'
import * as schema from '../schemas'
- const createDrizzle = () => drizzle({ client: sql, schema, logger: true })
+ const createDrizzle = (client: NeonQueryFunction<false, false>) =>
+ drizzle({ client, schema, logger: true })
export class DbManager {
- private drizzle: ReturnType<typeof createDrizzle>
+ private drizzle: ReturnType<typeof createDrizzle> | undefined
private ready = false
+ private client: NeonQueryFunction<false, false> | undefined
static shared = new DbManager()
```
```diff
}
- const sql = postgres(env.PG_CONNECTION_STRING, {
+ const sql = neon(env.PG_CONNECTION_STRING!, {
prepare: false,
})
- this.ready = true
this.client = sql
await sql`select 1`
this.drizzle = createDrizzle(sql)
+ this.ready = true
}
```
```diff
}
- if (!this.isReady()) {
+ if (!this.isReady() || !this.drizzle) {
throw new Error('Database is not ready')
}
```
```diff
throw new Error('Database is not enabled')
}
+
+ if (!this.isReady() || !this.client) {
+ throw new Error('Database is not ready')
+ }
return this.client
```
---
---
I see there's a linter error. It seems the `prepare` option is not expected in the `neon` function call within this context. The most common usage of the `neon` function from `@neondatabase/serverless` doesn't require a second argument for basic setup, and the default behavior is what's needed here.
I will correct this by removing the options object from the `neon()` call in `apps/ssr/src/lib/db.ts`.
---
```diff
}
- const sql = neon(env.PG_CONNECTION_STRING!, {
- prepare: false,
- })
+ const sql = neon(env.PG_CONNECTION_STRING!)
this.client = sql
```
---
---
I've updated the database connection logic to use `neondatabase` and resolved the linter errors. Everything should be working correctly now. Let me know if there is anything else I can help with.
---

View File

@@ -0,0 +1,72 @@
<!-- Generated by SpecStory -->
# 实现原子性upsert操作 (2025-06-27 01:13:17)
_**User**_
这里需要 upsert 如果没有就插入,有就更新。需要原子性
---
_**Assistant**_
我来帮你修改这个API使其支持原子性的upsert操作。让我先查看一下views表的schema然后实现原子性的upsert逻辑。
---
Read file: apps/ssr/src/schemas/view.ts
---
现在我了解了views表的结构。我需要实现一个原子性的upsert操作当refKey存在时更新views计数不存在时插入新记录。在PostgreSQL中我们可以使用`ON CONFLICT`子句来实现这个功能。
---
```diff
import { ViewDto } from './dto'
+ export const runtime = 'edge'
export const POST = guardDbEnabled(async (req: NextRequest) => {
const { refKey } = ViewDto.parse(await req.json())
const db = DbManager.shared.getDb()
await db
- .update(views)
- .set({
+ .insert(views)
+ .values({
+ refKey,
+ views: 1,
+ })
+ .onConflictDoUpdate({
+ target: views.refKey,
+ set: {
views: sql`${views.views} + 1`,
+ },
})
- .where(eq(views.refKey, refKey))
})
```
---
完美我已经修改了代码现在使用原子性的upsert操作
## 修改说明
1. **原子性操作**:使用`insert().onConflictDoUpdate()`替代了原来的`update().where()`,这是一个原子性操作
2. **upsert逻辑**
- 如果`refKey`不存在,插入新记录,`views`初始值为1
- 如果`refKey`已存在(冲突),则更新现有记录,将`views`字段加1
3. **冲突处理**:使用`target: views.refKey`指定冲突检测字段因为refKey有unique约束
这样的实现确保了:
- **原子性**:整个操作在数据库层面是原子的,不会出现竞态条件
- **性能**:只需要一次数据库操作,比先查询再决定插入或更新要高效
- **正确性**无论是新的refKey还是已存在的refKey都能正确处理
现在这个API可以安全地处理并发请求不会出现数据不一致的问题。
---

View File

@@ -6,18 +6,16 @@ import { reactions } from '~/schemas'
import { ReactionDto } from './dto'
export const runtime = 'edge'
export const POST = guardDbEnabled(async (req: NextRequest) => {
const { refKey, reaction } = ReactionDto.parse(await req.json())
const db = DbManager.shared.getDb()
try {
const data = await db.query.reactions.findFirst()
console.info(data)
await db.insert(reactions).values({
refKey,
reaction,
})
return new Response('', { status: 201 })
} catch (error) {
console.error('Failed to add reaction:', error)
return new Response('Failed to add reaction', { status: 500 })

View File

@@ -1,4 +1,4 @@
import { eq, sql } from 'drizzle-orm'
import { sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { guardDbEnabled } from '~/lib/api-guard'
@@ -7,15 +7,21 @@ import { views } from '~/schemas'
import { DbManager } from '../../../lib/db'
import { ViewDto } from './dto'
export const runtime = 'edge'
export const POST = guardDbEnabled(async (req: NextRequest) => {
const { refKey } = ViewDto.parse(await req.json())
const db = DbManager.shared.getDb()
await db
.update(views)
.set({
views: sql`${views.views} + 1`,
.insert(views)
.values({
refKey,
views: 1,
})
.where(eq(views.refKey, refKey))
.onConflictDoUpdate({
target: views.refKey,
set: {
views: sql`${views.views} + 1`,
},
})
return new Response('', { status: 201 })
})

View File

@@ -84,6 +84,7 @@
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"ast-kit": "2.1.0",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"code-inspector-plugin": "0.20.12",
"daisyui": "5.0.43",
@@ -99,6 +100,7 @@
"tailwindcss-animate": "1.0.7",
"tailwindcss-safe-area": "0.6.0",
"tailwindcss-uikit-colors": "1.0.0-alpha.1",
"unplugin-ast": "0.15.0",
"vite-plugin-html": "3.2.2"
}
}

View File

@@ -0,0 +1,35 @@
import { isTaggedFunctionCallOf } from 'ast-kit'
import type { Transformer } from 'unplugin-ast'
import AST from 'unplugin-ast/vite'
// Custom transformer for tw function that compresses template strings
const TwTransformer: Transformer<any> = {
onNode: (node) => isTaggedFunctionCallOf(node, ['tw']),
transform(node) {
if (node.type === 'TaggedTemplateExpression') {
const { quasi } = node
// Process template literals
if (quasi.type === 'TemplateLiteral') {
// Get the raw string content
const rawString = quasi.quasis[0]?.value?.raw || ''
// Compress the string: remove extra whitespace, newlines, and normalize spaces
const compressedString = rawString
.replaceAll(/\s+/g, ' ') // Replace multiple whitespace with single space
.trim() // Remove leading and trailing whitespace
// Update the template literal
quasi.quasis[0].value.raw = compressedString
quasi.quasis[0].value.cooked = compressedString
}
return quasi
}
return node.arguments[0]
},
}
export const astPlugin = AST({
transformer: [TwTransformer],
})

View File

@@ -15,6 +15,7 @@ import {
} from 'react'
import { Blurhash } from 'react-blurhash'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import type { Swiper as SwiperType } from 'swiper'
import { Keyboard, Navigation, Virtual } from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/react'
@@ -244,11 +245,12 @@ export const PhotoViewer = ({
{!isMobile && injectConfig.useApi && (
<ReactionButton
className="absolute right-4 bottom-4"
onReaction={(reaction) => {
client.actReaction({
onReaction={async (reaction) => {
await client.actReaction({
refKey: currentPhoto.id,
reaction,
})
toast.success(t('photo.reaction.success'))
}}
/>
)}

View File

@@ -95,6 +95,11 @@ export const ReactionButton = ({
<m.div
className="relative"
initial="closed"
exit={{
opacity: 0,
scale: 0,
transition: { duration: 0.2 },
}}
animate={isOpen ? 'open' : 'closed'}
>
<AnimatePresence>
@@ -149,13 +154,12 @@ export const ReactionButton = ({
<m.i
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
exit={{ opacity: 0, transition: { duration: 0 } }}
transition={Spring.presets.smooth}
key={isOpen ? 'close' : 'emoji'}
className={clsxm(
styles.mainButtonIcon(),
isOpen ? 'i-mingcute-close-line' : 'i-mingcute-emoji-line',
)}
className={
isOpen ? 'i-mingcute-close-line' : 'i-mingcute-emoji-line'
}
/>
</AnimatePresence>
</m.div>

View File

@@ -3,7 +3,84 @@ import { Toaster as Sonner } from 'sonner'
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => (
<Sonner theme="dark" {...props} />
<Sonner
theme="dark"
gap={12}
toastOptions={{
unstyled: true,
classNames: {
toast: tw`
group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg
backdrop-blur-[70px] border border-border/50
bg-material-ultra-thick
transition-all duration-300 ease-out
hover:shadow-xl
data-[type=success]:border-green/30 data-[type=success]:bg-green/20
data-[type=error]:border-red/30 data-[type=error]:bg-red/20
data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/20
data-[type=info]:border-blue/30 data-[type=info]:bg-blue/20
data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/20
max-w-md min-w-[320px]
`,
title: tw`
text-sm font-medium text-text
leading-tight
`,
description: tw`
text-xs text-text-secondary
leading-relaxed mt-1
`,
content: tw`
flex-1 min-w-0
`,
icon: tw`
flex-shrink-0 mt-0.5 size-5
[li[data-type="success"]_&]:text-green
[li[data-type="error"]_&]:text-red
[li[data-type="warning"]_&]:text-orange
[li[data-type="info"]_&]:text-blue
[li[data-type="loading"]_&]:text-gray
`,
actionButton: tw`
px-2.5 py-1 text-xs font-medium rounded-md
transition-all duration-200
focus:outline-none focus:shadow-lg bg-accent
group-data-[type=success]:bg-green group-data-[type=success]:text-white group-data-[type=success]:hover:bg-green/90 group-data-[type=success]:focus:shadow-green/50
group-data-[type=error]:bg-red group-data-[type=error]:text-white group-data-[type=error]:hover:bg-red/90 group-data-[type=error]:focus:shadow-red/50
group-data-[type=warning]:bg-orange group-data-[type=warning]:text-white group-data-[type=warning]:hover:bg-orange/90 group-data-[type=warning]:focus:shadow-orange/50
group-data-[type=info]:bg-blue group-data-[type=info]:text-white group-data-[type=info]:hover:bg-blue/90 group-data-[type=info]:focus:shadow-blue/50
group-data-[type=loading]:bg-gray group-data-[type=loading]:text-white group-data-[type=loading]:hover:bg-gray/90 group-data-[type=loading]:focus:shadow-gray/50
hover:shadow-md active:scale-95
`,
cancelButton: tw`
px-2.5 py-1 text-xs font-medium rounded-md
bg-fill-secondary text-text-secondary
hover:bg-fill-tertiary hover:text-text
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-fill/50 focus:ring-offset-1
`,
closeButton: tw`
absolute top-2 right-2 w-6 h-6 rounded-full
flex items-center justify-center
bg-fill text-text-tertiary
hover:bg-fill-secondary hover:text-text-secondary
active:bg-fill-tertiary active:text-text
transition-all duration-200
opacity-0 group-hover:opacity-100
focus:outline-none focus:ring-2 focus:ring-accent/50
focus:opacity-100
`,
},
}}
icons={{
success: <i className="i-mingcute-check-circle-fill" />,
error: <i className="i-mingcute-close-circle-fill" />,
warning: <i className="i-mingcute-warning-fill" />,
info: <i className="i-mingcute-information-fill" />,
loading: <i className="i-mingcute-loading-3-fill animate-spin" />,
}}
{...props}
/>
)
export { Toaster }

View File

@@ -33,6 +33,10 @@ declare global {
const __MANIFEST__: AfilmoryManifest
const __CONFIG__: InjectConfig
/**
* This function is a macro, will replace in the build stage.
*/
export function tw(strings: TemplateStringsArray, ...values: any[]): string
}
export {}

View File

@@ -3,6 +3,7 @@ import { atom, useAtom, useAtomValue } from 'jotai'
import { useCallback, useMemo } from 'react'
import { gallerySettingAtom } from '~/atoms/app'
import { trackView } from '~/lib/tracker'
const openAtom = atom(false)
const currentIndexAtom = atom(0)
@@ -52,6 +53,9 @@ export const usePhotoViewer = () => {
const [currentIndex, setCurrentIndex] = useAtom(currentIndexAtom)
const [triggerElement, setTriggerElement] = useAtom(triggerElementAtom)
const id = useMemo(() => {
return photos[currentIndex].id
}, [photos, currentIndex])
const openViewer = useCallback(
(index: number, element?: HTMLElement) => {
setCurrentIndex(index)
@@ -59,8 +63,10 @@ export const usePhotoViewer = () => {
setIsOpen(true)
// 防止背景滚动
document.body.style.overflow = 'hidden'
trackView(id)
},
[setCurrentIndex, setIsOpen, setTriggerElement],
[id, setCurrentIndex, setIsOpen, setTriggerElement],
)
const closeViewer = useCallback(() => {
@@ -70,25 +76,14 @@ export const usePhotoViewer = () => {
document.body.style.overflow = ''
}, [setIsOpen, setTriggerElement])
const goToNext = useCallback(() => {
if (currentIndex < photos.length - 1) {
setCurrentIndex(currentIndex + 1)
}
}, [currentIndex, photos.length, setCurrentIndex])
const goToPrevious = useCallback(() => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)
}
}, [currentIndex, setCurrentIndex])
const goToIndex = useCallback(
(index: number) => {
if (index >= 0 && index < photos.length) {
setCurrentIndex(index)
trackView(photos[index].id)
}
},
[photos.length, setCurrentIndex],
[photos, setCurrentIndex],
)
return {
@@ -97,8 +92,7 @@ export const usePhotoViewer = () => {
triggerElement,
openViewer,
closeViewer,
goToNext,
goToPrevious,
goToIndex,
}
}

View File

@@ -0,0 +1,10 @@
import { injectConfig } from '~/config'
import { client } from '~/lib/client'
export const trackView = (photoId: string) => {
if (!injectConfig.useApi) return
client.actView({
refKey: photoId,
})
}

View File

@@ -1,6 +1,7 @@
import { execSync } from 'node:child_process'
import { rmSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
@@ -18,6 +19,9 @@ import { createFeedSitemapPlugin } from '../../plugins/vite/feed-sitemap'
import { localesJsonPlugin } from '../../plugins/vite/locales-json'
import { manifestInjectPlugin } from '../../plugins/vite/manifest-inject'
import { siteConfig } from '../../site.config'
import { astPlugin } from './plugins/vite/ast'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
if (process.env.CI) {
rmSync(path.join(process.cwd(), 'src/pages/(debug)'), {
@@ -40,10 +44,12 @@ export default defineConfig({
},
}),
astPlugin,
tsconfigPaths(),
checker({
typescript: true,
enableBuild: true,
root: __dirname,
}),
codeInspectorPlugin({
bundler: 'vite',

View File

@@ -240,6 +240,7 @@
"photo.live.tooltip.desktop.zoom": "Hover to play Live Photo / Double-click to zoom",
"photo.live.tooltip.mobile.main": "Long press to play Live Photo",
"photo.live.tooltip.mobile.zoom": "Long press to play Live Photo / Double-tap to zoom",
"photo.reaction.success": "Reaction added",
"photo.share.actions": "Actions",
"photo.share.copy.failed": "Copy failed",
"photo.share.copy.link": "Copy Link",

49
pnpm-lock.yaml generated
View File

@@ -363,6 +363,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.6.0
version: 4.6.0(vite@7.0.0(@types/node@24.0.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0))
ast-kit:
specifier: 2.1.0
version: 2.1.0
babel-plugin-react-compiler:
specifier: 19.1.0-rc.2
version: 19.1.0-rc.2
@@ -408,6 +411,9 @@ importers:
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
version: 1.0.0-alpha.1
unplugin-ast:
specifier: 0.15.0
version: 0.15.0
vite-plugin-html:
specifier: 3.2.2
version: 3.2.2(vite@7.0.0(@types/node@24.0.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0))
@@ -4573,6 +4579,10 @@ packages:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
magic-string-ast@0.9.1:
resolution: {integrity: sha512-18dv2ZlSSgJ/jDWlZGKfnDJx56ilNlYq9F7NnwuWTErsmYmqJ2TWE4l1o2zlUHBYUGBy3tIhPCC1gxq8M5HkMA==}
engines: {node: '>=20.18.0'}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -5772,6 +5782,10 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unplugin-ast@0.15.0:
resolution: {integrity: sha512-3ReKQUmmYEcNhjoyiwfFuaJU0jkZNcNk8+iLdLVWk73iojVjJLiF/QhnpAFf3O7CJd6bqhWBzNyQ68Udp2fi5Q==}
engines: {node: '>=20.18.0'}
unplugin-dts@1.0.0-beta.2:
resolution: {integrity: sha512-sgM7qrwWNyiBwoUexkHRrTQBb4xn6EGLkkrcovpG+MvOhYQPY0XdtNGtlxVdExcxTTtWIjpoAAYu5p0iQVQdAA==}
peerDependencies:
@@ -6653,14 +6667,14 @@ snapshots:
'@babel/generator@7.27.5':
dependencies:
'@babel/parser': 7.27.5
'@babel/types': 7.27.3
'@babel/types': 7.27.6
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.27.1':
dependencies:
'@babel/types': 7.27.3
'@babel/types': 7.27.6
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@@ -6686,14 +6700,14 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.3
'@babel/types': 7.27.6
transitivePeerDependencies:
- supports-color
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.27.6
transitivePeerDependencies:
- supports-color
@@ -6717,7 +6731,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.27.3
'@babel/types': 7.27.6
'@babel/helper-plugin-utils@7.27.1': {}
@@ -6733,7 +6747,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.3
'@babel/types': 7.27.6
transitivePeerDependencies:
- supports-color
@@ -6759,7 +6773,7 @@ snapshots:
'@babel/parser@7.27.5':
dependencies:
'@babel/types': 7.27.3
'@babel/types': 7.27.6
'@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.1)':
dependencies:
@@ -6790,7 +6804,7 @@ snapshots:
'@babel/traverse@7.27.1':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.1
'@babel/generator': 7.27.5
'@babel/parser': 7.27.2
'@babel/template': 7.27.2
'@babel/types': 7.27.1
@@ -8976,7 +8990,7 @@ snapshots:
'@vue/compiler-core@3.5.16':
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.27.5
'@vue/shared': 3.5.16
entities: 4.5.0
estree-walker: 2.0.2
@@ -10046,8 +10060,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.3
esquery@1.6.0:
@@ -10642,6 +10656,10 @@ snapshots:
luxon@3.6.1: {}
magic-string-ast@0.9.1:
dependencies:
magic-string: 0.30.17
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -11765,6 +11783,13 @@ snapshots:
universalify@2.0.1: {}
unplugin-ast@0.15.0:
dependencies:
'@babel/generator': 7.27.5
ast-kit: 2.1.0
magic-string-ast: 0.9.1
unplugin: 2.3.5
unplugin-dts@1.0.0-beta.2(@microsoft/api-extractor@7.52.8(@types/node@24.0.4))(esbuild@0.25.4)(rolldown@1.0.0-beta.11-commit.f051675)(rollup@4.41.1)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.41.1)
@@ -11794,7 +11819,7 @@ snapshots:
unplugin@2.3.5:
dependencies:
acorn: 8.14.1
acorn: 8.15.0
picomatch: 4.0.2
webpack-virtual-modules: 0.6.2