From 0c4286b252ebf5a2670c2bf08af0983702af4852 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 8 Feb 2026 15:54:12 +0100 Subject: [PATCH] refactor: prod deployment --- .dockerignore | 2 + Dockerfile | 6 +- app/client/api-client/client.gen.ts | 2 +- app/client/api-client/types.gen.ts | 2 +- app/client/lib/auth-client.ts | 1 - app/routeTree.gen.ts | 21 ++ app/routes/__root.tsx | 5 +- app/routes/api.$.ts | 18 ++ app/server.ts | 31 +- app/server/app.ts | 8 - app/server/core/config.ts | 4 +- app/server/index.ts | 50 ---- bun.lock | 136 ++++++++- docker-compose.yml | 1 + package.json | 8 +- server.ts | 430 ++++++++++++++++++++++++++++ 16 files changed, 632 insertions(+), 93 deletions(-) create mode 100644 app/routes/api.$.ts delete mode 100644 app/server/index.ts create mode 100644 server.ts diff --git a/.dockerignore b/.dockerignore index 1322a729..a2b66005 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ !turbo.json !bun.lock !package.json +!server.ts !**/package.json !**/bun.lock @@ -22,3 +23,4 @@ !LICENSES/** node_modules/** +dist/** diff --git a/Dockerfile b/Dockerfile index a743cf9e..2386cb5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,8 +103,8 @@ RUN bun install --production --frozen-lockfile && rm -rf $HOME/.bun/install/cach COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/rclone /usr/local/bin/rclone COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr -COPY --from=builder /app/dist/client ./dist/client -COPY --from=builder /app/dist/server ./dist/server +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/server.ts ./server.ts COPY --from=builder /app/app/drizzle ./assets/migrations # Include third-party licenses and attribution @@ -114,5 +114,5 @@ COPY ./LICENSE ./LICENSE.md EXPOSE 4096 -CMD ["bun", "run", "start"] +CMD ["bun", "server.ts"] diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index fb37e2b1..457f3c3d 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -16,4 +16,4 @@ export type CreateClientConfig = ( override?: Config, ) => Config & T>; -export const client = createClient(createConfig({ baseUrl: "http://192.168.2.42:4096" })); +export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index da9b6e98..0492947e 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2,7 +2,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: "http://192.168.2.42:4096" | (string & {}); + baseUrl: "http://localhost:4096" | (string & {}); }; export type GetStatusData = { diff --git a/app/client/lib/auth-client.ts b/app/client/lib/auth-client.ts index 10a640f3..15744898 100644 --- a/app/client/lib/auth-client.ts +++ b/app/client/lib/auth-client.ts @@ -9,7 +9,6 @@ import { import type { auth } from "~/server/lib/auth"; export const authClient = createAuthClient({ - baseURL: "http://localhost:3000/api/auth", plugins: [ inferAdditionalFields(), usernameClient(), diff --git a/app/routeTree.gen.ts b/app/routeTree.gen.ts index 59e72d76..83a8e813 100644 --- a/app/routeTree.gen.ts +++ b/app/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as dashboardRouteRouteImport } from './routes/(dashboard)/route' import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiSplatRouteImport } from './routes/api.$' import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding' import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key' @@ -40,6 +41,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const ApiSplatRoute = ApiSplatRouteImport.update({ + id: '/api/$', + path: '/api/$', + getParentRoute: () => rootRouteImport, +} as any) const authOnboardingRoute = authOnboardingRouteImport.update({ id: '/(auth)/onboarding', path: '/onboarding', @@ -152,6 +158,7 @@ export interface FileRoutesByFullPath { '/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute + '/api/$': typeof ApiSplatRoute '/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute @@ -174,6 +181,7 @@ export interface FileRoutesByTo { '/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute + '/api/$': typeof ApiSplatRoute '/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute @@ -198,6 +206,7 @@ export interface FileRoutesById { '/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/(auth)/login': typeof authLoginRoute '/(auth)/onboarding': typeof authOnboardingRoute + '/api/$': typeof ApiSplatRoute '/(dashboard)/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute '/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute @@ -222,6 +231,7 @@ export interface FileRouteTypes { | '/download-recovery-key' | '/login' | '/onboarding' + | '/api/$' | '/backups/$scheduleId' | '/backups/create' | '/notifications/$notificationId' @@ -244,6 +254,7 @@ export interface FileRouteTypes { | '/download-recovery-key' | '/login' | '/onboarding' + | '/api/$' | '/backups/$scheduleId' | '/backups/create' | '/notifications/$notificationId' @@ -267,6 +278,7 @@ export interface FileRouteTypes { | '/(auth)/download-recovery-key' | '/(auth)/login' | '/(auth)/onboarding' + | '/api/$' | '/(dashboard)/backups/$scheduleId' | '/(dashboard)/backups/create' | '/(dashboard)/notifications/$notificationId' @@ -291,6 +303,7 @@ export interface RootRouteChildren { authDownloadRecoveryKeyRoute: typeof authDownloadRecoveryKeyRoute authLoginRoute: typeof authLoginRoute authOnboardingRoute: typeof authOnboardingRoute + ApiSplatRoute: typeof ApiSplatRoute } declare module '@tanstack/react-router' { @@ -309,6 +322,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/api/$': { + id: '/api/$' + path: '/api/$' + fullPath: '/api/$' + preLoaderRoute: typeof ApiSplatRouteImport + parentRoute: typeof rootRouteImport + } '/(auth)/onboarding': { id: '/(auth)/onboarding' path: '/onboarding' @@ -498,6 +518,7 @@ const rootRouteChildren: RootRouteChildren = { authDownloadRecoveryKeyRoute: authDownloadRecoveryKeyRoute, authLoginRoute: authLoginRoute, authOnboardingRoute: authOnboardingRoute, + ApiSplatRoute: ApiSplatRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx index 44830cad..b4561a96 100644 --- a/app/routes/__root.tsx +++ b/app/routes/__root.tsx @@ -1,5 +1,4 @@ import { Outlet, HeadContent, Scripts, createRootRouteWithContext } from "@tanstack/react-router"; -import appCss from "../app.css?url"; import { apiClientMiddleware } from "~/middleware/api-client"; import type { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; @@ -7,6 +6,8 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { Toaster } from "~/client/components/ui/sonner"; import { useServerEvents } from "~/client/hooks/use-server-events"; +import appCss from "../app.css?url"; + export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ server: { middleware: [apiClientMiddleware], @@ -15,7 +16,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( meta: [ { charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, - { title: "TanStack Start Starter" }, + { title: "Zerobyte - Open Source Backup Solution" }, ], links: [ { diff --git a/app/routes/api.$.ts b/app/routes/api.$.ts new file mode 100644 index 00000000..f7aa21ed --- /dev/null +++ b/app/routes/api.$.ts @@ -0,0 +1,18 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { createApp } from "~/server/app"; + +const app = createApp(); + +const handle = ({ request }: { request: Request }) => app.fetch(request); + +export const Route = createFileRoute("/api/$")({ + server: { + handlers: { + GET: handle, + POST: handle, + DELETE: handle, + PUT: handle, + PATCH: handle, + }, + }, +}); diff --git a/app/server.ts b/app/server.ts index c857d77a..37c20d69 100644 --- a/app/server.ts +++ b/app/server.ts @@ -3,12 +3,10 @@ import { setSchema, runDbMigrations } from "./server/db/db"; import { startup } from "./server/modules/lifecycle/startup"; import { logger } from "./server/utils/logger"; import { shutdown } from "./server/modules/lifecycle/shutdown"; -import { createApp } from "./server/app"; -import { config } from "./server/core/config"; import { runCLI } from "./server/cli"; import { runMigrations } from "./server/modules/lifecycle/migrations"; -import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; import { createStartHandler, defaultStreamHandler, defineHandlerCallback } from "@tanstack/react-start/server"; +import { createServerEntry } from "@tanstack/react-start/server-entry"; setSchema(schema); @@ -19,12 +17,18 @@ if (cliRun) { runDbMigrations(); -const app = createApp(); - await runMigrations(); await startup(); -export type AppType = typeof app; +const customHandler = defineHandlerCallback((ctx) => { + return defaultStreamHandler(ctx); +}); + +const fetch = createStartHandler(customHandler); + +export default createServerEntry({ + fetch, +}); process.on("SIGTERM", async () => { logger.info("SIGTERM received, starting graceful shutdown..."); @@ -37,18 +41,3 @@ process.on("SIGINT", async () => { await shutdown(); process.exit(0); }); - -export default createServerEntry({ - fetch: app.fetch, -}); -// export default await createHonoServer({ -// app, -// port: config.port, -// defaultLogger: false, -// customBunServer: { -// idleTimeout: config.serverIdleTimeout, -// error(err) { -// logger.error(`[Bun.serve] Server error: ${err.message}`); -// }, -// }, -// }); diff --git a/app/server/app.ts b/app/server/app.ts index 29d31cfb..e97ec007 100644 --- a/app/server/app.ts +++ b/app/server/app.ts @@ -18,7 +18,6 @@ import { handleServiceError } from "./utils/errors"; import { logger } from "./utils/logger"; import { config } from "./core/config"; import { auth } from "~/server/lib/auth"; -import { createStartHandler, defaultStreamHandler, defineHandlerCallback } from "@tanstack/react-start/server"; export const generalDescriptor = (app: Hono) => openAPIRouteHandler(app, { @@ -84,13 +83,6 @@ export const createApp = () => { app.get("/api/v1/openapi.json", generalDescriptor(app)); app.get("/api/v1/docs", requireAuth, scalarDescriptor); - const customHandler = defineHandlerCallback((ctx) => { - return defaultStreamHandler(ctx); - }); - const fetch = createStartHandler(customHandler); - - app.all("*", (ctx) => fetch(ctx.req.raw)); - app.onError((err, c) => { logger.error(`${c.req.url}: ${err.message}`); diff --git a/app/server/core/config.ts b/app/server/core/config.ts index 2b6e76a9..a2d81db2 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -46,7 +46,9 @@ const envSchema = type({ port: s.PORT, migrationsPath: s.MIGRATIONS_PATH, appVersion: s.APP_VERSION, - trustedOrigins: s.TRUSTED_ORIGINS?.split(",").map((origin) => origin.trim()), + trustedOrigins: s.TRUSTED_ORIGINS?.split(",") + .map((origin) => origin.trim()) + .push(s.BASE_URL) ?? [s.BASE_URL], disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", appSecret: s.APP_SECRET, baseUrl: s.BASE_URL, diff --git a/app/server/index.ts b/app/server/index.ts deleted file mode 100644 index bdd9e327..00000000 --- a/app/server/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createHonoServer } from "react-router-hono-server/bun"; -import * as schema from "./db/schema"; -import { setSchema, runDbMigrations } from "./db/db"; -import { startup } from "./modules/lifecycle/startup"; -import { logger } from "./utils/logger"; -import { shutdown } from "./modules/lifecycle/shutdown"; -import { createApp } from "./app"; -import { config } from "./core/config"; -import { runCLI } from "./cli"; -import { runMigrations } from "./modules/lifecycle/migrations"; - -setSchema(schema); - -const cliRun = await runCLI(Bun.argv); -if (cliRun) { - process.exit(0); -} - -runDbMigrations(); - -const app = createApp(); - -await runMigrations(); -await startup(); - -export type AppType = typeof app; - -process.on("SIGTERM", async () => { - logger.info("SIGTERM received, starting graceful shutdown..."); - await shutdown(); - process.exit(0); -}); - -process.on("SIGINT", async () => { - logger.info("SIGINT received, starting graceful shutdown..."); - await shutdown(); - process.exit(0); -}); - -export default await createHonoServer({ - app, - port: config.port, - defaultLogger: false, - customBunServer: { - idleTimeout: config.serverIdleTimeout, - error(err) { - logger.error(`[Bun.serve] Server error: ${err.message}`); - }, - }, -}); diff --git a/bun.lock b/bun.lock index 1722e7a1..086bb904 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,9 @@ "@radix-ui/react-tooltip": "^1.2.8", "@scalar/hono-api-reference": "^0.9.38", "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router": "^1.157.18", + "@tanstack/react-router-devtools": "^1.158.0", "@tanstack/react-router-ssr-query": "^1.157.18", "@tanstack/react-start": "^1.157.18", "arktype": "^2.1.28", @@ -66,6 +68,7 @@ "yaml": "^2.8.2", }, "devDependencies": { + "@babel/preset-typescript": "^7.28.5", "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.4.0", "@hey-api/openapi-ts": "^0.91.0", @@ -73,8 +76,6 @@ "@playwright/test": "^1.58.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router-devtools": "^1.158.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/bun": "^1.3.8", @@ -87,6 +88,7 @@ "dotenv-cli": "^11.0.0", "drizzle-kit": "^1.0.0-beta.12-a5629fb", "lightningcss": "^1.31.1", + "nitro": "^3.0.1-alpha.2", "oxfmt": "^0.27.0", "oxlint": "^1.42.0", "oxlint-tsgolint": "^0.11.3", @@ -152,16 +154,28 @@ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -176,10 +190,16 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -210,6 +230,12 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -362,6 +388,8 @@ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -376,6 +404,86 @@ "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], + "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-43fMTO8/5bMlqfOiNSZNKUzIqeLIYuB9Hr1Ohyf58B1wU11S2dPGibTXOGNaWsfgHy99eeZ1bSgeIHy/fEYqbw=="], + + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5oQrnn9eK/ccOp80PTrNj0Vq893NPNNRryjGpOIVsYNgWFuoGCfpnKg68oEFcN8bArizYAqw4nvgHljEnar69w=="], + + "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dqBDgTG9tF2z2lrZp9E8wU+Godz1i8gCGSei2eFKS2hRploBOD5dmOLp1j4IMornkPvSQmbwB3uSjPq7fjx4EA=="], + + "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0AqabqaooDOpYmeeOye8wClv8PSScELXgOfYqyqgrwH9J9KrpCE1jL8Rlqgz68QbL4mPw3V6sKiiHssI4CLeQ=="], + + "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H0w8o/Wo1072WSdLfhwwrpFpwZnPpjQODlHuRYkTfsSSSJbTxQtjJd4uxk7YJsRv5RQp69y0I7zvdH6f8Xueyw=="], + + "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qd6sW0AvEVYZhbVVMGtmKZw3b1zDYGIW+54Uh42moWRAj6i4Jhk/LGr6r9YNZpOINeuvZfkFuEeDD/jbu7xPUA=="], + + "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7WXP0aXMrWSn0ScppUBi3jf68ebfBG0eri8kxLmBOVSBj6jw1repzkHMITJMBeLr5d0tT/51qFEptiAk2EP2iA=="], + + "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-LYfADrq5x1W5gs+u9OIbMbDQNYkAECTXX0ufnAuf3oGmO51rF98kGFR5qJqC/6/csokDyT3wwTpxhE0TkcF/Og=="], + + "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-53GjCVY8kvymk9P6qNDh6zyblcehF5QHstq9QgCjv13ONGRnSHjeds0PxIwiihD7h295bxsWs84DN39syLPH4Q=="], + + "@oxc-minify/binding-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-li8XcN81dxbJDMBESnTgGhoiAQ+CNIdM0QGscZ4duVPjCry1RpX+5FJySFbGqG3pk4s9ZzlL/vtQtbRzZIZOzg=="], + + "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-SweKfsnLKShu6UFV8mwuj1d1wmlNoL/FlAxPUzwjEBgwiT2HQkY24KnjBH+TIA+//1O83kzmWKvvs4OuEhdIEQ=="], + + "@oxc-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-oH8G4aFMP8XyTsEpdANC5PQyHgSeGlopHZuW1rpyYcaErg5YaK0vXjQ4EM5HVvPm+feBV24JjxgakTnZoF3aOQ=="], + + "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.110.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-W9na+Vza7XVUlpf8wMt4QBfH35KeTENEmnpPUq3NSlbQHz8lSlSvhAafvo43NcKvHAXV3ckD/mUf2VkqSdbklg=="], + + "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XJdA4mmmXOjJxSRgNJXsDP7Xe8h3gQhmb56hUcCrvq5d+h5UcEi2pR8rxsdIrS8QmkLuBA3eHkGK8E27D7DTgQ=="], + + "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqzvALuOTtSckI8x467R4GNArzYDb/yEh6aNzLoeaY1O7vfT7SPDwlOEcchaTznutpeS9Dy8gUS/AfqtUHaufw=="], + + "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-gAMssLs2Q3+uhLZxanh1DF+27Kaug3cf4PXb9AB7XK81DR+LVcKySXaoGYoOs20Co0fFSphd6rRzKge2qDK3dA=="], + + "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.110.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-7Wqi5Zjl022bs2zXq+ICdalDPeDuCH/Nhbi8q2isLihAonMVIT0YH2hqqnNEylRNGYck+FJ6gRZwMpGCgrNxPg=="], + + "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZPx+0Tj4dqn41ecyoGotlvekQKy6JxJCixn9Rw7h/dafZ3eDuBcEVh3c2ZoldXXsyMIt5ywI8IWzFZsjNedd5Q=="], + + "@oxc-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.110.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-H0Oyd3RWBfpEyvJIrFK94RYiY7KKSQl11Ym7LMDwLEagelIAfRCkt1amHZhFa/S3ZRoaOJFXzEw4YKeSsjVFsg=="], + + "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hr3nK90+qXKJ2kepXwFIcNfQQIOBecB4FFCyaMMypthoEEhVP08heRynj4eSXZ8NL9hLjs3fQzH8PJXfpznRnQ=="], + + "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-sE9dxvqqAax1YYJ3t7j+h5ZSI9jl6dYuDfngl6ieZUrIy5P89/8JKVgAzgp8o3wQSo7ndpJvYsi1K4ZqrmbP7w=="], + + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-nqtbP4aMCtsCZ6qpHlHaQoWVHSBtlKzwaAgwEOvR+9DWqHjk31BHvpGiDXlMeed6CVNpl3lCbWgygb3RcSjcfw=="], + + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.110.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oeSeHnL4Z4cMXtc8V0/rwoVn0dgwlS9q0j6LcHn9dIhtFEdp3W0iSBF8YmMQA+E7sILeLDjsHmHE4Kp0sOScXw=="], + + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.110.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nL9K5x7OuZydobAGPylsEW9d4APs2qEkIBLMgQPA+kY8dtVD3IR87QsTbs4l4DBQYyun/+ay6qVCDlxqxdX2Jg=="], + + "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.110.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GS29zXXirDQhZEUq8xKJ1azAWMuUy3Ih3W5Bc5ddk12LRthO5wRLFcKIyeHpAXCoXymQ+LmxbMtbPf84GPxouw=="], + + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-glzDHak8ISyZJemCUi7RCvzNSl+MQ1ly9RceT2qRufhUsvNZ4C/2QLJ1HJwd2N6E88bO4laYn+RofdRzNnGGEA=="], + + "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.110.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8JThvgJ2FRoTVfbp7e4wqeZqCZbtudM06SfZmNzND9kPNu/LVYygIR+72RWs+xm4bWkuYHg/islo/boNPtMT5Q=="], + + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IRh21Ub/g4bkHoErZ0AUWMlWfoZaS0A6EaOVtbcY70RSYIMlrsbjiFwJCzM+b/1DD1rXbH5tsGcH7GweTbfRqg=="], + + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.110.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-e5JN94/oy+wevk76q+LMr+2klTTcO60uXa+Wkq558Ms7mdF2TvkKFI++d/JeiuIwJLTi/BxQ4qdT5FWcsHM/ug=="], + + "@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.110.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y3/Tnnz1GvDpmv8FXBIKtdZPsdZklOEPdrL6NHrN5i2u54BOkybFaDSptgWF53wOrJlTrcmAVSE6fRKK9XCM2Q=="], + + "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-Y0E35iA9/v9jlkNcP6tMJ+ZFOS0rLsWDqG6rU9z+X2R3fBFJBO9UARIK6ngx8upxk81y1TFR2CmBFhupfYdH6Q=="], + + "@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.110.0", "", { "os": "linux", "cpu": "none" }, "sha512-JOUSYFfHjBUs7xp2FHmZHb8eTYD/oEu0NklS6JgUauqnoXZHiTLPLVW2o2uVCqldnabYHcomuwI2iqVFYJNhTw=="], + + "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.110.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7blgoXF9D3Ngzb7eun23pNrHJpoV/TtE6LObwlZ3Nmb4oZ6Z+yMvBVaoW68NarbmvNGfZ95zrOjgm6cVETLYBA=="], + + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YQ2joGWCVDZVEU2cD/r/w49hVjDm/Qu1BvC/7zs8LvprzdLS/HyMXGF2oA0puw0b+AqgYaz3bhwKB2xexHyITQ=="], + + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.110.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fkjr5qE632ULmNgvFXWDR/8668WxERz3tU7TQFp6JebPBneColitjSkdx6VKNVXEoMmQnOvBIGeP5tUNT384oA=="], + + "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.110.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HWH9Zj+lMrdSTqFRCZsvDWMz7OnMjbdGsm3xURXWfRZpuaz0bVvyuZNDQXc4FyyhRDsemICaJbU1bgeIpUJDGw=="], + + "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.110.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ejdxHmYfIcHDPhZUe3WklViLt9mDEJE5BzcW7+R1vc5i/5JFA8D0l7NUSsHBJ7FB8Bu9gF+5iMDm6cXGAgaghw=="], + + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.110.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9VTwpXCZs7xkV+mKhQ62dVk7KLnLXtEUxNS2T4nLz3iMl1IJbA4h5oltK0JoobtiUAnbkV53QmMVGW8+Nh3bDQ=="], + + "@oxc-transform/binding-win32-ia32-msvc": ["@oxc-transform/binding-win32-ia32-msvc@0.110.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-5y0fzuNON7/F2hh2P94vANFaRPJ/3DI1hVl5rseCT8VUVqOGIjWaza0YS/D1g6t1WwycW2LWDMi2raOKoWU5GQ=="], + + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.110.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QROrowwlrApI1fEScMknGWKM6GTM/Z2xwMnDqvSaEmzNazBsDUlE08Jasw610hFEsYAVU2K5sp/YaCa9ORdP4A=="], + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3vwqyzNlVTVFVzHMlrqxb4tgVgHp6FYS0uIxsIZ/SeEDG0azaqiOw/2t8LlJ9f72PKRLWSey+Ak99tiKgpbsnQ=="], "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5u8mZVLm70v6l1wLZ2MmeNIEzGsruwKw5F7duePzpakPfxGtLpiFNUwe4aBUJULTP6aMzH+A4dA0JOn8lb7Luw=="], @@ -662,6 +770,8 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -854,6 +964,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], @@ -888,6 +1000,8 @@ "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], @@ -1004,6 +1118,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "h3": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="], + "h3-v2": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="], "happy-dom": ["happy-dom@20.5.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^4.5.0", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-VQe+Q5CYiGOgcCERXhcfNsbnrN92FDEKciMH/x6LppU9dd0j4aTjCTlqONFOIMcAm/5JxS3+utowbXV1OoFr+g=="], @@ -1262,6 +1378,10 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "nf3": ["nf3@0.3.9", "", {}, "sha512-vgmoL6pUXpKXx+WepG4H1xc2nf1N/5vKgU9QerryuSFIyef8EDkj7esYcIgqMe/mE6nGsy6M6b8wfssEBQizVQ=="], + + "nitro": ["nitro@3.0.1-alpha.2", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ=="], + "node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -1278,6 +1398,8 @@ "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], + "ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], @@ -1286,6 +1408,10 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "oxc-minify": ["oxc-minify@0.110.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.110.0", "@oxc-minify/binding-android-arm64": "0.110.0", "@oxc-minify/binding-darwin-arm64": "0.110.0", "@oxc-minify/binding-darwin-x64": "0.110.0", "@oxc-minify/binding-freebsd-x64": "0.110.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.110.0", "@oxc-minify/binding-linux-arm64-gnu": "0.110.0", "@oxc-minify/binding-linux-arm64-musl": "0.110.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-musl": "0.110.0", "@oxc-minify/binding-linux-s390x-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-musl": "0.110.0", "@oxc-minify/binding-openharmony-arm64": "0.110.0", "@oxc-minify/binding-wasm32-wasi": "0.110.0", "@oxc-minify/binding-win32-arm64-msvc": "0.110.0", "@oxc-minify/binding-win32-ia32-msvc": "0.110.0", "@oxc-minify/binding-win32-x64-msvc": "0.110.0" } }, "sha512-KWGTzPo83QmGrXC4ml83PM9HDwUPtZFfasiclUvTV4i3/0j7xRRqINVkrL77CbQnoWura3CMxkRofjQKVDuhBw=="], + + "oxc-transform": ["oxc-transform@0.110.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.110.0", "@oxc-transform/binding-android-arm64": "0.110.0", "@oxc-transform/binding-darwin-arm64": "0.110.0", "@oxc-transform/binding-darwin-x64": "0.110.0", "@oxc-transform/binding-freebsd-x64": "0.110.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.110.0", "@oxc-transform/binding-linux-arm64-gnu": "0.110.0", "@oxc-transform/binding-linux-arm64-musl": "0.110.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-musl": "0.110.0", "@oxc-transform/binding-linux-s390x-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-musl": "0.110.0", "@oxc-transform/binding-openharmony-arm64": "0.110.0", "@oxc-transform/binding-wasm32-wasi": "0.110.0", "@oxc-transform/binding-win32-arm64-msvc": "0.110.0", "@oxc-transform/binding-win32-ia32-msvc": "0.110.0", "@oxc-transform/binding-win32-x64-msvc": "0.110.0" } }, "sha512-/fymQNzzUoKZweH0nC5yvbI2eR0yWYusT9TEKDYVgOgYrf9Qmdez9lUFyvxKR9ycx+PTHi/reIOzqf3wkShQsw=="], + "oxfmt": ["oxfmt@0.27.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.27.0", "@oxfmt/darwin-x64": "0.27.0", "@oxfmt/linux-arm64-gnu": "0.27.0", "@oxfmt/linux-arm64-musl": "0.27.0", "@oxfmt/linux-x64-gnu": "0.27.0", "@oxfmt/linux-x64-musl": "0.27.0", "@oxfmt/win32-arm64": "0.27.0", "@oxfmt/win32-x64": "0.27.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-FHR0HR3WeMKBuVEQvW3EeiRZXs/cQzNHxGbhCoAIEPr1FVcOa9GCqrKJXPqv2jkzmCg6Wqot+DvN9RzemyFJhw=="], "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="], @@ -1494,6 +1620,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -1508,6 +1636,8 @@ "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "unstorage": ["unstorage@2.0.0-alpha.5", "", { "peerDependencies": { "@azure/app-configuration": "^1.9.0", "@azure/cosmos": "^4.7.0", "@azure/data-tables": "^13.3.1", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.29.1", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.12.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.35.6", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.8.2", "lru-cache": "^11.2.2", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -1578,6 +1708,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@hey-api/openapi-ts/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], diff --git a/docker-compose.yml b/docker-compose.yml index dfe9a8f3..d4278671 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: environment: - APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b - BASE_URL=http://localhost:4096 + - PORT=4096 volumes: - /etc/localtime:/etc/localtime:ro - /var/lib/zerobyte:/var/lib/zerobyte diff --git a/package.json b/package.json index 6f4c13e7..f3fcb773 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "lint": "oxlint --type-aware", "dev": "NODE_ENV=development bunx --bun vite", "build": "vite build", - "preview": "vite preview", "start": "bun ./dist/server/index.js", + "preview": "bunx --bun vite preview", "cli:dev": "bun run app/server/cli/main.ts", "cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js", "tsc": "tsc --noEmit", @@ -48,7 +48,9 @@ "@radix-ui/react-tooltip": "^1.2.8", "@scalar/hono-api-reference": "^0.9.38", "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router": "^1.157.18", + "@tanstack/react-router-devtools": "^1.158.0", "@tanstack/react-router-ssr-query": "^1.157.18", "@tanstack/react-start": "^1.157.18", "arktype": "^2.1.28", @@ -87,6 +89,7 @@ "yaml": "^2.8.2" }, "devDependencies": { + "@babel/preset-typescript": "^7.28.5", "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.4.0", "@hey-api/openapi-ts": "^0.91.0", @@ -94,8 +97,6 @@ "@playwright/test": "^1.58.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router-devtools": "^1.158.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/bun": "^1.3.8", @@ -108,6 +109,7 @@ "dotenv-cli": "^11.0.0", "drizzle-kit": "^1.0.0-beta.12-a5629fb", "lightningcss": "^1.31.1", + "nitro": "^3.0.1-alpha.2", "oxfmt": "^0.27.0", "oxlint": "^1.42.0", "oxlint-tsgolint": "^0.11.3", diff --git a/server.ts b/server.ts new file mode 100644 index 00000000..ad1660c4 --- /dev/null +++ b/server.ts @@ -0,0 +1,430 @@ +import path from "node:path"; + +// Configuration +const SERVER_PORT = Number(process.env.PORT ?? 3000); +const CLIENT_DIRECTORY = "./dist/client"; +const SERVER_ENTRY_POINT = "./dist/server/server.js"; + +// Logging utilities for professional output +const log = { + info: (message: string) => { + console.log(`[INFO] ${message}`); + }, + success: (message: string) => { + console.log(`[SUCCESS] ${message}`); + }, + warning: (message: string) => { + console.log(`[WARNING] ${message}`); + }, + error: (message: string) => { + console.log(`[ERROR] ${message}`); + }, + header: (message: string) => { + console.log(`\n${message}\n`); + }, +}; + +// Preloading configuration from environment variables +const MAX_PRELOAD_BYTES = Number( + process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default +); + +// Parse comma-separated include patterns (no defaults) +const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + .map((pattern: string) => convertGlobToRegExp(pattern)); + +// Parse comma-separated exclude patterns (no defaults) +const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + .map((pattern: string) => convertGlobToRegExp(pattern)); + +// Verbose logging flag +const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === "true"; + +// Optional ETag feature +const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? "true") === "true"; + +// Optional Gzip feature +const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? "true") === "true"; +const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024); // 1KB +const GZIP_TYPES = ( + process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ?? + "text/,application/javascript,application/json,application/xml,image/svg+xml" +) + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + +/** + * Convert a simple glob pattern to a regular expression + * Supports * wildcard for matching any characters + */ +function convertGlobToRegExp(globPattern: string): RegExp { + // Escape regex special chars except *, then replace * with .* + const escapedPattern = globPattern.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&").replace(/\*/g, ".*"); + return new RegExp(`^${escapedPattern}$`, "i"); +} + +/** + * Compute ETag for a given data buffer + */ +function computeEtag(data: Uint8Array): string { + const hash = Bun.hash(data); + return `W/"${hash.toString(16)}-${data.byteLength.toString()}"`; +} + +/** + * Metadata for preloaded static assets + */ +interface AssetMetadata { + route: string; + size: number; + type: string; +} + +/** + * In-memory asset with ETag and Gzip support + */ +interface InMemoryAsset { + raw: Uint8Array; + gz?: Uint8Array; + etag?: string; + type: string; + immutable: boolean; + size: number; +} + +/** + * Result of static asset preloading process + */ +interface PreloadResult { + routes: Record Response | Promise>; + loaded: AssetMetadata[]; + skipped: AssetMetadata[]; +} + +/** + * Check if a file is eligible for preloading based on configured patterns + */ +function isFileEligibleForPreloading(relativePath: string): boolean { + return false; + const fileName = relativePath.split(/[/\\]/).pop() ?? relativePath; + + // If include patterns are specified, file must match at least one + if (INCLUDE_PATTERNS.length > 0) { + if (!INCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) { + return false; + } + } + + // If exclude patterns are specified, file must not match any + if (EXCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) { + return false; + } + + return true; +} + +/** + * Check if a MIME type is compressible + */ +function isMimeTypeCompressible(mimeType: string): boolean { + return GZIP_TYPES.some((type) => (type.endsWith("/") ? mimeType.startsWith(type) : mimeType === type)); +} + +/** + * Conditionally compress data based on size and MIME type + */ +function compressDataIfAppropriate(data: Uint8Array, mimeType: string): Uint8Array | undefined { + if (!ENABLE_GZIP) return undefined; + if (data.byteLength < GZIP_MIN_BYTES) return undefined; + if (!isMimeTypeCompressible(mimeType)) return undefined; + try { + return Bun.gzipSync(data.buffer as ArrayBuffer); + } catch { + return undefined; + } +} + +/** + * Create response handler function with ETag and Gzip support + */ +function createResponseHandler(asset: InMemoryAsset): (req: Request) => Response { + return (req: Request) => { + const headers: Record = { + "Content-Type": asset.type, + "Cache-Control": asset.immutable ? "public, max-age=31536000, immutable" : "public, max-age=3600", + }; + + if (ENABLE_ETAG && asset.etag) { + const ifNone = req.headers.get("if-none-match"); + if (ifNone && ifNone === asset.etag) { + return new Response(null, { + status: 304, + headers: { ETag: asset.etag }, + }); + } + headers.ETag = asset.etag; + } + + if (ENABLE_GZIP && asset.gz && req.headers.get("accept-encoding")?.includes("gzip")) { + headers["Content-Encoding"] = "gzip"; + headers["Content-Length"] = String(asset.gz.byteLength); + const gzCopy = new Uint8Array(asset.gz); + return new Response(gzCopy, { status: 200, headers }); + } + + headers["Content-Length"] = String(asset.raw.byteLength); + const rawCopy = new Uint8Array(asset.raw); + return new Response(rawCopy, { status: 200, headers }); + }; +} + +/** + * Create composite glob pattern from include patterns + */ +function createCompositeGlobPattern(): Bun.Glob { + const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + if (raw.length === 0) return new Bun.Glob("**/*"); + if (raw.length === 1) return new Bun.Glob(raw[0]); + return new Bun.Glob(`{${raw.join(",")}}`); +} + +/** + * Initialize static routes with intelligent preloading strategy + * Small files are loaded into memory, large files are served on-demand + */ +async function initializeStaticRoutes(clientDirectory: string): Promise { + const routes: Record Response | Promise> = {}; + const loaded: AssetMetadata[] = []; + const skipped: AssetMetadata[] = []; + + log.info(`Loading static assets from ${clientDirectory}...`); + + let totalPreloadedBytes = 0; + + try { + const glob = createCompositeGlobPattern(); + for await (const relativePath of glob.scan({ cwd: clientDirectory })) { + const filepath = path.join(clientDirectory, relativePath); + const route = `/${relativePath.split(path.sep).join(path.posix.sep)}`; + + try { + // Get file metadata + const file = Bun.file(filepath); + + // Skip if file doesn't exist or is empty + if (!(await file.exists()) || file.size === 0) { + continue; + } + + const metadata: AssetMetadata = { + route, + size: file.size, + type: file.type || "application/octet-stream", + }; + + // Determine if file should be preloaded + const matchesPattern = isFileEligibleForPreloading(relativePath); + const withinSizeLimit = file.size <= MAX_PRELOAD_BYTES; + + if (matchesPattern && withinSizeLimit) { + // Preload small files into memory with ETag and Gzip support + const bytes = new Uint8Array(await file.arrayBuffer()); + const gz = compressDataIfAppropriate(bytes, metadata.type); + const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined; + const asset: InMemoryAsset = { + raw: bytes, + gz, + etag, + type: metadata.type, + immutable: true, + size: bytes.byteLength, + }; + routes[route] = createResponseHandler(asset); + + loaded.push({ ...metadata, size: bytes.byteLength }); + totalPreloadedBytes += bytes.byteLength; + } else { + // Serve large or filtered files on-demand + routes[route] = () => { + const fileOnDemand = Bun.file(filepath); + return new Response(fileOnDemand, { + headers: { + "Content-Type": metadata.type, + "Cache-Control": "public, max-age=3600", + }, + }); + }; + + skipped.push(metadata); + } + } catch (error: unknown) { + if (error instanceof Error && error.name !== "EISDIR") { + log.error(`Failed to load ${filepath}: ${error.message}`); + } + } + } + + // Show detailed file overview only when verbose mode is enabled + if (VERBOSE && (loaded.length > 0 || skipped.length > 0)) { + const allFiles = [...loaded, ...skipped].sort((a, b) => a.route.localeCompare(b.route)); + + // Calculate max path length for alignment + const maxPathLength = Math.min(Math.max(...allFiles.map((f) => f.route.length)), 60); + + // Format file size with KB and actual gzip size + const formatFileSize = (bytes: number, gzBytes?: number) => { + const kb = bytes / 1024; + const sizeStr = kb < 100 ? kb.toFixed(2) : kb.toFixed(1); + + if (gzBytes !== undefined) { + const gzKb = gzBytes / 1024; + const gzStr = gzKb < 100 ? gzKb.toFixed(2) : gzKb.toFixed(1); + return { + size: sizeStr, + gzip: gzStr, + }; + } + + // Rough gzip estimation (typically 30-70% compression) if no actual gzip data + const gzipKb = kb * 0.35; + return { + size: sizeStr, + gzip: gzipKb < 100 ? gzipKb.toFixed(2) : gzipKb.toFixed(1), + }; + }; + + if (loaded.length > 0) { + console.log("\nšŸ“ Preloaded into memory:"); + console.log("Path │ Size │ Gzip Size"); + loaded + .sort((a, b) => a.route.localeCompare(b.route)) + .forEach((file) => { + const { size, gzip } = formatFileSize(file.size); + const paddedPath = file.route.padEnd(maxPathLength); + const sizeStr = `${size.padStart(7)} kB`; + const gzipStr = `${gzip.padStart(7)} kB`; + console.log(`${paddedPath} │ ${sizeStr} │ ${gzipStr}`); + }); + } + + if (skipped.length > 0) { + console.log("\nšŸ’¾ Served on-demand:"); + console.log("Path │ Size │ Gzip Size"); + skipped + .sort((a, b) => a.route.localeCompare(b.route)) + .forEach((file) => { + const { size, gzip } = formatFileSize(file.size); + const paddedPath = file.route.padEnd(maxPathLength); + const sizeStr = `${size.padStart(7)} kB`; + const gzipStr = `${gzip.padStart(7)} kB`; + console.log(`${paddedPath} │ ${sizeStr} │ ${gzipStr}`); + }); + } + } + + // Show detailed verbose info if enabled + if (VERBOSE) { + if (loaded.length > 0 || skipped.length > 0) { + const allFiles = [...loaded, ...skipped].sort((a, b) => a.route.localeCompare(b.route)); + console.log("\nšŸ“Š Detailed file information:"); + console.log("Status │ Path │ MIME Type │ Reason"); + allFiles.forEach((file) => { + const isPreloaded = loaded.includes(file); + const status = isPreloaded ? "MEMORY" : "ON-DEMAND"; + const reason = + !isPreloaded && file.size > MAX_PRELOAD_BYTES ? "too large" : !isPreloaded ? "filtered" : "preloaded"; + const route = file.route.length > 30 ? file.route.substring(0, 27) + "..." : file.route; + console.log(`${status.padEnd(12)} │ ${route.padEnd(30)} │ ${file.type.padEnd(28)} │ ${reason.padEnd(10)}`); + }); + } else { + console.log("\nšŸ“Š No files found to display"); + } + } + + // Log summary after the file list + console.log(); // Empty line for separation + if (loaded.length > 0) { + log.success( + `Preloaded ${String(loaded.length)} files (${(totalPreloadedBytes / 1024 / 1024).toFixed(2)} MB) into memory`, + ); + } else { + log.info("No files preloaded into memory"); + } + + if (skipped.length > 0) { + const tooLarge = skipped.filter((f) => f.size > MAX_PRELOAD_BYTES).length; + const filtered = skipped.length - tooLarge; + log.info( + `${String(skipped.length)} files will be served on-demand (${String(tooLarge)} too large, ${String(filtered)} filtered)`, + ); + } + } catch (error) { + log.error(`Failed to load static files from ${clientDirectory}: ${String(error)}`); + } + + return { routes, loaded, skipped }; +} + +async function initializeServer() { + log.header("Starting Production Server"); + + // Load TanStack Start server handler + let handler: { fetch: (request: Request) => Response | Promise }; + try { + const serverModule = (await import(SERVER_ENTRY_POINT)) as { + default: { fetch: (request: Request) => Response | Promise }; + }; + handler = serverModule.default; + log.success("TanStack Start application handler initialized"); + } catch (error) { + log.error(`Failed to load server handler: ${String(error)}`); + process.exit(1); + } + + // Build static routes with intelligent preloading + const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY); + + // Create Bun server + const server = Bun.serve({ + port: SERVER_PORT, + + routes: { + // Serve static assets (preloaded or on-demand) + ...routes, + + // Fallback to TanStack Start handler for all other routes + "/*": (req: Request) => { + try { + return handler.fetch(req); + } catch (error) { + log.error(`Server handler error: ${String(error)}`); + return new Response("Internal Server Error", { status: 500 }); + } + }, + }, + + // Global error handler + error(error) { + log.error(`Uncaught server error: ${error instanceof Error ? error.message : String(error)}`); + return new Response("Internal Server Error", { status: 500 }); + }, + }); + + log.success(`Server listening on http://localhost:${String(server.port)}`); +} + +// Initialize the server +initializeServer().catch((error: unknown) => { + log.error(`Failed to start server: ${String(error)}`); + process.exit(1); +});