mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
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:
122
.specstory/history/2025-06-26_16-50-改成-neondatabase.md
Normal file
122
.specstory/history/2025-06-26_16-50-改成-neondatabase.md
Normal 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.
|
||||
|
||||
---
|
||||
|
||||
72
.specstory/history/2025-06-26_17-13-实现原子性upsert操作.md
Normal file
72
.specstory/history/2025-06-26_17-13-实现原子性upsert操作.md
Normal 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可以安全地处理并发请求,不会出现数据不一致的问题。
|
||||
|
||||
---
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
35
apps/web/plugins/vite/ast.ts
Normal file
35
apps/web/plugins/vite/ast.ts
Normal 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],
|
||||
})
|
||||
@@ -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'))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
4
apps/web/src/global.d.ts
vendored
4
apps/web/src/global.d.ts
vendored
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
10
apps/web/src/lib/tracker.ts
Normal file
10
apps/web/src/lib/tracker.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
49
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user