diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index d546bb00f2a..f3ae7f4e54f 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -39,11 +39,19 @@ jobs:
with:
files: |
web/**
+ cli/**
+ e2e/**
packages/**
+ sdks/nodejs-client/**
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
.nvmrc
+ vite.config.ts
+ eslint.config.mjs
+ .vscode/**
+ .github/workflows/autofix.yml
+ .github/workflows/style.yml
- name: Check api inputs
if: github.event_name != 'merge_group'
id: api-changes
@@ -154,14 +162,19 @@ jobs:
cd api
uv run dev/generate_swagger_markdown_docs.py --swagger-dir ../packages/contracts/openapi --markdown-dir openapi/markdown --keep-swagger-json
- - name: Generate frontend contracts
- if: github.event_name != 'merge_group' && steps.frontend-contract-changes.outputs.any_changed == 'true'
- run: pnpm --dir packages/contracts gen-api-contract
+ # TODO: Enable after the repository-wide vp fmt baseline lands.
+ # - name: Generate frontend contracts
+ # if: github.event_name != 'merge_group' && steps.frontend-contract-changes.outputs.any_changed == 'true'
+ # run: pnpm --dir packages/contracts gen-api-contract
- - name: ESLint autofix
- if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
- run: |
- vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
+ # - name: ESLint autofix
+ # if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
+ # run: |
+ # vp exec eslint --fix --concurrency=2 --prune-suppressions --quiet || true
+
+ # - name: Format frontend files
+ # if: github.event_name != 'merge_group' && (steps.web-changes.outputs.any_changed == 'true' || steps.frontend-contract-changes.outputs.any_changed == 'true')
+ # run: vp fmt
- if: github.event_name != 'merge_group'
uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml
index 3854d4ecd4d..407ec773068 100644
--- a/.github/workflows/style.yml
+++ b/.github/workflows/style.yml
@@ -162,7 +162,10 @@ jobs:
pnpm-lock.yaml
pnpm-workspace.yaml
.nvmrc
+ vite.config.ts
eslint.config.mjs
+ .vscode/**
+ .github/workflows/autofix.yml
.github/workflows/style.yml
.github/actions/setup-web/**
@@ -170,6 +173,11 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-web
+ # TODO: Enable after the repository-wide vp fmt baseline lands.
+ # - name: Format check
+ # if: steps.changed-files.outputs.any_changed == 'true'
+ # run: vp fmt --check
+
- name: Restore ESLint cache
if: steps.changed-files.outputs.any_changed == 'true'
id: eslint-cache-restore
diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml
index 2c3ccd35433..39685dc8154 100644
--- a/.github/workflows/translate-i18n-claude.yml
+++ b/.github/workflows/translate-i18n-claude.yml
@@ -223,7 +223,7 @@ jobs:
- Match the existing terminology and register used by each locale.
- Prefer one Edit per file when stable, but prioritize correctness over batching.
6. Verify only the edited files.
- - Run `vp run dify-web#lint:fix --quiet -- `
+ - Run `vp fmt `
- Run `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
- If verification fails, fix the remaining problems before continuing.
7. Stop after the scoped locale files are updated and verification passes.
@@ -322,7 +322,7 @@ jobs:
'## Verification',
'',
`- \`vp run dify-web#i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``,
- `- \`vp run dify-web#lint:fix --quiet -- \``,
+ `- \`vp fmt \``,
'',
'## Notes',
'',
diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json
index 7cdbc51a3bd..9a33096c9ad 100644
--- a/.vscode/settings.example.json
+++ b/.vscode/settings.example.json
@@ -1,54 +1,47 @@
{
- "cucumber.features": [
- "e2e/features/**/*.feature",
- ],
- "cucumber.glue": [
- "e2e/features/**/*.ts",
- ],
+ "cucumber.features": ["e2e/features/**/*.feature"],
+ "cucumber.glue": ["e2e/features/**/*.ts"],
"tailwindCSS.experimental.configFile": "web/app/styles/globals.css",
- // Auto fix
+ // Format
+ "[javascript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[javascriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[typescript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[typescriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[json]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[jsonc]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[markdown]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[mdx]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[yaml]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[toml]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[css]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[scss]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[less]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[html]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[vue]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[svelte]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[graphql]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "[handlebars]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
+ "editor.formatOnSave": true,
+ "editor.formatOnSaveMode": "file",
+ "oxc.fmt.configPath": "./vite.config.ts",
+
+ // Lint fix
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": "explicit",
+ "source.fixAll.eslint": "explicit"
},
- // Silent the stylistic rules in your IDE, but still auto fix them
- "eslint.rules.customizations": [
- { "rule": "style/*", "severity": "off", "fixable": true },
- { "rule": "format/*", "severity": "off", "fixable": true },
- { "rule": "*-indent", "severity": "off", "fixable": true },
- { "rule": "*-spacing", "severity": "off", "fixable": true },
- { "rule": "*-spaces", "severity": "off", "fixable": true },
- { "rule": "*-order", "severity": "off", "fixable": true },
- { "rule": "*-dangle", "severity": "off", "fixable": true },
- { "rule": "*-newline", "severity": "off", "fixable": true },
- { "rule": "*quotes", "severity": "off", "fixable": true },
- { "rule": "*semi", "severity": "off", "fixable": true }
- ],
-
- // Enable eslint for all supported languages
+ // Enable ESLint for linted languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
- "vue",
- "html",
"markdown",
"json",
"jsonc",
"yaml",
- "toml",
- "xml",
- "gql",
- "graphql",
- "astro",
- "svelte",
- "css",
- "less",
- "scss",
- "pcss",
- "postcss"
+ "toml"
]
}
diff --git a/AGENTS.md b/AGENTS.md
index c6ebf7ae8a0..1cdd4a67fa0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -31,7 +31,7 @@ The codebase is split into:
## Language Style
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
-- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check`, and avoid `any` types.
+- **TypeScript**: Use the strict config, format with `vp fmt`, run ESLint for code-quality checks and fixes, run `pnpm type-check`, and avoid `any` types.
## General Practices
diff --git a/cli/AGENTS.md b/cli/AGENTS.md
index 96df6f2bdca..212a6329d30 100644
--- a/cli/AGENTS.md
+++ b/cli/AGENTS.md
@@ -1,6 +1,6 @@
# AGENTS.md — difyctl (TypeScript CLI)
-TypeScript port of difyctl. Stack: custom CLI framework (`src/framework/`), Node 22+, ESM, ky for HTTP, vitest, eslint via @antfu/eslint-config.
+TypeScript port of difyctl. Stack: custom CLI framework (`src/framework/`), Node 22+, ESM, ky for HTTP, Vitest, Vite+ formatting, and ESLint via `@antfu/eslint-config`.
> Architecture patterns, scaffolding recipe, printer chain, strategy pattern, testing conventions, anti-patterns: see **[`ARD.md`]**.
@@ -59,7 +59,8 @@ pnpm test # vitest
pnpm test:coverage # with coverage
pnpm type-check # tsc, no emit
pnpm lint # eslint
-pnpm lint:fix # eslint --fix
+pnpm lint:fix # eslint semantic fixes
+vp fmt # format with Oxfmt
pnpm build # production bundle (vp pack)
pnpm tree:gen # regenerate src/commands/tree.ts (registry)
pnpm tree:check # verify tree.ts is up-to-date with the fs
diff --git a/cli/ARD.md b/cli/ARD.md
index cc0e2f3d664..9a4d3d5fee2 100644
--- a/cli/ARD.md
+++ b/cli/ARD.md
@@ -80,7 +80,14 @@ export default class MyCommand extends DifyCommand {
// Authed: authedCtx() sets outputFormat + builds context
const ctx = await this.authedCtx({ format: flags.output })
- process.stdout.write(await runMyThing({ /* args */ }, { bundle: ctx.bundle, http: ctx.http, io: ctx.io }))
+ process.stdout.write(
+ await runMyThing(
+ {
+ /* args */
+ },
+ { bundle: ctx.bundle, http: ctx.http, io: ctx.io },
+ ),
+ )
}
}
```
@@ -102,7 +109,7 @@ import { ErrorCode } from '../../errors/codes.js'
throw new BaseError({
code: ErrorCode.UsageMissingArg,
message: 'workspace id required',
- hint: 'pass --workspace or run \'difyctl use workspace \'',
+ hint: "pass --workspace or run 'difyctl use workspace '",
})
```
@@ -150,10 +157,7 @@ export type IOStreams = {
`runWithSpinner` wraps async call with animated spinner on stderr. Auto-disables for structured output — no manual `enabled:` flag needed.
```typescript
-const result = await runWithSpinner(
- { io, label: 'Fetching apps' },
- () => client.list(params),
-)
+const result = await runWithSpinner({ io, label: 'Fetching apps' }, () => client.list(params))
```
`STRUCTURED_FORMATS = new Set(['json', 'yaml', 'name'])` drives disable check. New structured format = add to this set only — no other callsites change.
@@ -174,9 +178,15 @@ Output rendering separated from data fetching via protocol objects.
```typescript
// handlers.ts — implement the protocol on the data object
export class MyListOutput implements TablePrintable {
- tableColumns() { return COLUMNS }
- tableRows() { return this.rows.map(r => r.tableRow()) }
- json() { return { items: this.rows.map(r => r.json()) } }
+ tableColumns() {
+ return COLUMNS
+ }
+ tableRows() {
+ return this.rows.map((r) => r.tableRow())
+ }
+ json() {
+ return { items: this.rows.map((r) => r.json()) }
+ }
}
// index.ts — wrap and return
@@ -215,10 +225,16 @@ One file per resource under `src/api/`. Each exports class wrapping `KyInstance`
```typescript
export class AppsClient {
private readonly http: KyInstance
- constructor(http: KyInstance) { this.http = http }
+ constructor(http: KyInstance) {
+ this.http = http
+ }
- async list(params: ListParams): Promise { /* ... */ throw new Error('elided') }
- async describe(id: string, workspaceId: string, fields: string[]): Promise { /* ... */ throw new Error('elided') }
+ async list(params: ListParams): Promise {
+ /* ... */ throw new Error('elided')
+ }
+ async describe(id: string, workspaceId: string, fields: string[]): Promise {
+ /* ... */ throw new Error('elided')
+ }
}
```
@@ -292,7 +308,8 @@ expect(JSON.parse(out).workspaces).toHaveLength(2)
| `pnpm test:coverage` | Coverage report |
| `pnpm type-check` | `tsc --noEmit` — catches type errors without build |
| `pnpm lint` | ESLint check |
-| `pnpm lint:fix` | ESLint auto-fix (perfectionist sort, chaining) |
+| `pnpm lint:fix` | ESLint code-quality fixes |
+| `vp fmt` | Oxfmt formatting and import sorting |
| `pnpm build` | Production bundle (`vp pack`) |
| `pnpm tree:gen` | Regenerate `src/commands/tree.ts` (registry) |
| `pnpm tree:check` | Verify `tree.ts` matches the filesystem |
@@ -306,7 +323,7 @@ expect(JSON.parse(out).workspaces).toHaveLength(2)
## Lint rules that catch contributors
-Repo runs `@antfu/eslint-config` + perfectionist + unicorn.
+Repo runs `@antfu/eslint-config` for code-quality rules and Vite+ Oxfmt for formatting.
| Rule | What it catches |
| ---------------------------------- | -------------------------------------------------- |
@@ -316,7 +333,7 @@ Repo runs `@antfu/eslint-config` + perfectionist + unicorn.
| `unicorn/no-new-array` | Use `Array.from({ length: n })` not `new Array(n)` |
| `noUncheckedIndexedAccess` (tsc) | `arr[i]` is `T \| undefined`; guard before use |
-`pnpm lint:fix` resolves perfectionist + chaining auto.
+Run `pnpm lint:fix` for ESLint fixes, then `vp fmt` so Oxfmt produces the final layout.
---
diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs
index 697368ac5cd..c5b1d372db2 100644
--- a/cli/eslint.config.mjs
+++ b/cli/eslint.config.mjs
@@ -6,13 +6,31 @@ import markdownPreferences from 'eslint-plugin-markdown-preferences'
export default antfu(
{
- ignores: original => [
- 'context/**',
- 'docs/**',
- 'dist/**',
- 'coverage/**',
- ...original,
- ],
+ stylistic: false,
+ perfectionist: {
+ overrides: {
+ 'perfectionist/sort-imports': 'off',
+ },
+ },
+ jsonc: {
+ overrides: {
+ 'jsonc/space-unary-ops': 'off',
+ },
+ },
+ yaml: {
+ overrides: {
+ 'yaml/block-mapping': 'off',
+ 'yaml/block-sequence': 'off',
+ 'yaml/plain-scalar': 'off',
+ },
+ },
+ toml: {
+ overrides: {
+ 'toml/comma-style': 'off',
+ 'toml/no-space-dots': 'off',
+ },
+ },
+ ignores: (original) => ['context/**', 'docs/**', 'dist/**', 'coverage/**', ...original],
typescript: {
overrides: {
'ts/consistent-type-definitions': ['error', 'type'],
@@ -26,36 +44,30 @@ export default antfu(
'test/prefer-lowercase-title': 'off',
},
},
- stylistic: {
- overrides: {
- 'antfu/top-level-function': 'off',
- },
- },
e18e: false,
},
- markdownPreferences.configs.standard,
{
files: [GLOB_MARKDOWN],
- plugins: { md },
+ plugins: {
+ md,
+ 'markdown-preferences': markdownPreferences,
+ },
rules: {
'md/no-url-trailing-slash': 'error',
+ 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/prefer-link-reference-definitions': [
'error',
{
minLinks: 1,
},
],
- 'markdown-preferences/ordered-list-marker-sequence': [
- 'error',
- { increment: 'never' },
- ],
- 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/sort-definitions': 'error',
},
},
{
rules: {
'node/prefer-global/process': 'off',
+ 'unicorn/number-literal-case': 'off',
},
},
{
@@ -67,14 +79,22 @@ export default antfu(
{
files: ['src/**/*.ts'],
rules: {
- 'no-restricted-imports': ['error', {
- patterns: [
- {
- group: ['../**', './*/**', '..'],
- message: 'Use the @/ (or @test/) alias for parent-directory or nested relative imports; keep ./ only for same-folder siblings.',
- },
- ],
- }],
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: ['../**', './*/**', '..'],
+ message:
+ 'Use the @/ (or @test/) alias for parent-directory or nested relative imports; keep ./ only for same-folder siblings.',
+ },
+ ],
+ },
+ ],
},
},
-)
+).override('antfu/sort/package-json', {
+ rules: {
+ 'jsonc/sort-keys': 'off',
+ },
+})
diff --git a/cli/src/commands/AGENTS.md b/cli/src/commands/AGENTS.md
index 0a4a1ec3fa5..5df83849779 100644
--- a/cli/src/commands/AGENTS.md
+++ b/cli/src/commands/AGENTS.md
@@ -38,6 +38,7 @@ registry discovery and from coverage checks.
## Adding an agent guide
+
1. Create `src/commands///guide.ts` exporting a plain string:
```ts
export const agentGuide = `
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index d767bfc4301..8499309c4e9 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -790,9 +790,6 @@
"react/set-state-in-effect": {
"count": 6
},
- "style/multiline-ternary": {
- "count": 2
- },
"ts/no-explicit-any": {
"count": 5
}
@@ -1091,9 +1088,6 @@
}
},
"web/app/components/base/chat/chat/answer/agent-content.tsx": {
- "style/multiline-ternary": {
- "count": 2
- },
"ts/no-explicit-any": {
"count": 1
}
@@ -5551,11 +5545,6 @@
"count": 2
}
},
- "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx": {
- "style/multiline-ternary": {
- "count": 2
- }
- },
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": {
"react-refresh/only-export-components": {
"count": 2
@@ -7160,4 +7149,4 @@
"count": 2
}
}
-}
\ No newline at end of file
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 880ef4cdc56..c6ab41a0936 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -18,7 +18,31 @@ const GENERATED_IGNORES = [
export default antfu(
{
- ignores: original => [
+ stylistic: false,
+ perfectionist: {
+ overrides: {
+ 'perfectionist/sort-imports': 'off',
+ },
+ },
+ jsonc: {
+ overrides: {
+ 'jsonc/space-unary-ops': 'off',
+ },
+ },
+ yaml: {
+ overrides: {
+ 'yaml/block-mapping': 'off',
+ 'yaml/block-sequence': 'off',
+ 'yaml/plain-scalar': 'off',
+ },
+ },
+ toml: {
+ overrides: {
+ 'toml/comma-style': 'off',
+ 'toml/no-space-dots': 'off',
+ },
+ },
+ ignores: (original) => [
'**',
'!packages/**',
'!web/**',
@@ -43,36 +67,34 @@ export default antfu(
'test/prefer-lowercase-title': 'off',
},
},
- stylistic: {
- overrides: {
- 'antfu/top-level-function': 'off',
- },
- },
e18e: false,
},
- markdownPreferences.configs.standard,
{
files: [GLOB_MARKDOWN],
- plugins: { md },
+ plugins: {
+ md,
+ 'markdown-preferences': markdownPreferences,
+ },
rules: {
'md/no-url-trailing-slash': 'error',
+ 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/prefer-link-reference-definitions': [
'error',
{
minLinks: 1,
},
],
- 'markdown-preferences/ordered-list-marker-sequence': [
- 'error',
- { increment: 'never' },
- ],
- 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/sort-definitions': 'error',
},
},
{
rules: {
'node/prefer-global/process': 'off',
+ 'unicorn/number-literal-case': 'off',
},
},
-)
+).override('antfu/sort/package-json', {
+ rules: {
+ 'jsonc/sort-keys': 'off',
+ },
+})
diff --git a/package.json b/package.json
index 79c022eb99a..67be592bca3 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,32 @@
{
"name": "dify",
- "type": "module",
"private": true,
- "packageManager": "pnpm@11.10.0",
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently -k -n vinext,proxy \"vp run dify-web#dev:vinext\" \"vp run dify-web#dev:proxy\"",
+ "fmt": "vp fmt",
+ "fmt:check": "vp fmt --check",
+ "prepare": "vp config",
+ "type-check": "vp run -r type-check",
+ "lint": "eslint --cache --concurrency=auto",
+ "lint:ci": "eslint --cache --cache-strategy content --concurrency 2",
+ "lint:fix": "vp run lint --fix",
+ "lint:quiet": "vp run lint --quiet"
+ },
+ "devDependencies": {
+ "@antfu/eslint-config": "catalog:",
+ "@iconify-json/heroicons": "catalog:",
+ "@iconify-json/ri": "catalog:",
+ "@typescript/native": "catalog:",
+ "concurrently": "catalog:",
+ "eslint": "catalog:",
+ "eslint-markdown": "catalog:",
+ "eslint-plugin-markdown-preferences": "catalog:",
+ "eslint-plugin-no-barrel-files": "catalog:",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vite-plus": "catalog:"
+ },
"devEngines": {
"runtime": {
"name": "node",
@@ -13,25 +37,5 @@
"engines": {
"node": "^22.22.1"
},
- "scripts": {
- "dev": "concurrently -k -n vinext,proxy \"vp run dify-web#dev:vinext\" \"vp run dify-web#dev:proxy\"",
- "prepare": "vp config",
- "type-check": "vp run -r type-check",
- "lint": "eslint --cache --concurrency=auto",
- "lint:ci": "eslint --cache --cache-strategy content --concurrency 2",
- "lint:fix": "vp run lint --fix",
- "lint:quiet": "vp run lint --quiet"
- },
- "devDependencies": {
- "@antfu/eslint-config": "catalog:",
- "@typescript/native": "catalog:",
- "concurrently": "catalog:",
- "eslint": "catalog:",
- "eslint-markdown": "catalog:",
- "eslint-plugin-markdown-preferences": "catalog:",
- "eslint-plugin-no-barrel-files": "catalog:",
- "typescript": "catalog:",
- "vite": "catalog:",
- "vite-plus": "catalog:"
- }
+ "packageManager": "pnpm@11.10.0"
}
diff --git a/packages/contracts/openapi-ts.enterprise.config.ts b/packages/contracts/openapi-ts.enterprise.config.ts
index 15fe460e16f..2ed8eb72e61 100644
--- a/packages/contracts/openapi-ts.enterprise.config.ts
+++ b/packages/contracts/openapi-ts.enterprise.config.ts
@@ -61,20 +61,19 @@ const isOpenApiSchema = (value: unknown): value is OpenApiSchema => {
}
const asOpenApiOperation = (value: unknown): OpenApiOperation | undefined => {
- return isObject(value) ? value as OpenApiOperation : undefined
+ return isObject(value) ? (value as OpenApiOperation) : undefined
}
const asOpenApiResponse = (value: unknown): OpenApiResponse | undefined => {
- return isObject(value) ? value as OpenApiResponse : undefined
+ return isObject(value) ? (value as OpenApiResponse) : undefined
}
const asOpenApiMediaType = (value: unknown): OpenApiMediaType | undefined => {
- return isObject(value) ? value as OpenApiMediaType : undefined
+ return isObject(value) ? (value as OpenApiMediaType) : undefined
}
const stripConsoleApiPrefix = (routePath: string) => {
- if (isConsoleApiPath(routePath))
- return routePath.replace('/console/api', '')
+ if (isConsoleApiPath(routePath)) return routePath.replace('/console/api', '')
return routePath
}
@@ -88,8 +87,7 @@ const stripSchemaNamePrefix = (schemaName: string) => {
}
const contractTagSegment = (tag?: string) => {
- if (tag === 'EnterpriseAppDeployConsole')
- return 'AppDeploy'
+ if (tag === 'EnterpriseAppDeployConsole') return 'AppDeploy'
return tag || 'default'
}
@@ -109,13 +107,11 @@ const contractPathSegments = (operation: ContractOperation) => {
}
const hasSchemaLessResponseContent = (operation: OpenApiOperation) => {
- if (!isObject(operation.responses))
- return false
+ if (!isObject(operation.responses)) return false
return Object.values(operation.responses).some((response) => {
const openApiResponse = asOpenApiResponse(response)
- if (!openApiResponse || !isObject(openApiResponse.content))
- return false
+ if (!openApiResponse || !isObject(openApiResponse.content)) return false
return Object.values(openApiResponse.content).some((mediaType) => {
const openApiMediaType = asOpenApiMediaType(mediaType)
@@ -129,8 +125,7 @@ const hasSchemaLessResponseContent = (operation: OpenApiOperation) => {
const stripSchemaLessResponseOperations = (pathItem: OpenApiPathItem) => {
return Object.fromEntries(
Object.entries(pathItem).filter(([method, operation]) => {
- if (!operationMethods.has(method.toLowerCase()))
- return true
+ if (!operationMethods.has(method.toLowerCase())) return true
const openApiOperation = asOpenApiOperation(operation)
return !openApiOperation || !hasSchemaLessResponseContent(openApiOperation)
@@ -147,17 +142,16 @@ const toWords = (value: string) => {
}
const toPascalCase = (words: string[]) => {
- return words.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join('')
+ return words.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join('')
}
const commonWordPrefix = (values: string[]) => {
- const wordLists = values.map(value => value.split('_'))
+ const wordLists = values.map((value) => value.split('_'))
const firstWords = wordLists[0] ?? []
const prefix: string[] = []
for (const [index, word] of firstWords.entries()) {
- if (!wordLists.every(words => words[index] === word))
- break
+ if (!wordLists.every((words) => words[index] === word)) break
prefix.push(word)
}
@@ -166,22 +160,19 @@ const commonWordPrefix = (values: string[]) => {
}
const enumSchemaNameFromValues = (values: unknown[]) => {
- if (values.length === 0 || !values.every(value => typeof value === 'string'))
- return undefined
+ if (values.length === 0 || !values.every((value) => typeof value === 'string')) return undefined
const prefix = commonWordPrefix(values)
- if (prefix.length < 2)
- return undefined
+ if (prefix.length < 2) return undefined
- return toPascalCase(prefix.map(word => word.toLowerCase()))
+ return toPascalCase(prefix.map((word) => word.toLowerCase()))
}
const findSchemaEntry = (
schemas: Record,
schemaName: string,
): [string, OpenApiSchema] | undefined => {
- return Object.entries(schemas)
- .find(([name]) => stripSchemaNamePrefix(name) === schemaName)
+ return Object.entries(schemas).find(([name]) => stripSchemaNamePrefix(name) === schemaName)
}
const enumValuesKey = (values: unknown[]) => JSON.stringify(values)
@@ -201,12 +192,10 @@ const enumSchemaKey = (
propertyName: string,
) => {
const existingKey = valuesToSchemaKey.get(valuesKey)
- if (existingKey)
- return existingKey
+ if (existingKey) return existingKey
const existingEnumEntry = findSchemaEntry(schemas, preferredName)
- if (!existingEnumEntry)
- return preferredName
+ if (!existingEnumEntry) return preferredName
const existingEnumValues = existingEnumEntry[1].enum
if (Array.isArray(existingEnumValues) && enumValuesKey(existingEnumValues) === valuesKey)
@@ -223,18 +212,22 @@ const promoteInlineEnumSchema = (
propertySchema: OpenApiSchema,
valuesToSchemaKey: Map,
) => {
- if (!Array.isArray(propertySchema.enum))
- return
+ if (!Array.isArray(propertySchema.enum)) return
const preferredName = enumSchemaNameFromValues(propertySchema.enum)
- if (!preferredName)
- return
+ if (!preferredName) return
const valuesKey = enumValuesKey(propertySchema.enum)
- const key = enumSchemaKey(schemas, preferredName, valuesKey, valuesToSchemaKey, schemaName, propertyName)
+ const key = enumSchemaKey(
+ schemas,
+ preferredName,
+ valuesKey,
+ valuesToSchemaKey,
+ schemaName,
+ propertyName,
+ )
- if (!schemas[key])
- schemas[key] = reusableEnumSchema(propertySchema)
+ if (!schemas[key]) schemas[key] = reusableEnumSchema(propertySchema)
valuesToSchemaKey.set(valuesKey, key)
properties[propertyName] = {
@@ -247,21 +240,25 @@ const promoteInlineEnumSchema = (
// runtime enum objects from the generated contract.
const promoteReusableEnumSchemasForHeyApi = (document: OpenApiDocument) => {
const schemas = document.components?.schemas
- if (!schemas)
- return
+ if (!schemas) return
const valuesToSchemaKey = new Map()
Object.entries(schemas).forEach(([schemaName, schema]) => {
const properties = schema.properties
- if (!properties)
- return
+ if (!properties) return
Object.entries(properties).forEach(([propertyName, propertySchema]) => {
- if (!isOpenApiSchema(propertySchema))
- return
+ if (!isOpenApiSchema(propertySchema)) return
- promoteInlineEnumSchema(schemas, schemaName, properties, propertyName, propertySchema, valuesToSchemaKey)
+ promoteInlineEnumSchema(
+ schemas,
+ schemaName,
+ properties,
+ propertyName,
+ propertySchema,
+ valuesToSchemaKey,
+ )
})
})
}
@@ -279,8 +276,7 @@ const normalizeEnterpriseOpenApi = () => {
Object.entries(paths)
.filter(([routePath]) => isConsoleApiPath(routePath))
.map(([routePath, pathItem]) => {
- if (!isObject(pathItem))
- return [stripConsoleApiPrefix(routePath), pathItem]
+ if (!isObject(pathItem)) return [stripConsoleApiPrefix(routePath), pathItem]
return [stripConsoleApiPrefix(routePath), stripSchemaLessResponseOperations(pathItem)]
})
@@ -301,14 +297,14 @@ export default defineConfig({
suffix: '.gen',
},
postProcess: [
- {
- command: 'vp',
- args: ['fmt', '{{path}}'],
- },
{
command: 'eslint',
args: ['--fix', '{{path}}/*.ts'],
},
+ {
+ command: 'vp',
+ args: ['fmt', '{{path}}'],
+ },
],
},
parser: {
diff --git a/packages/contracts/package.json b/packages/contracts/package.json
index 01dae1125dd..10d34959349 100644
--- a/packages/contracts/package.json
+++ b/packages/contracts/package.json
@@ -1,8 +1,8 @@
{
"name": "@dify/contracts",
- "type": "module",
"version": "0.0.0-private",
"private": true,
+ "type": "module",
"exports": {
"./marketplace": {
"types": "./marketplace.ts",
@@ -18,7 +18,7 @@
}
},
"scripts": {
- "gen-api-contract": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi && uv run --project ../../api ../../api/dev/generate_fastopenapi_specs.py --output-dir openapi && node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api",
+ "gen-api-contract": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi && uv run --project ../../api ../../api/dev/generate_fastopenapi_specs.py --output-dir openapi && node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && eslint --fix generated/api && vp fmt generated/api",
"gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts",
"test": "vp test openapi-yaml.test.ts",
"type-check": "tsc"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d7ae153abdb..837e613c2d9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -647,6 +647,12 @@ importers:
'@antfu/eslint-config':
specifier: 'catalog:'
version: 9.1.0(fffcbddf9b9e5649ceb44b1934cdee79)
+ '@iconify-json/heroicons':
+ specifier: 'catalog:'
+ version: 1.2.3
+ '@iconify-json/ri':
+ specifier: 'catalog:'
+ version: 1.2.10
'@typescript/native':
specifier: 'catalog:'
version: typescript@7.0.2
diff --git a/sdks/nodejs-client/eslint.config.js b/sdks/nodejs-client/eslint.config.js
index 21ac872f2a8..7a94c300ce9 100644
--- a/sdks/nodejs-client/eslint.config.js
+++ b/sdks/nodejs-client/eslint.config.js
@@ -1,45 +1,45 @@
-import js from "@eslint/js";
-import tsParser from "@typescript-eslint/parser";
-import tsPlugin from "@typescript-eslint/eslint-plugin";
-import { fileURLToPath } from "node:url";
-import path from "node:path";
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import js from '@eslint/js'
+import tsPlugin from '@typescript-eslint/eslint-plugin'
+import tsParser from '@typescript-eslint/parser'
-const tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url));
+const tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url))
const typeCheckedRules =
- tsPlugin.configs["recommended-type-checked"]?.rules ??
+ tsPlugin.configs['recommended-type-checked']?.rules ??
tsPlugin.configs.recommendedTypeChecked?.rules ??
- {};
+ {}
export default [
{
- ignores: ["dist", "node_modules", "scripts"],
+ ignores: ['dist', 'node_modules', 'scripts'],
},
js.configs.recommended,
{
- files: ["src/**/*.ts", "tests/**/*.ts"],
+ files: ['src/**/*.ts', 'tests/**/*.ts'],
languageOptions: {
parser: tsParser,
- ecmaVersion: "latest",
+ ecmaVersion: 'latest',
parserOptions: {
- project: "./tsconfig.json",
+ project: './tsconfig.json',
tsconfigRootDir,
- sourceType: "module",
+ sourceType: 'module',
},
},
plugins: {
- "@typescript-eslint": tsPlugin,
+ '@typescript-eslint': tsPlugin,
},
rules: {
...tsPlugin.configs.recommended.rules,
...typeCheckedRules,
- "no-undef": "off",
- "no-unused-vars": "off",
- "@typescript-eslint/no-unsafe-call": "error",
- "@typescript-eslint/no-unsafe-return": "error",
- "@typescript-eslint/consistent-type-imports": [
- "error",
- { prefer: "type-imports", fixStyle: "separate-type-imports" },
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unsafe-call': 'error',
+ '@typescript-eslint/no-unsafe-return': 'error',
+ '@typescript-eslint/consistent-type-imports': [
+ 'error',
+ { prefer: 'type-imports', fixStyle: 'separate-type-imports' },
],
},
},
-];
+]
diff --git a/sdks/nodejs-client/src/http/client.test.ts b/sdks/nodejs-client/src/http/client.test.ts
index 4dc20877828..421bd7abfb8 100644
--- a/sdks/nodejs-client/src/http/client.test.ts
+++ b/sdks/nodejs-client/src/http/client.test.ts
@@ -28,6 +28,9 @@ const getFetchCall = (
return call as [string, RequestInit | undefined];
};
+const getDuplex = (init: RequestInit | undefined) =>
+ init && "duplex" in init ? init.duplex : undefined;
+
const toHeaderRecord = (headers: HeadersInit | undefined): Record =>
Object.fromEntries(new Headers(headers).entries());
@@ -185,7 +188,7 @@ describe("HttpClient", () => {
authorization: "Bearer test",
"content-type": "multipart/form-data; boundary=test",
});
- expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe(
+ expect(getDuplex(init)).toBe(
"half"
);
expect(init?.body).not.toBe(legacyForm);
@@ -241,7 +244,7 @@ describe("HttpClient", () => {
});
const [, init] = getFetchCall(fetchMock);
- expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe(
+ expect(getDuplex(init)).toBe(
"half"
);
});
@@ -392,7 +395,7 @@ describe("HttpClient", () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, init] = getFetchCall(fetchMock);
- expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe(
+ expect(getDuplex(init)).toBe(
"half"
);
});
diff --git a/sdks/nodejs-client/src/http/client.ts b/sdks/nodejs-client/src/http/client.ts
index c233d9807d0..bc6ecbc3bd3 100644
--- a/sdks/nodejs-client/src/http/client.ts
+++ b/sdks/nodejs-client/src/http/client.ts
@@ -509,7 +509,7 @@ export class HttpClient {
const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = this.settings;
if (query) {
- validateParams(query as Record);
+ validateParams(query);
}
if (isRecord(data) && !Array.isArray(data) && !isFormData(data) && !isPipeableStream(data)) {
diff --git a/sdks/nodejs-client/tests/test-utils.ts b/sdks/nodejs-client/tests/test-utils.ts
index 5d45629e31a..cc48f3ef603 100644
--- a/sdks/nodejs-client/tests/test-utils.ts
+++ b/sdks/nodejs-client/tests/test-utils.ts
@@ -1,6 +1,6 @@
import { vi } from "vitest";
import { HttpClient } from "../src/http/client";
-import type { DifyClientConfig, DifyResponse } from "../src/types/common";
+import type { DifyClientConfig } from "../src/types/common";
type FetchMock = ReturnType;
type RequestSpy = ReturnType;
@@ -31,7 +31,7 @@ export const createHttpClientWithSpies = (
const { client, fetchMock } = createHttpClient(configOverrides);
const request = vi
.spyOn(client, "request")
- .mockResolvedValue({ data: "ok", status: 200, headers: {} } as DifyResponse);
+ .mockResolvedValue({ data: "ok", status: 200, headers: {} });
const requestStream = vi
.spyOn(client, "requestStream")
.mockResolvedValue({ data: null, status: 200, headers: {} } as never);
diff --git a/vite.config.ts b/vite.config.ts
index e8f94d964ae..bb40330eb31 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,11 +1,72 @@
import { defineConfig } from 'vite-plus'
+const lintFiles = '*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,json,jsonc,json5,md,yml,yaml,toml}'
+const formatOnlyFiles = '*.{mdx,css,scss,less,html,vue,svelte,gql,graphql,hbs,handlebars}'
+const lintFix = 'eslint --fix --pass-on-unpruned-suppressions --no-error-on-unmatched-pattern'
+const format = 'vp fmt --no-error-on-unmatched-pattern'
+
+const nonFrontendIgnores = [
+ '.agents/**',
+ '.devcontainer/**',
+ '.github/**',
+ '/*.md',
+ 'api/**',
+ 'codecov.yml',
+ 'depot.json',
+ 'dify-agent/**',
+ 'docker/**',
+ 'docs/**',
+ 'scripts/**',
+ 'sdks/php-client/**',
+ 'sdks/python-client/**',
+]
+
+const generatedIgnores = [
+ '**/.next/**',
+ '**/.vinext/**',
+ '**/coverage/**',
+ '**/dist/**',
+ '**/storybook-static/**',
+ 'e2e/.auth/**',
+ 'e2e/cucumber-report/**',
+ 'eslint-suppressions.json',
+ 'web/next/**',
+ 'web/next-env.d.ts',
+ 'web/public/embed.min.js',
+ 'web/public/pdf.worker.min.mjs',
+ 'web/public/vs/**',
+]
+
+const formatterUnstableInputs = ['web/app/components/develop/template/*.mdx']
+
export default defineConfig({
staged: {
- '*': 'eslint --fix --pass-on-unpruned-suppressions',
+ [lintFiles]: [lintFix, format],
+ [formatOnlyFiles]: format,
},
fmt: {
+ ignorePatterns: [...nonFrontendIgnores, ...generatedIgnores, ...formatterUnstableInputs],
singleQuote: true,
semi: false,
+ sortImports: {
+ groups: [
+ 'type-import',
+ ['type-parent', 'type-sibling', 'type-index', 'type-internal', 'type-subpath'],
+ 'value-builtin',
+ 'value-external',
+ ['value-internal', 'value-subpath'],
+ ['value-parent', 'value-sibling', 'value-index'],
+ ['side_effect_style', 'side_effect'],
+ 'unknown',
+ ],
+ newlinesBetween: false,
+ sortSideEffects: false,
+ },
+ sortPackageJson: true,
+ sortTailwindcss: {
+ functions: ['cn', 'clsx', 'cva', 'tw', 'twMerge'],
+ preserveDuplicates: true,
+ stylesheet: 'web/app/styles/globals.css',
+ },
},
})
diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json
index 01235650ab0..dcb93221acb 100644
--- a/web/.vscode/extensions.json
+++ b/web/.vscode/extensions.json
@@ -1,8 +1,10 @@
{
"recommendations": [
+ "VoidZero.vite-plus-extension-pack",
"bradlc.vscode-tailwindcss",
"kisstkondoros.vscode-codemetrics",
"johnsoncodehk.vscode-tsslint",
- "dbaeumer.vscode-eslint"
+ "dbaeumer.vscode-eslint",
+ "oxc.oxc-vscode"
]
}
diff --git a/web/app/components/explore/try-app/index.tsx b/web/app/components/explore/try-app/index.tsx
index db002d03b64..74cb8235c4b 100644
--- a/web/app/components/explore/try-app/index.tsx
+++ b/web/app/components/explore/try-app/index.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable style/multiline-ternary */
'use client'
import type { FC } from 'react'
import type { App as AppType } from '@/models/explore'
diff --git a/web/app/components/explore/try-app/preview/basic-app-preview.tsx b/web/app/components/explore/try-app/preview/basic-app-preview.tsx
index d45149ff86e..bea8e348ee4 100644
--- a/web/app/components/explore/try-app/preview/basic-app-preview.tsx
+++ b/web/app/components/explore/try-app/preview/basic-app-preview.tsx
@@ -379,7 +379,6 @@ const BasicAppPreview: FC = ({
system_parameters: DEFAULT_SYSTEM_PARAMETERS,
dataSets,
agentConfig: appDetail?.mode === 'agent-chat'
- // eslint-disable-next-line style/multiline-ternary
? ({
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
// remove dataset
diff --git a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx
index ee04025fcbe..b203f4e7190 100644
--- a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx
@@ -1,4 +1,4 @@
-/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+/* eslint-disable ts/no-explicit-any */
import type { AgentNodeType } from '../types'
import type { StrategyParamItem } from '@/app/components/plugins/types'
import type { PanelProps } from '@/types/workflow'
diff --git a/web/app/components/workflow/nodes/assigner/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/assigner/__tests__/integration.spec.tsx
index f1feb0a6696..fcdc6ee519a 100644
--- a/web/app/components/workflow/nodes/assigner/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/assigner/__tests__/integration.spec.tsx
@@ -1,4 +1,4 @@
-/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+/* eslint-disable ts/no-explicit-any */
import type { AssignerNodeOperation, AssignerNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
diff --git a/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx
index 85237f9ba39..61e783b952a 100644
--- a/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx
@@ -1,4 +1,4 @@
-/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+/* eslint-disable ts/no-explicit-any */
import type { ListFilterNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx
index c4f8a41d47b..a169fe9d096 100644
--- a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx
@@ -1,4 +1,4 @@
-/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+/* eslint-disable ts/no-explicit-any */
import type { QuestionClassifierNodeType, Topic } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx
index 38fd595b9e1..32becc265a4 100644
--- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx
+++ b/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx
@@ -1,4 +1,4 @@
-/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+/* eslint-disable ts/no-explicit-any */
import type { ScheduleTriggerNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
diff --git a/web/docs/lint.md b/web/docs/lint.md
index 047a30d25f5..9a3573e0c1e 100644
--- a/web/docs/lint.md
+++ b/web/docs/lint.md
@@ -1,6 +1,23 @@
-# Lint Guide
+# Format and Lint Guide
-We use ESLint and Typescript to maintain code quality and consistency across the project.
+We use Vite+ Oxfmt for formatting, ESLint for code-quality rules, and TypeScript for type safety.
+
+## Format
+
+Format the supported frontend and TypeScript workspace files from the repository root:
+
+```sh
+vp fmt
+```
+
+Check formatting without writing changes:
+
+```sh
+vp fmt --check
+```
+
+The shared formatter options and ignore boundaries live in the root `vite.config.ts`.
+Editor format-on-save must point Oxc at that file so local edits match the commit hook and CI.
## ESLint
@@ -21,8 +38,8 @@ Keep this enabled when linting multiple files.
- [ESLint multi-thread linting blog post]
-**`--fix`**: Automatically fixes auto-fixable rule violations.
-Keep this enabled so that you do not have to care about auto-fixable errors (e.g., formatting issues) and can focus on more important errors.
+**`--fix`**: Automatically fixes code-quality violations such as unused imports and preferred syntax.
+Run `vp fmt` after ESLint fixes so Oxfmt produces the final layout.
Always review the diff before committing to ensure no unintended changes.
**`--quiet`**: Suppresses warnings and only shows errors.
@@ -38,7 +55,8 @@ Treat this as an escape hatch—fix these errors when time permits.
### The Auto-Fix Workflow and Suppression Strategy
To streamline your development process, we recommend configuring your editor to automatically fix lint errors on save.
-As a fallback, the commit hook runs `vp staged`, which applies autofixable ESLint changes to staged files before the commit continues.
+Use Oxfmt for format-on-save and ESLint code actions for lint-only fixes.
+As a fallback, the commit hook runs `vp staged`, which applies ESLint fixes and then formats staged files with `vp fmt`.
To prevent workflow disruptions, these commit hooks are intentionally bypassed when you are merging branches, rebasing, or cherry-picking.
Additionally, we currently track many existing legacy errors in eslint-suppressions.json.
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index bd9a0680404..5efbd72a0d0 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -1,7 +1,13 @@
// @ts-check
import path from 'node:path'
-import antfu, { GLOB_MARKDOWN, GLOB_MARKDOWN_CODE, GLOB_TESTS, GLOB_TS, GLOB_TSX } from '@antfu/eslint-config'
+import antfu, {
+ GLOB_MARKDOWN,
+ GLOB_MARKDOWN_CODE,
+ GLOB_TESTS,
+ GLOB_TS,
+ GLOB_TSX,
+} from '@antfu/eslint-config'
import pluginQuery from '@tanstack/eslint-plugin-query'
import md from 'eslint-markdown'
import tailwindcss from 'eslint-plugin-better-tailwindcss'
@@ -22,6 +28,30 @@ import dify from './plugins/eslint/index.js'
export default antfu(
{
+ stylistic: false,
+ perfectionist: {
+ overrides: {
+ 'perfectionist/sort-imports': 'off',
+ },
+ },
+ jsonc: {
+ overrides: {
+ 'jsonc/space-unary-ops': 'off',
+ },
+ },
+ yaml: {
+ overrides: {
+ 'yaml/block-mapping': 'off',
+ 'yaml/block-sequence': 'off',
+ 'yaml/plain-scalar': 'off',
+ },
+ },
+ toml: {
+ overrides: {
+ 'toml/comma-style': 'off',
+ 'toml/no-space-dots': 'off',
+ },
+ },
react: {
overrides: {
'react/set-state-in-effect': 'error',
@@ -42,11 +72,6 @@ export default antfu(
'test/prefer-lowercase-title': 'off',
},
},
- stylistic: {
- overrides: {
- 'antfu/top-level-function': 'off',
- },
- },
e18e: false,
pnpm: false,
},
@@ -69,29 +94,28 @@ export default antfu(
'no-barrel-files/no-barrel-files': 'error',
},
},
- markdownPreferences.configs.standard,
{
files: [GLOB_MARKDOWN],
- plugins: { md },
+ plugins: {
+ md,
+ 'markdown-preferences': markdownPreferences,
+ },
rules: {
'md/no-url-trailing-slash': 'error',
+ 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/prefer-link-reference-definitions': [
'error',
{
minLinks: 1,
},
],
- 'markdown-preferences/ordered-list-marker-sequence': [
- 'error',
- { increment: 'never' },
- ],
- 'markdown-preferences/definitions-last': 'error',
'markdown-preferences/sort-definitions': 'error',
},
},
{
rules: {
'node/prefer-global/process': 'off',
+ 'unicorn/number-literal-case': 'off',
},
},
{
@@ -111,9 +135,7 @@ export default antfu(
tailwindcss,
},
rules: {
- 'tailwindcss/enforce-consistent-class-order': 'error',
'tailwindcss/no-duplicate-classes': 'error',
- 'tailwindcss/no-unnecessary-whitespace': 'error',
'tailwindcss/no-unknown-classes': 'warn',
},
settings: {
@@ -139,8 +161,8 @@ export default antfu(
{
files: ['i18n/**/*.json'],
rules: {
- 'max-lines': 'off',
'jsonc/sort-keys': 'error',
+ 'max-lines': 'off',
'hyoban/i18n-flat-key': 'error',
'dify/no-extra-keys': 'error',
@@ -152,40 +174,43 @@ export default antfu(
files: [GLOB_TS, GLOB_TSX],
ignores: ['next/**'],
rules: {
- 'no-restricted-imports': ['error', {
- paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS,
- patterns: WEB_RESTRICTED_IMPORT_PATTERNS,
- }],
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS,
+ patterns: WEB_RESTRICTED_IMPORT_PATTERNS,
+ },
+ ],
},
},
{
name: 'dify/service-base-restricted-imports',
files: ['service/**/*.ts', 'service/**/*.tsx'],
rules: {
- 'no-restricted-imports': ['error', {
- paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS,
- patterns: [
- ...WEB_RESTRICTED_IMPORT_PATTERNS,
- ...WEB_SERVICE_BASE_RESTRICTED_IMPORT_PATTERNS,
- ...WEB_SERVICE_FETCH_RESTRICTED_IMPORT_PATTERNS,
- ],
- }],
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS,
+ patterns: [
+ ...WEB_RESTRICTED_IMPORT_PATTERNS,
+ ...WEB_SERVICE_BASE_RESTRICTED_IMPORT_PATTERNS,
+ ...WEB_SERVICE_FETCH_RESTRICTED_IMPORT_PATTERNS,
+ ],
+ },
+ ],
},
},
{
name: 'dify/restricted-local-storage-access',
files: [GLOB_TS, GLOB_TSX],
- ignores: [
- ...GLOB_TESTS,
- 'vitest.setup.ts',
- 'instrumentation-client.ts',
- ],
+ ignores: [...GLOB_TESTS, 'vitest.setup.ts', 'instrumentation-client.ts'],
rules: {
'no-restricted-globals': [
'error',
{
name: 'localStorage',
- message: 'Do not use localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
+ message:
+ 'Do not use localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
],
'no-restricted-properties': [
@@ -193,21 +218,28 @@ export default antfu(
{
object: 'window',
property: 'localStorage',
- message: 'Do not use window.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
+ message:
+ 'Do not use window.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
{
object: 'globalThis',
property: 'localStorage',
- message: 'Do not use globalThis.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
+ message:
+ 'Do not use globalThis.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
],
'no-restricted-syntax': [
'error',
{
- selector: 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]',
+ selector:
+ 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]',
message: 'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
},
],
},
},
-)
+).override('antfu/sort/package-json', {
+ rules: {
+ 'jsonc/sort-keys': 'off',
+ },
+})
diff --git a/web/i18n-config/README.md b/web/i18n-config/README.md
index e173947c0d4..84b5e53c79c 100644
--- a/web/i18n-config/README.md
+++ b/web/i18n-config/README.md
@@ -172,7 +172,7 @@ Translation is handled automatically by Claude Code GitHub Actions. When changes
- **DELETE**: Removed keys that need to be deleted from other languages
1. Runs `i18n:check` to verify the initial sync status.
1. Translates missing/updated keys while preserving placeholders (`{{var}}`, `${var}`, ``) and removes deleted keys.
-1. Runs `lint:fix` to sort JSON keys and `i18n:check` again to ensure everything is synchronized.
+1. Runs `vp fmt` on edited locale files and `i18n:check` again to ensure everything is synchronized.
1. Creates a PR with the translations.
### Manual Trigger
diff --git a/web/package.json b/web/package.json
index ee6b5957ab5..104f75aff8b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,26 +1,14 @@
{
"name": "dify-web",
- "type": "module",
"version": "1.16.0-rc1",
"private": true,
+ "type": "module",
"imports": {
"#i18n": {
"react-server": "./i18n-config/lib.server.ts",
"default": "./i18n-config/lib.client.ts"
}
},
- "browserslist": [
- "last 1 Chrome version",
- "last 1 Firefox version",
- "last 1 Edge version",
- "last 1 Safari version",
- "iOS >=15",
- "Android >= 10",
- "and_chr >= 126",
- "and_ff >= 137",
- "and_uc >= 15.5",
- "and_qq >= 14.9"
- ],
"scripts": {
"analyze": "next experimental-analyze",
"build": "next build",
@@ -29,8 +17,8 @@
"dev:inspect": "next dev --inspect",
"dev:proxy": "dev-proxy --config ./dev-proxy.config.ts --env-file ./.env.local",
"dev:vinext": "vinext dev",
- "gen-doc-paths": "tsx ./scripts/gen-doc-paths.ts",
- "gen-icons": "pnpm --filter @dify/iconify-collections generate && node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/",
+ "gen-doc-paths": "tsx ./scripts/gen-doc-paths.ts && vp fmt types/doc-paths.ts",
+ "gen-icons": "pnpm --filter @dify/iconify-collections generate && node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/ && vp fmt app/components/base/icons/src/",
"i18n:check": "tsx ./scripts/check-i18n.js",
"i18n:migrate-selectors": "tsx ./scripts/migrate-i18n-selectors.ts",
"i18n:prune-unused": "tsx ./scripts/prune-unused-i18n.ts",
@@ -233,5 +221,17 @@
"vite-plus": "catalog:",
"vitest": "catalog:",
"vitest-canvas-mock": "catalog:"
- }
+ },
+ "browserslist": [
+ "last 1 Chrome version",
+ "last 1 Firefox version",
+ "last 1 Edge version",
+ "last 1 Safari version",
+ "iOS >=15",
+ "Android >= 10",
+ "and_chr >= 126",
+ "and_ff >= 137",
+ "and_uc >= 15.5",
+ "and_qq >= 14.9"
+ ]
}