Compare commits

...
15 changed files with 123 additions and 33 deletions
+3 -1
View File
@@ -49,6 +49,7 @@ jobs:
.nvmrc
vite.config.ts
eslint.config.mjs
eslint.migrated-rules.mjs
.vscode/**
.github/workflows/autofix.yml
.github/workflows/style.yml
@@ -166,9 +167,10 @@ jobs:
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
- name: Frontend lint autofix
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
run: |
vp lint --fix --quiet || true
vp exec eslint --fix --concurrency=2 --prune-suppressions --quiet || true
- name: Format frontend files
+3 -2
View File
@@ -164,6 +164,7 @@ jobs:
.nvmrc
vite.config.ts
eslint.config.mjs
eslint.migrated-rules.mjs
.vscode/**
.github/workflows/autofix.yml
.github/workflows/style.yml
@@ -183,9 +184,9 @@ jobs:
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .eslintcache
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'packages/dify-ui/eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'eslint.migrated-rules.mjs', 'packages/dify-ui/eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'packages/dify-ui/eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'eslint.migrated-rules.mjs', 'packages/dify-ui/eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
- name: Style check
if: steps.changed-files.outputs.any_changed == 'true'
+1
View File
@@ -29,6 +29,7 @@
// Lint fix
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit",
"source.fixAll.eslint": "explicit"
},
-11
View File
@@ -977,12 +977,6 @@
"web/app/components/base/auto-height-textarea/index.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
},
"react-hooks/rules-of-hooks": {
"count": 1
},
"react/rules-of-hooks": {
"count": 1
}
},
"web/app/components/base/badge/index.tsx": {
@@ -5657,11 +5651,6 @@
"count": 3
}
},
"web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/list.spec.tsx": {
"unused-imports/no-unused-vars": {
"count": 1
}
},
"web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
+15 -1
View File
@@ -1,8 +1,16 @@
// @ts-check
import antfu, { GLOB_MARKDOWN } from '@antfu/eslint-config'
import antfu, {
GLOB_JS,
GLOB_JSX,
GLOB_MARKDOWN,
GLOB_MARKDOWN_CODE,
GLOB_TS,
GLOB_TSX,
} from '@antfu/eslint-config'
import md from 'eslint-markdown'
import markdownPreferences from 'eslint-plugin-markdown-preferences'
import { rulesMigratedToOxlint } from './eslint.migrated-rules.mjs'
const GENERATED_IGNORES = [
'**/storybook-static/',
@@ -48,6 +56,7 @@ export default antfu(
'!web/**',
'!e2e/**',
'!eslint.config.mjs',
'!eslint.migrated-rules.mjs',
'!package.json',
'!pnpm-workspace.yaml',
'!vite.config.ts',
@@ -93,6 +102,11 @@ export default antfu(
'unicorn/number-literal-case': 'off',
},
},
{
files: [GLOB_JS, GLOB_JSX, GLOB_TS, GLOB_TSX],
ignores: [GLOB_MARKDOWN_CODE],
rules: rulesMigratedToOxlint,
},
).override('antfu/sort/package-json', {
rules: {
'jsonc/sort-keys': 'off',
+13
View File
@@ -0,0 +1,13 @@
// @ts-check
/** @satisfies {import('eslint').Linter.RulesRecord} */
export const rulesMigratedToOxlint = {
'no-unmodified-loop-condition': 'off',
'react/rules-of-hooks': 'off',
'unicorn/prefer-dom-node-text-content': 'off',
'unicorn/prefer-includes': 'off',
'unicorn/prefer-node-protocol': 'off',
'unicorn/prefer-string-starts-ends-with': 'off',
'unused-imports/no-unused-imports': 'off',
'unused-imports/no-unused-vars': 'off',
}
+4 -4
View File
@@ -8,10 +8,10 @@
"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"
"lint": "vp lint && eslint --cache --concurrency=auto",
"lint:ci": "vp lint && eslint --cache --cache-strategy content --concurrency 2",
"lint:fix": "vp lint --fix && eslint --cache --concurrency=auto --fix && vp fmt",
"lint:quiet": "vp lint --quiet && eslint --cache --concurrency=auto --quiet"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:",
-1
View File
@@ -95,7 +95,6 @@ export default antfu(
tailwindcss,
},
rules: {
'tailwindcss/no-duplicate-classes': 'error',
'tailwindcss/no-deprecated-classes': 'error',
'tailwindcss/no-unknown-classes': 'error',
},
+47 -4
View File
@@ -1,8 +1,10 @@
import { defineConfig } from 'vite-plus'
const lintFiles = '*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,json,jsonc,json5,md,yml,yaml,toml}'
const codeFiles = '*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'
const eslintOnlyFiles = '*.{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 oxlintFix = 'vp lint --fix --no-error-on-unmatched-pattern'
const eslintFix = 'eslint --fix --pass-on-unpruned-suppressions --no-error-on-unmatched-pattern'
const format = 'vp fmt --no-error-on-unmatched-pattern'
const nonFrontendIgnores = [
@@ -37,13 +39,54 @@ const generatedIgnores = [
'web/public/vs/**',
]
const lintIgnores = [
...nonFrontendIgnores,
...generatedIgnores,
'.claude/**',
'cli/**',
'packages/dify-ui/**',
'sdks/**',
'web/public/**',
'web/types/doc-paths.ts',
]
const formatterUnstableInputs = ['web/app/components/develop/template/*.mdx']
export default defineConfig({
staged: {
[lintFiles]: [lintFix, format],
[codeFiles]: [oxlintFix, eslintFix, format],
[eslintOnlyFiles]: [eslintFix, format],
[formatOnlyFiles]: format,
},
lint: {
categories: {
correctness: 'off',
},
ignorePatterns: lintIgnores,
plugins: ['eslint', 'react', 'unicorn'],
rules: {
'no-unmodified-loop-condition': 'error',
'no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
fix: {
imports: 'safe-fix',
variables: 'off',
},
ignoreRestSiblings: true,
vars: 'all',
varsIgnorePattern: '^_',
},
],
'react/rules-of-hooks': 'error',
'unicorn/prefer-dom-node-text-content': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-node-protocol': 'error',
'unicorn/prefer-string-starts-ends-with': 'error',
},
},
fmt: {
ignorePatterns: [...nonFrontendIgnores, ...generatedIgnores, ...formatterUnstableInputs],
singleQuote: true,
@@ -65,7 +108,7 @@ export default defineConfig({
sortPackageJson: true,
sortTailwindcss: {
functions: ['cn', 'clsx', 'cva', 'tw', 'twMerge'],
preserveDuplicates: true,
preserveDuplicates: false,
stylesheet: 'web/app/styles/globals.css',
},
},
@@ -7,6 +7,7 @@ import ConfigModalFormFields from '../form-fields'
vi.mock('react-i18next', async () => {
const { withSelectorKey, withSelectorKeyProps } = await import('@/test/i18n-mock')
// oxlint-disable-next-line no-unused-vars -- Keep React available to the hoisted JSX mock factory.
const React = await import('react')
return {
useTranslation: () => ({
@@ -70,9 +70,9 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
<AppTypeSelectorItem
key={mode}
type={mode}
checked={Boolean(value.length > 0 && value?.indexOf(mode) !== -1)}
checked={value.length > 0 && value.includes(mode)}
onClick={() => {
if (value?.indexOf(mode) !== -1) onChange(value?.filter((v) => v !== mode) ?? [])
if (value.includes(mode)) onChange(value.filter((v) => v !== mode))
else onChange([...(value || []), mode])
}}
/>
@@ -32,7 +32,7 @@ const AutoHeightTextarea = ({
}: IProps & {
ref?: React.RefObject<HTMLTextAreaElement>
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
// oxlint-disable-next-line react/rules-of-hooks -- Use the provided ref when available and create a local fallback otherwise.
const ref = outerRef || useRef<HTMLTextAreaElement>(null)
const doFocus = () => {
@@ -30,6 +30,7 @@ describe('parameter-extractor/extract-parameter/list', () => {
// TODO: Fix this test.
// This test only failed in the merge queue, and I don't know why.
it.skip('edits and deletes parameters through the real item and modal flow', async () => {
// oxlint-disable-next-line no-unused-vars -- Keep the interaction setup with the temporarily skipped test.
const user = userEvent.setup()
const handleChange = vi.fn()
const { rerender } = render(
+24 -5
View File
@@ -1,6 +1,6 @@
# Format and Lint Guide
We use Vite+ Oxfmt for formatting, ESLint for code-quality rules, and TypeScript for type safety.
We use Vite+ Oxfmt for formatting, Vite+ Oxlint for selected performance-sensitive code-quality rules, ESLint for the remaining code-quality rules, and TypeScript for type safety.
## Format
@@ -19,7 +19,26 @@ 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
## Lint
Run the complete lint pipeline from the repository root:
```sh
pnpm lint
```
This runs `vp lint` first for the selected rules migrated to native Oxlint, then ESLint for all remaining rules. Oxlint configuration lives in the root `vite.config.ts`; migrated rules stay disabled in the corresponding ESLint configs so they are not evaluated twice. Package-specific lint scopes that are not part of the migration continue to use ESLint.
Use the matching scripts when applying fixes or suppressing warning output:
```sh
pnpm lint:fix
pnpm lint:quiet
```
Do not migrate the entire ESLint configuration. Before moving another rule, measure it with `TIMING=all npx eslint --quiet`, verify that Oxlint has a compatible native implementation, and compare diagnostics and fixes before disabling the ESLint rule.
### ESLint
### Common Flags
@@ -39,7 +58,7 @@ Keep this enabled when linting multiple files.
- [ESLint multi-thread linting blog post]
**`--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.
Use `pnpm lint:fix` to apply Oxlint and ESLint fixes, then produce the final layout with Oxfmt.
Always review the diff before committing to ensure no unintended changes.
**`--quiet`**: Suppresses warnings and only shows errors.
@@ -55,8 +74,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.
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`.
Use Oxfmt for format-on-save, with Oxc and ESLint code actions for their respective lint fixes.
As a fallback, the commit hook runs `vp staged`, which applies Oxlint fixes, applies the remaining 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.
+8 -1
View File
@@ -2,6 +2,8 @@
import path from 'node:path'
import antfu, {
GLOB_JS,
GLOB_JSX,
GLOB_MARKDOWN,
GLOB_MARKDOWN_CODE,
GLOB_TESTS,
@@ -16,6 +18,7 @@ import jsxA11y from 'eslint-plugin-jsx-a11y'
import markdownPreferences from 'eslint-plugin-markdown-preferences'
import noBarrelFiles from 'eslint-plugin-no-barrel-files'
import storybook from 'eslint-plugin-storybook'
import { rulesMigratedToOxlint } from '../eslint.migrated-rules.mjs'
import {
GENERATED_IGNORES,
HYOBAN_PREFER_TAILWIND_ICONS_OPTIONS,
@@ -118,6 +121,11 @@ export default antfu(
'unicorn/number-literal-case': 'off',
},
},
{
files: [GLOB_JS, GLOB_JSX, GLOB_TS, GLOB_TSX],
ignores: [GLOB_MARKDOWN_CODE],
rules: rulesMigratedToOxlint,
},
{
files: ['**/*.ts', '**/*.tsx'],
settings: {
@@ -135,7 +143,6 @@ export default antfu(
tailwindcss,
},
rules: {
'tailwindcss/no-duplicate-classes': 'error',
'tailwindcss/no-unknown-classes': 'warn',
},
settings: {