From 53a67240391485edc618e875db731ac4ecb64f23 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 17 Sep 2025 12:12:37 -0400 Subject: [PATCH] refactor: hot module reload component (#22135) --- .../lib/components/timeline/Timeline.svelte | 67 ++++++++----------- web/src/lib/elements/HotModuleReload.svelte | 36 ++++++++++ 2 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 web/src/lib/elements/HotModuleReload.svelte diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index bbbb919ae4..2849b50815 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -12,6 +12,7 @@ setFocusTo as setFocusToInit, } from '$lib/components/timeline/actions/focus-actions'; import { AppRoute, AssetAction } from '$lib/constants'; + import HotModuleReload from '$lib/elements/HotModuleReload.svelte'; import Portal from '$lib/elements/Portal.svelte'; import Skeleton from '$lib/elements/Skeleton.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; @@ -223,45 +224,33 @@ complete.then(completeNav, completeNav); }); - const hmrSupport = () => { - // when hmr happens, skeleton is initialized to true by default - // normally, loading timeline is part of a navigation event, and the completion of - // that event triggers a scroll-to-asset, if necessary, when then clears the skeleton. - // this handler will run the navigation/scroll-to-asset handler when hmr is performed, - // preventing skeleton from showing after hmr - if (import.meta && import.meta.hot) { - const afterApdate = (payload: UpdatePayload) => { - const assetGridUpdate = payload.updates.some( - (update) => update.path.endsWith('Timeline.svelte') || update.path.endsWith('assets-store.ts'), - ); + const handleAfterUpdate = (payload: UpdatePayload) => { + const timelineUpdate = payload.updates.some( + (update) => update.path.endsWith('Timeline.svelte') || update.path.endsWith('assets-store.ts'), + ); - if (assetGridUpdate) { - setTimeout(() => { - const asset = $page.url.searchParams.get('at'); - if (asset) { - $gridScrollTarget = { at: asset }; - void navigate( - { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, - { replaceState: true, forceNavigate: true }, - ); - } else { - scrollToTop(); - } - showSkeleton = false; - }, 500); + if (timelineUpdate) { + setTimeout(() => { + const asset = $page.url.searchParams.get('at'); + if (asset) { + $gridScrollTarget = { at: asset }; + void navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { replaceState: true, forceNavigate: true }, + ); + } else { + scrollToTop(); } - }; - import.meta.hot?.on('vite:afterUpdate', afterApdate); - import.meta.hot?.on('vite:beforeUpdate', (payload) => { - const assetGridUpdate = payload.updates.some((update) => update.path.endsWith('Timeline.svelte')); - if (assetGridUpdate) { - timelineManager.destroy(); - } - }); - - return () => import.meta.hot?.off('vite:afterUpdate', afterApdate); + showSkeleton = false; + }, 500); + } + }; + + const handleBeforeUpdate = (payload: UpdatePayload) => { + const timelineUpdate = payload.updates.some((update) => update.path.endsWith('Timeline.svelte')); + if (timelineUpdate) { + timelineManager.destroy(); } - return () => void 0; }; const updateIsScrolling = () => (timelineManager.scrolling = true); @@ -287,10 +276,6 @@ if (!enableRouting) { showSkeleton = false; } - const disposeHmr = hmrSupport(); - return () => { - disposeHmr(); - }; }); const getMaxScrollPercent = () => { @@ -833,6 +818,8 @@ + + {#if isShowDeleteConfirmation} + import { onDestroy, onMount } from 'svelte'; + import type { UpdatePayload } from 'vite'; + + type Props = { + onBeforeUpdate?: (payload: UpdatePayload) => void; + onAfterUpdate?: (payload: UpdatePayload) => void; + }; + + let { onBeforeUpdate, onAfterUpdate }: Props = $props(); + + const unsubscribes: (() => void)[] = []; + + onMount(() => { + const hot = import.meta.hot; + if (!hot) { + return; + } + + if (onBeforeUpdate) { + hot.on('vite:beforeUpdate', onBeforeUpdate); + unsubscribes.push(() => hot.off('vite:beforeUpdate', onBeforeUpdate)); + } + + if (onAfterUpdate) { + hot.on('vite:afterUpdate', onAfterUpdate); + unsubscribes.push(() => hot.off('vite:afterUpdate', onAfterUpdate)); + } + }); + + onDestroy(() => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }); +