Skip to content

feat: update dependencies, migrate to Rstest and enable React Compiler - #339

Merged
ipwallarm merged 16 commits into
mainfrom
chore/update-deps
Oct 5, 2026
Merged

ipwallarm merged 16 commits into
mainfrom
chore/update-deps

Conversation

@ispashkov

@ispashkov ispashkov commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR updates dependencies in stages (patch → minor → major), moves unit tests from Vitest to Rstest, and turns on React Compiler. It is rebased on main v2.0.0. Each step is its own commit, so review is easiest commit by commit.

Commit What
build(deps): bump patch-level dependencies About 33 packages: biome 2.5.15, rsbuild/rspack patches, testing-library, TanStack router/virtual and others
build(deps): bump minor-level dependencies React 19.3, Storybook 10.6.1 + storybook-react-rsbuild 3.6.0, Rsbuild 2.2 / Rspack 2.2, Playwright 1.63 (CI image v1.63.0-noble), TanStack Table 9.2.4, Ark UI 5.39.2, zod 4.6, turbo 2.11 (agentGuidance: false)
feat(dropdown-menu): add onRequestDismiss and keep FilterInput menus open on hand-off zag-js 1.43.1+ now closes a menu straight away if it opens from another menu's item selection. This commit adds an optional onRequestDismiss prop to DropdownMenu, and FilterInput's field → operator → value chain uses it to keep the next menu open
build(deps): bump major-level dependencies @rslib/core 1.0.3, @types/node 24.19
test: migrate unit tests from Vitest to Rstest @rstest/core 0.12.3; @wallarm-org/vitest-config → @wallarm-org/rstest-config; vitest, @vitest/*, vite and its override are removed. Test count is the same before and after
perf: enable React Compiler Uses the native SWC compiler (pluginReact({ reactCompiler: true })) in the library build, Storybook and playground. Rstest gets it via tools.swc. Also adds a pnpm compiler:audit script
fix(build): stop publishing test helpers, Figma bindings and stories Removes test/story/figma/mock/fixture files from dist. Figma bindings are now type-checked through tsconfig.figma.json
build(deps): remove unused postcss-import Tailwind v4 handles @import itself
perf(table): make Table compatible with React Compiler Removes every 'use no memo' from Table. Getters are now read through a subscription to table.store (useTableValue). Consumer render functions are called on every render (useRenderEveryTime)
test: wait for zag's next-frame focus moves instead of asserting synchronously Fixes 7 test races (FilterDropdown, Select, Table) that also exist on main. Zag moves focus and registers Escape on the next requestAnimationFrame, but the tests asserted before that frame. No assertion was removed
build(deps): bump turbo to 2.11.7 and @modelcontextprotocol/sdk to 1.32.0 Two minor bumps released after the earlier stages
test(table): make large mock data deterministic createLargeSecurityEvents used Math.random() for Requests, so two Table screenshots changed on every run
fix(overlay): keep popup positioner z-index in sync with its layer Fixes a regression from @zag-js/dismissable 1.43.3 (via Ark UI 5.39.2): Select/DropdownMenu opened inside a nested Dialog/Drawer rendered under it. zag now registers the layer after popper's one-time z-index read, so the positioner stayed at 50 while the nested overlay is at 70. New utils/syncLayerZIndex mirrors the content's z-index onto the positioner (MutationObserver); wired into DropdownMenu, Select, Popover and Calendar

Notes for reviewers / consumers

  • Release type: feat, for the new DropdownMenu.onRequestDismiss prop and the compiled output.
  • Compiled output: published components are compiled by React Compiler and import react/compiler-runtime, which ships with React 19 and so matches the ^19 peer range. If a consumer mutates an array or object prop in place (for example, pushes into options) and re-renders, the component may not pick up the change. Pass a new reference instead.
  • .d.ts paths: with rslib 1.0, relative paths inside .d.ts now end in .js / /index.js. Every rewritten path points to an existing file, and the JS output did not change.
  • Table:
    • The public API is unchanged: dist/components/Table/index.d.ts and dist/index.d.ts are byte-identical to before. Internal Table/lib changed, for example getPinningStyles is gone.
    • The DOM is unchanged: 87 Table story states render identical HTML before and after.
    • In a 500-row jsdom benchmark, select-all is about 20–40% slower and single-row selection is a bit faster. Rows and cells re-render the same number of times as before.
  • Code from main v2.0.0: CodeEditor autocomplete, NumericBadge and Stepper all work with Rstest and the compiler (compiler:audit: 1346 functions compiled, 89 bailouts). Nothing from them leaks into dist.
  • Skipped:
    • @types/node 26: runtime is Node 24.
    • @figma/code-connect 2: v2 drops the React parser that all *.figma.tsx files are written for.
    • vitest 5: replaced by Rstest.
    • @zag-js/file-utils 1.44: kept in step with the zag version that Ark UI pins.
  • Not available in Rstest: a UI mode and vi.dynamicImportSettled. The test:ui scripts are removed.
  • Pre-existing, not fixed here: the design-system typecheck script runs tsc --noEmit on a solution-style tsconfig, so it checks nothing in src. Use tsc --build tsconfig.app.json --noEmit instead (clean). Worth a separate follow-up.

Test plan

  • pnpm lint, real tsc --build tsconfig.app.json, pnpm test:run (also with --retry 0), pnpm build and build-storybook pass locally after the rebase onto v2.0.0
  • Storybook smoke test in Chromium (22 checks): Ark UI popups land in the right place on Storybook 10.6; Table sort, selection, expand and virtualization work; CodeEditor autocomplete and NumericBadge render
  • dorny/test-reporter reads Rstest's JUnit output (checked on an earlier CI run)
  • CI: screenshots regenerated with Chromium 1.63 — only 2 Table baselines changed, both from random mock data (now deterministic)
  • CI: full E2E (3 shards) green, including the nested Dialog/Drawer stacking tests

🤖 Generated with Claude Code

ispashkov and others added 12 commits October 2, 2026 21:52
Catalog: @biomejs/biome 2.5.7->2.5.15, @rsbuild/core 2.1.10->2.1.13,
@rsbuild/plugin-react 2.1.0->2.1.1, @rspack/core 2.1.8->2.1.10,
@testing-library/react 16.3.2->16.3.3, @testing-library/user-event
14.6.3->14.6.7, @types/node 24.10.4->24.10.15, @types/react-dom
19.2.4->19.2.7, @vitest/coverage-v8 4.1.10->4.1.11, vitest 4.1.10->4.1.11.
Fix stray trailing comma on the @tanstack/react-table catalog entry.

Root: @commitlint/cli 21.2.1->21.2.3, @commitlint/config-conventional
21.2.0->21.2.3, @semantic-release/github 12.0.9->12.0.10, chalk
6.0.0->6.0.1, tsx 4.23.10->4.23.15, turbo 2.10.8->2.10.13; raise the
postcss override floor >=8.5.16 -> >=8.5.28 so the direct bump resolves.

design-system: @ark-ui/react 5.38.0->5.38.1, @figma/code-connect
1.5.2->1.5.3, @internationalized/date 3.12.3->3.12.4, @shikijs/types and
shiki 4.4.2->4.4.3, @tanstack/react-router 1.170.21->1.170.41,
@tanstack/react-virtual 3.14.9->3.14.13, @testing-library/jest-dom
7.0.0->7.0.1, highlight.js 11.11.1->11.11.2, postcss 8.5.26->8.5.28,
react-resizable-panels 4.12.3->4.12.4, tsx 4.23.10->4.23.15.

playground: @tanstack/react-router 1.170.21->1.170.41,
@tanstack/router-devtools 1.167.1->1.167.2, @tanstack/router-plugin
1.168.26->1.168.42.
mcp: @modelcontextprotocol/sdk 1.30.0->1.30.1.
configs: @rsdoctor/rspack-plugin 1.6.1->1.6.4, @testing-library/jest-dom
7.0.0->7.0.1, @vitest/ui 4.1.10->4.1.11, tsx 4.23.10->4.23.15.

@ark-ui/react held at 5.38.1: 5.38.2 (zag-js 1.43.1) breaks the
FilterInput field->operator menu chaining in 17 unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog: @playwright/test 1.62.1->1.63.0, @rsbuild/core 2.1.13->2.2.11,
@rspack/core 2.1.10->2.2.8, @tanstack/react-table 9.0.0->9.2.4,
@types/react 19.2.18->19.3.0, @types/react-dom 19.2.7->19.3.0,
react/react-dom 19.2.8->19.3.0, storybook 10.4.6->10.6.1, zod 4.4.3->4.6.5.
Root: @semantic-release/npm 13.1.5->13.2.0,
conventional-changelog-conventionalcommits 10.2.1->10.4.0,
turbo 2.10.13->2.11.6, vite 8.2.1->8.3.2 (override floor >=8.3.2),
overrides @types/react 19.3.0, @storybook/react(-dom-shim) 10.6.1.
design-system: @ark-ui/react 5.38.1->5.39.2, @zag-js/file-utils
1.43.0->1.43.3, @shikijs/types/shiki 4.4.3->4.5.0,
@storybook/addon-docs/addon-themes 10.4.6->10.6.1,
storybook-react-rsbuild 3.3.4->3.6.0, highlight.js 11.11.2->11.12.0,
react-resizable-panels 4.12.4->4.14.1, tailwind-merge 3.6.0->3.7.0;
react/react-dom added as catalog devDependencies so tests run on the
catalog React instead of an auto-installed peer.
mcp: @modelcontextprotocol/sdk 1.30.1->1.31.0.
vitest-config: jsdom 30.0.1->30.1.1.
Playwright container image v1.62.1-noble -> v1.63.0-noble (CI, compose,
docs).

Fixes needed by the bumps:
- vitest setup: zag-js highlights menu items on pointermove only in the
  "pointer" modality and skips moves at an unchanged position; user-event
  fires every jsdom event at (0, 0). A capture listener shows zag's
  document-level tracker a move at a fresh position first.
- FileUpload test helper: jsdom 30.1 keeps wrapper implementations in
  private fields; read them through jsdom's idl utils.
- turbo.json: agentGuidance false, so turbo 2.11 stops writing a managed
  block into AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…open on hand-off

Since zag-js 1.43.1 (pulled in by @ark-ui/react 5.39) a menu that mounts
while its content node already exists registers its dismissable layer
synchronously. During FilterInput's field -> operator -> value hand-off
the next menu therefore lands on the layer stack above the menu that is
closing, and removing that closing layer dismisses every layer above
it, so the freshly opened menu closed immediately (in a real browser
too).

- DropdownMenu: new optional onRequestDismiss prop, forwarded to Ark UI
  Menu.Root. It is called when another layer asks the menu to dismiss;
  event.preventDefault() keeps the menu open.
- FilterInput: field, operator, value (flat/nested) and date menus pass
  keepOpenOnMenuHandoff, which ignores dismiss requests coming from
  another FilterInput menu. FilterInput owns its menus' open state, so
  nothing else changes for consumers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- @rslib/core 0.23.2 -> 1.0.3 (catalog; now shares @rsbuild/core 2.2.11,
  rsbuild-plugin-dts 1.0.3). JS output byte-identical; bundleless .d.ts
  relative specifiers now carry .js extensions (redirect.dts.extension)
- postcss-import 16.1.1 -> 17.0.0 (design-system dev; not loaded by any
  PostCSS config, dist CSS byte-identical)
- @types/node 24.10.15 -> 24.19.0 (catalog; stays on the Node 24 runtime major)

Skipped: vitest/@vitest/* 5.x (replaced by Rstest in next stage),
@figma/code-connect 2.x (retires the React parser used by all
*.figma.tsx bindings), @types/node 26.x (runtime is Node 24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace Vitest with Rstest (Rspack-based) across the monorepo.

Versions:
- add @rstest/core 0.12.3, @rstest/coverage-v8 0.12.3, jsdom 30.1.1 (catalog)
- remove vitest 4.1.11, @vitest/coverage-v8 4.1.11, @vitest/ui 4.1.11
- remove root vite 8.3.2 devDependency and pnpm.overrides.vite
  (nothing non-optional depends on vite any more; pnpm why vite/vitest -r is empty)
- @rsbuild/core stays a single 2.2.11 copy (shared with @rstest/core)

Config:
- packages/configs/vitest-config -> packages/configs/rstest-config
  (@wallarm-org/rstest-config), shipped as TS source with no build step,
  so the turbo dependsOn vitest-config#build edge is gone
- base config writes JUnit to ./test-results/junit.xml (plus github-actions
  annotations) when CI is set, since Rstest has no --outputFile; CI now runs
  plain `pnpm test:run`
- vitest.config.ts -> rstest.config.ts in design-system, playground, mcp;
  vitest.setup.ts -> rstest.setup.ts with jest-dom matchers via expect.extend
  and an explicit Testing Library cleanup; matcher types in
  src/testUtils/rstest-env.d.ts
- design-system loads jsdom natively (testEnvironment.prebundle: false) so the
  FileUpload helpers reach the same jsdom instance through its idl utils
- coverage provider v8 (@rstest/coverage-v8); test:ui removed (no Rstest UI
  mode), use `pnpm test` (rstest watch)
- turbo test inputs: rstest.*, scripts/**, the shared config sources; env CI;
  test-results/junit.xml added to outputs
- tsconfig unit types vitest/globals -> @rstest/core/globals; fix the
  "src/testUtils/**" include that TS 7 rejects (TS5010), which also clears
  the ~1.7k jest-dom matcher type errors in tsconfig.unit.json
- biome ignores coverage/ output

Tests:
- `from 'vitest'` -> `from '@rstest/core'`, vi.* -> rs.* (242 files)
- async vi.mock factories -> importActual import attributes plus sync
  factories (CodeEditor.stories.test, ParameterPath.test)
- vi.dynamicImportSettled -> macrotask flush (index.languages.test)
- hook callbacks that returned rs.* values use block bodies (Rstest treats a
  hook return value as cleanup)

Before (Vitest 4.1.11): design-system 234 passed + 2 skipped files (236),
2664 passed + 2 expected-fail + 37 skipped tests (2703); mcp 3 files,
36 passed; playground 0 files.
After (Rstest 0.12.3): design-system 234 passed + 2 skipped files (236),
2666 passed (it.fails counted as passed) + 37 skipped tests (2703); mcp
3 files, 36 passed; playground 0 files. Per-file pass/skip counts match exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compile all React code with React Compiler, using Rspack's native (SWC)
implementation. It is enabled with pluginReact({ reactCompiler: true }) in
the shared rsbuild config (Storybook, playground) and in the design-system
rslib build, plus tools.swc.jsc.transform.reactCompiler in the shared Rstest
react config (Rstest builds for the node target, where pluginReact skips the
option), so unit tests exercise the compiled components. Target is React 19
(the peer range), so the published dist imports react/compiler-runtime from
the consumer's React; no new runtime dependency.

- 1231 of 1873 dist modules now ship compiled output (646 components,
  581 icons, 4 hooks/utils).
- Bailout audit (new `pnpm compiler:audit`, babel-plugin-react-compiler
  1.0.0 logger) reports 1317 functions compiled and 91 safe bailouts in
  48 files (mostly refs-during-render). The Babel audit and the shipped
  SWC port disagree on 4 files, so treat the audit as an approximation.
- Breadcrumbs keys separators by position instead of mutating a module
  counter during render, so it is now compiled.
- Table gets 'use no memo' in 25 files: 24 that read TanStack row/column/
  header/cell getters (stable object identities, changing results) plus
  TableBodyVirtualizedCore (mutable virtualizer prop); without it sorting,
  selection and expansion rendered stale state (4 failing unit tests).

Consumer note: the published components are now memoized, so a consumer
that mutates an array or object prop in place (e.g. pushing into
`options`) and re-renders may no longer see the update. Pass new
references instead.

Size of dist JS goes from 3379.8 kB to 4429.3 kB raw and from 1143.1 kB
to 1453.4 kB gzip (sum of per-file gzip). The playground bundle goes from
3008.3 kB to 3018.1 kB gzip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The published @wallarm-org/design-system dist shipped files that are not
part of the runtime package and import devDependencies:

- 35 *.figma.tsx Code Connect bindings (as .js + .d.ts, importing
  @figma/code-connect)
- FileUpload.test.helpers.js (importing @rstest/core / Testing Library);
  the entry glob only excluded *.test.{ts,tsx}
- theme/Pixel.stories.tsx, copied verbatim by output.copy
- story-only fixtures: FilterInput/stories/* and Table/mocks

Exclude all test/spec/e2e/story/figma/__tests__/stories/story-content/
mocks/testUtils files from both the rslib source entry and
tsconfig.app.json (which drives the emitted .d.ts), and skip *.stories.*
when copying src/theme. dist goes from 3780 to 3702 files (npm pack: 3782
-> 3704); only the files above are dropped and nothing left in dist
imports them.

The Figma bindings were previously type-checked only as a side effect of
the rslib dts build. Add tsconfig.figma.json and run it from the
package's typecheck script so they stay type-checked in CI. The
`figma connect` CLI reads source files, so it is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing references postcss-import: every postcss.config.js delegates to
@wallarm-org/tailwind-config/postcss, which only registers
@tailwindcss/postcss (it inlines @import itself), and no rsbuild, rslib,
Storybook config, script or CI step loads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TanStack Table v9 keeps row/column/header/cell objects at a stable
identity while their getter results change, so compiled components that
call getters in render memoized stale values. Instead of opting the
Table tree out of compilation, route every render-time getter read
through helpers in Table/lib:

- useTableValue(read): scalar reads (or TanStack-memoized references)
  via useSyncExternalStore over table.store, the v9-documented
  useSelector(table.store, selector) pattern.
- withTableState(table.state, compute): internal output that cannot be
  compared by value (filler cells, skeleton rows, drag overlay, settings
  menu column lists); re-runs on every table state change.
- useRenderEveryTime(render): consumer callbacks the Table calls inline
  during render (a function header, the cell renderer,
  meta.renderMenuAction, renderExpandedRow). They may call hooks or read
  context, so they must never sit in a compiler memo scope: a cached
  call skips the consumer's hooks ("Rendered fewer hooks than expected")
  or keeps stale context. The hook lives in its own file with a
  file-level 'use no memo'; a compiled caller never skips a hook call.
  renderExpandedRow additionally renders through RenderCallback, so its
  hooks belong to a component of their own.

Also:
- useColumnPinning replaces getPinningStyles/isLastPinnedLeft reads in
  header and body cells; useColumnDnd/useRowDnd read pinning, resizing
  and sub-rows reactively (useMasterCell already takes primitives).
- TableBodyVirtualizedCore receives virtualItems/totalSize/scrollMargin/
  measureElement from its (compiler-skipped) useVirtualizer hosts instead
  of the mutable virtualizer instance.
- TableRow reads "is last row" as a primitive instead of subscribing to
  the row-model array; the select-all header scans rows once.
- New Table.reactivity.test.tsx drives sort x3, select-all/deselect with
  the action bar, expand/collapse of the last row, column sizing, pin/
  unpin and settings-menu hide/show repeatedly, plus consumer callbacks
  with hooks/context (function header across isLoading toggles, context
  in header, renderMenuAction and renderExpandedRow).

Removed all 25 file-level 'use no memo' directives under
components/Table; the only directive left is the intentional one in
lib/useRenderEveryTime.ts. Compiler audit: repo-wide 1318 -> 1347
functions compiled. Remaining Table bailouts are pre-existing and
unrelated to getters: StickyGroupParent and TableBody (ref access in
render), TableBodyVirtualized{Container,Window} (useVirtualizer, skipped
by design), TableSettingsMenu (compiler invariant), useWindowScrollRoot.

This makes Table compile with render behaviour at parity; it does not
reduce row/cell re-renders on table state changes, since the table
context changes identity on every state change.

Public API unchanged: dist/components/Table/index.d.ts and the root
index.d.ts are byte-identical; only internal .d.ts files differ. Rendered
DOM unchanged: normalized outerHTML of 35 Table story scenarios
(including sort, selection, expansion, pinning, resizing, settings menu
and virtualization interactions) is identical before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hronously

Zag (select, menu) moves focus and registers its dismiss listeners inside
requestAnimationFrame, one frame after the state change that flips
aria-expanded and fires onOpenChange. Several tests asserted focus right
after that state change, or acted on the menu before its first frame:

- FilterDropdown "Escape ... returns focus to the trigger" asserted
  toHaveFocus() synchronously after waitFor(aria-expanded=false); the
  trigger is only focused on the next frame.
- FilterDropdown "Footer Clear ... Space" and the four Select
  SelectSearchInput keyboard tests focused an element by hand before the
  menu's initial-focus frame, which could then pull focus back to the
  list between keydown and keyup.
- FilterDropdown "starts every session with an empty query" and Table
  "onSettingsOpenChange" pressed Escape before the menu had registered
  its Escape listener.

How often these fail depends on where jsdom's 16ms rAF interval falls
relative to the assertion. Main (Vitest) has the same race (2/12 runs
with --retry 0). On CI it failed all 3 attempts. With rAF delayed to
40ms the FilterDropdown Escape and Table tests fail every time, and at
10-30ms the Select tests fail. With this change those runs pass. A
Chromium run of the built Storybook shows the same order: aria-expanded
is false in the keydown microtask and focus reaches the trigger on the
next frame. Component behaviour is correct.

Each test now waits for the focus the menu actually takes (the trigger,
the list or the search input) before it continues. No assertion was
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…32.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eenshots]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot and others added 4 commits October 5, 2026 05:59
createLargeSecurityEvents filled the Requests column with Math.random(),
so the 'action bar pinned to the viewport bottom' and 'bidirectional
infinite scroll' baselines changed on every screenshot run. Use a fixed
multiplicative hash of the row index instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since @zag-js/dismissable 1.43.3 (pulled in by @ark-ui/react 5.39.2)
the dismissable layer can register, and set --layer-index on the
content, after zag's popper has already copied the content's computed
z-index into the positioner's --z-index. Popper does that copy only
once, so Select and DropdownMenu positioners stayed at z-index 50 and
rendered below a nested dialog/drawer (positioner at 70).

Add syncLayerZIndex/useLayerZIndexRef, which watch the content's
style/class and re-copy its computed z-index to the positioner whenever
it changes. Stacking no longer depends on zag's registration order.
Wire it into DropdownMenuContent, SelectPositioner (Select,
FilterDropdown, Pagination, editable cells), PopoverContent and
CalendarContent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ipwallarm
ipwallarm merged commit 603a409 into main Oct 5, 2026
16 checks passed
@ipwallarm
ipwallarm deleted the chore/update-deps branch October 5, 2026 11:19
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