[WEB-5386] refactor: update all apps to use react-router for development and enable SSR for space app. (#8095)

This commit is contained in:
Prateek Shourya 2025-11-11 14:08:42 +05:30 committed by GitHub
parent 4ae0763d0f
commit 433b5a4fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 358 additions and 1109 deletions

View file

@ -5,9 +5,9 @@
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=3000 node server.mjs",
"dev": "react-router dev --port 3000",
"build": "react-router build",
"preview": "react-router build && cross-env NODE_ENV=production PORT=3000 node server.mjs",
"preview": "react-router build && serve -s build/client -l 3000",
"start": "serve -s build/client -l 3000",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "eslint . --max-warnings 821",
@ -35,28 +35,22 @@
"@plane/utils": "workspace:*",
"@popperjs/core": "^2.11.8",
"@react-pdf/renderer": "^3.4.5",
"@react-router/express": "^7.9.3",
"@react-router/node": "^7.9.3",
"@tanstack/react-table": "^8.21.3",
"axios": "catalog:",
"clsx": "^2.0.0",
"cmdk": "^1.0.0",
"comlink": "^4.4.1",
"compression": "^1.8.1",
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"emoji-picker-react": "^4.5.16",
"export-to-csv": "^1.4.0",
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.5",
"isbot": "^5.1.31",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"mobx-react": "catalog:",
"mobx-utils": "catalog:",
"mobx": "catalog:",
"morgan": "^1.10.1",
"next-themes": "^0.2.1",
"posthog-js": "^1.131.3",
"react-color": "^2.19.3",
@ -85,10 +79,7 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@react-router/dev": "^7.9.1",
"@types/compression": "^1.8.1",
"@types/express": "4.17.23",
"@types/lodash-es": "catalog:",
"@types/morgan": "^1.9.10",
"@types/node": "catalog:",
"@types/react-color": "^3.0.6",
"@types/react-dom": "catalog:",

View file

@ -2,7 +2,6 @@ import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "app",
basename: process.env.NEXT_PUBLIC_WEB_BASE_PATH,
// Web runs as a client-side app; build a static client bundle only
ssr: false,
} satisfies Config;

View file

@ -1,77 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import compression from "compression";
import dotenv from "dotenv";
import express from "express";
import morgan from "morgan";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV !== "production";
// Derive the port from NEXT_PUBLIC_WEB_BASE_URL when available, otherwise
// default to http://localhost:3000 and fall back to PORT env if explicitly set.
const DEFAULT_BASE_URL = "http://localhost:3000";
const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || DEFAULT_BASE_URL;
let parsedBaseUrl;
try {
parsedBaseUrl = new URL(WEB_BASE_URL);
} catch {
parsedBaseUrl = new URL(DEFAULT_BASE_URL);
}
const PORT = Number.parseInt(parsedBaseUrl.port, 10);
async function start() {
const app = express();
app.use(compression());
app.disable("x-powered-by");
if (DEVELOPMENT) {
console.log("Starting development server");
const vite = await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
appType: "custom",
})
);
app.use(vite.middlewares);
app.use(async (req, res, next) => {
try {
const source = await vite.ssrLoadModule("./server/app.ts");
return source.app(req, res, next);
} catch (error) {
if (error instanceof Error) {
vite.ssrFixStacktrace(error);
}
next(error);
}
});
} else {
console.log("Starting production server");
app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.use(express.static("build/client", { maxAge: "1h" }));
app.use(await import(BUILD_PATH).then((mod) => mod.app));
}
app.listen(PORT, () => {
const origin = `${parsedBaseUrl.protocol}//${parsedBaseUrl.hostname}:${PORT}`;
console.log(`Server is running on ${origin}`);
});
}
start().catch((error) => {
console.error(error);
process.exit(1);
});

View file

@ -1,47 +0,0 @@
import "react-router";
import { createRequestHandler } from "@react-router/express";
import express from "express";
import type { Express } from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import type { ServerBuild } from "react-router";
const NEXT_PUBLIC_API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL.replace(/\/$/, "")
: "http://127.0.0.1:8000";
const NEXT_PUBLIC_API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH
? process.env.NEXT_PUBLIC_API_BASE_PATH.replace(/\/+$/, "")
: "/api";
const NORMALIZED_API_BASE_PATH = NEXT_PUBLIC_API_BASE_PATH.startsWith("/")
? NEXT_PUBLIC_API_BASE_PATH
: `/${NEXT_PUBLIC_API_BASE_PATH}`;
const NEXT_PUBLIC_WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH
? process.env.NEXT_PUBLIC_WEB_BASE_PATH.replace(/\/$/, "")
: "/";
export const app: Express = express();
// Ensure proxy-aware hostname/URL handling (e.g., X-Forwarded-Host/Proto)
// so generated URLs/redirects reflect the public host when behind Nginx.
// See related fix in Remix Express adapter.
app.set("trust proxy", true);
app.use(
"/api",
createProxyMiddleware({
target: NEXT_PUBLIC_API_BASE_URL,
changeOrigin: true,
secure: false,
pathRewrite: (path: string) =>
NORMALIZED_API_BASE_PATH === "/api" ? path : path.replace(/^\/api/, NORMALIZED_API_BASE_PATH),
})
);
const router = express.Router();
router.use(
createRequestHandler({
build: () => import("virtual:react-router/server-build") as Promise<ServerBuild>,
})
);
app.use(NEXT_PUBLIC_WEB_BASE_PATH, router);

View file

@ -1,67 +1,36 @@
import path from "node:path";
import { reactRouter } from "@react-router/dev/vite";
import dotenv from "dotenv";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
const PUBLIC_ENV_KEYS = [
"ENABLE_EXPERIMENTAL_COREPACK",
"NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_API_BASE_PATH",
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_LIVE_BASE_PATH",
"NEXT_PUBLIC_LIVE_BASE_URL",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SUPPORT_EMAIL",
"NEXT_PUBLIC_WEB_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_WEBSITE_URL",
"NODE_ENV",
];
dotenv.config({ path: path.resolve(__dirname, ".env") });
const publicEnv = PUBLIC_ENV_KEYS.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
// Automatically expose all environment variables prefixed with NEXT_PUBLIC_
const publicEnv = Object.keys(process.env)
.filter((key) => key.startsWith("NEXT_PUBLIC_"))
.reduce<Record<string, string>>((acc, key) => {
acc[key] = process.env[key] ?? "";
return acc;
}, {});
export default defineConfig(({ isSsrBuild }) => {
// Only produce an SSR bundle when explicitly enabled.
// For static deployments (default), we skip the server build entirely.
const enableSsrBuild = process.env.WEB_ENABLE_SSR_BUILD === "true";
return {
define: {
"process.env": JSON.stringify(publicEnv),
export default defineConfig(() => ({
define: {
"process.env": JSON.stringify(publicEnv),
},
build: {
assetsInlineLimit: 0,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within web
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
"next/script": path.resolve(__dirname, "app/compat/next/script.tsx"),
},
build: {
assetsInlineLimit: 0,
rollupOptions:
isSsrBuild && enableSsrBuild
? {
input: path.resolve(__dirname, "server/app.ts"),
}
: undefined,
},
plugins: [reactRouter(), tsconfigPaths({ projects: [path.resolve(__dirname, "tsconfig.json")] })],
resolve: {
alias: {
// Next.js compatibility shims used within web
"next/image": path.resolve(__dirname, "app/compat/next/image.tsx"),
"next/link": path.resolve(__dirname, "app/compat/next/link.tsx"),
"next/navigation": path.resolve(__dirname, "app/compat/next/navigation.ts"),
"next/script": path.resolve(__dirname, "app/compat/next/script.tsx"),
},
dedupe: ["react", "react-dom", "@headlessui/react"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
};
});
dedupe: ["react", "react-dom", "@headlessui/react"],
},
// No SSR-specific overrides needed; alias resolves to ESM build
}));