Skip to content

refactor: no barrel imports or import cycles inside the library - #1619

Merged
tenphi merged 8 commits into
mainfrom
andrew/internal-imports-past-barrels
Nov 9, 2026
Merged

tenphi merged 8 commits into
mainfrom
andrew/internal-imports-past-barrels

Conversation

@tenphi

@tenphi tenphi commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Describe changes

The first of three PRs that restructure UI Kit's src/. This one moves no files and changes no public export. It makes the library's internal imports load only what they use, and adds the checks that keep them that way and that hold the next two PRs (filenames, then folder moves) to an unchanged public API.

  • Public API snapshot. src/public-api.test.ts writes every export of every package.json subpath (., ./eslint-plugin, ./probe) to src/__snapshots__/public-api.md as value, type or value+type, and checks that every value of the main entry exists at runtime. A package the root re-exports whole (@tenphi/tasty, @internationalized/date, …) is one export * line. The snapshot is identical to what main generates. A re-export that resolves to nothing fails the test instead of being recorded.
  • repo/no-barrel-import, a repo-local oxlint rule. A relative value import that resolves to an index.ts is an error in runtime code and in .storybook. A story may also import a component's own entry for its compound API (Button.Group). Type-only imports, index files, specs, fixtures and the eslint-plugin tooling are exempt. The rules are in docs/rules/coding.md#imports.
  • import/no-cycle is on for src and .storybook. One cycle stays, with a reason: ColorPicker and ColorSwatchGroup render each other, only inside render.
  • The rewrite. About 200 files now import the defining file instead of a barrel, via a codemod that resolved each name with the TypeScript checker. Beyond import lines:
    • The compound Button (Button.Group, Button.Split) moves from actions/index.ts to actions/Button/index.ts, next to the Layout and Form compounds. Runtime code imports the base Button; none of it uses .Group or .Split.
    • The Form.tsx ↔ ModernFormRoot.tsx cycle is broken by moving code verbatim. The contexts, FormScopeMask and useFormProps go to Form/FormContext.tsx. The <form> element and formPropNames go to Form/FormElement.tsx.
    • Four barrels that nothing imports any more are deleted: _internal, _internal/hooks, components/helpers and portal/storybook/templates. None is a package entry.
    • The Notifications story keeps importing its entry, because .design-sync/config.json shims that module path.
    • pnpm oxlint now lints .storybook as well, as lint-staged already did.

Measured: the library modules a file reaches, counting value imports only.

File main this PR
Root.tsx 195 126
actions/Button/Button.tsx 74 47
fields/TextInput/TextInput.tsx 152 97

There were 5 import cycles; 1 remains.

The size report shows All 0.45% larger. That is compressed size only: the minified bundle grew by 17 bytes (2,036,043 → 2,036,060, from the size-limit-report artifacts of this PR's run and main's). The same code, concatenated in the new import order, compresses slightly worse. The Button-only bundle is 0.07% smaller.

ItemActionsWrapper.browser.test.tsx › "leaves a measured row alone while its container closes and reopens" failed in 3 of 5 full browser runs on this branch; it passed both runs on main. The click in the previous spec leaves the pointer where the row reappears, and whether data-hovered lands inside the settle window comes down to timing. The assertion now leaves hover out. With that change, 4 of 4 full runs pass.

There is no changeset. The public surface is unchanged, which the snapshot proves. The package exports three subpaths and sets sideEffects: false, so no consumer imports a file this PR touches.

Local gates on cab3d15f, all passing: pnpm lint, check:component-tokens, test:types, diagnostics:hooks --check, diagnostics:form --check, diagnostics:compiler, test:form-store, test:form-react, test:form-compiled, build, check:react-package, test:types:consumer, pack:test, check:consumer-declarations, test:package (compiled and uncompiled), test:compiled, test:browser, test:once and build-storybook.

Checklist
  • Pipeline is passed
  • Tests are added (the public API snapshot)
  • Tests are passed successfully
  • Commit message follows commit guidelines

Part of CUB-7438

Other information

PR 2 (filename conventions, in place) and PR 3 (folder moves, via a committed script that open branches can re-run) follow. The published docs move to docs/components/<Name>.md in PR 3, and Cloud's links are updated in the bump PR.

🤖 Generated with Claude Code

tenphi and others added 7 commits November 5, 2026 16:20
List every export of every package.json subpath, with its kind (value,
type, or both), in src/__snapshots__/public-api.md, as tasty does. A
package re-exported whole (`export * from '@tenphi/tasty'`) is one line,
so a dependency bump does not rewrite the snapshot. A runtime check of the
main entry catches a value that silently became type-only.

The restructure that follows moves files and rewrites imports; this is
what proves it leaves the public exports unchanged.

Part of CUB-7438

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Point every relative value import in runtime code and stories at the
module that defines the name, resolved with the TypeScript compiler, so
a file loads only what it uses. Root now reaches 126 of the library's
modules instead of 195, Button 47 instead of 74, TextInput 97 instead
of 152.

The compound Button moves from the actions category barrel into its own
entry, actions/Button/index.ts, next to the Layout and Form compounds:
ButtonSplit renders the base Button, so it cannot live in Button.tsx.
Stories import compounds from a component's own entry; runtime code
imports the base.

Break the import cycles the barrels hid:
- Form.tsx and ModernFormRoot.tsx: what both roots render moves to
  Form/FormContext.tsx (contexts, FormScopeMask, useFormProps) and
  Form/FormElement.tsx (the form element, formPropNames).
- Modal: the type-only imports become `import type`.
- ColorPicker and ColorSwatchGroup render each other, only at render
  time; the cycle stays, with a reason.

The public exports are unchanged (src/__snapshots__/public-api.md).

Part of CUB-7438

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add repo/no-barrel-import to the repo-local oxlint plugin. A relative
value import that resolves to an index file is an error in runtime code;
stories may also import a component's own entry (`../Button`) for its
compound API. Type-only imports, index files, specs, fixtures and the
eslint-plugin tooling are exempt.

Enable import/no-cycle for src and .storybook, and lint .storybook in
`pnpm oxlint` as lint-staged already does. Oxlint runs a module
graph only when the import plugin is on at the config root; categories
stay off, so it adds no other rule.

docs/rules/coding.md#imports, AGENTS.md and the form AGENTS.md file map
describe the rules and the new Form files.

Part of CUB-7438

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four barrels lost their last importer in the rewrite and are not package
entries: _internal/index.ts, _internal/hooks/index.ts,
components/helpers/index.ts and portal/storybook/templates/index.ts.

Stories and the DialogFormApp helper that render a plain Button import
it from Button.tsx; the compound entry is for stories that use
Button.Group or Button.Split.

The Notifications story goes back to its entry: .design-sync shims that
one module path to the built package, so previews share Root's
notification context.

Part of CUB-7438

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- no-barrel-import also checks import(), an explicit .ts/.tsx path, and
  `.`, `..` or a trailing slash, which name a directory even when a file
  of the same name sits beside it.
- The story allowance covers *.stories files only, so helpers under
  src/stories follow the runtime rule.
- The snapshot fails on a re-export that resolves to nothing instead of
  recording it as a value, treats `import type * as` as type-only, and
  sorts in a fixed locale.
- coding.md names the index files that still define code, and the
  Icons story's exception. The i18n README imports useI18n from its file.

Part of CUB-7438

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

"leaves a measured row alone while its container closes and reopens"
failed in 3 of 5 full browser runs on this branch with two `data-hovered`
mutations: the click in the StatefulPanel spec leaves the pointer where
the Select row reappears, and Chrome's hover after the layout change
lands inside the settle window or not. Hover is the pointer's state, not
a change the wrapper made, so the assertion leaves it out. 4 of 4 full
runs pass with it.

Part of CUB-7438

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The entry allowance covers *.stories.{tsx,jsx} only; the Portal
  templates under storybook/ follow the runtime rule.
- no-barrel-import reads import() with a substitution-free template
  literal.
- The snapshot counts `export type * as` as type-only, and compares a
  package's `export *` names by symbol before resolving, so a broken
  re-export inside a dependency's declarations no longer fails it.
- coding.md: runtime code may take the types defined in an index with
  `import type`, only the values are out of reach.

Part of CUB-7438

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

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cube-ui-kit Ready Ready Preview Nov 9, 2026 5:55pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 99eb6ee

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-4df6a4e.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Change
All 633.21 KB +0.45% 🔺
Tree shaking (just a Button) 160.49 KB · 25.3% of All -0.05% 🔽👏

Compared against main at d29a8bf — run 42668868916, 2026-11-09T17:52:42Z.

Third-party packages

✅ No third-party package entered or left the bundle, and none grew more than 10 KB past its approved size.


To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html.

@tenphi
tenphi merged commit 9936253 into main Nov 9, 2026
33 checks passed
@tenphi
tenphi deleted the andrew/internal-imports-past-barrels branch November 9, 2026 18:12

This branch was successfully deployed

2 active deployments
Preview — 99eb6eea Deployed Nov 9, 2026 by vercel[bot]
Chromatic staging — 99eb6eea Deployed Nov 9, 2026 by tenphi via Prepare Storybook for review & tests #4334
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.

1 participant