From b54107e9b97a97209b867832a738362031a69725 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Sun, 12 Jul 2026 22:59:00 +0800 Subject: [PATCH] chore: migrate formatting to vp fmt (#38705) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/workflows/autofix.yml | 27 +++-- .github/workflows/style.yml | 8 ++ .github/workflows/translate-i18n-claude.yml | 4 +- .vscode/settings.example.json | 65 +++++------ AGENTS.md | 2 +- cli/AGENTS.md | 5 +- cli/ARD.md | 47 +++++--- cli/eslint.config.mjs | 76 +++++++----- cli/src/commands/AGENTS.md | 1 + eslint-suppressions.json | 13 +-- eslint.config.mjs | 50 +++++--- package.json | 50 ++++---- .../contracts/openapi-ts.enterprise.config.ts | 92 +++++++-------- packages/contracts/package.json | 4 +- pnpm-lock.yaml | 6 + sdks/nodejs-client/eslint.config.js | 44 +++---- sdks/nodejs-client/src/http/client.test.ts | 9 +- sdks/nodejs-client/src/http/client.ts | 2 +- sdks/nodejs-client/tests/test-utils.ts | 4 +- vite.config.ts | 63 +++++++++- web/.vscode/extensions.json | 4 +- web/app/components/explore/try-app/index.tsx | 1 - .../try-app/preview/basic-app-preview.tsx | 1 - .../agent/__tests__/integration.spec.tsx | 2 +- .../assigner/__tests__/integration.spec.tsx | 2 +- .../__tests__/integration.spec.tsx | 2 +- .../__tests__/integration.spec.tsx | 2 +- .../trigger-schedule/__tests__/panel.spec.tsx | 2 +- web/docs/lint.md | 28 ++++- web/eslint.config.mjs | 108 ++++++++++++------ web/i18n-config/README.md | 2 +- web/package.json | 32 +++--- 32 files changed, 471 insertions(+), 287 deletions(-) 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" + ] }