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(),
],