Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,98 @@
]
}
},
{
// Part-authoring rules: shipped source only. Tests define throwaway
// components and contexts that don't need to follow them.
"files": ["packages/react/src/**/*.{ts,tsx}"],
"rules": {
"bazza/forward-ref-named": "error",
"bazza/part-namespace": "error"
}
},
{
"files": [
"packages/react/src/**/*.tsx",
"packages/react/src/**/*-context.ts"
],
"rules": { "bazza/use-client": "error" }
},
{
"files": [
"packages/react/src/**/*.data-attrs.ts",
"packages/react/src/**/*.data-attributes.ts",
"packages/react/src/**/*.css-vars.ts"
],
"rules": { "bazza/data-attrs-enum": "error" }
},
{
"files": ["packages/react/src/**/*.test.{ts,tsx}"],
"rules": {
"bazza/forward-ref-named": "off",
"bazza/part-namespace": "off",
"bazza/use-client": "off"
}
},

// --- Allowlists -----------------------------------------------------------
// Files that broke a rule when the rule was added. Each file is exempt from
// that one rule until it's fixed. Shrink these lists: fix the file, then
// delete its line. Never add a file here to get a new change through; use a
// reasoned `oxlint-disable-next-line` instead.

{
// `as const` objects the docs can't read. Convert each to an enum.
"files": [
"packages/react/src/combobox/clear/clear.data-attrs.ts",
"packages/react/src/combobox/icon/icon.data-attrs.ts",
"packages/react/src/combobox/input-wrapper/input-wrapper.data-attrs.ts",
"packages/react/src/combobox/input/input.data-attrs.ts",
"packages/react/src/combobox/item-indicator/item-indicator.data-attrs.ts",
"packages/react/src/combobox/item-label/item-label.data-attrs.ts",
"packages/react/src/combobox/item/item.data-attrs.ts",
"packages/react/src/combobox/positioner/positioner.data-attrs.ts",
"packages/react/src/combobox/scroll-arrow/scroll-arrow.data-attrs.ts",
"packages/react/src/combobox/surface/surface.data-attrs.ts",
"packages/react/src/context-menu/icon/icon.data-attrs.ts",
"packages/react/src/context-menu/scroll-arrow/scroll-arrow.data-attrs.ts",
"packages/react/src/dropdown-menu/icon/icon.data-attrs.ts",
"packages/react/src/dropdown-menu/scroll-arrow/scroll-arrow.data-attrs.ts",
"packages/react/src/internal/popup-menu/components/icon/icon.data-attrs.ts",
"packages/react/src/internal/popup-menu/components/scroll-arrow/scroll-arrow.data-attrs.ts",
"packages/react/src/select/icon/icon.data-attrs.ts",
"packages/react/src/select/item-indicator/item-indicator.data-attrs.ts",
"packages/react/src/select/item-label/item-label.data-attrs.ts",
"packages/react/src/select/item/item.data-attrs.ts",
"packages/react/src/select/positioner/positioner.data-attrs.ts",
"packages/react/src/select/scroll-arrow/scroll-arrow.data-attrs.ts",
"packages/react/src/select/surface/surface.data-attrs.ts",
"packages/react/src/select/trigger/trigger.data-attrs.ts",
"packages/react/src/select/value/value.data-attrs.ts"
],
"rules": { "bazza/data-attrs-enum": "off" }
},
{
// Exported parts with no namespace of their own.
"files": [
"packages/react/src/internal/popup-menu/components/list/list.tsx",
"packages/react/src/internal/popup-menu/data-first/data-list.tsx",
"packages/react/src/video-player/components/captions-menu/captions-menu.tsx",
"packages/react/src/video-player/components/playback-rate-menu/playback-rate-menu.tsx",
"packages/react/src/video-player/components/quality-menu/quality-menu.tsx",
"packages/react/src/video-player/components/seek-slider/seek-slider.tsx",
"packages/react/src/video-player/components/volume-slider/volume-slider.tsx"
],
"rules": { "bazza/part-namespace": "off" }
},
{
// Context modules without `'use client'`.
"files": [
"packages/react/src/combobox/input-wrapper/input-wrapper-context.ts",
"packages/react/src/internal/popup-menu/contexts/graft-point-context.ts",
"packages/react/src/internal/popup-menu/contexts/menu-tree-resolver-context.ts"
],
"rules": { "bazza/use-client": "off" }
},
{
// A class method calls `useRefWithInit`.
"files": ["packages/react/src/internal/listbox/store/ListboxStore.ts"],
Expand Down
15 changes: 14 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,18 @@
"linter": {
"enabled": false
}
}
},
"overrides": [
{
// Parts are reached through their family's namespace
// (`DropdownMenu.Item`), never a default import. See "Component
// Pattern" in packages/react/AGENTS.md.
"includes": [
"packages/react/src/**",
"!packages/react/src/**/*.test.ts",
"!packages/react/src/**/*.test.tsx"
],
"linter": { "rules": { "style": { "noDefaultExport": "error" } } }
}
]
}
14 changes: 14 additions & 0 deletions packages/react/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ src/
## Component Pattern

```typescript
'use client'

import { useRender } from '@base-ui/react/use-render'
import * as React from 'react'
import type { ComponentProps } from '../../utils/types.js'

export interface MyComponentState extends Record<string, unknown> {
Expand Down Expand Up @@ -58,6 +61,13 @@ export namespace MyComponent {
}
```

Every part follows this shape, and lint checks it:

- **`'use client'` first.** Parts and `*-context.ts` modules use hooks, so a React Server Components app can only import them across a client boundary (`bazza/use-client`, fixable with `bun run check:fix`).
- **A named render function.** Parts don't set `displayName`, so React DevTools and error messages show the function's name. Write `forwardRef(function MyComponent(…))`, or declare a named function and pass it by name, as generic parts do with `forwardRef(MyComponentImpl) as <…>` (`bazza/forward-ref-named`).
- **A namespace with the part's types.** Consumers write `DropdownMenu.Item.Props`, and the docs type tables read the same names. Export `namespace MyComponent { Props }`, plus `State` when the part passes `state` to `useRender` (`bazza/part-namespace`).
- **No default exports.** Parts are reached through their family's namespace (`DropdownMenu.Item`). Biome's `noDefaultExport` is on for `src`.

## State to Data Attributes

Automatic conversion: `highlighted: true` becomes `data-highlighted=""`.
Expand All @@ -79,6 +89,8 @@ return useRender({ render, ref, state, stateAttributesMapping, props, defaultTag

Filename: `<name>.data-attrs.ts`

Export only `enum`s named `*DataAttributes`. The docs type generator (`apps/web/scripts/build-types-meta.ts`) reads nothing else, so an `as const` object renders an empty `DataAttrsTable`. To share an engine part's attributes, re-export its enum by name (`export { PopupMenuPopupDataAttributes } from '…'`), never `export *` (`bazza/data-attrs-enum`).

```typescript
export enum DropdownMenuItemDataAttributes {
/** Present when the item is highlighted. */
Expand All @@ -92,6 +104,8 @@ export enum DropdownMenuItemDataAttributes {

Filename: `<name>.css-vars.ts`

Export only `enum`s named `*CssVars`, for the same reason as data attributes (`bazza/data-attrs-enum`).

```typescript
export enum DropdownMenuPositionerCssVars {
/** @type {number} */
Expand Down
8 changes: 8 additions & 0 deletions tooling/lint/bazza-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* Each rule's message says why the rule exists, what to write instead, and
* which section of `packages/react/AGENTS.md` describes the convention.
*/
import { dataAttrsEnum } from './rules/data-attrs-enum.mjs'
import { disableNeedsReason } from './rules/disable-needs-reason.mjs'
import { forwardRefNamed } from './rules/forward-ref-named.mjs'
import { partNamespace } from './rules/part-namespace.mjs'
import { useClient } from './rules/use-client.mjs'

export {
directiveProblem,
Expand All @@ -16,6 +20,10 @@ export { bazzaRuleNames } from './rules/rule-names.mjs'
export default {
meta: { name: 'bazza' },
rules: {
'data-attrs-enum': dataAttrsEnum,
'disable-needs-reason': disableNeedsReason,
'forward-ref-named': forwardRefNamed,
'part-namespace': partNamespace,
'use-client': useClient,
},
}
Loading
Loading