From f355c8d595a010a4fb0d3f512e6b4fd0a271aeb0 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:55:11 +0800 Subject: [PATCH] refactor: type safe env, update to zod v4 (#32035) --- .../workflow-parallel-limit.test.tsx | 261 ------------------ .../field/input-type-select/types.tsx | 2 +- .../base/form/form-scenarios/base/utils.ts | 2 +- .../base/form/form-scenarios/demo/types.ts | 6 +- .../form/form-scenarios/input-field/utils.ts | 2 +- .../components/base/param-item/top-k-item.tsx | 8 +- .../base/with-input-validation/index.spec.tsx | 2 +- .../with-input-validation/index.stories.tsx | 8 +- .../create/step-two/components/inputs.tsx | 3 +- .../step-two/hooks/use-segmentation-state.ts | 6 +- .../process-documents/components.spec.tsx | 4 +- .../header/account-dropdown/index.tsx | 3 +- web/app/components/provider/serwist.tsx | 3 +- .../panel/input-field/editor/form/schema.ts | 2 +- web/app/components/sentry-initializer.tsx | 3 +- .../top-k-and-score-threshold.tsx | 8 +- .../components/workflow/nodes/llm/utils.ts | 2 +- .../workflow/variable-inspect/utils.tsx | 4 +- .../forgot-password/ForgotPasswordForm.tsx | 10 +- web/app/install/installForm.tsx | 18 +- web/app/layout.tsx | 37 +-- web/app/serwist/[path]/route.ts | 3 +- web/config/index.ts | 169 +++--------- web/context/app-context.tsx | 3 +- web/env.ts | 235 ++++++++++++++++ web/eslint-suppressions.json | 13 - web/next.config.ts | 20 +- web/package.json | 6 +- web/pnpm-lock.yaml | 178 +++++------- web/proxy.ts | 7 +- web/service/client.spec.ts | 2 +- web/types/feature.ts | 34 --- web/utils/var.ts | 3 +- web/utils/zod.spec.ts | 173 ------------ 34 files changed, 401 insertions(+), 839 deletions(-) delete mode 100644 web/__tests__/workflow-parallel-limit.test.tsx create mode 100644 web/env.ts delete mode 100644 web/utils/zod.spec.ts diff --git a/web/__tests__/workflow-parallel-limit.test.tsx b/web/__tests__/workflow-parallel-limit.test.tsx deleted file mode 100644 index ba3840ac3e..0000000000 --- a/web/__tests__/workflow-parallel-limit.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -/** - * MAX_PARALLEL_LIMIT Configuration Bug Test - * - * This test reproduces and verifies the fix for issue #23083: - * MAX_PARALLEL_LIMIT environment variable does not take effect in iteration panel - */ - -import { render, screen } from '@testing-library/react' -import * as React from 'react' - -// Mock environment variables before importing constants -const originalEnv = process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT - -// Test with different environment values -function setupEnvironment(value?: string) { - if (value) - process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = value - else - delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT - - // Clear module cache to force re-evaluation - vi.resetModules() -} - -function restoreEnvironment() { - if (originalEnv) - process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = originalEnv - else - delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT - - vi.resetModules() -} - -// Mock i18next with proper implementation -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => { - if (key.includes('MaxParallelismTitle')) - return 'Max Parallelism' - if (key.includes('MaxParallelismDesc')) - return 'Maximum number of parallel executions' - if (key.includes('parallelMode')) - return 'Parallel Mode' - if (key.includes('parallelPanelDesc')) - return 'Enable parallel execution' - if (key.includes('errorResponseMethod')) - return 'Error Response Method' - return key - }, - }), - initReactI18next: { - type: '3rdParty', - init: vi.fn(), - }, -})) - -// Mock i18next module completely to prevent initialization issues -vi.mock('i18next', () => ({ - use: vi.fn().mockReturnThis(), - init: vi.fn().mockReturnThis(), - t: vi.fn(key => key), - isInitialized: true, -})) - -// Mock the useConfig hook -vi.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({ - default: () => ({ - inputs: { - is_parallel: true, - parallel_nums: 5, - error_handle_mode: 'terminated', - }, - changeParallel: vi.fn(), - changeParallelNums: vi.fn(), - changeErrorHandleMode: vi.fn(), - }), -})) - -// Mock other components -vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({ - default: function MockVarReferencePicker() { - return
VarReferencePicker
- }, -})) - -vi.mock('@/app/components/workflow/nodes/_base/components/split', () => ({ - default: function MockSplit() { - return
Split
- }, -})) - -vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({ - default: function MockField({ title, children }: { title: string, children: React.ReactNode }) { - return ( -
- - {children} -
- ) - }, -})) - -const getParallelControls = () => ({ - numberInput: screen.getByRole('spinbutton'), - slider: screen.getByRole('slider'), -}) - -describe('MAX_PARALLEL_LIMIT Configuration Bug', () => { - const mockNodeData = { - id: 'test-iteration-node', - type: 'iteration' as const, - data: { - title: 'Test Iteration', - desc: 'Test iteration node', - iterator_selector: ['test'], - output_selector: ['output'], - is_parallel: true, - parallel_nums: 5, - error_handle_mode: 'terminated' as const, - }, - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - afterEach(() => { - restoreEnvironment() - }) - - afterAll(() => { - restoreEnvironment() - }) - - describe('Environment Variable Parsing', () => { - it('should parse MAX_PARALLEL_LIMIT from NEXT_PUBLIC_MAX_PARALLEL_LIMIT environment variable', async () => { - setupEnvironment('25') - const { MAX_PARALLEL_LIMIT } = await import('@/config') - expect(MAX_PARALLEL_LIMIT).toBe(25) - }) - - it('should fallback to default when environment variable is not set', async () => { - setupEnvironment() // No environment variable - const { MAX_PARALLEL_LIMIT } = await import('@/config') - expect(MAX_PARALLEL_LIMIT).toBe(10) - }) - - it('should handle invalid environment variable values', async () => { - setupEnvironment('invalid') - const { MAX_PARALLEL_LIMIT } = await import('@/config') - - // Should fall back to default when parsing fails - expect(MAX_PARALLEL_LIMIT).toBe(10) - }) - - it('should handle empty environment variable', async () => { - setupEnvironment('') - const { MAX_PARALLEL_LIMIT } = await import('@/config') - - // Should fall back to default when empty - expect(MAX_PARALLEL_LIMIT).toBe(10) - }) - - // Edge cases for boundary values - it('should clamp MAX_PARALLEL_LIMIT to MIN when env is 0 or negative', async () => { - setupEnvironment('0') - let { MAX_PARALLEL_LIMIT } = await import('@/config') - expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default - - setupEnvironment('-5') - ;({ MAX_PARALLEL_LIMIT } = await import('@/config')) - expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default - }) - - it('should handle float numbers by parseInt behavior', async () => { - setupEnvironment('12.7') - const { MAX_PARALLEL_LIMIT } = await import('@/config') - // parseInt truncates to integer - expect(MAX_PARALLEL_LIMIT).toBe(12) - }) - }) - - describe('UI Component Integration (Main Fix Verification)', () => { - it('should render iteration panel with environment-configured max value', async () => { - // Set environment variable to a different value - setupEnvironment('30') - - // Import Panel after setting environment - const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default) - const { MAX_PARALLEL_LIMIT } = await import('@/config') - - render( - , - ) - - // Behavior-focused assertion: UI max should equal MAX_PARALLEL_LIMIT - const { numberInput, slider } = getParallelControls() - expect(numberInput).toHaveAttribute('max', String(MAX_PARALLEL_LIMIT)) - expect(slider).toHaveAttribute('aria-valuemax', String(MAX_PARALLEL_LIMIT)) - - // Verify the actual values - expect(MAX_PARALLEL_LIMIT).toBe(30) - expect(numberInput.getAttribute('max')).toBe('30') - expect(slider.getAttribute('aria-valuemax')).toBe('30') - }) - - it('should maintain UI consistency with different environment values', async () => { - setupEnvironment('15') - const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default) - const { MAX_PARALLEL_LIMIT } = await import('@/config') - - render( - , - ) - - // Both input and slider should use the same max value from MAX_PARALLEL_LIMIT - const { numberInput, slider } = getParallelControls() - - expect(numberInput.getAttribute('max')).toBe(slider.getAttribute('aria-valuemax')) - expect(numberInput.getAttribute('max')).toBe(String(MAX_PARALLEL_LIMIT)) - }) - }) - - describe('Legacy Constant Verification (For Transition Period)', () => { - // Marked as transition/deprecation tests - it('should maintain MAX_ITERATION_PARALLEL_NUM for backward compatibility', async () => { - const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants') - expect(typeof MAX_ITERATION_PARALLEL_NUM).toBe('number') - expect(MAX_ITERATION_PARALLEL_NUM).toBe(10) // Hardcoded legacy value - }) - - it('should demonstrate MAX_PARALLEL_LIMIT vs legacy constant difference', async () => { - setupEnvironment('50') - const { MAX_PARALLEL_LIMIT } = await import('@/config') - const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants') - - // MAX_PARALLEL_LIMIT is configurable, MAX_ITERATION_PARALLEL_NUM is not - expect(MAX_PARALLEL_LIMIT).toBe(50) - expect(MAX_ITERATION_PARALLEL_NUM).toBe(10) - expect(MAX_PARALLEL_LIMIT).not.toBe(MAX_ITERATION_PARALLEL_NUM) - }) - }) - - describe('Constants Validation', () => { - it('should validate that required constants exist and have correct types', async () => { - const { MAX_PARALLEL_LIMIT } = await import('@/config') - const { MIN_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants') - expect(typeof MAX_PARALLEL_LIMIT).toBe('number') - expect(typeof MIN_ITERATION_PARALLEL_NUM).toBe('number') - expect(MAX_PARALLEL_LIMIT).toBeGreaterThanOrEqual(MIN_ITERATION_PARALLEL_NUM) - }) - }) -}) diff --git a/web/app/components/base/form/components/field/input-type-select/types.tsx b/web/app/components/base/form/components/field/input-type-select/types.tsx index abf4bbd2a7..6104ea26b2 100644 --- a/web/app/components/base/form/components/field/input-type-select/types.tsx +++ b/web/app/components/base/form/components/field/input-type-select/types.tsx @@ -1,5 +1,5 @@ import type { RemixiconComponentType } from '@remixicon/react' -import { z } from 'zod' +import * as z from 'zod' export const InputTypeEnum = z.enum([ 'text-input', diff --git a/web/app/components/base/form/form-scenarios/base/utils.ts b/web/app/components/base/form/form-scenarios/base/utils.ts index 2c617aa1c6..221d43e000 100644 --- a/web/app/components/base/form/form-scenarios/base/utils.ts +++ b/web/app/components/base/form/form-scenarios/base/utils.ts @@ -1,6 +1,6 @@ import type { ZodNumber, ZodSchema, ZodString } from 'zod' import type { BaseConfiguration } from './types' -import { z } from 'zod' +import * as z from 'zod' import { BaseFieldType } from './types' export const generateZodSchema = (fields: BaseConfiguration[]) => { diff --git a/web/app/components/base/form/form-scenarios/demo/types.ts b/web/app/components/base/form/form-scenarios/demo/types.ts index c4e626ef63..91ab1c7747 100644 --- a/web/app/components/base/form/form-scenarios/demo/types.ts +++ b/web/app/components/base/form/form-scenarios/demo/types.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import * as z from 'zod' const ContactMethod = z.union([ z.literal('email'), @@ -22,10 +22,10 @@ export const UserSchema = z.object({ .min(3, 'Surname must be at least 3 characters long') .regex(/^[A-Z]/, 'Surname must start with a capital letter'), isAcceptingTerms: z.boolean().refine(val => val, { - message: 'You must accept the terms and conditions', + error: 'You must accept the terms and conditions', }), contact: z.object({ - email: z.string().email('Invalid email address'), + email: z.email('Invalid email address'), phone: z.string().optional(), preferredContactMethod: ContactMethod, }), diff --git a/web/app/components/base/form/form-scenarios/input-field/utils.ts b/web/app/components/base/form/form-scenarios/input-field/utils.ts index cd670c448c..151d7979b8 100644 --- a/web/app/components/base/form/form-scenarios/input-field/utils.ts +++ b/web/app/components/base/form/form-scenarios/input-field/utils.ts @@ -1,6 +1,6 @@ import type { ZodSchema, ZodString } from 'zod' import type { InputFieldConfiguration } from './types' -import { z } from 'zod' +import * as z from 'zod' import { SupportedFileTypes, TransferMethod } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/schema' import { InputFieldType } from './types' diff --git a/web/app/components/base/param-item/top-k-item.tsx b/web/app/components/base/param-item/top-k-item.tsx index 2692875df1..9e9b7323db 100644 --- a/web/app/components/base/param-item/top-k-item.tsx +++ b/web/app/components/base/param-item/top-k-item.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { env } from '@/env' import ParamItem from '.' type Props = { @@ -11,12 +12,7 @@ type Props = { enable: boolean } -const maxTopK = (() => { - const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10) - if (configValue && !isNaN(configValue)) - return configValue - return 10 -})() +const maxTopK = env.NEXT_PUBLIC_TOP_K_MAX_VALUE const VALUE_LIMIT = { default: 2, step: 1, diff --git a/web/app/components/base/with-input-validation/index.spec.tsx b/web/app/components/base/with-input-validation/index.spec.tsx index daf3fd9a74..3bfcbfc9e4 100644 --- a/web/app/components/base/with-input-validation/index.spec.tsx +++ b/web/app/components/base/with-input-validation/index.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import { noop } from 'es-toolkit/function' -import { z } from 'zod' +import * as z from 'zod' import withValidation from '.' describe('withValidation HOC', () => { diff --git a/web/app/components/base/with-input-validation/index.stories.tsx b/web/app/components/base/with-input-validation/index.stories.tsx index cb06d45956..bd5230c68b 100644 --- a/web/app/components/base/with-input-validation/index.stories.tsx +++ b/web/app/components/base/with-input-validation/index.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { z } from 'zod' +import * as z from 'zod' import withValidation from '.' // Sample components to wrap with validation @@ -65,7 +65,7 @@ const ProductCard = ({ name, price, category, inStock }: ProductCardProps) => { // Create validated versions const userSchema = z.object({ name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email'), + email: z.email('Invalid email'), age: z.number().min(0).max(150), }) @@ -371,7 +371,7 @@ export const ConfigurationValidation: Story = { ) const configSchema = z.object({ - apiUrl: z.string().url('Must be valid URL'), + apiUrl: z.url('Must be valid URL'), timeout: z.number().min(0).max(30000), retries: z.number().min(0).max(5), debug: z.boolean(), @@ -430,7 +430,7 @@ export const UsageDocumentation: Story = {

Usage Example

-            {`import { z } from 'zod'
+            {`import * as z from 'zod'
 import withValidation from './withValidation'
 
 // Define your component
diff --git a/web/app/components/datasets/create/step-two/components/inputs.tsx b/web/app/components/datasets/create/step-two/components/inputs.tsx
index 4568431356..349796858e 100644
--- a/web/app/components/datasets/create/step-two/components/inputs.tsx
+++ b/web/app/components/datasets/create/step-two/components/inputs.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
 import Input from '@/app/components/base/input'
 import { InputNumber } from '@/app/components/base/input-number'
 import Tooltip from '@/app/components/base/tooltip'
+import { env } from '@/env'
 
 const TextLabel: FC = (props) => {
   return 
@@ -46,7 +47,7 @@ export const DelimiterInput: FC = (props) =>
 }
 
 export const MaxLengthInput: FC = (props) => {
-  const maxValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
+  const maxValue = env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
 
   const { t } = useTranslation()
   return (
diff --git a/web/app/components/datasets/create/step-two/hooks/use-segmentation-state.ts b/web/app/components/datasets/create/step-two/hooks/use-segmentation-state.ts
index 503704276e..abef8a98cb 100644
--- a/web/app/components/datasets/create/step-two/hooks/use-segmentation-state.ts
+++ b/web/app/components/datasets/create/step-two/hooks/use-segmentation-state.ts
@@ -1,5 +1,6 @@
 import type { ParentMode, PreProcessingRule, ProcessRule, Rules, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
 import { useCallback, useRef, useState } from 'react'
+import { env } from '@/env'
 import { ChunkingMode, ProcessMode } from '@/models/datasets'
 import escape from './escape'
 import unescape from './unescape'
@@ -8,10 +9,7 @@ import unescape from './unescape'
 export const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
 export const DEFAULT_MAXIMUM_CHUNK_LENGTH = 1024
 export const DEFAULT_OVERLAP = 50
-export const MAXIMUM_CHUNK_TOKEN_LENGTH = Number.parseInt(
-  globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000',
-  10,
-)
+export const MAXIMUM_CHUNK_TOKEN_LENGTH = env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
 
 export type ParentChildConfig = {
   chunkForContext: ParentMode
diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/components.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/process-documents/components.spec.tsx
index 322e6edd49..6f47575b27 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/components.spec.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/components.spec.tsx
@@ -1,7 +1,7 @@
 import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
 import { fireEvent, render, screen, waitFor } from '@testing-library/react'
 import * as React from 'react'
-import { z } from 'zod'
+import * as z from 'zod'
 import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
 import Toast from '@/app/components/base/toast'
 import Actions from './actions'
@@ -53,7 +53,7 @@ const createFailingSchema = () => {
         issues: [{ path: ['field1'], message: 'is required' }],
       },
     }),
-  } as unknown as z.ZodSchema
+  } as unknown as z.ZodType
 }
 
 // ==========================================
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx
index 07dd0fca3d..983f9e434d 100644
--- a/web/app/components/header/account-dropdown/index.tsx
+++ b/web/app/components/header/account-dropdown/index.tsx
@@ -28,6 +28,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
 import { useDocLink } from '@/context/i18n'
 import { useModalContext } from '@/context/modal-context'
 import { useProviderContext } from '@/context/provider-context'
+import { env } from '@/env'
 import { useLogout } from '@/service/use-common'
 import { cn } from '@/utils/classnames'
 import AccountAbout from '../account-about'
@@ -178,7 +179,7 @@ export default function AppSelector() {
                           
                         
                         {
-                          document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
+                          env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
                             
                               
{children} } - const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + const basePath = env.NEXT_PUBLIC_BASE_PATH const swUrl = `${basePath}/serwist/sw.js` return ( diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/schema.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/schema.ts index 7433111466..056f399a70 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/schema.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/schema.ts @@ -1,6 +1,6 @@ import type { TFunction } from 'i18next' import type { SchemaOptions } from './types' -import { z } from 'zod' +import * as z from 'zod' import { InputTypeEnum } from '@/app/components/base/form/components/field/input-type-select/types' import { MAX_VAR_KEY_LENGTH } from '@/config' import { PipelineInputVarType } from '@/models/pipeline' diff --git a/web/app/components/sentry-initializer.tsx b/web/app/components/sentry-initializer.tsx index ee161647e3..8a7286f908 100644 --- a/web/app/components/sentry-initializer.tsx +++ b/web/app/components/sentry-initializer.tsx @@ -4,12 +4,13 @@ import * as Sentry from '@sentry/react' import { useEffect } from 'react' import { IS_DEV } from '@/config' +import { env } from '@/env' const SentryInitializer = ({ children, }: { children: React.ReactElement }) => { useEffect(() => { - const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn') + const SENTRY_DSN = env.NEXT_PUBLIC_SENTRY_DSN if (!IS_DEV && SENTRY_DSN) { Sentry.init({ dsn: SENTRY_DSN, diff --git a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx index d00ace49bf..bf3b8297c3 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { InputNumber } from '@/app/components/base/input-number' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' +import { env } from '@/env' export type TopKAndScoreThresholdProps = { topK: number @@ -15,12 +16,7 @@ export type TopKAndScoreThresholdProps = { hiddenScoreThreshold?: boolean } -const maxTopK = (() => { - const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10) - if (configValue && !isNaN(configValue)) - return configValue - return 10 -})() +const maxTopK = env.NEXT_PUBLIC_TOP_K_MAX_VALUE const TOP_K_VALUE_LIMIT = { amount: 1, min: 1, diff --git a/web/app/components/workflow/nodes/llm/utils.ts b/web/app/components/workflow/nodes/llm/utils.ts index 604a1f8408..31b942ee64 100644 --- a/web/app/components/workflow/nodes/llm/utils.ts +++ b/web/app/components/workflow/nodes/llm/utils.ts @@ -1,6 +1,6 @@ import type { ValidationError } from 'jsonschema' import type { ArrayItems, Field, LLMNodeType } from './types' -import { z } from 'zod' +import * as z from 'zod' import { draft07Validator, forbidBooleanProperties } from '@/utils/validators' import { ArrayType, Type } from './types' diff --git a/web/app/components/workflow/variable-inspect/utils.tsx b/web/app/components/workflow/variable-inspect/utils.tsx index 482ed46c68..16f06d1bb0 100644 --- a/web/app/components/workflow/variable-inspect/utils.tsx +++ b/web/app/components/workflow/variable-inspect/utils.tsx @@ -1,4 +1,4 @@ -import { z } from 'zod' +import * as z from 'zod' const arrayStringSchemaParttern = z.array(z.string()) const arrayNumberSchemaParttern = z.array(z.number()) @@ -7,7 +7,7 @@ const arrayNumberSchemaParttern = z.array(z.number()) const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) type Literal = z.infer type Json = Literal | { [key: string]: Json } | Json[] -const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) +const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(z.string(), jsonSchema)])) const arrayJsonSchema: z.ZodType = z.lazy(() => z.array(jsonSchema)) export const validateJSONSchema = (schema: any, type: string) => { diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index ff33cccc82..274c2fd4e6 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { z } from 'zod' +import * as z from 'zod' import Button from '@/app/components/base/button' import { formContext, useAppForm } from '@/app/components/base/form' import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator' @@ -22,10 +22,10 @@ import Input from '../components/base/input' import Loading from '../components/base/loading' const accountFormSchema = z.object({ - email: z - .string() - .min(1, { message: 'error.emailInValid' }) - .email('error.emailInValid'), + email: z.email('error.emailInValid') + .min(1, { + error: 'error.emailInValid', + }), }) const ForgotPasswordForm = () => { diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 1cd5dce19a..47de6d1fb3 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { z } from 'zod' +import * as z from 'zod' import Button from '@/app/components/base/button' import { formContext, useAppForm } from '@/app/components/base/form' import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator' @@ -22,13 +22,15 @@ import { encryptPassword as encodePassword } from '@/utils/encryption' import Loading from '../components/base/loading' const accountFormSchema = z.object({ - email: z - .string() - .min(1, { message: 'error.emailInValid' }) - .email('error.emailInValid'), - name: z.string().min(1, { message: 'error.nameEmpty' }), + email: z.email('error.emailInValid') + .min(1, { + error: 'error.emailInValid', + }), + name: z.string().min(1, { + error: 'error.nameEmpty', + }), password: z.string().min(8, { - message: 'error.passwordLengthInValid', + error: 'error.passwordLengthInValid', }).regex(validPassword, 'error.passwordInvalid'), }) @@ -197,7 +199,7 @@ const InstallForm = () => {
0, + '!text-sm text-red-400': passwordErrors && passwordErrors.length > 0, })} > {t('error.passwordInvalid', { ns: 'login' })} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 845cae2d4e..a19d5e1e57 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -5,8 +5,8 @@ import { Instrument_Serif } from 'next/font/google' import { NuqsAdapter } from 'nuqs/adapters/next/app' import GlobalPublicStoreProvider from '@/context/global-public-context' import { TanstackQueryInitializer } from '@/context/query-client' +import { getDatasetMap } from '@/env' import { getLocaleOnServer } from '@/i18n-config/server' -import { DatasetAttr } from '@/types/feature' import { cn } from '@/utils/classnames' import { ToastProvider } from './components/base/toast' import BrowserInitializer from './components/browser-initializer' @@ -39,40 +39,7 @@ const LocaleLayout = async ({ children: React.ReactNode }) => { const locale = await getLocaleOnServer() - - const datasetMap: Record = { - [DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX, - [DatasetAttr.DATA_PUBLIC_API_PREFIX]: process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX, - [DatasetAttr.DATA_MARKETPLACE_API_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX, - [DatasetAttr.DATA_MARKETPLACE_URL_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX, - [DatasetAttr.DATA_PUBLIC_EDITION]: process.env.NEXT_PUBLIC_EDITION, - [DatasetAttr.DATA_PUBLIC_AMPLITUDE_API_KEY]: process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY, - [DatasetAttr.DATA_PUBLIC_COOKIE_DOMAIN]: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, - [DatasetAttr.DATA_PUBLIC_SUPPORT_MAIL_LOGIN]: process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN, - [DatasetAttr.DATA_PUBLIC_SENTRY_DSN]: process.env.NEXT_PUBLIC_SENTRY_DSN, - [DatasetAttr.DATA_PUBLIC_MAINTENANCE_NOTICE]: process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE, - [DatasetAttr.DATA_PUBLIC_SITE_ABOUT]: process.env.NEXT_PUBLIC_SITE_ABOUT, - [DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS]: process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, - [DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM]: process.env.NEXT_PUBLIC_MAX_TOOLS_NUM, - [DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT]: process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT, - [DatasetAttr.DATA_PUBLIC_TOP_K_MAX_VALUE]: process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE, - [DatasetAttr.DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH]: process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH, - [DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT]: process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT, - [DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM]: process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, - [DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH]: process.env.NEXT_PUBLIC_MAX_TREE_DEPTH, - [DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME]: process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, - [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER, - [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, - [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, - [DatasetAttr.DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX]: process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_WIDGET_KEY]: process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, - [DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, - [DatasetAttr.DATA_PUBLIC_BATCH_CONCURRENCY]: process.env.NEXT_PUBLIC_BATCH_CONCURRENCY, - } + const datasetMap = getDatasetMap() return ( diff --git a/web/app/serwist/[path]/route.ts b/web/app/serwist/[path]/route.ts index beca2cd412..aac0aad17d 100644 --- a/web/app/serwist/[path]/route.ts +++ b/web/app/serwist/[path]/route.ts @@ -1,6 +1,7 @@ import { createSerwistRoute } from '@serwist/turbopack' +import { env } from '@/env' -const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' +const basePath = env.NEXT_PUBLIC_BASE_PATH export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute({ swSrc: 'app/sw.ts', diff --git a/web/config/index.ts b/web/config/index.ts index c3a4c5c3b1..167c87ae34 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -1,101 +1,51 @@ import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' import { InputVarType } from '@/app/components/workflow/types' +import { env } from '@/env' import { PromptRole } from '@/models/debug' import { PipelineInputVarType } from '@/models/pipeline' import { AgentStrategy } from '@/types/app' -import { DatasetAttr } from '@/types/feature' import pkg from '../package.json' -const getBooleanConfig = ( - envVar: string | undefined, - dataAttrKey: DatasetAttr, - defaultValue: boolean = true, -) => { - if (envVar !== undefined && envVar !== '') - return envVar === 'true' - const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) - if (attrValue !== undefined && attrValue !== '') - return attrValue === 'true' - return defaultValue -} - -const getNumberConfig = ( - envVar: string | undefined, - dataAttrKey: DatasetAttr, - defaultValue: number, -) => { - if (envVar) { - const parsed = Number.parseInt(envVar) - if (!Number.isNaN(parsed) && parsed > 0) - return parsed - } - - const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) - if (attrValue) { - const parsed = Number.parseInt(attrValue) - if (!Number.isNaN(parsed) && parsed > 0) - return parsed - } - return defaultValue -} - const getStringConfig = ( envVar: string | undefined, - dataAttrKey: DatasetAttr, defaultValue: string, ) => { if (envVar) return envVar - - const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) - if (attrValue) - return attrValue return defaultValue } export const API_PREFIX = getStringConfig( - process.env.NEXT_PUBLIC_API_PREFIX, - DatasetAttr.DATA_API_PREFIX, + env.NEXT_PUBLIC_API_PREFIX, 'http://localhost:5001/console/api', ) export const PUBLIC_API_PREFIX = getStringConfig( - process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX, - DatasetAttr.DATA_PUBLIC_API_PREFIX, + env.NEXT_PUBLIC_PUBLIC_API_PREFIX, 'http://localhost:5001/api', ) export const MARKETPLACE_API_PREFIX = getStringConfig( - process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX, - DatasetAttr.DATA_MARKETPLACE_API_PREFIX, + env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX, 'http://localhost:5002/api', ) export const MARKETPLACE_URL_PREFIX = getStringConfig( - process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX, - DatasetAttr.DATA_MARKETPLACE_URL_PREFIX, + env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX, '', ) -const EDITION = getStringConfig( - process.env.NEXT_PUBLIC_EDITION, - DatasetAttr.DATA_PUBLIC_EDITION, - 'SELF_HOSTED', -) +const EDITION = env.NEXT_PUBLIC_EDITION export const IS_CE_EDITION = EDITION === 'SELF_HOSTED' export const IS_CLOUD_EDITION = EDITION === 'CLOUD' export const AMPLITUDE_API_KEY = getStringConfig( - process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY, - DatasetAttr.DATA_PUBLIC_AMPLITUDE_API_KEY, + env.NEXT_PUBLIC_AMPLITUDE_API_KEY, '', ) -export const IS_DEV = process.env.NODE_ENV === 'development' -export const IS_PROD = process.env.NODE_ENV === 'production' +export const IS_DEV = env.NODE_ENV === 'development' +export const IS_PROD = env.NODE_ENV === 'production' -export const SUPPORT_MAIL_LOGIN = !!( - process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN - || globalThis.document?.body?.getAttribute('data-public-support-mail-login') -) +export const SUPPORT_MAIL_LOGIN = env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN export const TONE_LIST = [ { @@ -161,16 +111,11 @@ export const getMaxToken = (modelId: string) => { export const LOCALE_COOKIE_NAME = 'locale' const COOKIE_DOMAIN = getStringConfig( - process.env.NEXT_PUBLIC_COOKIE_DOMAIN, - DatasetAttr.DATA_PUBLIC_COOKIE_DOMAIN, + env.NEXT_PUBLIC_COOKIE_DOMAIN, '', ).trim() -export const BATCH_CONCURRENCY = getNumberConfig( - process.env.NEXT_PUBLIC_BATCH_CONCURRENCY, - DatasetAttr.DATA_PUBLIC_BATCH_CONCURRENCY, - 5, // default -) +export const BATCH_CONCURRENCY = env.NEXT_PUBLIC_BATCH_CONCURRENCY export const CSRF_COOKIE_NAME = () => { if (COOKIE_DOMAIN) @@ -344,112 +289,62 @@ export const resetReg = () => (VAR_REGEX.lastIndex = 0) export const HITL_INPUT_REG = /\{\{(#\$output\.(?:[a-z_]\w{0,29}){1,10}#)\}\}/gi export const resetHITLInputReg = () => HITL_INPUT_REG.lastIndex = 0 -export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' +export const DISABLE_UPLOAD_IMAGE_AS_ICON = env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON export const GITHUB_ACCESS_TOKEN - = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || '' + = env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl' export const FULL_DOC_PREVIEW_LENGTH = 50 export const JSON_SCHEMA_MAX_DEPTH = 10 -export const MAX_TOOLS_NUM = getNumberConfig( - process.env.NEXT_PUBLIC_MAX_TOOLS_NUM, - DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM, - 10, -) -export const MAX_PARALLEL_LIMIT = getNumberConfig( - process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT, - DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT, - 10, -) -export const TEXT_GENERATION_TIMEOUT_MS = getNumberConfig( - process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, - DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, - 60000, -) -export const LOOP_NODE_MAX_COUNT = getNumberConfig( - process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT, - DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT, - 100, -) -export const MAX_ITERATIONS_NUM = getNumberConfig( - process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, - DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM, - 99, -) -export const MAX_TREE_DEPTH = getNumberConfig( - process.env.NEXT_PUBLIC_MAX_TREE_DEPTH, - DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH, - 50, -) +export const MAX_TOOLS_NUM = env.NEXT_PUBLIC_MAX_TOOLS_NUM +export const MAX_PARALLEL_LIMIT = env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT +export const TEXT_GENERATION_TIMEOUT_MS = env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS +export const LOOP_NODE_MAX_COUNT = env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT +export const MAX_ITERATIONS_NUM = env.NEXT_PUBLIC_MAX_ITERATIONS_NUM +export const MAX_TREE_DEPTH = env.NEXT_PUBLIC_MAX_TREE_DEPTH -export const ALLOW_UNSAFE_DATA_SCHEME = getBooleanConfig( - process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, - DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, - false, -) -export const ENABLE_WEBSITE_JINAREADER = getBooleanConfig( - process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER, - DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER, - true, -) -export const ENABLE_WEBSITE_FIRECRAWL = getBooleanConfig( - process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, - DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, - true, -) -export const ENABLE_WEBSITE_WATERCRAWL = getBooleanConfig( - process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, - DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, - false, -) -export const ENABLE_SINGLE_DOLLAR_LATEX = getBooleanConfig( - process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX, - DatasetAttr.DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX, - false, -) +export const ALLOW_UNSAFE_DATA_SCHEME = env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME +export const ENABLE_WEBSITE_JINAREADER = env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER +export const ENABLE_WEBSITE_FIRECRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL +export const ENABLE_WEBSITE_WATERCRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL +export const ENABLE_SINGLE_DOLLAR_LATEX = env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX export const VALUE_SELECTOR_DELIMITER = '@@@' export const validPassword = /^(?=.*[a-z])(?=.*\d)\S{8,}$/i export const ZENDESK_WIDGET_KEY = getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY, - DatasetAttr.NEXT_PUBLIC_ZENDESK_WIDGET_KEY, + env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY, '', ) export const ZENDESK_FIELD_IDS = { ENVIRONMENT: getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT, - DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT, + env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT, '', ), VERSION: getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION, - DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION, + env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION, '', ), EMAIL: getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL, - DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL, + env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL, '', ), WORKSPACE_ID: getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, - DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, + env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, '', ), PLAN: getStringConfig( - process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, - DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, + env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, '', ), } export const APP_VERSION = pkg.version -export const IS_MARKETPLACE = globalThis.document?.body?.getAttribute('data-is-marketplace') === 'true' +export const IS_MARKETPLACE = env.NEXT_PUBLIC_IS_MARKETPLACE export const RAG_PIPELINE_PREVIEW_CHUNK_NUM = 20 diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 12000044d6..dfcada3423 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -10,6 +10,7 @@ import { setUserId, setUserProperties } from '@/app/components/base/amplitude' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import MaintenanceNotice from '@/app/components/header/maintenance-notice' import { ZENDESK_FIELD_IDS } from '@/config' +import { env } from '@/env' import { useCurrentWorkspace, useLangGeniusVersion, @@ -204,7 +205,7 @@ export const AppContextProvider: FC = ({ children }) => }} >
- {globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && } + {env.NEXT_PUBLIC_MAINTENANCE_NOTICE && }
{children}
diff --git a/web/env.ts b/web/env.ts new file mode 100644 index 0000000000..f240fcd980 --- /dev/null +++ b/web/env.ts @@ -0,0 +1,235 @@ +import type { CamelCase, Replace } from 'string-ts' +import { createEnv } from '@t3-oss/env-nextjs' +import { concat, kebabCase, length, slice } from 'string-ts' +import * as z from 'zod' +import { isClient, isServer } from './utils/client' +import { ObjectFromEntries, ObjectKeys } from './utils/object' + +const CLIENT_ENV_PREFIX = 'NEXT_PUBLIC_' +type ClientSchema = Record<`${typeof CLIENT_ENV_PREFIX}${string}`, z.ZodType> + +const coercedBoolean = z.string() + .refine(s => s === 'true' || s === 'false' || s === '0' || s === '1') + .transform(s => s === 'true' || s === '1') +const coercedNumber = z.coerce.number().int().positive() + +/// keep-sorted +const clientSchema = { + /** + * Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking + */ + NEXT_PUBLIC_ALLOW_EMBED: coercedBoolean.default(false), + /** + * Allow rendering unsafe URLs which have "data:" scheme. + */ + NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME: coercedBoolean.default(false), + /** + * The API key of amplitude + */ + NEXT_PUBLIC_AMPLITUDE_API_KEY: z.string().optional(), + /** + * The base URL of console application, refers to the Console base URL of WEB service if console domain is + * different from api or web app domain. + * example: http://cloud.dify.ai/console/api + */ + NEXT_PUBLIC_API_PREFIX: z.string().optional(), + /** + * The base path for the application + */ + NEXT_PUBLIC_BASE_PATH: z.string().regex(/^\/.*[^/]$/).or(z.literal('')).default(''), + /** + * number of concurrency + */ + NEXT_PUBLIC_BATCH_CONCURRENCY: coercedNumber.default(5), + /** + * When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. + */ + NEXT_PUBLIC_COOKIE_DOMAIN: z.string().optional(), + /** + * CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + */ + NEXT_PUBLIC_CSP_WHITELIST: z.string().optional(), + /** + * For production release, change this to PRODUCTION + */ + NEXT_PUBLIC_DEPLOY_ENV: z.enum(['DEVELOPMENT', 'PRODUCTION', 'TESTING']).optional(), + NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON: coercedBoolean.default(false), + /** + * The deployment edition, SELF_HOSTED + */ + NEXT_PUBLIC_EDITION: z.enum(['SELF_HOSTED', 'CLOUD']).default('SELF_HOSTED'), + /** + * Enable inline LaTeX rendering with single dollar signs ($...$) + * Default is false for security reasons to prevent conflicts with regular text + */ + NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: coercedBoolean.default(false), + NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: coercedBoolean.default(true), + NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: coercedBoolean.default(true), + NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: coercedBoolean.default(false), + /** + * Github Access Token, used for invoking Github API + */ + NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: z.string().optional(), + /** + * The maximum number of tokens for segmentation + */ + NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: coercedNumber.default(4000), + NEXT_PUBLIC_IS_MARKETPLACE: coercedBoolean.default(false), + /** + * Maximum loop count in the workflow + */ + NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: coercedNumber.default(100), + NEXT_PUBLIC_MAINTENANCE_NOTICE: z.string().optional(), + /** + * The API PREFIX for MARKETPLACE + */ + NEXT_PUBLIC_MARKETPLACE_API_PREFIX: z.url().optional(), + /** + * The URL for MARKETPLACE + */ + NEXT_PUBLIC_MARKETPLACE_URL_PREFIX: z.url().optional(), + /** + * The maximum number of iterations for agent setting + */ + NEXT_PUBLIC_MAX_ITERATIONS_NUM: coercedNumber.default(99), + /** + * Maximum number of Parallelism branches in the workflow + */ + NEXT_PUBLIC_MAX_PARALLEL_LIMIT: coercedNumber.default(10), + /** + * Maximum number of tools in the agent/workflow + */ + NEXT_PUBLIC_MAX_TOOLS_NUM: coercedNumber.default(10), + /** + * The maximum number of tree node depth for workflow + */ + NEXT_PUBLIC_MAX_TREE_DEPTH: coercedNumber.default(50), + /** + * The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from + * console or api domain. + * example: http://udify.app/api + */ + NEXT_PUBLIC_PUBLIC_API_PREFIX: z.string().optional(), + /** + * SENTRY + */ + NEXT_PUBLIC_SENTRY_DSN: z.string().optional(), + NEXT_PUBLIC_SITE_ABOUT: z.string().optional(), + NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: coercedBoolean.default(false), + /** + * The timeout for the text generation in millisecond + */ + NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: coercedNumber.default(60000), + /** + * The maximum number of top-k value for RAG. + */ + NEXT_PUBLIC_TOP_K_MAX_VALUE: coercedNumber.default(10), + /** + * Disable Upload Image as WebApp icon default is false + */ + NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON: coercedBoolean.default(false), + NEXT_PUBLIC_WEB_PREFIX: z.url().optional(), + NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL: z.string().optional(), + NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT: z.string().optional(), + NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN: z.string().optional(), + NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION: z.string().optional(), + NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID: z.string().optional(), + NEXT_PUBLIC_ZENDESK_WIDGET_KEY: z.string().optional(), +} satisfies ClientSchema + +export const env = createEnv({ + server: { + /** + * Maximum length of segmentation tokens for indexing + */ + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: coercedNumber.default(4000), + /** + * Disable Next.js Telemetry (https://nextjs.org/telemetry) + */ + NEXT_TELEMETRY_DISABLED: coercedBoolean.optional(), + PORT: coercedNumber.default(3000), + /** + * The timeout for the text generation in millisecond + */ + TEXT_GENERATION_TIMEOUT_MS: coercedNumber.default(60000), + }, + shared: { + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + }, + client: clientSchema, + experimental__runtimeEnv: { + NODE_ENV: process.env.NODE_ENV, + NEXT_PUBLIC_ALLOW_EMBED: isServer ? process.env.NEXT_PUBLIC_ALLOW_EMBED : getRuntimeEnvFromBody('allowEmbed'), + NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME: isServer ? process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME : getRuntimeEnvFromBody('allowUnsafeDataScheme'), + NEXT_PUBLIC_AMPLITUDE_API_KEY: isServer ? process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY : getRuntimeEnvFromBody('amplitudeApiKey'), + NEXT_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('apiPrefix'), + NEXT_PUBLIC_BASE_PATH: isServer ? process.env.NEXT_PUBLIC_BASE_PATH : getRuntimeEnvFromBody('basePath'), + NEXT_PUBLIC_BATCH_CONCURRENCY: isServer ? process.env.NEXT_PUBLIC_BATCH_CONCURRENCY : getRuntimeEnvFromBody('batchConcurrency'), + NEXT_PUBLIC_COOKIE_DOMAIN: isServer ? process.env.NEXT_PUBLIC_COOKIE_DOMAIN : getRuntimeEnvFromBody('cookieDomain'), + NEXT_PUBLIC_CSP_WHITELIST: isServer ? process.env.NEXT_PUBLIC_CSP_WHITELIST : getRuntimeEnvFromBody('cspWhitelist'), + NEXT_PUBLIC_DEPLOY_ENV: isServer ? process.env.NEXT_PUBLIC_DEPLOY_ENV : getRuntimeEnvFromBody('deployEnv'), + NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON: isServer ? process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON : getRuntimeEnvFromBody('disableUploadImageAsIcon'), + NEXT_PUBLIC_EDITION: isServer ? process.env.NEXT_PUBLIC_EDITION : getRuntimeEnvFromBody('edition'), + NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: isServer ? process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX : getRuntimeEnvFromBody('enableSingleDollarLatex'), + NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL : getRuntimeEnvFromBody('enableWebsiteFirecrawl'), + NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER : getRuntimeEnvFromBody('enableWebsiteJinareader'), + NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL : getRuntimeEnvFromBody('enableWebsiteWatercrawl'), + NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: isServer ? process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN : getRuntimeEnvFromBody('githubAccessToken'), + NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: isServer ? process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH : getRuntimeEnvFromBody('indexingMaxSegmentationTokensLength'), + NEXT_PUBLIC_IS_MARKETPLACE: isServer ? process.env.NEXT_PUBLIC_IS_MARKETPLACE : getRuntimeEnvFromBody('isMarketplace'), + NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: isServer ? process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT : getRuntimeEnvFromBody('loopNodeMaxCount'), + NEXT_PUBLIC_MAINTENANCE_NOTICE: isServer ? process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE : getRuntimeEnvFromBody('maintenanceNotice'), + NEXT_PUBLIC_MARKETPLACE_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX : getRuntimeEnvFromBody('marketplaceApiPrefix'), + NEXT_PUBLIC_MARKETPLACE_URL_PREFIX: isServer ? process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX : getRuntimeEnvFromBody('marketplaceUrlPrefix'), + NEXT_PUBLIC_MAX_ITERATIONS_NUM: isServer ? process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM : getRuntimeEnvFromBody('maxIterationsNum'), + NEXT_PUBLIC_MAX_PARALLEL_LIMIT: isServer ? process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT : getRuntimeEnvFromBody('maxParallelLimit'), + NEXT_PUBLIC_MAX_TOOLS_NUM: isServer ? process.env.NEXT_PUBLIC_MAX_TOOLS_NUM : getRuntimeEnvFromBody('maxToolsNum'), + NEXT_PUBLIC_MAX_TREE_DEPTH: isServer ? process.env.NEXT_PUBLIC_MAX_TREE_DEPTH : getRuntimeEnvFromBody('maxTreeDepth'), + NEXT_PUBLIC_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('publicApiPrefix'), + NEXT_PUBLIC_SENTRY_DSN: isServer ? process.env.NEXT_PUBLIC_SENTRY_DSN : getRuntimeEnvFromBody('sentryDsn'), + NEXT_PUBLIC_SITE_ABOUT: isServer ? process.env.NEXT_PUBLIC_SITE_ABOUT : getRuntimeEnvFromBody('siteAbout'), + NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: isServer ? process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN : getRuntimeEnvFromBody('supportMailLogin'), + NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: isServer ? process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS : getRuntimeEnvFromBody('textGenerationTimeoutMs'), + NEXT_PUBLIC_TOP_K_MAX_VALUE: isServer ? process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE : getRuntimeEnvFromBody('topKMaxValue'), + NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON: isServer ? process.env.NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON : getRuntimeEnvFromBody('uploadImageAsIcon'), + NEXT_PUBLIC_WEB_PREFIX: isServer ? process.env.NEXT_PUBLIC_WEB_PREFIX : getRuntimeEnvFromBody('webPrefix'), + NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL : getRuntimeEnvFromBody('zendeskFieldIdEmail'), + NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT : getRuntimeEnvFromBody('zendeskFieldIdEnvironment'), + NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN : getRuntimeEnvFromBody('zendeskFieldIdPlan'), + NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION : getRuntimeEnvFromBody('zendeskFieldIdVersion'), + NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID : getRuntimeEnvFromBody('zendeskFieldIdWorkspaceId'), + NEXT_PUBLIC_ZENDESK_WIDGET_KEY: isServer ? process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY : getRuntimeEnvFromBody('zendeskWidgetKey'), + }, + emptyStringAsUndefined: true, +}) + +type ClientEnvKey = keyof typeof clientSchema +type DatasetKey = CamelCase> + +/** + * Browser-only function to get runtime env value from HTML body dataset. + */ +function getRuntimeEnvFromBody(key: DatasetKey) { + if (typeof window === 'undefined') { + throw new TypeError('getRuntimeEnvFromBody can only be called in the browser') + } + + const value = document.body.dataset[key] + return value || undefined +} + +/** + * Server-only function to get dataset map for embedding into the HTML body. + */ +export function getDatasetMap() { + if (isClient) { + throw new TypeError('getDatasetMap can only be called on the server') + } + return ObjectFromEntries( + ObjectKeys(clientSchema) + .map(envKey => [ + concat('data-', kebabCase(slice(envKey, length(CLIENT_ENV_PREFIX)))), + env[envKey], + ]), + ) +} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 02aa8707b4..3d2de0fa37 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -2512,11 +2512,6 @@ "count": 1 } }, - "app/components/base/param-item/top-k-item.tsx": { - "unicorn/prefer-number-properties": { - "count": 1 - } - }, "app/components/base/portal-to-follow-elem/index.tsx": { "react-refresh/only-export-components": { "count": 2 @@ -7266,9 +7261,6 @@ "app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 2 - }, - "unicorn/prefer-number-properties": { - "count": 1 } }, "app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": { @@ -8584,11 +8576,6 @@ "count": 7 } }, - "app/install/installForm.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - } - }, "app/reset-password/check-code/page.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 4 diff --git a/web/next.config.ts b/web/next.config.ts index 0bbdbaf32c..2236278a74 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -1,10 +1,9 @@ import type { NextConfig } from 'next' -import process from 'node:process' -import withBundleAnalyzerInit from '@next/bundle-analyzer' import createMDX from '@next/mdx' import { codeInspectorPlugin } from 'code-inspector-plugin' +import { env } from './env' -const isDev = process.env.NODE_ENV === 'development' +const isDev = env.NODE_ENV === 'development' const withMDX = createMDX({ extension: /\.mdx?$/, options: { @@ -17,20 +16,17 @@ const withMDX = createMDX({ // providerImportSource: "@mdx-js/react", }, }) -const withBundleAnalyzer = withBundleAnalyzerInit({ - enabled: process.env.ANALYZE === 'true', -}) // the default url to prevent parse url error when running jest -const hasSetWebPrefix = process.env.NEXT_PUBLIC_WEB_PREFIX -const port = process.env.PORT || 3000 +const hasSetWebPrefix = env.NEXT_PUBLIC_WEB_PREFIX +const port = env.PORT const locImageURLs = !hasSetWebPrefix ? [new URL(`http://localhost:${port}/**`), new URL(`http://127.0.0.1:${port}/**`)] : [] -const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[] +const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[] const nextConfig: NextConfig = { - basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', + basePath: env.NEXT_PUBLIC_BASE_PATH, serverExternalPackages: ['esbuild'], - transpilePackages: ['echarts', 'zrender'], + transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'], turbopack: { rules: codeInspectorPlugin({ bundler: 'turbopack', @@ -72,4 +68,4 @@ const nextConfig: NextConfig = { }, } -export default withBundleAnalyzer(withMDX(nextConfig)) +export default withMDX(nextConfig) diff --git a/web/package.json b/web/package.json index 297b83fe00..66eb8c7d66 100644 --- a/web/package.json +++ b/web/package.json @@ -54,7 +54,7 @@ "storybook": "storybook dev -p 6006", "storybook:build": "storybook build", "preinstall": "npx only-allow pnpm", - "analyze": "ANALYZE=true pnpm build", + "analyze": "next experimental-analyze", "knip": "knip" }, "dependencies": { @@ -82,6 +82,7 @@ "@remixicon/react": "4.7.0", "@sentry/react": "8.55.0", "@svgdotjs/svg.js": "3.2.5", + "@t3-oss/env-nextjs": "0.13.10", "@tailwindcss/typography": "0.5.19", "@tanstack/react-form": "1.23.7", "@tanstack/react-query": "5.90.5", @@ -159,7 +160,7 @@ "ufo": "1.6.3", "use-context-selector": "2.0.0", "uuid": "10.0.0", - "zod": "3.25.76", + "zod": "4.3.6", "zundo": "2.3.0", "zustand": "5.0.9" }, @@ -172,7 +173,6 @@ "@iconify-json/ri": "1.2.9", "@mdx-js/loader": "3.1.1", "@mdx-js/react": "3.1.1", - "@next/bundle-analyzer": "16.1.5", "@next/eslint-plugin-next": "16.1.6", "@next/mdx": "16.1.5", "@rgrove/parse-xml": "4.2.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 7267b7bbfb..ae618e857e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: '@svgdotjs/svg.js': specifier: 3.2.5 version: 3.2.5 + '@t3-oss/env-nextjs': + specifier: 0.13.10 + version: 0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6) '@tailwindcss/typography': specifier: 0.5.19 version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) @@ -357,8 +360,8 @@ importers: specifier: 10.0.0 version: 10.0.0 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.3.6 + version: 4.3.6 zundo: specifier: 2.3.0 version: 2.3.0(zustand@5.0.9(@types/react@19.2.9)(immer@11.1.0)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) @@ -390,9 +393,6 @@ importers: '@mdx-js/react': specifier: 3.1.1 version: 3.1.1(@types/react@19.2.9)(react@19.2.4) - '@next/bundle-analyzer': - specifier: 16.1.5 - version: 16.1.5 '@next/eslint-plugin-next': specifier: 16.1.6 version: 16.1.6 @@ -1767,9 +1767,6 @@ packages: '@neoconfetti/react@1.0.0': resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} - '@next/bundle-analyzer@16.1.5': - resolution: {integrity: sha512-/iPMrxbvgMZQX1huKZu+rnh7bxo2m5/o0PpOWLMRcAlQ2METpZ7/a3SP/aXFePZAyrQpgpndTldXW3LxPXM/KA==} - '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} @@ -2859,6 +2856,40 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@t3-oss/env-core@0.13.10': + resolution: {integrity: sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + + '@t3-oss/env-nextjs@0.13.10': + resolution: {integrity: sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + '@tailwindcss/typography@0.5.19': resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: @@ -3629,10 +3660,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -4292,9 +4319,6 @@ packages: dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} - debounce@1.2.1: - resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -4407,9 +4431,6 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - echarts-for-react@3.0.5: resolution: {integrity: sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg==} peerDependencies: @@ -5079,10 +5100,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - gzip-size@6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -5359,10 +5376,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -6095,10 +6108,6 @@ packages: openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6840,10 +6849,6 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -7556,11 +7561,6 @@ packages: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} - webpack-bundle-analyzer@4.10.1: - resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} - engines: {node: '>= 10.13.0'} - hasBin: true - webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -7627,18 +7627,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -7709,9 +7697,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -8417,7 +8402,7 @@ snapshots: eslint: 9.39.2(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 - zod: 3.25.76 + zod: 4.3.6 transitivePeerDependencies: - supports-color @@ -9149,13 +9134,6 @@ snapshots: '@neoconfetti/react@1.0.0': {} - '@next/bundle-analyzer@16.1.5': - dependencies: - webpack-bundle-analyzer: 4.10.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@next/env@16.0.0': {} '@next/env@16.1.5': {} @@ -9456,7 +9434,8 @@ snapshots: '@pkgr/core@0.2.9': {} - '@polka/url@1.0.0-next.29': {} + '@polka/url@1.0.0-next.29': + optional: true '@preact/signals-core@1.12.2': {} @@ -10171,6 +10150,20 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@t3-oss/env-core@0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)': + optionalDependencies: + typescript: 5.9.3 + valibot: 1.2.0(typescript@5.9.3) + zod: 4.3.6 + + '@t3-oss/env-nextjs@0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)': + dependencies: + '@t3-oss/env-core': 0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6) + optionalDependencies: + typescript: 5.9.3 + valibot: 1.2.0(typescript@5.9.3) + zod: 4.3.6 + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 @@ -11176,10 +11169,6 @@ snapshots: dependencies: acorn: 8.15.0 - acorn-walk@8.3.4: - dependencies: - acorn: 8.15.0 - acorn@8.15.0: {} agent-base@7.1.4: {} @@ -11857,8 +11846,6 @@ snapshots: dayjs@1.11.19: {} - debounce@1.2.1: {} - debug@4.4.3: dependencies: ms: 2.1.3 @@ -11952,8 +11939,6 @@ snapshots: dotenv@16.6.1: {} - duplexer@0.1.2: {} - echarts-for-react@3.0.5(echarts@5.6.0)(react@19.2.4): dependencies: echarts: 5.6.0 @@ -12257,8 +12242,8 @@ snapshots: '@babel/parser': 7.28.6 eslint: 9.39.2(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 3.25.76 - zod-validation-error: 4.0.2(zod@3.25.76) + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -12822,10 +12807,6 @@ snapshots: graphemer@1.4.0: {} - gzip-size@6.0.0: - dependencies: - duplexer: 0.1.2 - hachure-fill@0.5.2: {} has-flag@4.0.0: {} @@ -13158,8 +13139,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-plain-object@5.0.0: {} - is-potential-custom-element-name@1.0.1: {} is-stream@3.0.0: {} @@ -14053,7 +14032,8 @@ snapshots: mri@1.2.0: {} - mrmime@2.0.1: {} + mrmime@2.0.1: + optional: true ms@2.1.3: {} @@ -14173,8 +14153,6 @@ snapshots: openapi-types@12.1.3: {} - opener@1.5.2: {} - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15116,12 +15094,6 @@ snapshots: dependencies: is-arrayish: 0.3.4 - sirv@2.0.4: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -15442,7 +15414,8 @@ snapshots: dependencies: eslint-visitor-keys: 5.0.0 - totalist@3.0.1: {} + totalist@3.0.1: + optional: true tough-cookie@6.0.0: dependencies: @@ -15828,25 +15801,6 @@ snapshots: webidl-conversions@8.0.1: {} - webpack-bundle-analyzer@4.10.1: - dependencies: - '@discoveryjs/json-ext': 0.5.7 - acorn: 8.15.0 - acorn-walk: 8.3.4 - commander: 7.2.0 - debounce: 1.2.1 - escape-string-regexp: 4.0.0 - gzip-size: 6.0.0 - html-escaper: 2.0.2 - is-plain-object: 5.0.0 - opener: 1.5.2 - picocolors: 1.1.1 - sirv: 2.0.4 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - webpack-sources@3.3.3: optional: true @@ -15935,8 +15889,6 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} - ws@8.19.0: {} wsl-utils@0.1.0: @@ -15980,11 +15932,9 @@ snapshots: zen-observable@0.8.15: {} - zod-validation-error@4.0.2(zod@3.25.76): + zod-validation-error@4.0.2(zod@4.3.6): dependencies: - zod: 3.25.76 - - zod@3.25.76: {} + zod: 4.3.6 zod@4.3.6: {} diff --git a/web/proxy.ts b/web/proxy.ts index 05436557d7..bc4a4a3d89 100644 --- a/web/proxy.ts +++ b/web/proxy.ts @@ -1,13 +1,14 @@ import type { NextRequest } from 'next/server' import { Buffer } from 'node:buffer' import { NextResponse } from 'next/server' +import { env } from '@/env' const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com' const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => { // prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking // Chatbot page should be allowed to be embedded in iframe. It's a feature - if (process.env.NEXT_PUBLIC_ALLOW_EMBED !== 'true' && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin')) + if (env.NEXT_PUBLIC_ALLOW_EMBED !== true && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin')) response.headers.set('X-Frame-Options', 'DENY') return response @@ -21,11 +22,11 @@ export function proxy(request: NextRequest) { }, }) - const isWhiteListEnabled = !!process.env.NEXT_PUBLIC_CSP_WHITELIST && process.env.NODE_ENV === 'production' + const isWhiteListEnabled = !!env.NEXT_PUBLIC_CSP_WHITELIST && env.NODE_ENV === 'production' if (!isWhiteListEnabled) return wrapResponseWithXFrameOptions(response, pathname) - const whiteList = `${process.env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}` + const whiteList = `${env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}` const nonce = Buffer.from(crypto.randomUUID()).toString('base64') const csp = `'nonce-${nonce}'` diff --git a/web/service/client.spec.ts b/web/service/client.spec.ts index d8b46ad4b6..95bf720bfe 100644 --- a/web/service/client.spec.ts +++ b/web/service/client.spec.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const loadGetBaseURL = async (isClientValue: boolean) => { vi.resetModules() - vi.doMock('@/utils/client', () => ({ isClient: isClientValue })) + vi.doMock('@/utils/client', () => ({ isClient: isClientValue, isServer: !isClientValue })) const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) // eslint-disable-next-line next/no-assign-module-variable const module = await import('./client') diff --git a/web/types/feature.ts b/web/types/feature.ts index 19980974da..a5c12a453e 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -107,37 +107,3 @@ export const defaultSystemFeatures: SystemFeatures = { enable_trial_app: false, enable_explore_banner: false, } - -export enum DatasetAttr { - DATA_API_PREFIX = 'data-api-prefix', - DATA_PUBLIC_API_PREFIX = 'data-public-api-prefix', - DATA_MARKETPLACE_API_PREFIX = 'data-marketplace-api-prefix', - DATA_MARKETPLACE_URL_PREFIX = 'data-marketplace-url-prefix', - DATA_PUBLIC_EDITION = 'data-public-edition', - DATA_PUBLIC_AMPLITUDE_API_KEY = 'data-public-amplitude-api-key', - DATA_PUBLIC_COOKIE_DOMAIN = 'data-public-cookie-domain', - DATA_PUBLIC_SUPPORT_MAIL_LOGIN = 'data-public-support-mail-login', - DATA_PUBLIC_SENTRY_DSN = 'data-public-sentry-dsn', - DATA_PUBLIC_MAINTENANCE_NOTICE = 'data-public-maintenance-notice', - DATA_PUBLIC_SITE_ABOUT = 'data-public-site-about', - DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS = 'data-public-text-generation-timeout-ms', - DATA_PUBLIC_MAX_TOOLS_NUM = 'data-public-max-tools-num', - DATA_PUBLIC_MAX_PARALLEL_LIMIT = 'data-public-max-parallel-limit', - DATA_PUBLIC_TOP_K_MAX_VALUE = 'data-public-top-k-max-value', - DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH = 'data-public-indexing-max-segmentation-tokens-length', - DATA_PUBLIC_LOOP_NODE_MAX_COUNT = 'data-public-loop-node-max-count', - DATA_PUBLIC_MAX_ITERATIONS_NUM = 'data-public-max-iterations-num', - DATA_PUBLIC_MAX_TREE_DEPTH = 'data-public-max-tree-depth', - DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME = 'data-public-allow-unsafe-data-scheme', - DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER = 'data-public-enable-website-jinareader', - DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL = 'data-public-enable-website-firecrawl', - DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL = 'data-public-enable-website-watercrawl', - DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX = 'data-public-enable-single-dollar-latex', - NEXT_PUBLIC_ZENDESK_WIDGET_KEY = 'next-public-zendesk-widget-key', - NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT = 'next-public-zendesk-field-id-environment', - NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION = 'next-public-zendesk-field-id-version', - NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL = 'next-public-zendesk-field-id-email', - NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID = 'next-public-zendesk-field-id-workspace-id', - NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN = 'next-public-zendesk-field-id-plan', - DATA_PUBLIC_BATCH_CONCURRENCY = 'data-public-batch-concurrency', -} diff --git a/web/utils/var.ts b/web/utils/var.ts index 1851084b2e..efad8794eb 100644 --- a/web/utils/var.ts +++ b/web/utils/var.ts @@ -8,6 +8,7 @@ import { } from '@/app/components/base/prompt-editor/constants' import { InputVarType } from '@/app/components/workflow/types' import { getMaxVarNameLength, MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW } from '@/config' +import { env } from '@/env' const otherAllowedRegex = /^\w+$/ @@ -129,7 +130,7 @@ export const getVars = (value: string) => { // Set the value of basePath // example: /dify -export const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' +export const basePath = env.NEXT_PUBLIC_BASE_PATH export function getMarketplaceUrl(path: string, params?: Record) { const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) }) diff --git a/web/utils/zod.spec.ts b/web/utils/zod.spec.ts deleted file mode 100644 index e3676aa054..0000000000 --- a/web/utils/zod.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { z, ZodError } from 'zod' - -describe('Zod Features', () => { - it('should support string', () => { - const stringSchema = z.string() - const numberLikeStringSchema = z.coerce.string() // 12 would be converted to '12' - const stringSchemaWithError = z.string({ - required_error: 'Name is required', - invalid_type_error: 'Invalid name type, expected string', - }) - - const urlSchema = z.string().url() - const uuidSchema = z.string().uuid() - - expect(stringSchema.parse('hello')).toBe('hello') - expect(() => stringSchema.parse(12)).toThrow() - expect(numberLikeStringSchema.parse('12')).toBe('12') - expect(numberLikeStringSchema.parse(12)).toBe('12') - expect(() => stringSchemaWithError.parse(undefined)).toThrow('Name is required') - expect(() => stringSchemaWithError.parse(12)).toThrow('Invalid name type, expected string') - - expect(urlSchema.parse('https://dify.ai')).toBe('https://dify.ai') - expect(uuidSchema.parse('123e4567-e89b-12d3-a456-426614174000')).toBe('123e4567-e89b-12d3-a456-426614174000') - }) - - it('should support enum', () => { - enum JobStatus { - waiting = 'waiting', - processing = 'processing', - completed = 'completed', - } - expect(z.nativeEnum(JobStatus).parse(JobStatus.waiting)).toBe(JobStatus.waiting) - expect(z.nativeEnum(JobStatus).parse('completed')).toBe('completed') - expect(() => z.nativeEnum(JobStatus).parse('invalid')).toThrow() - }) - - it('should support number', () => { - const numberSchema = z.number() - const numberWithMin = z.number().gt(0) // alias min - const numberWithMinEqual = z.number().gte(0) - const numberWithMax = z.number().lt(100) // alias max - - expect(numberSchema.parse(123)).toBe(123) - expect(numberWithMin.parse(50)).toBe(50) - expect(numberWithMinEqual.parse(0)).toBe(0) - expect(() => numberWithMin.parse(-1)).toThrow() - expect(numberWithMax.parse(50)).toBe(50) - expect(() => numberWithMax.parse(101)).toThrow() - }) - - it('should support boolean', () => { - const booleanSchema = z.boolean() - expect(booleanSchema.parse(true)).toBe(true) - expect(booleanSchema.parse(false)).toBe(false) - expect(() => booleanSchema.parse('true')).toThrow() - }) - - it('should support date', () => { - const dateSchema = z.date() - expect(dateSchema.parse(new Date('2023-01-01'))).toEqual(new Date('2023-01-01')) - }) - - it('should support object', () => { - const userSchema = z.object({ - id: z.union([z.string(), z.number()]), - name: z.string(), - email: z.string().email(), - age: z.number().min(0).max(120).optional(), - }) - - type User = z.infer - - const validUser: User = { - id: 1, - name: 'John', - email: 'john@example.com', - age: 30, - } - - expect(userSchema.parse(validUser)).toEqual(validUser) - }) - - it('should support object optional field', () => { - const userSchema = z.object({ - name: z.string(), - optionalField: z.optional(z.string()), - }) - type User = z.infer - - const user: User = { - name: 'John', - } - const userWithOptionalField: User = { - name: 'John', - optionalField: 'optional', - } - expect(userSchema.safeParse(user).success).toEqual(true) - expect(userSchema.safeParse(userWithOptionalField).success).toEqual(true) - }) - - it('should support object intersection', () => { - const Person = z.object({ - name: z.string(), - }) - - const Employee = z.object({ - role: z.string(), - }) - - const EmployedPerson = z.intersection(Person, Employee) - const validEmployedPerson = { - name: 'John', - role: 'Developer', - } - expect(EmployedPerson.parse(validEmployedPerson)).toEqual(validEmployedPerson) - }) - - it('should support record', () => { - const recordSchema = z.record(z.string(), z.number()) - const validRecord = { - a: 1, - b: 2, - } - expect(recordSchema.parse(validRecord)).toEqual(validRecord) - }) - - it('should support array', () => { - const numbersSchema = z.array(z.number()) - const stringArraySchema = z.string().array() - - expect(numbersSchema.parse([1, 2, 3])).toEqual([1, 2, 3]) - expect(stringArraySchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']) - }) - - it('should support promise', async () => { - const promiseSchema = z.promise(z.string()) - const validPromise = Promise.resolve('success') - - await expect(promiseSchema.parse(validPromise)).resolves.toBe('success') - }) - - it('should support unions', () => { - const unionSchema = z.union([z.string(), z.number()]) - - expect(unionSchema.parse('success')).toBe('success') - expect(unionSchema.parse(404)).toBe(404) - }) - - it('should support functions', () => { - const functionSchema = z.function().args(z.string(), z.number(), z.optional(z.string())).returns(z.number()) - const validFunction = (name: string, age: number, _optional?: string): number => { - return age - } - expect(functionSchema.safeParse(validFunction).success).toEqual(true) - }) - - it('should support undefined, null, any, and void', () => { - const undefinedSchema = z.undefined() - const nullSchema = z.null() - const anySchema = z.any() - - expect(undefinedSchema.parse(undefined)).toBeUndefined() - expect(nullSchema.parse(null)).toBeNull() - expect(anySchema.parse('anything')).toBe('anything') - expect(anySchema.parse(3)).toBe(3) - }) - - it('should safeParse would not throw', () => { - expect(z.string().safeParse('abc').success).toBe(true) - expect(z.string().safeParse(123).success).toBe(false) - expect(z.string().safeParse(123).error).toBeInstanceOf(ZodError) - }) -})