Skip to content

feat(vue): support experimental.componentDetection - #396

Merged
IgorShevchik merged 1 commit into
mainfrom
sync/nuxt-3a568b5
Aug 14, 2026
Merged

feat(vue): support experimental.componentDetection#396
IgorShevchik merged 1 commit into
mainfrom
sync/nuxt-3a568b5

Conversation

@IgorShevchik

Copy link
Copy Markdown
Collaborator

Sync with nuxt/ui — ports 3a568b5 (#6731).

experimental.componentDetection narrows the generated theme CSS to the components an app actually uses, plus their dependency closure. It only ever worked under Nuxt; this makes it work in the Vue (unplugin) integration, and fixes several things in the scanner along the way.

The mechanism differs between the two integrations

Worth stating, because it looks like an inconsistency and isn't. Nuxt narrows the @source list to the detected components' template files. The Vue plugin cannot: its templates live inside node_modules, where Tailwind widens a file @source to a scan of the whole parent directory — a narrowed list wouldn't narrow the CSS. So it sources the whole directory and instead blanks the theme of undetected components at write time, the same transformation theme.unstyled performs. The files still exist, because the #build/* aliases and the type imports need them to.

Scanner fixes that came with it

  • kebab-case tags<b24-button> is mandatory for in-DOM templates and was never matched. The kebab alternative only matches as a tag; bare kebab identifiers in scripts and prose would match far too much ordinary text.
  • Prose shadowing — the dependency graph is keyed by file basename, and eight components here have a prose/ twin (Accordion, Badge, Card, Collapsible, FieldGroup, Kbd, Table, Tabs). Letting them into the graph overwrote the regular component's dependency set.
  • Phantom edges and detections — the pattern matches ordinary identifiers, so both are now filtered against real component files. includeComponents names too, and unknown ones warn.
  • Declaration filescomponents.d.ts declares every component ever rendered, so scanning it kept anything used once detected forever.
  • .mts / .mjs / .cjs in the scan glob and the dev watcher.
  • resolveExtraScanDirsscanPackages packages live in node_modules (which the root scan skips) and components.dirs can point outside the root. Build-output ignores are skipped for package dirs, since published packages ship their code in dist/.

Fork adaptations

All plumbing, no logic:

  • getTemplates takes vue as its fourth argument, TemplatePlugin takes componentDir as its second — this fork dropped upstream's unused uiConfig/appConfig threading in earlier ports.
  • Bitrix24UIOptions stops omitting experimental, which is what made the option unreachable from the Vue plugin in the first place.
  • consola rather than @nuxt/kit's logger in utils/components.ts and the Vue plugin — both run outside Nuxt, and that file must not drag @nuxt/kit into a Vue build. templates.ts keeps logger; it is Nuxt-only there.
  • The scan prefix is the literal 'B24' at both call sites (this fork has no configurable component prefix). kebabCase('B24') is 'b24', so <b24-button> and <lazy-b24-tooltip> match — verified, not assumed.
  • Upstream's PageCTA acronym case is n/a: no component here has an acronym in its name.

Two defects this surfaced

playgrounds/vue/tsconfig.app.json had a broken alias. It mapped "#build/b24ui" to "./node_modules/.b24ui-nuxt/b24ui/*" — a wildcard on the value with none on the key, plus a trailing comma. TypeScript treats a pattern without * as an exact match, so #build/b24ui/button never resolved through it. Upstream's commit adds the same alias correctly; taken as the fix.

The docs named a component that does not exist. Both installation guides showed componentDetection: ['Modal', 'Dropdown', 'Popover']. There is no Dropdown in either project — it is DropdownMenu. That used to be harmless; it isn't any more, now that unknown names warn.

One deliberate divergence

Upstream keeps the scanned-extension list as a literal in two places — the scan glob and the builder:watch filter — and had to edit both in this very commit when .mts/.mjs/.cjs were added. An extension in one and not the other means a file that changes the answer never triggers a refresh: no error, no signal. Exported as COMPONENT_DETECTION_EXTENSIONS and used by both. This fork had already split COMPONENT_DETECTION_WATCH_RE out for the same reason.

Tests

test/utils/components.spec.ts (new) — upstream's spec, adapted. Real temporary directories rather than a mocked fs, because half of what is checked is glob and module-resolution behaviour, which a mock would define away.

Two tests are this fork's rather than upstream's:

  • does not build graph edges to names that are not components — upstream's spec does not cover that filter. Verified by deleting it and watching all 38 tests still pass. It needs a stand-in component directory: the real tree has no component that mentions a B24-prefixed non-component, so no fixture under src/ can show the difference.
  • componentDetection examples in the docs name components that exist — the Dropdown defect, now guarded.

Upstream's junk-identifier fixture (URLRL, UUIDUID) doesn't apply with a B24 prefix and is replaced with B24Config/B24Endpoint. The first attempt used B24Theme — which turns out to be a real component here, so the test failed and said so.

test/utils/templates.spec.ts — the extension test now iterates the shared constant instead of a five-item literal, and a second test holds the glob to the same constant so the two can't drift back apart with every test green.

Mutations tried, each red: drop the prose/** ignore; drop the graph-edge filter; drop the validComponents filter; stop ignoring *.d.ts; drop the kebab alternative; make the graph follow the scan prefix; apply build-output ignores to package dirs; point the watcher at a literal; point the glob at a literal; put Dropdown back in the docs.

End-to-end check

Built playgrounds/vue with componentDetection: true — a real exercise of resolveExtraScanDirs, since that playground's components.dirs points at ../nuxt/app/components, outside the Vite root:

[success] Bitrix24 UI detected 43 components in use (including dependencies)

node_modules/.b24ui-nuxt/b24ui/color-picker.ts comes out with every slot blanked; button.ts does not.

The CSS saving there is modest, and worth stating plainly rather than rounding up: 599,544 → 596,929 bytes, about 0.4%. That app uses 43 components, and most of the bundle is shared utilities and design tokens rather than per-component classes. The feature does what it says; the headline number depends entirely on how much of the library an app leaves unused.

Verify (CI=true)

lint · typecheck · test · build — all green. Tests 6410 passed | 6 skipped across 280 files.

Ledger: cursor → 3a568b5, 241 entries; the 9b08a84 and edf73d3 entries are reconciled to PR #395 / c4d291c.


Generated by Claude Code

Sync with nuxt/ui 3a568b5 (#6731). Component detection narrows the generated
theme CSS to the components an app actually uses; it only ever worked under
Nuxt.

The mechanism has to differ between the integrations. Nuxt narrows the
`@source` list to the detected components' template files. The Vue plugin
can't: its templates live inside node_modules, where Tailwind widens a file
`@source` to a scan of the whole parent directory. It sources the directory
and blanks the theme of undetected components at write time instead — the
files still have to exist for the `#build` aliases and the type imports.

Scanner fixes that came with it: kebab-case tags, which are mandatory for
in-DOM templates and were never matched; prose components overwriting the
dependency set of the eight regular components they share a basename with;
graph edges and detections to names that are not components; `components.d.ts`,
which declares every component ever rendered; `.mts`/`.mjs`/`.cjs`; and
scanning `scanPackages` packages and `components.dirs` outside the root.

Two defects this surfaced here: `playgrounds/vue/tsconfig.app.json` mapped
`#build/b24ui` with a wildcard on the value but none on the key, so
`#build/b24ui/button` never resolved through it; and both installation guides
named a `Dropdown` component that does not exist (it is `DropdownMenu`) — now
a build-time warning rather than nothing.

The scanned-extension list is shared between the glob and the dev watcher
rather than spelled twice. An extension in one and not the other means a file
that changes the answer never triggers a refresh, with no error and no signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb
@IgorShevchik
IgorShevchik merged commit 530b961 into main Aug 14, 2026
1 check passed
@IgorShevchik
IgorShevchik deleted the sync/nuxt-3a568b5 branch August 14, 2026 08:27
IgorShevchik added a commit that referenced this pull request Aug 22, 2026
Ports three upstream commits, contiguous in `v4` (§6 4b):
`d6c3802a` (nuxt/ui #6848) · `f62aa143` (no-op) · `aa5f4af0`.

**`d6c3802a`, against `Range`.** Upstream's `Slider` is this fork's `Range`; the
reka primitives keep their own names, so only the wrapper differs. This is the
second commit in that family — `f3c2ac21` (#431) already moved `useFormField`'s
derived attributes onto the thumb. What still did not get there was **the
caller's own** `aria-*`: `v-bind="rootProps"` put them on `SliderRoot`, which is
not the element with `role="slider"`. A screen reader announcing the thumb read
Reka UI's placeholder name `Thumb` while the author's `aria-label` sat on a
wrapper that announces nothing.

The fix stops inheriting attributes and routes six of them by hand. With one
thumb they go to the thumb and everything else stays on the root; the `'Thumb'`
fallback applies only when the caller gave neither a label nor a `labelledby`.
With several thumbs they stay on the root, because Reka UI already names each
thumb by position (`Minimum`/`Maximum`, `Value n of m`) and one repeated label
would make them indistinguishable — the root takes `role="group"` instead, unless
the caller supplied a `role`, which wins.

Our `Range.vue` matched upstream's pre-image on both thumb lines and on the
`SliderRoot` binding, so the diff applies unchanged apart from `ui`→`b24ui` and
`UTooltip`→`B24Tooltip`. `pick`/`omit` already exist in `../utils`.

**`f62aa143`** is a no-op: upstream's release bookkeeping. This fork has its own
release train and its `CHANGELOG.md` is generated from our commits.

**`aa5f4af0`** turns upstream's "coming" badges into what shipped. The shape
transfers; every value is fork-specific, and each was checked against this
repo's tags with `git tag --contains` rather than assumed to match upstream's
timing:

- `experimental.componentDetection`: `Soon` → **`2.12+`**, not their `4.11+` —
  `530b9616` (#396) is in no tag before `v2.12.0`
- `input-rating.md`: `New` dropped — `d08b5830` (#283) shipped in `v2.10.0`, two
  releases back, so the badge was stale here too
- `splitter.md`, `progress-group.md`: badge **added**, not edited — both first
  appear in `v2.12.0` and our pages never carried one
- the `::note` badges upstream removes were never added here: `07f3fe8d` ported
  those notes without them, precisely because this commit deletes them

Left alone: `calendar.md`, `drawer.md`, `modal.md`, `slideover.md` and the two
`With external scroll element` badges — this fork's own markers on sections
upstream's commit does not mention.

Verified by mutation, not by a passing suite. Upstream's 14 `aria` cases and 2
render cases are ported; with `Range.vue` reverted and the tests kept, **11 of
the 14 fail** plus all 3 affected render cases. The 3 that pass either way pin
behaviour that was already right — the `'Thumb'` fallback, not grouping an
unlabelled range, leaving non-`aria` attributes on the root — and are kept as
regression pins rather than counted as evidence.

Badges checked in the built navigation data: `splitter` and `progress-group`
carry `New`, `input-rating` no longer does, `scroll-area` is untouched, and
`2.12+` renders on the Vue installation page.

Gate with `CI=true`: `dev:prepare` · `lint` · `typecheck` · `test` (7016 passed,
6 skipped, 308 files) · `build` · `docs:generate`.

Ledger: cursor → `aa5f4af0` (upstream HEAD), three entries, parity snapshot
refreshed (one line, zero package differences).

Co-authored-by: Shevchik Igor <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants