From 0c84619e88cc2209507a5e2a55fcac723269d300 Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 1 Nov 2025 02:22:24 +0800 Subject: [PATCH] feat(static-web): implement static web module with controller and service for asset handling Signed-off-by: Innei --- be/apps/core/Dockerfile | 40 +++ be/apps/core/package.json | 3 + be/apps/core/src/modules/index.module.ts | 2 + .../static-web/static-web.controller.ts | 21 ++ .../modules/static-web/static-web.module.ts | 10 + .../modules/static-web/static-web.service.ts | 301 ++++++++++++++++++ pnpm-lock.yaml | 112 +++++-- 7 files changed, 460 insertions(+), 29 deletions(-) create mode 100644 be/apps/core/Dockerfile create mode 100644 be/apps/core/src/modules/static-web/static-web.controller.ts create mode 100644 be/apps/core/src/modules/static-web/static-web.module.ts create mode 100644 be/apps/core/src/modules/static-web/static-web.service.ts diff --git a/be/apps/core/Dockerfile b/be/apps/core/Dockerfile new file mode 100644 index 00000000..175a4704 --- /dev/null +++ b/be/apps/core/Dockerfile @@ -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"] diff --git a/be/apps/core/package.json b/be/apps/core/package.json index c00fca29..34d925bd 100644 --- a/be/apps/core/package.json +++ b/be/apps/core/package.json @@ -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", diff --git a/be/apps/core/src/modules/index.module.ts b/be/apps/core/src/modules/index.module.ts index d661089c..199a5a04 100644 --- a/be/apps/core/src/modules/index.module.ts +++ b/be/apps/core/src/modules/index.module.ts @@ -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], diff --git a/be/apps/core/src/modules/static-web/static-web.controller.ts b/be/apps/core/src/modules/static-web/static-web.controller.ts new file mode 100644 index 00000000..db597a10 --- /dev/null +++ b/be/apps/core/src/modules/static-web/static-web.controller.ts @@ -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 }) + } +} diff --git a/be/apps/core/src/modules/static-web/static-web.module.ts b/be/apps/core/src/modules/static-web/static-web.module.ts new file mode 100644 index 00000000..abc23f18 --- /dev/null +++ b/be/apps/core/src/modules/static-web/static-web.module.ts @@ -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 {} diff --git a/be/apps/core/src/modules/static-web/static-web.service.ts b/be/apps/core/src/modules/static-web/static-web.service.ts new file mode 100644 index 00000000..5b70e3af --- /dev/null +++ b/be/apps/core/src/modules/static-web/static-web.service.ts @@ -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 { + 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 { + 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 { + 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() + + 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 { + 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 { + 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 { + 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') + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f12dddc1..f26a30b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: