diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 7af24b7ddb..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22.11.0 diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 0000000000..5767036af0 --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +22.21.1 diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index e5c9954626..095ed3f696 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -29,6 +29,7 @@ import { CheckModal } from '@/hooks/use-pay' import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' +import { isServer } from '@/utils/client' import AppCard from './app-card' import { AppCardSkeleton } from './app-card-skeleton' import Empty from './empty' @@ -71,7 +72,7 @@ const List = () => { // 1) Normalize legacy/incorrect query params like ?mode=discover -> ?category=all useEffect(() => { // avoid running on server - if (typeof window === 'undefined') + if (isServer) return const mode = searchParams.get('mode') if (!mode) diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx index 95ba6d212d..fe7afc9e22 100644 --- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx @@ -11,6 +11,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo' import Tooltip from '@/app/components/base/tooltip' import { useGlobalPublicStore } from '@/context/global-public-context' import { cn } from '@/utils/classnames' +import { isClient } from '@/utils/client' import { useEmbeddedChatbotContext, } from '../context' @@ -40,7 +41,6 @@ const Header: FC = ({ allInputsHidden, } = useEmbeddedChatbotContext() - const isClient = typeof window !== 'undefined' const isIframe = isClient ? window.self !== window.top : false const [parentOrigin, setParentOrigin] = useState('') const [showToggleExpandButton, setShowToggleExpandButton] = useState(false) diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index f2795e7ec0..12a1c59e0e 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -13,6 +13,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -49,14 +50,14 @@ const FeaturedTools = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -64,7 +65,7 @@ const FeaturedTools = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 9ae6181a4f..01cb5d100f 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -12,6 +12,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -42,14 +43,14 @@ const FeaturedTriggers = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -57,7 +58,7 @@ const FeaturedTriggers = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index 3c62f488dc..e934f27fd1 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -11,6 +11,7 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid import Loading from '@/app/components/base/loading' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' import { useRAGRecommendedPlugins } from '@/service/use-tools' +import { isServer } from '@/utils/client' import { getMarketplaceUrl } from '@/utils/var' import List from './list' @@ -29,14 +30,14 @@ const RAGToolRecommendations = ({ }: RAGToolRecommendationsProps) => { const { t } = useTranslation() const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -44,7 +45,7 @@ const RAGToolRecommendations = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/context/hooks/use-trigger-events-limit-modal.ts b/web/context/hooks/use-trigger-events-limit-modal.ts index 403df58378..72342cd0d3 100644 --- a/web/context/hooks/use-trigger-events-limit-modal.ts +++ b/web/context/hooks/use-trigger-events-limit-modal.ts @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { NUM_INFINITE } from '@/app/components/billing/config' import { Plan } from '@/app/components/billing/type' import { IS_CLOUD_EDITION } from '@/config' +import { isServer } from '@/utils/client' export type TriggerEventsLimitModalPayload = { usage: number @@ -46,7 +47,7 @@ export const useTriggerEventsLimitModal = ({ useEffect(() => { if (!IS_CLOUD_EDITION) return - if (typeof window === 'undefined') + if (isServer) return if (!currentWorkspaceId) return diff --git a/web/context/query-client.tsx b/web/context/query-client.tsx index a72393490c..1cd64b168b 100644 --- a/web/context/query-client.tsx +++ b/web/context/query-client.tsx @@ -5,12 +5,13 @@ import type { FC, PropsWithChildren } from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import { TanStackDevtoolsLoader } from '@/app/components/devtools/tanstack/loader' +import { isServer } from '@/utils/client' import { makeQueryClient } from './query-client-server' let browserQueryClient: QueryClient | undefined function getQueryClient() { - if (typeof window === 'undefined') { + if (isServer) { return makeQueryClient() } if (!browserQueryClient) diff --git a/web/hooks/use-query-params.spec.tsx b/web/hooks/use-query-params.spec.tsx index b187471809..35e234881d 100644 --- a/web/hooks/use-query-params.spec.tsx +++ b/web/hooks/use-query-params.spec.tsx @@ -12,6 +12,13 @@ import { usePricingModal, } from './use-query-params' +// Mock isServer to allow runtime control in tests +const mockIsServer = vi.hoisted(() => ({ value: false })) +vi.mock('@/utils/client', () => ({ + get isServer() { return mockIsServer.value }, + get isClient() { return !mockIsServer.value }, +})) + const renderWithAdapter = (hook: () => T, searchParams = '') => { const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>() const wrapper = ({ children }: { children: ReactNode }) => ( @@ -428,6 +435,7 @@ describe('clearQueryParams', () => { afterEach(() => { vi.unstubAllGlobals() + mockIsServer.value = false }) it('should remove a single key when provided one key', () => { @@ -463,13 +471,13 @@ describe('clearQueryParams', () => { replaceSpy.mockRestore() }) - it('should no-op when window is undefined', () => { + it('should no-op when running on server', () => { // Arrange const replaceSpy = vi.spyOn(window.history, 'replaceState') - vi.stubGlobal('window', undefined) + mockIsServer.value = true // Act - expect(() => clearQueryParams('foo')).not.toThrow() + clearQueryParams('foo') // Assert expect(replaceSpy).not.toHaveBeenCalled() diff --git a/web/hooks/use-query-params.ts b/web/hooks/use-query-params.ts index 73798a4a4f..0749a1ffa5 100644 --- a/web/hooks/use-query-params.ts +++ b/web/hooks/use-query-params.ts @@ -21,6 +21,7 @@ import { } from 'nuqs' import { useCallback } from 'react' import { ACCOUNT_SETTING_MODAL_ACTION } from '@/app/components/header/account-setting/constants' +import { isServer } from '@/utils/client' /** * Modal State Query Parameters @@ -176,7 +177,7 @@ export function usePluginInstallation() { * clearQueryParams(['param1', 'param2']) */ export function clearQueryParams(keys: string | string[]) { - if (typeof window === 'undefined') + if (isServer) return const url = new URL(window.location.href) diff --git a/web/package.json b/web/package.json index 5b13f9c1f4..7537b942fb 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ } }, "engines": { - "node": ">=v22.11.0" + "node": ">=22.12.0" }, "browserslist": [ "last 1 Chrome version", diff --git a/web/utils/client.ts b/web/utils/client.ts new file mode 100644 index 0000000000..5c4532997b --- /dev/null +++ b/web/utils/client.ts @@ -0,0 +1,3 @@ +export const isServer = typeof window === 'undefined' + +export const isClient = typeof window !== 'undefined' diff --git a/web/utils/gtag.ts b/web/utils/gtag.ts index 5af51a6564..5f199f1fbc 100644 --- a/web/utils/gtag.ts +++ b/web/utils/gtag.ts @@ -1,3 +1,5 @@ +import { isServer } from '@/utils/client' + /** * Send Google Analytics event * @param eventName - event name @@ -7,7 +9,7 @@ export const sendGAEvent = ( eventName: string, eventParams?: GtagEventParams, ): void => { - if (typeof window === 'undefined' || typeof (window as any).gtag !== 'function') { + if (isServer || typeof (window as any).gtag !== 'function') { return } (window as any).gtag('event', eventName, eventParams)