feat(web): tighten overlay migration lint governance

- narrow overlay-migration ignore scope to explicit legacy base file allowlist

- replace directory-level react-refresh disable with allowExportNames for base UI primitives

- extract long lint constants into eslint.constants.mjs for config readability

- add overlay migration guide and link it from lint docs

- refactor dropdown-menu internal popup helper to avoid react-refresh false positives
This commit is contained in:
yyh
2026-03-02 19:56:15 +08:00
parent 967f8caecd
commit 52d02b132e
5 changed files with 147 additions and 26 deletions

View File

@ -35,7 +35,7 @@ type DropdownMenuPopupProps = Required<Pick<DropdownMenuContentProps, 'children'
popupClassName?: string
}
function DropdownMenuPopup({
function renderDropdownMenuPopup({
children,
placement,
sideOffset,
@ -76,17 +76,14 @@ export function DropdownMenuContent({
className,
popupClassName,
}: DropdownMenuContentProps) {
return (
<DropdownMenuPopup
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={className}
popupClassName={popupClassName}
>
{children}
</DropdownMenuPopup>
)
return renderDropdownMenuPopup({
children,
placement,
sideOffset,
alignOffset,
className,
popupClassName,
})
}
type DropdownMenuSubTriggerProps = React.ComponentPropsWithoutRef<typeof Menu.SubmenuTrigger> & {
@ -128,17 +125,14 @@ export function DropdownMenuSubContent({
className,
popupClassName,
}: DropdownMenuSubContentProps) {
return (
<DropdownMenuPopup
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={className}
popupClassName={popupClassName}
>
{children}
</DropdownMenuPopup>
)
return renderDropdownMenuPopup({
children,
placement,
sideOffset,
alignOffset,
className,
popupClassName,
})
}
type DropdownMenuItemProps = React.ComponentPropsWithoutRef<typeof Menu.Item> & {

View File

@ -43,6 +43,8 @@ This command lints the entire project and is intended for final verification bef
If a new rule causes many existing code errors or automatic fixes generate too many diffs, do not use the `--fix` option for automatic fixes.
You can introduce the rule first, then use the `--suppress-all` option to temporarily suppress these errors, and gradually fix them in subsequent changes.
For overlay migration policy and cleanup phases, see [Overlay Migration Guide](./overlay-migration.md).
## Type Check
You should be able to see suggestions from TypeScript in your editor for all open files.

View File

@ -0,0 +1,50 @@
# Overlay Migration Guide
This document tracks the migration away from legacy `portal-to-follow-elem` APIs.
## Scope
- Deprecated API: `@/app/components/base/portal-to-follow-elem`
- Replacement primitives:
- `@/app/components/base/ui/tooltip`
- `@/app/components/base/ui/dropdown-menu`
- `@/app/components/base/ui/popover`
- `@/app/components/base/ui/dialog`
- `@/app/components/base/ui/select`
- Tracking issue: https://github.com/langgenius/dify/issues/32767
## ESLint policy
- `no-restricted-imports` blocks new usage of `portal-to-follow-elem`.
- The rule is enabled for normal source files and test files are excluded.
- Legacy `app/components/base/*` callers are temporarily allowlisted in ESLint config.
- New files must not be added to the allowlist without migration owner approval.
## Migration phases
1. Business/UI features outside `app/components/base/**`
- Migrate old calls to semantic primitives.
- Keep `eslint-suppressions.json` stable or shrinking.
2. Legacy base components in allowlist
- Migrate allowlisted base callers gradually.
- Remove migrated files from allowlist immediately.
3. Cleanup
- Remove remaining suppressions for `no-restricted-imports`.
- Remove legacy `portal-to-follow-elem` implementation.
## Suppression maintenance
- After each migration batch, run:
```sh
pnpm eslint --prune-suppressions --pass-on-unpruned-suppressions <changed-files>
```
- Never increase suppressions to bypass new code.
- Prefer direct migration over adding suppression entries.
## React Refresh policy for base UI primitives
- We keep primitive aliases (for example `DropdownMenu = Menu.Root`) in the same module.
- To avoid IDE noise, `react-refresh/only-export-components` is configured with explicit `allowExportNames` for the base UI primitive surface.
- Do not use file-level `eslint-disable` comments for this policy.

View File

@ -6,6 +6,7 @@ import hyoban from 'eslint-plugin-hyoban'
import sonar from 'eslint-plugin-sonarjs'
import storybook from 'eslint-plugin-storybook'
import dify from './eslint-rules/index.js'
import { BASE_UI_PRIMITIVE_EXPORT_NAMES, OVERLAY_MIGRATION_LEGACY_BASE_FILES } from './eslint.constants.mjs'
// Enable Tailwind CSS IntelliSense mode for ESLint runs
// See: tailwind-css-plugin.ts
@ -147,17 +148,19 @@ export default antfu(
},
{
name: 'dify/base-ui-primitives',
files: ['app/components/base/ui/**/*.ts', 'app/components/base/ui/**/*.tsx'],
files: ['app/components/base/ui/**/*.tsx'],
rules: {
'react-refresh/only-export-components': 'off',
'react-refresh/only-export-components': ['error', {
allowExportNames: BASE_UI_PRIMITIVE_EXPORT_NAMES,
}],
},
},
{
name: 'dify/overlay-migration',
files: [GLOB_TS, GLOB_TSX],
ignores: [
'app/components/base/**',
...GLOB_TESTS,
...OVERLAY_MIGRATION_LEGACY_BASE_FILES,
],
rules: {
'no-restricted-imports': ['error', {

72
web/eslint.constants.mjs Normal file
View File

@ -0,0 +1,72 @@
export const BASE_UI_PRIMITIVE_EXPORT_NAMES = [
'Dialog',
'DialogClose',
'DialogContent',
'DialogDescription',
'DialogTitle',
'DialogTrigger',
'DropdownMenu',
'DropdownMenuCheckboxItem',
'DropdownMenuCheckboxItemIndicator',
'DropdownMenuContent',
'DropdownMenuGroup',
'DropdownMenuGroupLabel',
'DropdownMenuItem',
'DropdownMenuPortal',
'DropdownMenuRadioGroup',
'DropdownMenuRadioItem',
'DropdownMenuRadioItemIndicator',
'DropdownMenuSeparator',
'DropdownMenuSub',
'DropdownMenuSubContent',
'DropdownMenuSubTrigger',
'DropdownMenuTrigger',
'Popover',
'PopoverClose',
'PopoverContent',
'PopoverDescription',
'PopoverTitle',
'PopoverTrigger',
'Select',
'SelectContent',
'SelectGroup',
'SelectGroupLabel',
'SelectItem',
'SelectSeparator',
'SelectTrigger',
'SelectValue',
'Tooltip',
'TooltipContent',
'TooltipProvider',
'TooltipTrigger',
]
export const OVERLAY_MIGRATION_LEGACY_BASE_FILES = [
'app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx',
'app/components/base/chat/chat-with-history/header/operation.tsx',
'app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown.tsx',
'app/components/base/chat/chat-with-history/sidebar/operation.tsx',
'app/components/base/chat/chat/citation/popup.tsx',
'app/components/base/chat/chat/citation/progress-tooltip.tsx',
'app/components/base/chat/chat/citation/tooltip.tsx',
'app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx',
'app/components/base/chip/index.tsx',
'app/components/base/date-and-time-picker/date-picker/index.tsx',
'app/components/base/date-and-time-picker/time-picker/index.tsx',
'app/components/base/dropdown/index.tsx',
'app/components/base/features/new-feature-panel/file-upload/setting-modal.tsx',
'app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx',
'app/components/base/file-uploader/file-from-link-or-local/index.tsx',
'app/components/base/image-uploader/chat-image-uploader.tsx',
'app/components/base/image-uploader/text-generation-image-uploader.tsx',
'app/components/base/modal/modal.tsx',
'app/components/base/prompt-editor/plugins/context-block/component.tsx',
'app/components/base/prompt-editor/plugins/history-block/component.tsx',
'app/components/base/select/custom.tsx',
'app/components/base/select/index.tsx',
'app/components/base/select/pure.tsx',
'app/components/base/sort/index.tsx',
'app/components/base/tag-management/filter.tsx',
'app/components/base/theme-selector.tsx',
'app/components/base/tooltip/index.tsx',
]