feat(static-web): implement static web module with controller and service for asset handling

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-01 02:22:24 +08:00
parent 3d7c528c45
commit 0c84619e88
7 changed files with 460 additions and 29 deletions

40
be/apps/core/Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=22.11.0
FROM node:${NODE_VERSION}-slim AS builder
ENV PNPM_HOME=/pnpm
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@10.19.0 --activate
WORKDIR /workspace
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY be/apps/core/package.json be/apps/core/package.json
COPY apps/web/package.json apps/web/package.json
RUN pnpm fetch --filter core...
RUN pnpm fetch --filter '@afilmory/web...'
COPY . .
RUN pnpm install --filter core... --filter '@afilmory/web...' --frozen-lockfile
RUN pnpm --filter '@afilmory/web' build
RUN pnpm --filter core build
RUN mkdir -p be/apps/core/dist/static/web && cp -r apps/web/dist/. be/apps/core/dist/static/web/
FROM node:${NODE_VERSION}-slim AS runner
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /workspace/be/apps/core/dist ./dist
COPY --from=builder /workspace/be/apps/core/drizzle ./drizzle
RUN if [ -f dist/package.json ]; then \
cd dist && \
npm install --omit=dev --no-audit --no-fund; \
fi
EXPOSE 1841
CMD ["node", "./dist/main.js"]

View File

@@ -28,6 +28,8 @@
"better-auth": "1.3.34",
"drizzle-orm": "^0.44.7",
"hono": "4.10.4",
"linkedom": "0.18.12",
"mime-types": "2.1.35",
"pg": "^8.16.3",
"picocolors": "1.1.1",
"reflect-metadata": "0.2.2",
@@ -35,6 +37,7 @@
"zod": "^4.1.11"
},
"devDependencies": {
"@types/mime-types": "2.1.4",
"@types/node": "^24.9.2",
"@types/pg": "8.15.6",
"nodemon": "3.1.10",

View File

@@ -14,6 +14,7 @@ import { OnboardingModule } from './onboarding/onboarding.module'
import { PhotoModule } from './photo/photo.module'
import { ReactionModule } from './reaction/reaction.module'
import { SettingModule } from './setting/setting.module'
import { StaticWebModule } from './static-web/static-web.module'
import { SuperAdminModule } from './super-admin/super-admin.module'
import { SystemSettingModule } from './system-setting/system-setting.module'
import { TenantModule } from './tenant/tenant.module'
@@ -38,6 +39,7 @@ function createEventModuleOptions(redis: RedisAccessor) {
DashboardModule,
TenantModule,
DataSyncModule,
StaticWebModule,
EventModule.forRootAsync({
useFactory: createEventModuleOptions,
inject: [RedisAccessor],

View File

@@ -0,0 +1,21 @@
import { ContextParam, Controller, Get, Head } from '@afilmory/framework'
import type { Context } from 'hono'
import { StaticWebService } from './static-web.service'
@Controller('static/web')
export class StaticWebController {
constructor(private readonly staticWebService: StaticWebService) {}
@Get('/*')
async getAsset(@ContextParam() context: Context) {
const response = await this.staticWebService.handleRequest(context.req.path, false)
return response ?? new Response('Not Found', { status: 404 })
}
@Head('/*')
async headAsset(@ContextParam() context: Context) {
const response = await this.staticWebService.handleRequest(context.req.path, true)
return response ?? new Response(null, { status: 404 })
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@afilmory/framework'
import { StaticWebController } from './static-web.controller'
import { StaticWebService } from './static-web.service'
@Module({
controllers: [StaticWebController],
providers: [StaticWebService],
})
export class StaticWebModule {}

View File

@@ -0,0 +1,301 @@
import type { Stats } from 'node:fs'
import { createReadStream } from 'node:fs'
import { readFile, stat } from 'node:fs/promises'
import { extname, isAbsolute, join, normalize, relative, resolve } from 'node:path'
import { Readable } from 'node:stream'
import { fileURLToPath } from 'node:url'
import { createLogger } from '@afilmory/framework'
import { DOMParser } from 'linkedom'
import { lookup as lookupMimeType } from 'mime-types'
import { injectable } from 'tsyringe'
const STATIC_ROOT_ENV = process.env.STATIC_WEB_ROOT?.trim()
const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
const STATIC_ROOT_CANDIDATES = Array.from(
new Set(
[
STATIC_ROOT_ENV,
resolve(MODULE_DIR, '../../static/web'),
resolve(process.cwd(), 'dist/static/web'),
resolve(process.cwd(), '../dist/static/web'),
resolve(process.cwd(), '../../dist/static/web'),
resolve(process.cwd(), '../../../dist/static/web'),
resolve(process.cwd(), 'static/web'),
resolve(process.cwd(), '../static/web'),
resolve(process.cwd(), '../../static/web'),
resolve(process.cwd(), '../../../static/web'),
resolve(process.cwd(), 'apps/web/dist'),
resolve(process.cwd(), '../apps/web/dist'),
resolve(process.cwd(), '../../apps/web/dist'),
resolve(process.cwd(), '../../../apps/web/dist'),
].filter((entry): entry is string => typeof entry === 'string' && entry.length > 0),
),
)
const STATIC_ROUTE_SEGMENT = '/static/web'
interface ResolvedFile {
absolutePath: string
relativePath: string
stats: Stats
}
@injectable()
export class StaticWebService {
private staticRoot: string | null | undefined
private warnedMissingRoot = false
private readonly logger = createLogger('StaticWebService')
async handleRequest(fullPath: string, headOnly: boolean): Promise<Response | null> {
const staticRoot = await this.resolveStaticRoot()
if (!staticRoot) {
return null
}
const relativeRequestPath = this.extractRelativePath(fullPath)
const target = await this.resolveFile(relativeRequestPath, staticRoot)
if (!target) {
return null
}
return await this.createResponse(target, headOnly)
}
private async resolveStaticRoot(): Promise<string | null> {
if (this.staticRoot !== undefined) {
return this.staticRoot
}
for (const candidate of STATIC_ROOT_CANDIDATES) {
try {
const stats = await stat(candidate)
if (stats.isDirectory()) {
this.staticRoot = candidate
this.logger.info(`Using static assets root: ${candidate}`)
return candidate
}
} catch {
continue
}
}
this.staticRoot = null
if (!this.warnedMissingRoot) {
this.warnedMissingRoot = true
this.logger.warn('No static web root found; static route will return 404')
}
return null
}
private extractRelativePath(fullPath: string): string {
const index = fullPath.indexOf(STATIC_ROUTE_SEGMENT)
if (index === -1) {
return ''
}
const sliceStart = index + STATIC_ROUTE_SEGMENT.length
const remainder = sliceStart < fullPath.length ? fullPath.slice(sliceStart) : ''
return this.stripLeadingSlashes(remainder)
}
private stripLeadingSlashes(pathname: string): string {
let result = pathname
while (result.startsWith('/')) {
result = result.slice(1)
}
return result
}
private async resolveFile(requestPath: string, root: string): Promise<ResolvedFile | null> {
const decoded = this.decodePath(requestPath)
const normalized = this.normalizePath(decoded)
const candidates = this.buildCandidatePaths(normalized)
for (const candidate of candidates) {
const resolved = await this.tryResolveFile(root, candidate)
if (resolved) {
return resolved
}
}
return null
}
private decodePath(pathname: string): string {
if (pathname.length === 0) {
return pathname
}
try {
return decodeURIComponent(pathname)
} catch {
return pathname
}
}
private normalizePath(pathname: string): string {
if (pathname.length === 0) {
return 'index.html'
}
const withoutLeadingSlash = this.stripLeadingSlashes(pathname)
if (withoutLeadingSlash.length === 0) {
return 'index.html'
}
return withoutLeadingSlash
}
private buildCandidatePaths(normalizedPath: string): string[] {
const candidates = new Set<string>()
const sanitized = this.removeLeadingDotSlash(normalize(normalizedPath))
candidates.add(sanitized)
if (sanitized.endsWith('/')) {
candidates.add(join(sanitized, 'index.html'))
}
if (!this.hasFileExtension(sanitized)) {
candidates.add('index.html')
}
return Array.from(candidates)
}
private removeLeadingDotSlash(pathname: string): string {
let result = pathname
while (result.startsWith('./')) {
result = result.slice(2)
}
return result
}
private hasFileExtension(pathname: string): boolean {
return extname(pathname) !== ''
}
private async tryResolveFile(root: string, relativePath: string): Promise<ResolvedFile | null> {
const safePath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath
const absolutePath = resolve(root, safePath)
if (!this.ensureWithinRoot(root, absolutePath)) {
return null
}
try {
const stats = await stat(absolutePath)
if (!stats.isFile()) {
return null
}
return { absolutePath, relativePath: safePath, stats }
} catch {
return null
}
}
private ensureWithinRoot(root: string, filePath: string): boolean {
const relativePath = relative(root, filePath)
return relativePath !== '' && !relativePath.startsWith('..') && !isAbsolute(relativePath)
}
private async createResponse(file: ResolvedFile, headOnly: boolean): Promise<Response> {
if (file.relativePath === 'index.html') {
return await this.createIndexResponse(file, headOnly)
}
const mimeType = lookupMimeType(file.absolutePath) || 'application/octet-stream'
const headers = new Headers()
headers.set('content-type', mimeType)
headers.set('content-length', `${file.stats.size}`)
headers.set('last-modified', file.stats.mtime.toUTCString())
this.applyCacheHeaders(headers, file.relativePath)
if (headOnly) {
return new Response(null, { headers, status: 200 })
}
const nodeStream = createReadStream(file.absolutePath)
const body = Readable.toWeb(nodeStream) as unknown as ReadableStream
return new Response(body, { headers, status: 200 })
}
private async createIndexResponse(file: ResolvedFile, headOnly: boolean): Promise<Response> {
const html = await readFile(file.absolutePath, 'utf-8')
const transformed = this.transformIndexHtml(html)
const headers = new Headers()
headers.set('content-type', 'text/html; charset=utf-8')
headers.set('content-length', `${Buffer.byteLength(transformed, 'utf-8')}`)
headers.set('last-modified', file.stats.mtime.toUTCString())
this.applyCacheHeaders(headers, file.relativePath)
if (headOnly) {
return new Response(null, { headers, status: 200 })
}
return new Response(transformed, { headers, status: 200 })
}
private transformIndexHtml(html: string): string {
try {
const document = new DOMParser().parseFromString(html, 'text/html')
const configScript = document.head?.querySelector('#config')
if (configScript) {
const payload = JSON.stringify({
useCloud: true,
})
configScript.textContent = `window.__CONFIG__ = ${payload}`
}
return document.documentElement.outerHTML
} catch (error) {
this.logger.warn('Failed to transform index.html for static web response', error)
return html
}
}
private shouldTreatAsImmutable(relativePath: string): boolean {
if (this.isHtml(relativePath)) {
return false
}
return this.hasFileExtension(relativePath)
}
private applyCacheHeaders(headers: Headers, relativePath: string): void {
const policy = this.resolveCachePolicy(relativePath)
headers.set('cache-control', policy.browser)
headers.set('cdn-cache-control', policy.cdn)
headers.set('surrogate-control', policy.cdn)
}
private resolveCachePolicy(relativePath: string): { browser: string; cdn: string } {
if (this.isHtml(relativePath)) {
return {
browser: 'no-cache',
cdn: 'no-cache',
}
}
if (this.shouldTreatAsImmutable(relativePath)) {
return {
browser: 'public, max-age=31536000, immutable',
cdn: 'public, max-age=31536000, immutable',
}
}
return {
browser: 'public, max-age=3600, must-revalidate',
cdn: 'public, max-age=86400, stale-while-revalidate=600',
}
}
private isHtml(relativePath: string): boolean {
return relativePath === 'index.html' || relativePath.endsWith('.html')
}
}

112
pnpm-lock.yaml generated
View File

@@ -603,13 +603,19 @@ importers:
version: 1.19.6(hono@4.10.4)
better-auth:
specifier: 1.3.34
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
drizzle-orm:
specifier: ^0.44.7
version: 0.44.7(@types/pg@8.15.6)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
hono:
specifier: 4.10.4
version: 4.10.4
linkedom:
specifier: 0.18.12
version: 0.18.12
mime-types:
specifier: 2.1.35
version: 2.1.35
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -626,6 +632,9 @@ importers:
specifier: ^4.1.11
version: 4.1.12
devDependencies:
'@types/mime-types':
specifier: 2.1.4
version: 2.1.4
'@types/node':
specifier: ^24.9.2
version: 24.9.2
@@ -715,7 +724,7 @@ importers:
version: 5.90.5(react@19.2.0)
better-auth:
specifier: 1.3.34
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
class-variance-authority:
specifier: 0.7.1
version: 0.7.1
@@ -733,7 +742,7 @@ importers:
version: 10.2.0
jotai:
specifier: 2.15.0
version: 2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
version: 2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
lucide-react:
specifier: 0.552.0
version: 0.552.0(react@19.2.0)
@@ -760,7 +769,7 @@ importers:
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-scan:
specifier: 0.4.3
version: 0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5)
version: 0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5)
sonner:
specifier: 2.0.7
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -1332,7 +1341,7 @@ importers:
version: 0.15.12(typescript@5.9.3)
unplugin-dts:
specifier: 1.0.0-beta.6
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.2))(esbuild@0.25.11)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.2))(esbuild@0.25.11)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
vite:
specifier: 7.1.12
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
@@ -5378,15 +5387,15 @@ packages:
'@types/mdx@2.0.13':
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
'@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@20.19.24':
resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==}
'@types/node@24.9.1':
resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
'@types/node@24.9.2':
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
@@ -6952,10 +6961,6 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.0:
resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -8716,6 +8721,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
@@ -16487,17 +16500,14 @@ snapshots:
'@types/mdx@2.0.13': {}
'@types/mime-types@2.1.4': {}
'@types/ms@2.1.0': {}
'@types/node@20.19.24':
dependencies:
undici-types: 6.21.0
'@types/node@24.9.1':
dependencies:
undici-types: 7.16.0
optional: true
'@types/node@24.9.2':
dependencies:
undici-types: 7.16.0
@@ -16515,7 +16525,7 @@ snapshots:
'@types/pg@8.11.6':
dependencies:
'@types/node': 24.9.1
'@types/node': 24.9.2
pg-protocol: 1.10.3
pg-types: 4.1.0
optional: true
@@ -17315,7 +17325,7 @@ snapshots:
batch-cluster@15.0.1: {}
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
'@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
@@ -17332,7 +17342,7 @@ snapshots:
nanostores: 1.0.1
zod: 4.1.12
optionalDependencies:
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next: 16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
@@ -18164,8 +18174,6 @@ snapshots:
entities@4.5.0: {}
entities@6.0.0: {}
entities@6.0.1: {}
environment@1.1.0: {}
@@ -19768,6 +19776,13 @@ snapshots:
jose@6.1.0: {}
jotai@2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
optionalDependencies:
'@babel/core': 7.28.4
'@babel/template': 7.27.2
'@types/react': 19.2.2
react: 19.2.0
jotai@2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
optionalDependencies:
'@babel/core': 7.28.5
@@ -20698,6 +20713,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-function@5.0.1: {}
minimatch@10.0.3:
@@ -20794,6 +20815,30 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@next/env': 16.0.1
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001752
postcss: 8.4.31
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 16.0.1
'@next/swc-darwin-x64': 16.0.1
'@next/swc-linux-arm64-gnu': 16.0.1
'@next/swc-linux-arm64-musl': 16.0.1
'@next/swc-linux-x64-gnu': 16.0.1
'@next/swc-linux-x64-musl': 16.0.1
'@next/swc-win32-arm64-msvc': 16.0.1
'@next/swc-win32-x64-msvc': 16.0.1
sharp: 0.34.4
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
optional: true
next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@next/env': 16.0.1
@@ -21005,7 +21050,7 @@ snapshots:
parse5@7.3.0:
dependencies:
entities: 6.0.0
entities: 6.0.1
pascal-case@3.1.2:
dependencies:
@@ -21904,7 +21949,7 @@ snapshots:
optionalDependencies:
react-dom: 19.2.0(react@19.2.0)
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5):
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.3
@@ -21913,7 +21958,7 @@ snapshots:
'@clack/prompts': 0.8.2
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@preact/signals': 1.3.2(preact@10.27.2)
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
'@types/node': 20.19.24
bippy: 0.3.27(@types/react@19.2.2)(react@19.2.0)
esbuild: 0.25.11
@@ -21926,7 +21971,7 @@ snapshots:
react-dom: 19.2.0(react@19.2.0)
tsx: 4.20.6
optionalDependencies:
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next: 16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
unplugin: 2.1.0
@@ -21935,7 +21980,7 @@ snapshots:
- rollup
- supports-color
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5):
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.3
@@ -21944,7 +21989,7 @@ snapshots:
'@clack/prompts': 0.8.2
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@preact/signals': 1.3.2(preact@10.27.2)
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
'@types/node': 20.19.24
bippy: 0.3.27(@types/react@19.2.2)(react@19.2.0)
esbuild: 0.25.11
@@ -22803,6 +22848,14 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0):
dependencies:
client-only: 0.0.1
react: 19.2.0
optionalDependencies:
'@babel/core': 7.28.4
optional: true
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
dependencies:
client-only: 0.0.1
@@ -23232,7 +23285,7 @@ snapshots:
magic-string-ast: 1.0.3
unplugin: 2.3.10
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.2))(esbuild@0.25.11)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.2))(esbuild@0.25.11)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
'@volar/typescript': 2.4.23
@@ -23246,6 +23299,7 @@ snapshots:
optionalDependencies:
'@microsoft/api-extractor': 7.52.13(@types/node@24.9.2)
esbuild: 0.25.11
rolldown: 1.0.0-beta.45
rollup: 4.52.5
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
transitivePeerDependencies: