From 86e154f47f5017abb60b4ade2c723c70d6c67386 Mon Sep 17 00:00:00 2001 From: lajczi Date: Tue, 21 Jul 2026 22:55:38 +0200 Subject: [PATCH 01/12] feat(lint-plugin-nuxt-ecosystem): complete Nuxt UI rules --- .../NUXT_UI_RESEARCH.md | 70 +++++++++++++ packages/lint-plugin-nuxt-ecosystem/README.md | 23 +++++ .../lint-plugin-nuxt-ecosystem/package.json | 1 + .../lint-plugin-nuxt-ecosystem/src/index.ts | 92 ++++++++++++++++- .../no-conflicting-state-props/index.md | 16 +++ .../no-conflicting-state-props/index.test.ts | 20 ++++ .../no-conflicting-state-props/index.ts | 41 ++++++++ .../nuxt-ui/no-deprecated-components/index.ts | 2 +- .../no-deprecated-model-modifiers/index.md | 2 + .../index.test.ts | 1 + .../no-deprecated-model-modifiers/index.ts | 28 +++++- .../nuxt-ui/no-invalid-icon-name/index.md | 12 +++ .../no-invalid-icon-name/index.test.ts | 18 ++++ .../nuxt-ui/no-invalid-icon-name/index.ts | 54 ++++++++++ .../no-invalid-prop-combinations/index.md | 13 +++ .../index.test.ts | 25 +++++ .../no-invalid-prop-combinations/index.ts | 91 +++++++++++++++++ .../nuxt-ui/no-invalid-prop-values/index.md | 20 ++++ .../no-invalid-prop-values/index.test.ts | 24 +++++ .../nuxt-ui/no-invalid-prop-values/index.ts | 98 +++++++++++++++++++ .../src/rules/nuxt-ui/prefer-link-to/index.md | 11 +++ .../nuxt-ui/prefer-link-to/index.test.ts | 14 +++ .../src/rules/nuxt-ui/prefer-link-to/index.ts | 27 +++++ .../rules/nuxt-ui/prefer-open-model/index.md | 12 +++ .../nuxt-ui/prefer-open-model/index.test.ts | 14 +++ .../rules/nuxt-ui/prefer-open-model/index.ts | 38 +++++++ .../nuxt-ui/prefer-semantic-colors/index.md | 7 ++ .../prefer-semantic-colors/index.test.ts | 17 ++++ .../nuxt-ui/prefer-semantic-colors/index.ts | 38 +++++++ .../rules/nuxt-ui/prefer-u-dialog/index.md | 13 +++ .../nuxt-ui/prefer-u-dialog/index.test.ts | 14 +++ .../rules/nuxt-ui/prefer-u-dialog/index.ts | 8 ++ .../src/rules/nuxt-ui/prefer-u-icon/index.md | 8 ++ .../rules/nuxt-ui/prefer-u-icon/index.test.ts | 14 +++ .../src/rules/nuxt-ui/prefer-u-icon/index.ts | 29 ++++++ .../src/rules/nuxt-ui/prefer-u-kbd/index.md | 9 ++ .../rules/nuxt-ui/prefer-u-kbd/index.test.ts | 14 +++ .../src/rules/nuxt-ui/prefer-u-kbd/index.ts | 8 ++ .../rules/nuxt-ui/prefer-u-progress/index.md | 9 ++ .../nuxt-ui/prefer-u-progress/index.test.ts | 14 +++ .../rules/nuxt-ui/prefer-u-progress/index.ts | 8 ++ .../rules/nuxt-ui/prefer-u-separator/index.md | 10 ++ .../nuxt-ui/prefer-u-separator/index.test.ts | 14 +++ .../rules/nuxt-ui/prefer-u-separator/index.ts | 8 ++ .../rules/nuxt-ui/require-avatar-alt/index.md | 9 ++ .../nuxt-ui/require-avatar-alt/index.test.ts | 17 ++++ .../rules/nuxt-ui/require-avatar-alt/index.ts | 27 +++++ .../require-form-control-label/index.md | 20 ++++ .../require-form-control-label/index.test.ts | 22 +++++ .../require-form-control-label/index.ts | 62 ++++++++++++ .../nuxt-ui/require-form-field-name/index.md | 13 +++ .../require-form-field-name/index.test.ts | 17 ++++ .../nuxt-ui/require-form-field-name/index.ts | 29 ++++++ .../rules/nuxt-ui/require-form-state/index.md | 15 +++ .../nuxt-ui/require-form-state/index.test.ts | 17 ++++ .../rules/nuxt-ui/require-form-state/index.ts | 26 +++++ .../require-icon-button-label/index.md | 8 ++ .../require-icon-button-label/index.test.ts | 17 ++++ .../require-icon-button-label/index.ts | 30 ++++++ .../nuxt-ui/require-overlay-title/index.md | 9 ++ .../require-overlay-title/index.test.ts | 18 ++++ .../nuxt-ui/require-overlay-title/index.ts | 32 ++++++ .../nuxt-ui/require-popover-content/index.md | 13 +++ .../require-popover-content/index.test.ts | 14 +++ .../nuxt-ui/require-popover-content/index.ts | 25 +++++ .../nuxt-ui/require-tooltip-content/index.md | 8 ++ .../require-tooltip-content/index.test.ts | 17 ++++ .../nuxt-ui/require-tooltip-content/index.ts | 25 +++++ .../src/rules/nuxt-ui/require-u-app/index.md | 15 +++ .../rules/nuxt-ui/require-u-app/index.test.ts | 17 ++++ .../src/rules/nuxt-ui/require-u-app/index.ts | 43 ++++++++ .../src/rules/nuxt-ui/utils.ts | 84 ++++++++++++++++ 72 files changed, 1652 insertions(+), 6 deletions(-) create mode 100644 packages/lint-plugin-nuxt-ecosystem/NUXT_UI_RESEARCH.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-conflicting-state-props/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-conflicting-state-props/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-conflicting-state-props/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-icon-name/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-icon-name/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-icon-name/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-combinations/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-combinations/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-combinations/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-values/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-values/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-invalid-prop-values/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-link-to/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-link-to/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-link-to/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-open-model/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-open-model/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-open-model/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-semantic-colors/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-semantic-colors/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-semantic-colors/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-dialog/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-dialog/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-dialog/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-icon/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-icon/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-icon/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-kbd/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-kbd/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-kbd/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-progress/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-progress/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-progress/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-separator/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-separator/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-separator/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-field-name/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-field-name/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-field-name/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-state/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-state/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-state/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-icon-button-label/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-icon-button-label/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-icon-button-label/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-overlay-title/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-overlay-title/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-overlay-title/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-popover-content/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-popover-content/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-popover-content/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-tooltip-content/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-tooltip-content/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-tooltip-content/index.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-u-app/index.md create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-u-app/index.test.ts create mode 100644 packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-u-app/index.ts diff --git a/packages/lint-plugin-nuxt-ecosystem/NUXT_UI_RESEARCH.md b/packages/lint-plugin-nuxt-ecosystem/NUXT_UI_RESEARCH.md new file mode 100644 index 0000000..1163799 --- /dev/null +++ b/packages/lint-plugin-nuxt-ecosystem/NUXT_UI_RESEARCH.md @@ -0,0 +1,70 @@ +# Nuxt UI lint research + +Research date: 2026-07-21. The implementation was checked against the live Nuxt UI v4.6.1 +documentation at `ui.nuxt.com`, including the complete component index, component API +tables, composable entry points, theming guidance, accessibility guidance, and the v3-to-v4 +migration guide. + +## Sources and enforceable conventions + +| Area | Documented convention | Lint implementation | +|---|---|---| +| Design system | Prefer semantic colors (`text-muted`, `bg-elevated`, `border-default`, semantic status aliases) so themes and dark mode remain portable. | `prefer-semantic-colors` | +| Application provider | `UApp` wraps the application and provides config, direction, toast, tooltip, and overlay providers. | `require-u-app` | +| Component props | `color`, `variant`, and `size` are documented finite unions. | `no-invalid-prop-values` (static values only, extensible for custom variants) | +| Form control labels | Inputs and choice controls expose labels/legends or are intended to be associated through `UFormField`. | `require-form-control-label` | +| Icons | Iconify names are passed through `UIcon` or component icon props. | `prefer-u-icon`; existing component-preference rules | +| Icon names | Docs use the `i--` naming convention, while Vue component bindings remain supported. | `no-invalid-icon-name` | +| Buttons | Icon-only buttons are supported, but still need an accessible name. | `require-icon-button-label` | +| Links | `ULink` wraps `NuxtLink`; link-capable components document `to`. | `prefer-link-to` | +| Avatars | `alt` is forwarded to the image and also supplies fallback initials. | `require-avatar-alt` | +| Forms | A root `UForm` receives reactive `state`; nested forms explicitly inherit parent state. | `require-form-state` with a `nested` exception | +| Form errors | `UForm` routes errors to `UFormField` through `name` or `error-pattern`. | `require-form-field-name`, scoped to fields inside `UForm` | +| Overlay state | Modal, drawer, slideover, popover, and tooltip expose the named `open` model. | `prefer-open-model` | +| Controlled state | `default-open`/`default-value` are initializers for uncontrolled components and should not be mixed with controlled state. | `no-conflicting-state-props` | +| Dialog accessibility | Modal-like overlays expose title props/slots backed by Reka UI dialog semantics. | `require-overlay-title` with prop, slot, and ARIA alternatives | +| Tooltip content | `UTooltip` documents `text` and `#content` as its content sources. | `require-tooltip-content` | +| Popover content | `UPopover`'s `content` prop configures positioning; visible content belongs in `#content`. | `require-popover-content` | +| Cross-prop constraints | `UFileUpload` restricts button/area, layout and position combinations; `UAccordion collapsible` applies to single mode. | `no-invalid-prop-combinations` | +| v4 migration | Three component changes and the `nullify` to `nullable` model-modifier rename. | Existing deprecation rules; object-form `model-modifiers` coverage added | + +## False-positive policy + +- Dynamic bindings are accepted where static correctness cannot be proven. +- `data-raw` remains the local escape hatch for deliberately native/component-level + markup and hard-coded brand styling. +- Rules that need application-wide knowledge (for example, verifying the single root + `UApp` provider) are not implemented as per-file template rules. +- `loading-auto`, `UForm` submit handlers, and structural replacements such as arbitrary + `div` to `UCard` are not required: the documentation supports valid cases without them, + so enforcing them would be guesswork. +- `require-u-app` intentionally scopes itself to the Nuxt `app.vue` root and ignores + component/page/layout files named `App.vue`. +- Nested `UForm` and standalone `UFormField` patterns are explicitly preserved because + both are documented use cases. +- Form controls with dynamic labels are accepted; the label rule only reports when no + direct ARIA/label API or `UFormField` association is visible in the template. + +## Third-stage coverage audit + +The complete component index was audited by category. The table records both what is +enforced and why additional rules were not invented for the remaining documentation. + +| Documentation category | Audited surface | Decision | +|---|---|---| +| Layout | `App`, `Container`, `Error`, `Footer`, `Header`, `Main`, `Sidebar`, `Theme` | `require-u-app` covers the provider boundary; the remaining components are structural wrappers whose correct nesting depends on the application layout. | +| Element | `Alert`, `Avatar`, `AvatarGroup`, `Badge`, `Banner`, `Button`, `Calendar`, `Card`, `Chip`, `Collapsible`, `FieldGroup`, `Icon`, `Kbd`, `Progress`, `Separator`, `Skeleton` | Covered by semantic colors, icon-name validation, accessible labels, avatar alt text, finite prop validation, and native-component preferences where the API is unambiguous. | +| Form | `Checkbox`, `CheckboxGroup`, `ColorPicker`, `FileUpload`, `Form`, `FormField`, `Input`, `InputDate`, `InputMenu`, `InputNumber`, `InputRating`, `InputTags`, `InputTime`, `Listbox`, `PinInput`, `RadioGroup`, `Select`, `SelectMenu`, `Slider`, `Switch`, `Textarea` | Covered by form state/name/label rules, controlled-state conflicts, static API unions, and file-upload relationship checks. Item schemas and runtime validation remain dynamic. | +| Data | `Accordion`, `Carousel`, `Empty`, `Marquee`, `ScrollArea`, `Table`, `Timeline`, `Tree`, `User` | Accordion's documented `type`/`collapsible` relationship is enforced. The rest are driven by runtime data, slots, or render functions; a per-file rule would guess at valid use cases. | +| Navigation | `Breadcrumb`, `CommandPalette`, `FooterColumns`, `Link`, `NavigationMenu`, `Pagination`, `Stepper`, `Tabs` | Link and button navigation semantics are covered. Menu, tab, step, and command item labels usually come from typed arrays or slots and cannot be validated from the template safely. | +| Overlay | `ContextMenu`, `Drawer`, `DropdownMenu`, `Modal`, `Popover`, `Slideover`, `Toast`, `Tooltip` | Open-state naming, overlay titles, tooltip/popover content, and provider setup are covered. Menu item contents and programmatic toast payloads require script/runtime analysis. | +| Page | `AuthForm`, `BlogPost`, `BlogPosts`, `ChangelogVersion`, `ChangelogVersions`, `Page`, `PageAnchors`, `PageAside`, `PageBody`, `PageCard`, `PageColumns`, `PageCTA`, `PageFeature`, `PageGrid`, `PageHeader`, `PageHero`, `PageLinks`, `PageList`, `PageLogos`, `PageSection`, `PricingPlan`, `PricingPlans`, `PricingTable` | These are composition primitives; enforcing a particular page hierarchy or content shape would be opinionated rather than a documented invariant. | +| Dashboard | `DashboardGroup`, `DashboardNavbar`, `DashboardPanel`, `DashboardResizeHandle`, `DashboardSearch`, `DashboardSearchButton`, `DashboardSidebar`, `DashboardSidebarCollapse`, `DashboardSidebarToggle`, `DashboardToolbar` | State persistence, panel sizing, and responsive composition are runtime concerns. No additional template-only rule met the false-positive bar. | +| AI Chat | `ChatMessage`, `ChatMessages`, `ChatPalette`, `ChatPrompt`, `ChatPromptSubmit`, `ChatReasoning`, `ChatShimmer`, `ChatTool` | APIs depend on Vercel AI SDK message/tool state and asynchronous status; static templates cannot establish correctness. | +| Editor | `Editor`, `EditorDragHandle`, `EditorEmojiMenu`, `EditorMentionMenu`, `EditorSuggestionMenu`, `EditorToolbar` | TipTap extensions, editor content, and menu lifecycle are configured in script; no safe template-only invariant remains. | +| Content | `ContentNavigation`, `ContentSearch`, `ContentSearchButton`, `ContentSurround`, `ContentToc` | Content routes and generated navigation are resolved by Nuxt Content at runtime/build time. | +| Color mode / i18n | `ColorModeAvatar`, `ColorModeButton`, `ColorModeImage`, `ColorModeSelect`, `ColorModeSwitch`, `LocaleSelect` | Integration correctness depends on installed modules and runtime locale/color-mode configuration; component APIs themselves expose no deterministic missing prop. | + +This audit deliberately favors a smaller set of high-signal rules over rules that merely +encode a preferred page architecture or require static knowledge of user-defined item +arrays. diff --git a/packages/lint-plugin-nuxt-ecosystem/README.md b/packages/lint-plugin-nuxt-ecosystem/README.md index 813b89a..b8a8bd1 100644 --- a/packages/lint-plugin-nuxt-ecosystem/README.md +++ b/packages/lint-plugin-nuxt-ecosystem/README.md @@ -6,6 +6,8 @@ ESLint and [Oxlint](https://oxc.rs) rules for the [Nuxt module ecosystem](https://nuxt.com/modules). The plugin currently supports `@nuxt/ui` with plans to expand. Every rule is based on the respective module's official documentation and recommendations. +The Nuxt UI source review, enforceable conventions, and false-positive policy are recorded +in [`NUXT_UI_RESEARCH.md`](./NUXT_UI_RESEARCH.md). If you are a developer of a popular module and want to add rules for it, please reach out. @@ -45,12 +47,33 @@ These rules are compatible with Oxlint, although some may not work perfectly due | Rule | Description | |---|---| +| [`@nustack/nuxt-ui/no-conflicting-state-props`](https://github.com/Zerya-Dev/nustack/blob/master/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/no-conflicting-state-props/index.md) | Disallow controlled state together with `default-open` or `default-value`. | | [`@nustack/nuxt-ui/prefer-u-button`](https://github.com/Zerya-Dev/nustack/blob/master/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-button/index.md) | Prefer `` over raw `