Skip to content

refactor(dashboard): migrate to Svelte 5 runes mode#987

Merged
oskarszoon merged 24 commits into
bsv-blockchain:mainfrom
oskarszoon:refactor/svelte-runes-migration
Jun 2, 2026
Merged

refactor(dashboard): migrate to Svelte 5 runes mode#987
oskarszoon merged 24 commits into
bsv-blockchain:mainfrom
oskarszoon:refactor/svelte-runes-migration

Conversation

@oskarszoon

Copy link
Copy Markdown
Contributor

Summary

Migrates all of ui/dashboard from Svelte 5 legacy/compat mode to runes mode, completing the deferred cleanup from #968. Closes #977.

Every .svelte component (~86 files) now opts into runes (<svelte:options runes={true} />) with $props/$state/$derived/$effect, snippet-based slots, and callback-prop events. The Playwright smoke harness from #981 stayed green at every step.

Approach: per-component opt-in (not a global flag)

The global compilerOptions.runes: true flag is not usable here: it applies to third-party .svelte deps too, and @zerodevx/svelte-toast ships legacy export let with no runes-native release at any version, so a global flag breaks vite build (and the carve-out is unreachable under sveltekit()). Instead each component opts in individually — #977 explicitly sanctions this. Third-party deps stay legacy (correct; not our code).

Work proceeded bottom-up by dependency (shared children before parents): leaf renderers → Icon/Button/Card/Pager/form inputs → nav/table/shell → viewer cards → route pages. Each shared component's conversion flips its callers' event tokens (on:xonx) and slot usage (<div slot="x">{#snippet x()}) in the same commit, for reviewable history.

Conversions applied

  • export let$props(); mutated/bind:-ed props → $bindable; bind:this$state
  • $:$derived (values) / $derived.by (blocks) / $effect (side effects); reassigned markup-read locals → $state
  • createEventDispatcher → callback props (dispatch('x', d)onx?.(d); consumers' e.detail.xe.x)
  • legacy event forwarding (bare on:click) → onclick callback prop
  • <slot> / <slot name="x"> / $$slots{@render children?.()} / {@render x?.()} snippet props
  • <svelte:component this={X}><X />; <svelte:self> → self-import; <script context="module"><script module>; hyphenated svelte-ignore a11y-* → underscore form
  • several legacy $: read-then-write blocks that would effect_update_depth_exceeded under runes → restructured with untrack

Dependency hygiene

  • svelte-exmarkdown 3.0.3 → 5.0.2: v3's peer range was svelte ^3 || ^4 — a real peer-dep conflict under Svelte 5. v5.0.2 is runes-native (svelte ^5.1.3); the <Markdown md plugins> API is unchanged. npm ls is now conflict-free.
  • @zerodevx/svelte-toast left as-is (no runes release; legacy third-party works fine from runes parents).

Also cleaned up

Fixed the 17 pre-existing svelte-check TYPE errors that svelte-check 4 (from the #968 v5 bump) surfaced — unrelated to runes but cleared here so the baseline is green: MerkleProofData missing fields (added as optional), a Uint8Array/BufferSource narrowing, node: unknown store typing, string | undefined guards, a duplicate best_height renderCells key, a stray serverSort prop (→ useServerSort), and an invalid Button variant="danger" (→ destructive). All behavior-preserving.

Verification

  • npm run check0 errors, 0 Svelte deprecation warnings (the ui/dashboard: migrate svelte 5 legacy mode to runes mode #977 acceptance criterion). Re-added as a CI gate in dashboard_pr_smoke.yaml (it was held out during the smoke-harness PR while the legacy baseline was dirty). 14 cosmetic a11y/CSS warnings remain (svelte-check exits 0 on those).
  • npm run build — succeeds.
  • npm run test:integration16/16 smoke tests green (route shells + /viewer block-detail click-through + subtree/tx/utxo detail). Green after every commit.

Closes #977.

oskarszoon added 24 commits May 29, 2026 12:02
Convert src/lib/components/pager/index.svelte to runes mode and update its
two real call sites (standard-table, div-table) from on:change to onchange.

Note: the 6 internal viewer/peers cards use a separate internal Pager
($internal/components/pager/index.svelte), not this shared one, so their
on:change directives are left legacy and untouched.
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Summary

Comprehensive Svelte 5 runes migration across 93 files, converting all components from legacy/compat mode to runes-native syntax. The migration is well-executed and thorough.

Key strengths:

  • Consistent conversion patterns throughout (~86 components)
  • Bottom-up dependency order prevents breaking changes during migration
  • Proper use of untrack() to avoid state_referenced_locally warnings
  • Fixed 17 pre-existing svelte-check type errors
  • Upgraded svelte-exmarkdown to v5 (resolves peer dependency conflict)
  • Comprehensive verification: 16/16 smoke tests passing, 0 svelte-check errors

Conversions applied correctly:

  • export let$props() with proper $bindable for two-way bindings
  • $:$derived / $derived.by() / $effect
  • createEventDispatcher → callback props pattern
  • <slot>{@render children?.()} snippet system
  • Legacy event forwarding → explicit callback props

No issues found. The migration follows Svelte 5 best practices and maintains backward compatibility where needed (e.g., third-party legacy components like @zerodevx/svelte-toast continue to work).

CI checks are currently running. This PR is ready for human review.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-987 (ab2c64c)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.727µ 1.603µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.23n 71.10n ~ 0.500
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.04n 71.29n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 71.24n 71.24n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 33.30n 33.29n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 58.56n 54.24n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 168.3n 126.4n ~ 0.100
MiningCandidate_Stringify_Short-4 221.4n 217.8n ~ 0.700
MiningCandidate_Stringify_Long-4 1.665µ 1.660µ ~ 0.700
MiningSolution_Stringify-4 848.0n 855.7n ~ 0.400
BlockInfo_MarshalJSON-4 1.772µ 1.730µ ~ 0.100
NewFromBytes-4 131.1n 159.1n ~ 0.700
AddTxBatchColumnar_Validation-4 2.665µ 2.477µ ~ 0.400
OffsetValidationLoop-4 633.3n 638.2n ~ 0.200
Mine_EasyDifficulty-4 66.91µ 67.89µ ~ 0.200
Mine_WithAddress-4 7.754µ 6.950µ ~ 0.100
BlockAssembler_AddTx-4 0.02804n 0.02765n ~ 1.000
AddNode-4 11.30 11.41 ~ 0.400
AddNodeWithMap-4 12.00 12.33 ~ 0.700
DiskTxMap_SetIfNotExists-4 3.655µ 3.649µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.373µ 3.545µ ~ 0.100
DiskTxMap_ExistenceOnly-4 316.4n 326.9n ~ 0.100
Queue-4 187.9n 192.3n ~ 0.100
AtomicPointer-4 4.929n 4.929n ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 896.8µ 868.2µ ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/10K-4 804.2µ 799.6µ ~ 0.400
ReorgOptimizations/AllMarkFalse/Old/10K-4 104.7µ 116.7µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.58µ 62.31µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 60.72µ 59.06µ ~ 1.000
ReorgOptimizations/HashSlicePool/New/10K-4 11.40µ 11.54µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 4.844µ 5.378µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.628µ 2.085µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.303m 10.179m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.768m 9.756m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.105m 1.122m ~ 1.000
ReorgOptimizations/AllMarkFalse/New/100K-4 683.3µ 677.4µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 622.3µ 656.3µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 302.9µ 337.8µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/100K-4 47.82µ 52.20µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.46µ 18.30µ ~ 0.100
TxMapSetIfNotExists-4 51.53n 52.05n ~ 0.200
TxMapSetIfNotExistsDuplicate-4 40.55n 40.25n ~ 0.700
ChannelSendReceive-4 624.4n 619.8n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 59.02n 62.19n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 29.56n 29.47n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 28.29n 28.25n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 26.88n 26.81n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 26.42n 26.36n ~ 0.300
SubtreeProcessorAdd/4_per_subtree-4 386.5n 348.4n ~ 0.400
SubtreeProcessorAdd/64_per_subtree-4 332.4n 332.7n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 332.6n 312.5n ~ 0.200
SubtreeProcessorAdd/1024_per_subtree-4 339.8n 309.7n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 332.6n 308.6n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 318.5n 310.1n ~ 0.700
SubtreeProcessorRotate/64_per_subtree-4 344.7n 323.6n ~ 1.000
SubtreeProcessorRotate/256_per_subtree-4 333.9n 320.9n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 302.8n 333.6n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.92n 56.31n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 36.50n 36.59n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 35.23n 35.50n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 34.79n 34.71n ~ 1.000
SubtreeCreationOnly/4_per_subtree-4 113.1n 114.9n ~ 0.400
SubtreeCreationOnly/64_per_subtree-4 378.3n 376.4n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.303µ 1.316µ ~ 0.200
SubtreeCreationOnly/1024_per_subtree-4 4.091µ 4.081µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 7.600µ 7.482µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 344.0n 298.2n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 316.2n 319.3n ~ 0.700
ParallelGetAndSetIfNotExists/1k_nodes-4 2.133m 2.121m ~ 0.400
ParallelGetAndSetIfNotExists/10k_nodes-4 5.844m 5.672m ~ 0.700
ParallelGetAndSetIfNotExists/50k_nodes-4 8.654m 8.494m ~ 0.400
ParallelGetAndSetIfNotExists/100k_nodes-4 11.87m 12.59m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.840m 1.859m ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 6.005m 6.192m ~ 0.400
SequentialGetAndSetIfNotExists/50k_nodes-4 25.67m 23.23m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 39.66m 48.80m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.146m 2.183m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.710m 9.075m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 15.63m 15.54m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.937m 1.936m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 11.62m 11.51m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 78.72m 83.21m ~ 0.100
CalcBlockWork-4 540.2n 507.1n ~ 1.000
CalculateWork-4 694.4n 692.5n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.361µ 1.360µ ~ 0.500
BuildBlockLocatorString_Helpers/Size_100-4 14.54µ 13.01µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 130.2µ 128.8µ ~ 0.700
CatchupWithHeaderCache-4 104.5m 104.6m ~ 0.700
_prepareTxsPerLevel-4 413.5m 430.0m ~ 0.400
_prepareTxsPerLevelOrdered-4 3.858m 4.209m ~ 0.200
_prepareTxsPerLevel_Comparison/Original-4 423.9m 413.4m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.910m 3.859m ~ 0.400
_BufferPoolAllocation/16KB-4 4.673µ 3.972µ ~ 0.700
_BufferPoolAllocation/32KB-4 9.056µ 8.849µ ~ 1.000
_BufferPoolAllocation/64KB-4 17.20µ 17.37µ ~ 1.000
_BufferPoolAllocation/128KB-4 33.99µ 34.11µ ~ 0.700
_BufferPoolAllocation/512KB-4 111.4µ 123.1µ ~ 0.700
_BufferPoolConcurrent/32KB-4 18.35µ 19.64µ ~ 0.100
_BufferPoolConcurrent/64KB-4 29.88µ 31.50µ ~ 0.200
_BufferPoolConcurrent/512KB-4 142.2µ 151.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 638.5µ 621.5µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/32KB-4 675.4µ 677.8µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/64KB-4 679.0µ 620.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 681.1µ 671.1µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/512KB-4 648.5µ 624.1µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.33m 37.06m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.75m 37.25m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.01m 38.00m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.99m 37.01m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.95m 37.02m ~ 0.100
_PooledVsNonPooled/Pooled-4 831.0n 832.8n ~ 0.400
_PooledVsNonPooled/NonPooled-4 8.156µ 8.234µ ~ 1.000
_MemoryFootprint/Current_512KB_32concurrent-4 6.877µ 7.763µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.409µ 9.927µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.135µ 9.811µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.339m 1.325m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 303.4µ 315.2µ ~ 0.200
SubtreeSizes/10k_tx_64_per_subtree-4 73.36µ 73.66µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 18.03µ 18.34µ ~ 0.700
SubtreeSizes/10k_tx_512_per_subtree-4 8.977µ 9.017µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.454µ 4.439µ ~ 1.000
SubtreeSizes/10k_tx_2k_per_subtree-4 2.208µ 2.233µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 70.38µ 70.37µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 17.75µ 17.77µ ~ 1.000
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.423µ 4.498µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 371.2µ 376.0µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 88.55µ 89.72µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.74µ 21.81µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 151.9µ 152.7µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 161.0µ 158.6µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 314.6µ 310.4µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 8.906µ 8.970µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.401µ 9.418µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 17.70µ 17.93µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.089µ 2.075µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.233µ 2.253µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.366µ 4.348µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 340.6µ 334.0µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 337.4µ 338.6µ ~ 0.700
GetUtxoHashes-4 258.4n 258.2n ~ 1.000
GetUtxoHashes_ManyOutputs-4 47.34µ 42.23µ ~ 0.100
_NewMetaDataFromBytes-4 226.5n 227.2n ~ 0.200
_Bytes-4 398.9n 395.4n ~ 0.400
_MetaBytes-4 136.3n 138.6n ~ 0.200

Threshold: >10% with p < 0.05 | Generated: 2026-05-29 15:24 UTC

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thorough, well-executed Svelte 5 runes migration across all ~86 dashboard components.

Key observations:

  • Zero legacy patterns remain (confirmed: no export let, createEventDispatcher, $$slots, or <slot> in any .svelte file)
  • Consistent conversion patterns applied bottom-up by dependency — excellent commit hygiene
  • Full TypeScript annotations on all $props() destructuring
  • Proper $bindable() for two-way bindings, $derived.by() for multi-line computations
  • Pre-existing svelte-check type errors fixed, CI gate restored
  • svelte-exmarkdown 3→5 bump correctly resolves peer-dep conflict

Minor nits (non-blocking):

  • Viewer components use $effect for the ready flag where $effect.pre would be the direct beforeUpdate equivalent (breadcrumbs correctly uses $effect.pre)
  • Button's onKeyDown casts KeyboardEvent as MouseEvent via as unknown as MouseEvent
  • subtreesPerPage in block-assembly-modal could be const instead of $state(10) since it's never reassigned

All CI checks green. Ship it.

@oskarszoon oskarszoon requested a review from sugh01 June 2, 2026 08:30
@oskarszoon oskarszoon merged commit 7ea18d2 into bsv-blockchain:main Jun 2, 2026
28 checks passed
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.

ui/dashboard: migrate svelte 5 legacy mode to runes mode

3 participants