Skip to content

refactor: fix circular dependencies across state, editor, and tldraw packages#7935

Merged
steveruizok merged 5 commits intomainfrom
steveruizok/fix-circular-deps
Feb 17, 2026
Merged

refactor: fix circular dependencies across state, editor, and tldraw packages#7935
steveruizok merged 5 commits intomainfrom
steveruizok/fix-circular-deps

Conversation

@steveruizok
Copy link
Copy Markdown
Collaborator

@steveruizok steveruizok commented Feb 14, 2026

In order to address circular dependency chains that cause issues with Jest mocking, tree-shaking, and code comprehension (Closes #2458), this PR breaks circular imports across @tldraw/state, @tldraw/editor, and @tldraw/tldraw using three categories of changes: import type conversions, structural file splits, and code relocation.

import type conversions (~23 files in editor)
Converts runtime import { Editor } to import type { Editor } across files where Editor and other values are only used in type positions (constructor parameter types, function parameter types, generics). Also converts type-only imports for TLErrorFallbackComponent, SnapManager types, BaseBoxShapeTool, and TLBaseBoxShape.

Split useEditorComponents (~15 cycles)
Extracts TLEditorComponents interface, EditorComponentsContext, and useEditorComponents() hook into a new EditorComponentsContext.tsx file that uses only import type for prop types. The provider with default component mappings stays in useEditorComponents.tsx. Default components now import the hook from the cycle-free context file.

Inline Vec utilities (1 cycle)
Inlines clamp() and toFixed() as private module-level functions in Vec.ts to eliminate the Vec.tsutils.ts circular import.

Move defaultEmptyAs (2 cycles in tldraw)
Moves defaultEmptyAs from FrameShapeUtil.tsx to frameHelpers.ts so that both FrameShapeUtil.tsx and FrameLabelInput.tsx import from frameHelpers instead of creating a cycle back through FrameShapeUtil.

State package brand checks (3 cycles)

  • Extracts isComputed() to a new isComputed.ts file using a __isComputed brand field instead of instanceof, breaking the capture.tsComputed.ts cycle.
  • Adds __isEffectScheduler brand field to EffectScheduler and replaces instanceof with a brand check in transactions.ts, removing the EffectScheduler import entirely and breaking the transactions.tsEffectScheduler.ts cycle.

Change type

  • improvement

Test plan

  • Existing unit tests cover all modified behavior
  • Unit tests

Release notes

  • Fixed circular dependencies across @tldraw/state, @tldraw/editor, and @tldraw/tldraw packages to improve compatibility with Jest mocking and tree-shaking.

Note

Medium Risk
Mostly refactoring to module boundaries and type-only imports, but it touches core reactive internals (transactions/Computed) where subtle identity/branding mistakes could affect effect scheduling and runtime type checks.

Overview
Breaks several circular dependency chains across @tldraw/state, @tldraw/editor, and @tldraw/tldraw by converting many value imports to import type and relocating a few small utilities.

Key structural changes: extracts TLEditorComponents/EditorComponentsContext/useEditorComponents() into a new EditorComponentsContext.tsx and updates default components to import the hook from there; inlines clamp/toFixed inside Vec.ts; moves defaultEmptyAs into frameHelpers.ts; and replaces instanceof checks in state internals with lightweight “brand” fields (__isComputed, __isEffectScheduler) plus a new isComputed.ts helper. Also updates vitest setup to serve translations from the new default translation source.

Written by Cursor Bugbot for commit b47e072. This will update automatically on new commits. Configure here.

…packages

Addresses circular dependency chains reported in #2458 across three packages:

- Convert ~23 runtime imports to `import type` in the editor package where
  values are only used in type positions
- Split useEditorComponents.tsx into EditorComponentsContext.tsx (interface +
  hook) and useEditorComponents.tsx (provider + defaults) to break ~15 cycles
  between default components and the hook
- Inline clamp/toFixed in Vec.ts to break Vec↔utils cycle
- Move defaultEmptyAs from FrameShapeUtil.tsx to frameHelpers.ts to break
  2 frame shape cycles
- Extract isComputed to its own file with brand field check to break
  capture↔Computed cycle in state package
- Add __isEffectScheduler brand field and replace instanceof check to break
  transactions↔EffectScheduler cycle in state package
@huppy-bot huppy-bot bot added the improvement Product improvement label Feb 14, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 14, 2026

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

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Feb 17, 2026 6:20pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Preview Feb 17, 2026 6:20pm
chat-template Ignored Ignored Preview Feb 17, 2026 6:20pm
tldraw-docs Ignored Ignored Preview Feb 17, 2026 6:20pm
tldraw-shader Ignored Ignored Preview Feb 17, 2026 6:20pm
workflow-template Ignored Ignored Preview Feb 17, 2026 6:20pm

Request Review

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Feb 14, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
multiplayer-template 67d6f90 Feb 14 2026, 11:01 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
🔵 In progress
View logs
agent-template 67d6f90 Feb 14 2026, 10:57 PM

@steveruizok
Copy link
Copy Markdown
Collaborator Author

We could split this up?

Update the window.fetch mock in setupVitest.js to import DEFAULT_TRANSLATION from the defaultTranslation.ts module instead of loading ../assets/translations/main.json. The fetch mock now returns DEFAULT_TRANSLATION directly (instead of json.default), ensuring tests use the canonical default translation source.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

if (child instanceof EffectScheduler) {
traverseReactors.add(child)
if ('__isEffectScheduler' in child) {
traverseReactors.add(child as unknown as Reactor)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brand checks break singleton cross-version compatibility

Medium Severity

The singleton() pattern in @tldraw/state ensures that when multiple copies of the library are loaded, they share the same class constructors via globalThis. The EffectScheduler and _Computed constructors use this pattern. If an older version (without brand fields) loads first and registers its class via singleton, the newer version receives that older class — whose instances lack __isEffectScheduler and __isComputed. In traverseChild, the '__isEffectScheduler' in child check would then fail for all EffectScheduler instances, causing the else branch to execute (child as Signal).children.visit(...) on an object with no children property, resulting in a TypeError crash. The previous instanceof EffectScheduler check was compatible with the singleton pattern because instanceof works against the shared constructor reference regardless of which version defined it.

Additional Locations (1)

Fix in Cursor Fix in Web

@steveruizok steveruizok added this pull request to the merge queue Feb 17, 2026
Merged via the queue into main with commit 4d67b67 Feb 17, 2026
18 checks passed
@steveruizok steveruizok deleted the steveruizok/fix-circular-deps branch February 17, 2026 18:39
mimecuvalo added a commit that referenced this pull request Feb 18, 2026
Add a CI guard using vite-plugin-circular-dependency so circular imports in package entrypoints are caught early and we don't regress after #7935.

Co-authored-by: Cursor <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request Feb 18, 2026
## Summary
- add `vite-plugin-circular-dependency` and a new `yarn
check-circular-deps` script
- run a Vite-based circular import check across all
`packages/*/src/index.ts` entrypoints
- wire the check into the main CI checks workflow to prevent regressions

Follow-up to #7935.
Related to #2458.

### Change type

- [x] `other`

## Test plan
- [x] Run `yarn check-circular-deps`
- [x] Verify CI workflow includes `Check circular dependencies`

Made with [Cursor](https://cursor.com)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new CI gate that may introduce flaky/slow builds or unexpected
failures on certain package graphs. The code refactor is mostly
import/move-only but touches core arrow geometry helpers used widely in
`tldraw`.
> 
> **Overview**
> **CI now blocks circular imports.** Adds `yarn check-circular-deps`
and wires it into the main `Checks` workflow, using
`vite-plugin-circular-dependency` to scan each `packages/*/src/index.ts`
entrypoint (with scoped splitting for `packages/tldraw` to avoid
large-graph crashes).
> 
> Separately, refactors arrow utilities by extracting `getArrowInfo`
from `shapes/arrow/shared.ts` into a new `getArrowInfo.ts` module and
updating all imports/exports accordingly, and tweaks editor component
imports plus `DefaultLoadingScreen` to render its own `.tl-loading`
wrapper instead of relying on `TldrawEditor`’s `LoadingScreen`
component.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4042079. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request Feb 18, 2026
i originally removed these but they're necessary to keep things speedy

## Summary
- document why tldraw checks use dynamic batched include sets with
`vite-plugin-circular-dependency`
- reduce tldraw scan runtime by combining safe scopes while keeping
`ui/components` and `ui/context` separate to avoid plugin `Invalid array
length` crashes
- run scoped checks with bounded concurrency to improve local and CI
performance without reducing coverage

Follow-up to #8003.
Related to #7935 and #2458.

### Change type

- [x] `other`

## Test plan
- [x] Run `yarn check-circular-deps`
- [x] Run `time yarn check-circular-deps` and verify improved runtime

Made with [Cursor](https://cursor.com)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Build-time CI script changes only; main risk is false
negatives/positives if the new include batching misses files or alters
scan boundaries.
> 
> **Overview**
> Speeds up `yarn check-circular-deps` by changing `tldraw`’s include
patterns from per-scope scans to **filesystem-discovered batches**,
scanning all non-UI scopes together while keeping `ui/components` and
`ui/context` isolated to avoid `vite-plugin-circular-dependency`
“Invalid array length” crashes.
> 
> Also runs each include-set check via a small worker queue with
**bounded concurrency** (up to 2 for `tldraw`) instead of strictly
serial execution, improving CI/local runtime without reducing coverage.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f2393ce. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Product improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix and prevent circular dependencies

1 participant