Skip to content

fix(blocks): stop registry.ts reading BLOCK_REGISTRY at module scope - #6083

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/blocks-registry-tdz-cycle
Jul 30, 2026
Merged

fix(blocks): stop registry.ts reading BLOCK_REGISTRY at module scope#6083
waleedlatif1 merged 1 commit into
stagingfrom
fix/blocks-registry-tdz-cycle

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

import('@/blocks/registry-maps') throws today:

ReferenceError: Cannot access 'BLOCK_REGISTRY' before initialization.
    at apps/sim/blocks/registry.ts:28:56

blocks/registry-maps and blocks/registry sit in an import cycle — a block config reaches back through providers/utilstools/paramsblocks/indexblocks/registry, which imports registry-maps. Whichever module the cycle is entered through evaluates first, so blocks/registry can run while registry-maps is still initializing.

Two module-scope reads, not one

const HAS_PREVIEW_BLOCKS = Object.values(BLOCK_REGISTRY).some(...)   // line 28
export const registry: Record<string, BlockConfig> = BLOCK_REGISTRY  // line 238

Only line 28 appears in the stack trace, so fixing it alone would have silently moved the failure to line 238. Both are now deferred:

  • anyPreviewBlocks() memoizes on first use — still "computed once", as the old comment promised, just not at import time
  • the raw map is exposed as getBlockRegistry() instead of an eager const alias

Scope: only the entry point was broken

Worth being precise, because the original report overstated it. @/blocks/registry and @/tools/registry both imported fine before this change; only @/blocks/registry-maps as the entry point threw. After:

entry point before after
@/blocks/registry-maps THROWS OK
@/blocks/registry OK OK
@/tools/registry OK OK
@/blocks/index, @/tools/params, @/tools/utils, @/providers/utils OK OK

Why fix it if the app works

It works under Turbopack only because that bundler happens to order the modules favourably. That's not a property to build on, and it makes blocks/registry-maps un-importable from scripts and tests under Bun.

More concretely: the tool-registry module-graph audit found the registry is 68–78% of every workspace route's graph and proposed a metadata/implementation split. Any such restructuring perturbs module ordering. This is a prerequisite to that work, not something to discover halfway through it.

tsc caught two consumers grep missed

Three consumers updated. Two of them destructure the binding off a dynamic import rather than naming it in a static one:

const { registry: blockRegistry } = await import('@/blocks/registry')

My rg for the named import found only one callsite; tsc --noEmit found the other two (use-mention-data.ts:195, process-contents.ts:548). Worth noting for anyone doing similar refactors — a grep-only sweep here would have shipped two runtime undefined lookups.

Behaviour preservation

getBlockRegistry()[type] is the same raw lookup the alias provided. Deliberately not getBlock(), which would additionally apply version normalization and custom-block overlay fallback — correct-looking, but a behaviour change smuggled into a cycle fix.

Type of Change

  • Bug fix

Testing

  • tsc --noEmit exit 0
  • All 7 cycle entry points import cleanly (table above)
  • 47 test files / 561 tests passing across apps/sim/blocks and apps/sim/lib/copilot/chat
  • biome check clean on all changed files

`blocks/registry-maps` and `blocks/registry` sit in an import cycle: a block
config reaches back through providers/utils -> tools/params -> blocks/index
-> blocks/registry, which imports registry-maps. Whichever module the cycle
is entered through evaluates first, so `blocks/registry` can run while
`registry-maps` is still initializing.

Two module-scope reads of the imported binding therefore threw
`ReferenceError: Cannot access 'BLOCK_REGISTRY' before initialization` for
anything importing `@/blocks/registry-maps` directly — scripts and tests
under Bun:

  const HAS_PREVIEW_BLOCKS = Object.values(BLOCK_REGISTRY).some(...)   // line 28
  export const registry: Record<string, BlockConfig> = BLOCK_REGISTRY  // line 238

Only line 28 showed in the stack trace; fixing it alone would have moved the
failure to line 238. Both are now deferred: `anyPreviewBlocks()` memoizes on
first use (still computed once, as the old comment promised), and the raw map
is exposed as `getBlockRegistry()`.

This worked under Turbopack only because that bundler happened to order the
modules favourably — not a property to build on, and any registry
restructuring would perturb it. Breaking it now is a prerequisite for the
metadata/implementation split that the tool-registry module-graph audit
proposes, not a fix to discover midway through one.

Three consumers updated. `tsc` found two that grep missed, because they
destructure the binding off a dynamic import (`const { registry: blockRegistry }
= await import(...)`) rather than naming it in a static import.

Behaviour-preserving: `getBlockRegistry()[type]` is the same raw lookup the
alias gave, deliberately not `getBlock()`, which would add version
normalization and custom-block overlay fallback.
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 30, 2026 1:45am

Request Review

@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Initialization-order fix with behavior-preserving registry access; limited to registry wiring and three consumers, with no auth or data-path changes.

Overview
Fixes ReferenceError: Cannot access 'BLOCK_REGISTRY' before initialization when the import cycle between blocks/registry and blocks/registry-maps is entered through registry-maps (common in Bun scripts/tests; Turbopack ordering masked it).

blocks/registry.ts no longer touches BLOCK_REGISTRY at load time: preview detection moves to memoized anyPreviewBlocks() on first use, and the eager registry export becomes getBlockRegistry() so both former module-scope reads are deferred.

Call sites that needed the raw map now call getBlockRegistry() — chat integration chips, copilot mention workflow-block icons, and server block-metadata validation — preserving direct keyed lookup (not getBlock() normalization/overlay).

Reviewed by Cursor Bugbot for commit b34b2d1. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Defers module-scope BLOCK_REGISTRY reads in blocks/registry.ts so import-cycle entry via registry-maps no longer throws TDZ ReferenceError.

  • Replaces eager HAS_PREVIEW_BLOCKS with lazy memoized anyPreviewBlocks()
  • Replaces exported registry alias with getBlockRegistry()
  • Updates three callers (static + dynamic imports) to the new accessor without switching to getBlock() (preserves raw-map lookup semantics)

Confidence Score: 5/5

Safe to merge; the cycle fix is localized, callers of the removed export are updated, and no behavioral or security regressions are evident.

Both module-scope BLOCK_REGISTRY reads are deferred; remaining production imports use getBlock/getAllBlocks or the new getter; no leftover named registry import was found.

Important Files Changed

Filename Overview
apps/sim/blocks/registry.ts Lazily reads BLOCK_REGISTRY for preview detection and raw-map export to avoid TDZ under the registry/registry-maps cycle.
apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx Switches static import from registry alias to getBlockRegistry() with the same raw type lookup.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts Dynamic import updated to call getBlockRegistry() instead of destructuring removed registry binding.
apps/sim/lib/copilot/chat/process-contents.ts Dynamic import updated to getBlockRegistry() for blockId existence check; behavior unchanged vs prior alias.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Entry: import registry-maps or registry] --> B{Import cycle}
  B --> C[registry-maps still initializing]
  B --> D[registry evaluating]
  C --> E[Old: module-scope BLOCK_REGISTRY read]
  E --> F[ReferenceError TDZ]
  D --> G[New: anyPreviewBlocks / getBlockRegistry on first use]
  G --> H[BLOCK_REGISTRY fully initialized]
  H --> I[Safe lookup]
Loading

Reviews (1): Last reviewed commit: "fix(blocks): stop registry.ts reading BL..." | Re-trigger Greptile

@waleedlatif1
waleedlatif1 merged commit 897eebd into staging Jul 30, 2026
27 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/blocks-registry-tdz-cycle branch July 30, 2026 07:29
waleedlatif1 added a commit that referenced this pull request Jul 31, 2026
…mizer (#6117)

* perf(tests): mock the tool registry globally, drop the dead deps optimizer

The test suite spent far more time importing modules than running them: on
the full suite, `import` was 1,399s aggregate against 88s of actual tests.
Per-file import cost showed exactly where it came from — lib/core, which
touches no registry, runs at 0.09s/file, while every area that reaches the
tool registry runs 12x-79x that (blocks 7.12s/file, providers 3.65, executor
3.07, tools 2.05, app/api 1.05).

The tool registry is 4,351 entries pulling ~5,907 modules, and almost nothing
under test needs the real thing. `@/blocks/registry` was already globally
mocked for this reason; this does the same for `@/tools/registry`.

Full suite, same commit, same machine:

  baseline  Duration 166.21s  (transform 141.80s, import 1399.17s)
  after     Duration  91.56s  (transform  61.98s, import  617.08s)

45% faster, import -56%, transform -56%. Identical results either way:
1252 files / 16873 tests pass, plus one failure that reproduces on unmodified
staging (cloud-review-tools.test.ts cannot find `rg` from its spawned python3
locally; CI installs ripgrep and it passes there).

Four test files genuinely assert tool registration or tool params, so they
opt out with `vi.unmock('@/tools/registry')` rather than being weakened or
deleted — outlook, azure_devops, and the two search-replace suites. No
coverage is lost.

Also removes `deps.optimizer.web`, which was dead config: it only applies to
client environments (jsdom/happy-dom) and 985 of 1,219 files declare
`@vitest-environment node`. Measured both ways to be sure — removing it is a
no-op (19.33s -> 19.23s), and switching it to the correct `ssr` side with an
include list for the heavy provider SDKs was also a no-op (19.19s). The cost
is first-party module graph, which the optimizer does not touch, so the
honest move is to delete it rather than leave config that reads as if it
does something.

Adds the missing `getBlockRegistry` accessor to the existing
`@/blocks/registry` mock. #6083 renamed that export and the mock was never
updated, so any test reaching those three consumers would have hit
"getBlockRegistry is not a function". Nothing exercises them today.

Not done, deliberately: `isolate: false` is ~20% faster but leaks state
between files and broke two doc-servable tests on the first run.

* docs(tests): explain why the search-replace registry opt-outs are load-bearing

Review read the missing direct import of @/tools/registry as evidence the
vi.unmock calls were no-ops. They are not: the dependency is transitive —
the search-replace planner resolves tool input params through real subblock
configs — and removing both opt-outs fails 8 tests across the two suites.
Comment now says that, so the next reader does not delete them.
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