From db53bdb468054ca43a824a104aa1279d64245b4c Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 24 Jun 2025 13:16:20 +0800 Subject: [PATCH] fix: ssr server move to esm (#3994) * chore: fix prettier error * refactor: migrate to ES modules and update global constants handling - Changed the global constants definition to use Object.assign for better clarity. - Replaced usage of `isDev` with `__DEV__` throughout the codebase for consistency. - Updated imports to use ES module syntax, including dynamic imports where necessary. - Removed the deprecated `env.ts` file and adjusted related logic in the build scripts. - Ensured compatibility with ES module format in various files, including Vite and API client configurations. Signed-off-by: Innei * refactor(api): migrate handler to ES module syntax - Updated the API handler to use ES module export syntax. - Removed commented-out configuration for Node.js runtime. Signed-off-by: Innei * feat(404): enhance 404 page with animations and improve accessibility - Replaced `motion` components with `m` shorthand for better readability. - Introduced animation controls for various elements on the 404 page to enhance user experience. - Updated the glitch text array for consistency. - Adjusted layout and styling for improved visual presentation. - Removed unused `framer-lazy-feature` file and updated `MotionProvider` to use `domMax` for performance optimization. Signed-off-by: Innei * feat(og): replace Folo text with SVG logo in OGCanvas component - Updated the OGCanvas component to replace the static "Folo" text with a new LogoText SVG component for improved branding. - Added the LogoText function that defines the SVG structure for the logo. Signed-off-by: Innei * refactor(tsdown): update font imports to use dynamic loading - Modified font imports in `tsdown.config.ts` to utilize dynamic loading with `then` for improved performance and error handling. - Introduced a no-operation function to handle the promise resolution of font imports. Signed-off-by: Innei * refactor(404): replace Jotai provider with RootProviders for improved context management - Updated the 404 component to use `RootProviders` instead of the Jotai store provider for better context handling. - Simplified the component structure while maintaining functionality. Signed-off-by: Innei --------- Signed-off-by: Innei --- apps/ssr/api/index.ts | 10 +- apps/ssr/client/components/common/404.tsx | 145 +++++++++++------- apps/ssr/global.ts | 10 +- apps/ssr/index.ts | 17 +- apps/ssr/package.json | 1 + apps/ssr/scripts/cleanup-vercel-build.ts | 11 +- apps/ssr/scripts/prepare-vercel-build.ts | 20 +-- apps/ssr/src/lib/api-client.ts | 5 +- apps/ssr/src/lib/dev-vite.ts | 4 +- apps/ssr/src/lib/env.ts | 1 - apps/ssr/src/lib/og/fonts.ts | 3 + apps/ssr/src/router/global.ts | 14 +- apps/ssr/src/router/og/__base.tsx | 35 ++++- apps/ssr/tsdown.config.ts | 13 +- apps/ssr/vite.config.mts | 5 +- .../components/src/common/MotionProvider.tsx | 5 +- .../components/src/framer-lazy-feature.ts | 1 - packages/internal/constants/package.json | 1 + 18 files changed, 182 insertions(+), 119 deletions(-) delete mode 100644 apps/ssr/src/lib/env.ts delete mode 100644 packages/internal/components/src/framer-lazy-feature.ts diff --git a/apps/ssr/api/index.ts b/apps/ssr/api/index.ts index ff2d9f11d..eaf5e8d32 100644 --- a/apps/ssr/api/index.ts +++ b/apps/ssr/api/index.ts @@ -1,10 +1,8 @@ -const { createApp } = require("../dist/server/index.js") +// @ts-ignore +// eslint-disable-next-line antfu/no-import-dist +import { createApp } from "../dist/server/index.js" -// export const config = { -// runtime: "nodejs", // this is a pre-requisite -// } - -module.exports = async function handler(req: any, res: any) { +export default async function handler(req: any, res: any) { const app = await createApp() await app.ready() app.server.emit("request", req, res) diff --git a/apps/ssr/client/components/common/404.tsx b/apps/ssr/client/components/common/404.tsx index 0c8676743..14d6c2bbf 100644 --- a/apps/ssr/client/components/common/404.tsx +++ b/apps/ssr/client/components/common/404.tsx @@ -1,30 +1,82 @@ import { Header } from "@client/components/layout/header" import { openInFollowApp } from "@client/lib/helper" -import { jotaiStore } from "@client/lib/store" +import { RootProviders } from "@client/providers/root-providers" import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.jsx" import { PoweredByFooter } from "@follow/components/common/PoweredByFooter.jsx" import { Button } from "@follow/components/ui/button/index.jsx" import { useSyncThemeWebApp, useTitle } from "@follow/hooks" -import { Provider } from "jotai" -import { m as motion } from "motion/react" +import { m, useAnimationControls } from "motion/react" import { Fragment, useEffect, useState } from "react" const NotFoundContent = () => { - const [isVisible, setIsVisible] = useState(false) const [glitchText, setGlitchText] = useState("404") const [isGlitching, setIsGlitching] = useState(false) + // Animation controls + const iconControls = useAnimationControls() + const messageControls = useAnimationControls() + const titleControls = useAnimationControls() + const descriptionControls = useAnimationControls() + const buttonsControls = useAnimationControls() + const helpControls = useAnimationControls() + useTitle("404 - Page Not Found") useSyncThemeWebApp() useEffect(() => { - setIsVisible(true) - }, []) + // Start all animations in parallel with their respective delays + iconControls.start({ + scale: 1, + rotate: 0, + transition: { + duration: 0.8, + type: "spring", + stiffness: 100, + delay: 0.2, + }, + }) + + messageControls.start({ + opacity: 1, + y: 0, + transition: { duration: 0.6, delay: 0.4 }, + }) + + titleControls.start({ + opacity: 1, + x: 0, + transition: { duration: 0.5, delay: 0.6 }, + }) + + descriptionControls.start({ + opacity: 1, + x: 0, + transition: { duration: 0.5, delay: 0.8 }, + }) + + buttonsControls.start({ + opacity: 1, + scale: 1, + transition: { duration: 0.5, delay: 1 }, + }) + + helpControls.start({ + opacity: 1, + transition: { duration: 0.5, delay: 1.2 }, + }) + }, [ + iconControls, + messageControls, + titleControls, + descriptionControls, + buttonsControls, + helpControls, + ]) useEffect(() => { if (!isGlitching) return - const glitchTexts = ["404", "40₄", "4Ø4", "404", "4◯4", "4○4", "4𝟘4"] + const glitchTexts = ["404", "40₄", "4Ø4", "404", "4◯4", "4○4", "4𝟘4"] const glitchInterval = setInterval(() => { const randomText = glitchTexts[Math.floor(Math.random() * glitchTexts.length)] @@ -95,21 +147,15 @@ const NotFoundContent = () => { }`}
-
+
{/* 404 Icon with animations */} - - { setIsGlitching(false) }} > - { transition={{ duration: 0.1 }} > {glitchText} - - - + + + {/* Error Message with stagger animation */} - - Page Not Found - - + Sorry, the page you are looking for doesn't exist or has been moved. Please check the URL or return to the homepage to continue browsing. - - + + {/* Action Buttons with hover effects */} - - + - + - + - - + + {/* Additional Help with fade in */} - +

If you believe this is an error, please submit a issue on{" "} - { style={{ display: "inline-block" }} > GitHub - +

-
+ {/* Floating particles effect */}
{Array.from({ length: 6 }).map((_, i) => ( - { export const NotFound = () => { return ( - + - + ) } diff --git a/apps/ssr/global.ts b/apps/ssr/global.ts index 52056c3b6..e3d3d94d4 100644 --- a/apps/ssr/global.ts +++ b/apps/ssr/global.ts @@ -1,5 +1,11 @@ -export const defineGlobalConstants = () => { +Object.assign(globalThis, { + APP_NAME: "Folo", +}) + +try { + void __DEV__ +} catch { Object.assign(globalThis, { - APP_NAME: "Folo", + __DEV__: process.env.NODE_ENV === "development", }) } diff --git a/apps/ssr/index.ts b/apps/ssr/index.ts index b14fce283..3b2e3becf 100644 --- a/apps/ssr/index.ts +++ b/apps/ssr/index.ts @@ -1,5 +1,8 @@ +import "./global" import "./src/lib/load-env" +import os from "node:os" + import middie from "@fastify/middie" import { fastifyRequestContext } from "@fastify/request-context" import { env } from "@follow/shared/env.ssr" @@ -8,16 +11,12 @@ import Fastify from "fastify" import { nanoid } from "nanoid" import { FetchError } from "ofetch" -import { isDev } from "~/lib/env" import { MetaError } from "~/meta-handler" import { staticRoute } from "~/router/static" -import { defineGlobalConstants } from "./global" import { globalRoute } from "./src/router/global" import { ogRoute } from "./src/router/og" -defineGlobalConstants() - const isVercel = process.env.VERCEL === "1" declare module "@fastify/request-context" { @@ -64,7 +63,7 @@ export const createApp = async () => { const finalHost = forwardedHost || host const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" - if (!isDev) req.requestContext.set("upstreamEnv", upstreamEnv) + if (!__DEV__) req.requestContext.set("upstreamEnv", upstreamEnv) if (upstreamEnv === "prod") { req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) } else { @@ -74,8 +73,8 @@ export const createApp = async () => { done() }) - if (isDev) { - const devVite = require("./src/lib/dev-vite") + if (__DEV__) { + const devVite = await import("./src/lib/dev-vite") await devVite.registerDevViteServer(app) } @@ -97,11 +96,11 @@ if (!isVercel) { } function getIPAddress() { - const interfaces = require("node:os").networkInterfaces() + const interfaces = os.networkInterfaces() for (const devName in interfaces) { const iface = interfaces[devName] - for (const alias of iface) { + for (const alias of iface || []) { if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) return alias.address } diff --git a/apps/ssr/package.json b/apps/ssr/package.json index 023a74725..9fd6111c3 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -1,5 +1,6 @@ { "name": "@follow/ssr", + "type": "module", "private": true, "scripts": { "build": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsdown && tsx scripts/cleanup-vercel-build.ts", diff --git a/apps/ssr/scripts/cleanup-vercel-build.ts b/apps/ssr/scripts/cleanup-vercel-build.ts index ecd6e75fc..9c0eb1113 100644 --- a/apps/ssr/scripts/cleanup-vercel-build.ts +++ b/apps/ssr/scripts/cleanup-vercel-build.ts @@ -1,10 +1,7 @@ -import { rmSync, writeFileSync } from "node:fs" -import { resolve } from "node:path" +import { rmSync } from "node:fs" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" +const __dirname = dirname(fileURLToPath(import.meta.url)) rmSync(resolve(__dirname, "../.generated"), { recursive: true, force: true }) // restore env file - -writeFileSync( - resolve(__dirname, "../src/lib/env.ts"), - `export const isDev = process.env.NODE_ENV === "development"`, -) diff --git a/apps/ssr/scripts/prepare-vercel-build.ts b/apps/ssr/scripts/prepare-vercel-build.ts index b9a34ec7a..f45eff263 100644 --- a/apps/ssr/scripts/prepare-vercel-build.ts +++ b/apps/ssr/scripts/prepare-vercel-build.ts @@ -1,6 +1,9 @@ import { mkdirSync } from "node:fs" import fs from "node:fs/promises" -import path from "node:path" +import path, { dirname } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) mkdirSync(path.join(__dirname, "../.generated"), { recursive: true }) @@ -12,21 +15,8 @@ async function generateIndexHtmlData() { ) } -async function replaceEnvFile() { - const envFile = await fs.readFile(path.join(__dirname, "../src/lib/env.ts"), "utf-8") - - await fs.writeFile( - path.join(__dirname, "../src/lib/env.ts"), - // For tree shaking - envFile.replace( - `export const isDev = process.env.NODE_ENV === "development"`, - `export const isDev = ${process.env.NODE_ENV === "development"}`, - ), - ) -} - async function main() { - await Promise.all([generateIndexHtmlData(), replaceEnvFile()]) + await generateIndexHtmlData() } main() diff --git a/apps/ssr/src/lib/api-client.ts b/apps/ssr/src/lib/api-client.ts index c5187f36a..db4472ab0 100644 --- a/apps/ssr/src/lib/api-client.ts +++ b/apps/ssr/src/lib/api-client.ts @@ -7,7 +7,6 @@ import { hc } from "hono/client" import { ofetch } from "ofetch" import PKG from "../../../desktop/package.json" -import { isDev } from "./env" const getBaseURL = () => { const req = requestContext.get("req")! @@ -34,7 +33,7 @@ export const createApiFetch = () => { credentials: "include", retry: false, onRequest(context) { - if (isDev) console.info(`request: ${context.request}`) + if (__DEV__) console.info(`request: ${context.request}`) context.options.headers.set("User-Agent", `Folo External Server Api Client/${PKG.version}`) }, @@ -57,7 +56,7 @@ export const createApiClient = () => { headers() { return { "X-App-Version": PKG.version, - "X-App-Dev": isDev ? "1" : "0", + "X-App-Dev": __DEV__ ? "1" : "0", "User-Agent": `Folo External Server Api Client/${PKG.version}`, Cookie: authSessionToken ? `__Secure-better-auth.session_token=${authSessionToken}` : "", } diff --git a/apps/ssr/src/lib/dev-vite.ts b/apps/ssr/src/lib/dev-vite.ts index 84fa90c83..b961318bd 100644 --- a/apps/ssr/src/lib/dev-vite.ts +++ b/apps/ssr/src/lib/dev-vite.ts @@ -1,8 +1,10 @@ -import { resolve } from "node:path" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import type { FastifyInstance } from "fastify" import type { ViteDevServer } from "vite" +const __dirname = dirname(fileURLToPath(import.meta.url)) const root = resolve(__dirname, "../..") let globalVite: ViteDevServer diff --git a/apps/ssr/src/lib/env.ts b/apps/ssr/src/lib/env.ts deleted file mode 100644 index 2dd9d4a4b..000000000 --- a/apps/ssr/src/lib/env.ts +++ /dev/null @@ -1 +0,0 @@ -export const isDev = process.env.NODE_ENV === "development" diff --git a/apps/ssr/src/lib/og/fonts.ts b/apps/ssr/src/lib/og/fonts.ts index 356eb94a4..4b5440fba 100644 --- a/apps/ssr/src/lib/og/fonts.ts +++ b/apps/ssr/src/lib/og/fonts.ts @@ -1,6 +1,9 @@ import fs from "node:fs" +import { createRequire } from "node:module" import path, { resolve } from "node:path" +const require = createRequire(import.meta.url) + const weights = [ { name: "Thin", diff --git a/apps/ssr/src/router/global.ts b/apps/ssr/src/router/global.ts index dfd0f86de..f93131db3 100644 --- a/apps/ssr/src/router/global.ts +++ b/apps/ssr/src/router/global.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs" -import path, { resolve } from "node:path" +import path, { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import { env } from "@follow/shared/env.ssr" import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" @@ -8,7 +9,6 @@ import { parseHTML } from "linkedom" import { FetchError } from "ofetch" import xss from "xss" -import { isDev } from "~/lib/env" import { NotFoundError } from "~/lib/not-found" import { buildSeoMetaTags } from "~/lib/seo" @@ -17,10 +17,11 @@ import { injectMetaHandler, MetaError } from "../meta-handler" const devHandler = (app: FastifyInstance) => { app.get("*", async (req, reply) => { const url = req.originalUrl + const __dirname = dirname(fileURLToPath(import.meta.url)) const root = resolve(__dirname, "../..") - const vite = require("../lib/dev-vite").getViteServer() + const vite = await import("../lib/dev-vite").then((m) => m.getViteServer()) try { let template = readFileSync(path.resolve(root, vite.config.root, "index.html"), "utf-8") template = await vite.transformIndexHtml(url, template) @@ -30,14 +31,15 @@ const devHandler = (app: FastifyInstance) => { reply.type("text/html") reply.send(document.toString()) } catch (e) { - vite.ssrFixStacktrace(e) + vite.ssrFixStacktrace(e as Error) reply.code(500).send(e) } }) } const prodHandler = (app: FastifyInstance) => { app.get("*", async (req, reply) => { - const template = require("../../.generated/index.template").default + // @ts-expect-error + const template = await import("../../.generated/index.template").then((m) => m.default) const { document } = parseHTML(template) await safeInjectMetaToTemplate(document, req, reply) @@ -91,7 +93,7 @@ injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_ ) }) } -export const globalRoute = isDev ? devHandler : prodHandler +export const globalRoute = __DEV__ ? devHandler : prodHandler async function safeInjectMetaToTemplate( document: Document, diff --git a/apps/ssr/src/router/og/__base.tsx b/apps/ssr/src/router/og/__base.tsx index c91d7f53c..ee58debb6 100644 --- a/apps/ssr/src/router/og/__base.tsx +++ b/apps/ssr/src/router/og/__base.tsx @@ -69,11 +69,7 @@ export const OGCanvas = ({ children, seed }: { children: React.ReactNode; seed: {/* Follow Logo */}
- - Folo - +
{/* AI RSS */} @@ -132,6 +128,35 @@ function FollowIcon() { ) } +function LogoText() { + return ( + + + + + + + ) +} + export async function getImageBase64(image: string | null | undefined) { if (!image) { return null diff --git a/apps/ssr/tsdown.config.ts b/apps/ssr/tsdown.config.ts index d641ac0a8..c110d7434 100644 --- a/apps/ssr/tsdown.config.ts +++ b/apps/ssr/tsdown.config.ts @@ -9,12 +9,14 @@ export default defineConfig({ outDir: "dist/server", clean: true, - format: ["cjs"], + format: ["esm"], external: ["lightningcss", "vite"], treeshake: true, + define: { __DEV__: JSON.stringify(process.env.NODE_ENV === "development"), }, + hooks(hooks) { hooks.hook("build:done", async () => { if (process.env.VERCEL !== "1") return @@ -24,10 +26,11 @@ export default defineConfig({ try { const insertCode = `try { -require.resolve("@fontsource/sn-pro") -require.resolve('kose-font') -require.resolve('kose-font/fonts/KosefontP-JP.ttf') -require.resolve('kose-font/fonts/Kosefont-JP.ttf') +const noop = () => {} +import("@fontsource/sn-pro").then(noop) +import('kose-font').then(noop) +import('kose-font/fonts/KosefontP-JP.ttf').then(noop) +import('kose-font/fonts/Kosefont-JP.ttf').then(noop) ${(() => { const require = createRequire(import.meta.url) const fontDepsPath = require.resolve("@fontsource/sn-pro") diff --git a/apps/ssr/vite.config.mts b/apps/ssr/vite.config.mts index 2a13089b6..0373224bd 100644 --- a/apps/ssr/vite.config.mts +++ b/apps/ssr/vite.config.mts @@ -1,4 +1,5 @@ -import { resolve } from "node:path" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import react from "@vitejs/plugin-react" import { codeInspectorPlugin } from "code-inspector-plugin" @@ -13,6 +14,8 @@ const routeBuilderPluginV2 = await tsImport( import.meta.url, ).then((m) => m.default) +const __dirname = dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ resolve: { alias: { diff --git a/packages/internal/components/src/common/MotionProvider.tsx b/packages/internal/components/src/common/MotionProvider.tsx index 898c9a31a..d2fb812d6 100644 --- a/packages/internal/components/src/common/MotionProvider.tsx +++ b/packages/internal/components/src/common/MotionProvider.tsx @@ -1,9 +1,8 @@ -import { LazyMotion, MotionConfig } from "motion/react" +import { domMax, LazyMotion, MotionConfig } from "motion/react" -const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default) export const MotionProvider = ({ children }: { children: React.ReactNode }) => { return ( - +