refactor: fix circular dependencies across state, editor, and tldraw packages#7935
refactor: fix circular dependencies across state, editor, and tldraw packages#7935steveruizok merged 5 commits intomainfrom
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
multiplayer-template | 67d6f90 | Feb 14 2026, 11:01 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| 🔵 In progress View logs |
agent-template | 67d6f90 | Feb 14 2026, 10:57 PM |
|
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
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]>
## 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]>
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]>


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/tldrawusing three categories of changes:import typeconversions, structural file splits, and code relocation.import typeconversions (~23 files in editor)Converts runtime
import { Editor }toimport type { Editor }across files whereEditorand other values are only used in type positions (constructor parameter types, function parameter types, generics). Also converts type-only imports forTLErrorFallbackComponent,SnapManagertypes,BaseBoxShapeTool, andTLBaseBoxShape.Split useEditorComponents (~15 cycles)
Extracts
TLEditorComponentsinterface,EditorComponentsContext, anduseEditorComponents()hook into a newEditorComponentsContext.tsxfile that uses onlyimport typefor prop types. The provider with default component mappings stays inuseEditorComponents.tsx. Default components now import the hook from the cycle-free context file.Inline Vec utilities (1 cycle)
Inlines
clamp()andtoFixed()as private module-level functions inVec.tsto eliminate theVec.ts↔utils.tscircular import.Move
defaultEmptyAs(2 cycles in tldraw)Moves
defaultEmptyAsfromFrameShapeUtil.tsxtoframeHelpers.tsso that bothFrameShapeUtil.tsxandFrameLabelInput.tsximport fromframeHelpersinstead of creating a cycle back throughFrameShapeUtil.State package brand checks (3 cycles)
isComputed()to a newisComputed.tsfile using a__isComputedbrand field instead ofinstanceof, breaking thecapture.ts↔Computed.tscycle.__isEffectSchedulerbrand field toEffectSchedulerand replacesinstanceofwith a brand check intransactions.ts, removing theEffectSchedulerimport entirely and breaking thetransactions.ts↔EffectScheduler.tscycle.Change type
improvementTest plan
Release notes
@tldraw/state,@tldraw/editor, and@tldraw/tldrawpackages 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/tldrawby converting many value imports toimport typeand relocating a few small utilities.Key structural changes: extracts
TLEditorComponents/EditorComponentsContext/useEditorComponents()into a newEditorComponentsContext.tsxand updates default components to import the hook from there; inlinesclamp/toFixedinsideVec.ts; movesdefaultEmptyAsintoframeHelpers.ts; and replacesinstanceofchecks in state internals with lightweight “brand” fields (__isComputed,__isEffectScheduler) plus a newisComputed.tshelper. 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.