diff --git a/apps/nextjs/src/app/[locale]/manage/about/page.tsx b/apps/nextjs/src/app/[locale]/manage/about/page.tsx
index 9577d39a0..21143678e 100644
--- a/apps/nextjs/src/app/[locale]/manage/about/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/about/page.tsx
@@ -40,8 +40,9 @@ import { getScopedI18n } from "@homarr/translation/server";
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
-import { getDependenciesAsync, getPackageVersion } from "~/versions/package-reader";
+import { getPackageVersion } from "~/versions/package-reader";
import githubContributors from "@static-data/contributors.json";
+import dependencies from "@static-data/dependencies.json";
import openCollectiveContributors from "@static-data/opencollective-contributors.json";
import crowdinContributors from "@static-data/translators.json";
import classes from "./about.module.css";
@@ -57,7 +58,6 @@ export async function generateMetadata() {
export default async function AboutPage() {
const t = await getScopedI18n("management.page.about");
const version = getPackageVersion();
- const dependencies = await getDependenciesAsync();
return (
@@ -159,34 +159,26 @@ export default async function AboutPage() {
- {Object.entries(dependencies)
- .filter(([, value]) => !value.includes("workspace:"))
- .toSorted(([key1], [key2]) => key1.localeCompare(key2))
- .map(([name, version]) => (
-
-
-
-
-
-
+ {Object.entries(dependencies).map(([name, version]) => (
+
+
+
+
+
+
-
-
- {name}
-
-
- v{version.replace("^", "").replace("~", "")}
-
-
-
-
-
- ))}
+
+
+ {name}
+
+
+ v{version.replace("^", "").replace("~", "")}
+
+
+
+
+
+ ))}
diff --git a/apps/nextjs/src/versions/package-reader.ts b/apps/nextjs/src/versions/package-reader.ts
index bf1cd6405..86177bd62 100644
--- a/apps/nextjs/src/versions/package-reader.ts
+++ b/apps/nextjs/src/versions/package-reader.ts
@@ -1,63 +1,3 @@
-import fsPromises from "fs/promises";
-import path from "path";
-import { fileURLToPath } from "url";
-import { glob } from "glob";
-import { parse as parseYaml } from "yaml";
-import z from "zod";
-
import packageJson from "../../../../package.json";
export const getPackageVersion = () => packageJson.version;
-export const getDependenciesAsync = async (): Promise
=> {
- const pathNames = await glob("**/package.json", {
- ignore: "**/node_modules/**",
- cwd: "../../",
- absolute: true,
- });
- const packageContents = await Promise.all(pathNames.map(async (path) => await fsPromises.readFile(path, "utf-8")));
- const packageDependencies = packageContents
- .map((packageContent) => (JSON.parse(packageContent) as PackageJson).dependencies)
- .filter((dependencies) => dependencies !== undefined);
-
- const catalog = await parseDependencyCatalogsAsync();
-
- let dependencies = {};
- for (const dependenciesOfPackage of packageDependencies) {
- const resolvedDependencies = Object.entries(dependenciesOfPackage).map(([name, version]) => {
- const catalogVersion = catalog.get(name);
- return [name, catalogVersion ?? version] as const;
- });
- dependencies = { ...dependencies, ...Object.fromEntries(resolvedDependencies) };
- }
- return dependencies;
-};
-
-type DependencyCatalog = Map;
-const parseDependencyCatalogsAsync = async (): Promise => {
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
- const workspaceConfigPath = path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml");
-
- try {
- const workspaceConfigContent = await fsPromises.readFile(workspaceConfigPath, "utf-8");
- const workspaceConfig: unknown = parseYaml(workspaceConfigContent);
- const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig);
- return new Map(Object.entries(parseResult.catalog ?? {}));
- } catch (error) {
- // The workspace config is absent in Docker (only the runner stage is shipped),
- // so a missing file is expected. Surface any other failure (e.g. malformed YAML).
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
- return new Map();
- }
-
- throw error;
- }
-};
-
-const workspaceConfigCatalogSchema = z.object({
- catalog: z.record(z.string(), z.string()).optional(),
-});
-
-export type PackageJsonDependencies = Record;
-interface PackageJson {
- dependencies: PackageJsonDependencies | undefined;
-}
diff --git a/scripts/benchmarks/build-compare.sh b/scripts/benchmarks/build-compare.sh
new file mode 100755
index 000000000..eb7f76da2
--- /dev/null
+++ b/scripts/benchmarks/build-compare.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+# Compares Next.js build output between origin/dev and the current branch.
+# Runs two full builds — expect ~5-10 minutes total.
+set -euo pipefail
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+NEXT_DIR="$REPO_ROOT/apps/nextjs"
+OUT_DIR="$REPO_ROOT/.bench"
+mkdir -p "$OUT_DIR"
+
+capture_metrics() {
+ local label="$1" out="$OUT_DIR/$label"
+ mkdir -p "$out"
+
+ echo "=== Building $label ==="
+ cd "$REPO_ROOT"
+
+ local start_ns
+ start_ns=$(date +%s)
+ pnpm turbo build --filter=@homarr/nextjs 2>&1 | tee "$out/build.log"
+ local end_ns
+ end_ns=$(date +%s)
+ echo $((end_ns - start_ns)) > "$out/build_time_s"
+
+ # .next size
+ du -sh "$NEXT_DIR/.next" 2>/dev/null | cut -f1 > "$out/next_size"
+
+ # Standalone size
+ if [ -d "$NEXT_DIR/.next/standalone" ]; then
+ du -sh "$NEXT_DIR/.next/standalone" | cut -f1 > "$out/standalone_size"
+ else
+ echo "n/a" > "$out/standalone_size"
+ fi
+
+ # Prerendered routes
+ if [ -d "$NEXT_DIR/.next/server/app" ]; then
+ find "$NEXT_DIR/.next/server/app" -name '*.html' | wc -l | tr -d ' ' > "$out/prerendered_count"
+ else
+ echo "0" > "$out/prerendered_count"
+ fi
+
+ # Route summary from build output
+ grep -E '(○|●|λ|ƒ|◐)\s' "$out/build.log" > "$out/routes.txt" 2>/dev/null || true
+ grep -c '^○' "$out/routes.txt" 2>/dev/null > "$out/static_routes" || echo "0" > "$out/static_routes"
+ grep -c '^ƒ\|^λ\|^●\|^◐' "$out/routes.txt" 2>/dev/null > "$out/dynamic_routes" || echo "0" > "$out/dynamic_routes"
+}
+
+echo "Stashing uncommitted changes..."
+git stash --include-untracked -q 2>/dev/null || true
+
+echo ""
+echo "=== Phase 1: Build origin/dev ==="
+git checkout origin/dev --detach -q
+capture_metrics "dev"
+
+echo ""
+echo "=== Phase 2: Build $CURRENT_BRANCH ==="
+git checkout "$CURRENT_BRANCH" -q
+git stash pop -q 2>/dev/null || true
+capture_metrics "branch"
+
+echo ""
+echo "========================================"
+echo " BUILD COMPARISON"
+echo "========================================"
+printf "%-25s %-15s %-15s\n" "Metric" "dev" "$CURRENT_BRANCH"
+printf "%-25s %-15s %-15s\n" "-------------------------" "---------------" "---------------"
+printf "%-25s %-15s %-15s\n" "Build time (s)" "$(cat "$OUT_DIR/dev/build_time_s")" "$(cat "$OUT_DIR/branch/build_time_s")"
+printf "%-25s %-15s %-15s\n" ".next size" "$(cat "$OUT_DIR/dev/next_size")" "$(cat "$OUT_DIR/branch/next_size")"
+printf "%-25s %-15s %-15s\n" "Standalone size" "$(cat "$OUT_DIR/dev/standalone_size")" "$(cat "$OUT_DIR/branch/standalone_size")"
+printf "%-25s %-15s %-15s\n" "Prerendered HTML files" "$(cat "$OUT_DIR/dev/prerendered_count")" "$(cat "$OUT_DIR/branch/prerendered_count")"
+printf "%-25s %-15s %-15s\n" "Static routes" "$(cat "$OUT_DIR/dev/static_routes")" "$(cat "$OUT_DIR/branch/static_routes")"
+printf "%-25s %-15s %-15s\n" "Dynamic routes" "$(cat "$OUT_DIR/dev/dynamic_routes")" "$(cat "$OUT_DIR/branch/dynamic_routes")"
+echo "========================================"
+echo ""
+echo "Detailed logs in $OUT_DIR/{dev,branch}/build.log"
diff --git a/scripts/dump-dependencies.mts b/scripts/dump-dependencies.mts
new file mode 100644
index 000000000..9248338da
--- /dev/null
+++ b/scripts/dump-dependencies.mts
@@ -0,0 +1,49 @@
+import fsPromises from "fs/promises";
+import path from "path";
+import { fileURLToPath } from "url";
+import { glob } from "glob";
+import { parse as parseYaml } from "yaml";
+
+const currentDir = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(currentDir, "..");
+const outPath = path.resolve(repoRoot, "static-data", "dependencies.json");
+
+interface PackageJson {
+ dependencies?: Record;
+}
+
+const parseCatalogAsync = async (): Promise