refactor: nitro build
This commit is contained in:
parent
62366e5edb
commit
9230bf8c4b
15 changed files with 29 additions and 454 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,3 +36,4 @@ playwright/temp
|
|||
|
||||
# OpenAPI error logs
|
||||
openapi-ts-error-*.log
|
||||
.output
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 } }),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: "/" },
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId")(
|
|||
component: RouteComponent,
|
||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||
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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Zerobyte" />
|
||||
<link rel="manifest" href="/images/favicon/site.webmanifest" />
|
||||
{/* <link rel="stylesheet" href={appCss} /> */}
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="dark">
|
||||
|
|
|
|||
5
bun.lock
5
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
430
server.ts
430
server.ts
|
|
@ -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<string, (req: Request) => Response | Promise<Response>>;
|
||||
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<string, string> = {
|
||||
"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<PreloadResult> {
|
||||
const routes: Record<string, (req: Request) => Response | Promise<Response>> = {};
|
||||
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<Response> };
|
||||
try {
|
||||
const serverModule = (await import(SERVER_ENTRY_POINT)) as {
|
||||
default: { fetch: (request: Request) => Response | Promise<Response> };
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue