From 8388e8c17ba8adb812eed59dd8116a9ba2859f16 Mon Sep 17 00:00:00 2001 From: rishipandey2 Date: Thu, 14 May 2026 12:15:27 +0530 Subject: [PATCH 1/3] C:/Program Files/Git/god-mode without trailing slash renders a blank/loading page #9068 --- apps/admin/react-router.config.ts | 5 +++-- apps/admin/vite.config.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/admin/react-router.config.ts b/apps/admin/react-router.config.ts index a4cef08832d..9710456e5f4 100644 --- a/apps/admin/react-router.config.ts +++ b/apps/admin/react-router.config.ts @@ -1,7 +1,8 @@ import type { Config } from "@react-router/dev/config"; -import { joinUrlPath } from "@plane/utils"; -const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/"; +const rawBasePath = process.env.VITE_ADMIN_BASE_PATH ?? ""; + +const basePath = rawBasePath === "/" || rawBasePath === "" ? "/" : rawBasePath.replace(/\/+$/, ""); export default { appDirectory: "app", diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index f61d9b49eb5..e33078b6085 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -3,7 +3,6 @@ import * as dotenv from "dotenv"; import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import { joinUrlPath } from "@plane/utils"; dotenv.config({ path: path.resolve(__dirname, ".env") }); @@ -15,7 +14,9 @@ const viteEnv = Object.keys(process.env) return a; }, {}); -const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/"; +const rawBasePath = process.env.VITE_ADMIN_BASE_PATH ?? ""; + +const basePath = rawBasePath === "/" || rawBasePath === "" ? "/" : rawBasePath.replace(/\/+$/, ""); export default defineConfig(() => ({ base: basePath, From 5a40abf88b2fd2d2a6739e51134bc1129640b4b9 Mon Sep 17 00:00:00 2001 From: rishipandey2 Date: Thu, 14 May 2026 13:12:36 +0530 Subject: [PATCH 2/3] Fix: Add normalizeBasePath utility and update imports --- apps/admin/react-router.config.ts | 5 ++--- apps/admin/vite.config.ts | 5 ++--- packages/utils/src/string.ts | 24 +++++++++++++++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/admin/react-router.config.ts b/apps/admin/react-router.config.ts index 9710456e5f4..89fb51ac059 100644 --- a/apps/admin/react-router.config.ts +++ b/apps/admin/react-router.config.ts @@ -1,8 +1,7 @@ import type { Config } from "@react-router/dev/config"; +import { normalizeBasePath } from "@plane/utils"; -const rawBasePath = process.env.VITE_ADMIN_BASE_PATH ?? ""; - -const basePath = rawBasePath === "/" || rawBasePath === "" ? "/" : rawBasePath.replace(/\/+$/, ""); +const basePath = normalizeBasePath(process.env.VITE_ADMIN_BASE_PATH ?? ""); export default { appDirectory: "app", diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index e33078b6085..2526ed92a38 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -3,6 +3,7 @@ import * as dotenv from "dotenv"; import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import { normalizeBasePath } from "@plane/utils"; dotenv.config({ path: path.resolve(__dirname, ".env") }); @@ -14,9 +15,7 @@ const viteEnv = Object.keys(process.env) return a; }, {}); -const rawBasePath = process.env.VITE_ADMIN_BASE_PATH ?? ""; - -const basePath = rawBasePath === "/" || rawBasePath === "" ? "/" : rawBasePath.replace(/\/+$/, ""); +const basePath = normalizeBasePath(process.env.VITE_ADMIN_BASE_PATH ?? ""); export default defineConfig(() => ({ base: basePath, diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 4d2f112e33e..829298b23e4 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -56,7 +56,7 @@ export const truncateText = (str: string, length: number) => { export const createSimilarString = (str: string) => { const shuffled = str .split("") - .sort(() => Math.random() - 0.5) + .toSorted(() => Math.random() - 0.5) .join(""); return shuffled; @@ -153,7 +153,7 @@ export const checkEmailValidity = (email: string): boolean => { if (!email) return false; const isEmailValid = - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( email ); @@ -236,7 +236,7 @@ export const isCommentEmpty = (comment: Content | undefined): boolean => { // Handle JSONContent[] (array) if (Array.isArray(comment)) { - return comment.length === 0 || comment.every(isJSONContentEmpty); + return comment.every(isJSONContentEmpty); } // Handle JSONContent (object) @@ -431,3 +431,21 @@ export const joinUrlPath = (...segments: string[]): string => { return pathParts.length > 0 ? `/${pathParts.join("/")}` : ""; } }; + +export const normalizeBasePath = (rawBasePath: string): string => { + const trimmed = rawBasePath.trim(); + + // Empty or slash-only paths become root + if (trimmed === "" || /^\/+$/.test(trimmed)) { + return "/"; + } + + // Collapse multiple slashes + const normalized = trimmed.replace(/\/{2,}/g, "/"); + + // Remove trailing slashes except root + const withoutTrailingSlashes = normalized.replace(/\/+$/, ""); + + // Ensure leading slash + return withoutTrailingSlashes.startsWith("/") ? withoutTrailingSlashes : `/${withoutTrailingSlashes}`; +}; From 3de5608aed2eaf26790a46cb378c22404ae3501f Mon Sep 17 00:00:00 2001 From: rishipandey2 Date: Thu, 14 May 2026 13:41:03 +0530 Subject: [PATCH 3/3] docs: add JSDoc for normalizeBasePath --- packages/utils/src/string.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 829298b23e4..891da8b915f 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -432,6 +432,22 @@ export const joinUrlPath = (...segments: string[]): string => { } }; +/** + * @description Normalizes a base path by: + * - converting empty or slash-only values to "/" + * - collapsing duplicate slashes + * - removing trailing slashes except for root + * - ensuring the path starts with a leading slash + * + * @param {string} rawBasePath - Raw base path to normalize + * @returns {string} Normalized base path + * + * @example + * normalizeBasePath("") // returns "/" + * normalizeBasePath("/") // returns "/" + * normalizeBasePath("///api//v1/") // returns "/api/v1" + * normalizeBasePath("api/v1") // returns "/api/v1" + */ export const normalizeBasePath = (rawBasePath: string): string => { const trimmed = rawBasePath.trim();