From 9230bf8c4b73300fdf50ec654f041fe4193800b3 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 10 Feb 2026 21:23:29 +0100 Subject: [PATCH] refactor: nitro build --- .gitignore | 1 + Dockerfile | 6 +- .../schedule-notifications-config.tsx | 2 +- .../backups/components/schedule-summary.tsx | 2 - .../modules/settings/routes/settings.tsx | 2 +- .../(dashboard)/backups/$scheduleId.tsx | 2 +- app/routes/(dashboard)/backups/create.tsx | 5 +- .../repositories/$repoId.$snapshotId.tsx | 4 +- .../repositories/$repositoryId.tsx | 10 +- app/routes/__root.tsx | 7 +- bun.lock | 5 +- package.json | 3 +- server.ts | 430 ------------------ tsconfig.json | 2 +- vite.config.ts | 2 + 15 files changed, 29 insertions(+), 454 deletions(-) delete mode 100644 server.ts diff --git a/.gitignore b/.gitignore index b0cc64c6..6d1d89b2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ playwright/temp # OpenAPI error logs openapi-ts-error-*.log +.output diff --git a/Dockerfile b/Dockerfile index 2386cb5d..80f90673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,8 +103,7 @@ 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 ./dist -COPY --from=builder /app/server.ts ./server.ts +COPY --from=builder /app/.output ./.output COPY --from=builder /app/app/drizzle ./assets/migrations # Include third-party licenses and attribution @@ -114,5 +113,4 @@ COPY ./LICENSE ./LICENSE.md EXPOSE 4096 -CMD ["bun", "server.ts"] - +CMD ["bun", ".output/server/index.mjs"] diff --git a/app/client/modules/backups/components/schedule-notifications-config.tsx b/app/client/modules/backups/components/schedule-notifications-config.tsx index b326e5e1..e67ff3ae 100644 --- a/app/client/modules/backups/components/schedule-notifications-config.tsx +++ b/app/client/modules/backups/components/schedule-notifications-config.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { Bell, Plus, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx index 16f45aac..7581b8cc 100644 --- a/app/client/modules/backups/components/schedule-summary.tsx +++ b/app/client/modules/backups/components/schedule-summary.tsx @@ -18,8 +18,6 @@ import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { handleRepositoryError } from "~/client/lib/errors"; -import { Link } from "react-router"; -import { parseError } from "~/client/lib/errors"; import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; import { Link } from "@tanstack/react-router"; diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 85f272fb..d46d2360 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -157,7 +157,7 @@ export function SettingsPage({ appContext }: Props) { }; const onTabChange = (value: string) => { - navigate({ to: ".", search: () => ({ tab: value }) }); + void navigate({ to: ".", search: () => ({ tab: value }) }); }; return ( diff --git a/app/routes/(dashboard)/backups/$scheduleId.tsx b/app/routes/(dashboard)/backups/$scheduleId.tsx index 0ffb6d76..7c25a3cc 100644 --- a/app/routes/(dashboard)/backups/$scheduleId.tsx +++ b/app/routes/(dashboard)/backups/$scheduleId.tsx @@ -22,7 +22,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$scheduleId")({ context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { scheduleId } }) }), ]); - context.queryClient.prefetchQuery({ + void context.queryClient.prefetchQuery({ ...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.shortId } }), }); diff --git a/app/routes/(dashboard)/backups/create.tsx b/app/routes/(dashboard)/backups/create.tsx index 6a540871..95a9a368 100644 --- a/app/routes/(dashboard)/backups/create.tsx +++ b/app/routes/(dashboard)/backups/create.tsx @@ -10,10 +10,7 @@ export const Route = createFileRoute("/(dashboard)/backups/create")({ ]); }, staticData: { - breadcrumb: () => [ - { label: "Backup Jobs", href: "/backups" }, - { label: "Create" }, - ], + breadcrumb: () => [{ label: "Backup Jobs", href: "/backups" }, { label: "Create" }], }, component: RouteComponent, }); diff --git a/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx b/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx index 77dbd620..4b1c470e 100644 --- a/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx +++ b/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx @@ -14,12 +14,12 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapsho ...getRepositoryOptions({ path: { id: params.repoId } }), }); - context.queryClient.prefetchQuery({ + void context.queryClient.prefetchQuery({ ...getSnapshotDetailsOptions({ path: { id: params.repoId, snapshotId: params.snapshotId }, }), }); - context.queryClient.prefetchQuery({ + void context.queryClient.prefetchQuery({ ...listSnapshotFilesOptions({ path: { id: params.repoId, snapshotId: params.snapshotId }, query: { path: "/" }, diff --git a/app/routes/(dashboard)/repositories/$repositoryId.tsx b/app/routes/(dashboard)/repositories/$repositoryId.tsx index 50a6a606..d9c88fd7 100644 --- a/app/routes/(dashboard)/repositories/$repositoryId.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId.tsx @@ -11,16 +11,16 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId")( component: RouteComponent, errorComponent: (e) =>
{e.error.message}
, loader: async ({ params, context }) => { - context.queryClient.prefetchQuery({ + void context.queryClient.prefetchQuery({ ...listSnapshotsOptions({ path: { id: params.repositoryId } }), - }) - context.queryClient.prefetchQuery({ + }); + void context.queryClient.prefetchQuery({ ...listBackupSchedulesOptions(), - }) + }); const res = await context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: params.repositoryId } }), - }) + }); return res; }, diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx index 9b3c9907..08f8daba 100644 --- a/app/routes/__root.tsx +++ b/app/routes/__root.tsx @@ -1,11 +1,11 @@ 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"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { Toaster } from "~/client/components/ui/sonner"; import { useServerEvents } from "~/client/hooks/use-server-events"; -import "../app.css"; export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ server: { @@ -18,6 +18,10 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( { title: "Zerobyte - Open Source Backup Solution" }, ], links: [ + { + rel: "stylesheet", + href: appCss, + }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", @@ -48,6 +52,7 @@ function RootLayout() { + {/* */} diff --git a/bun.lock b/bun.lock index 086bb904..99ae0f55 100644 --- a/bun.lock +++ b/bun.lock @@ -85,6 +85,7 @@ "@types/semver": "^7.7.1", "@vitejs/plugin-react": "^5.1.2", "babel-plugin-react-compiler": "^1.0.0", + "bun-types": "^1.3.9", "dotenv-cli": "^11.0.0", "drizzle-kit": "^1.0.0-beta.12-a5629fb", "lightningcss": "^1.31.1", @@ -902,7 +903,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1762,6 +1763,8 @@ "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "bl/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], diff --git a/package.json b/package.json index f3fcb773..01df52ed 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "lint": "oxlint --type-aware", "dev": "NODE_ENV=development bunx --bun vite", "build": "vite build", - "start": "bun ./dist/server/index.js", + "start": "bun run .output/server/index.mjs", "preview": "bunx --bun vite preview", "cli:dev": "bun run app/server/cli/main.ts", "cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js", @@ -106,6 +106,7 @@ "@types/semver": "^7.7.1", "@vitejs/plugin-react": "^5.1.2", "babel-plugin-react-compiler": "^1.0.0", + "bun-types": "^1.3.9", "dotenv-cli": "^11.0.0", "drizzle-kit": "^1.0.0-beta.12-a5629fb", "lightningcss": "^1.31.1", diff --git a/server.ts b/server.ts deleted file mode 100644 index ad1660c4..00000000 --- a/server.ts +++ /dev/null @@ -1,430 +0,0 @@ -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); -}); diff --git a/tsconfig.json b/tsconfig.json index 4967a03c..1b25bccc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["node", "vite/client"], + "types": ["node", "vite/client", "bun-types"], "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", diff --git a/vite.config.ts b/vite.config.ts index 22b4f33b..0b2e460e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import { nitro } from "nitro/vite"; import tsconfigPaths from "vite-tsconfig-paths"; import viteReact from "@vitejs/plugin-react"; import babel from "vite-plugin-babel"; @@ -21,6 +22,7 @@ export default defineConfig({ routesDirectory: "routes", }, }), + nitro({ preset: "bun" }), viteReact(), tailwindcss(), ],