From 1f57f84321c0d30daf8315e63d5a8f30c16635d1 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Thu, 3 Nov 2022 12:05:53 -0700 Subject: [PATCH] feat: add `vite:afterUpdate` event (#9810) --- docs/guide/api-hmr.md | 1 + packages/vite/src/client/client.ts | 49 ++++++++++++------- packages/vite/types/customEvent.d.ts | 1 + playground/hmr/__tests__/hmr.spec.ts | 22 ++++++--- playground/hmr/hmr.ts | 9 ++-- playground/tailwind/__test__/tailwind.spec.ts | 6 +-- 6 files changed, 56 insertions(+), 32 deletions(-) diff --git a/docs/guide/api-hmr.md b/docs/guide/api-hmr.md index 7733c8e91ba477..d5d716fb13df79 100644 --- a/docs/guide/api-hmr.md +++ b/docs/guide/api-hmr.md @@ -145,6 +145,7 @@ Listen to an HMR event. The following HMR events are dispatched by Vite automatically: - `'vite:beforeUpdate'` when an update is about to be applied (e.g. a module will be replaced) +- `'vite:afterUpdate'` when an update has just been applied (e.g. a module has been replaced) - `'vite:beforeFullReload'` when a full reload is about to occur - `'vite:beforePrune'` when modules that are no longer needed are about to be pruned - `'vite:invalidate'` when a module is invalidated with `import.meta.hot.invalidate()` diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 3f974e77d9b52b..3e0830ae537823 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -154,10 +154,12 @@ async function handleMessage(payload: HMRPayload) { clearErrorOverlay() isFirstUpdate = false } - payload.updates.forEach((update) => { - if (update.type === 'js-update') { - queueUpdate(fetchUpdate(update)) - } else { + await Promise.all( + payload.updates.map(async (update): Promise => { + if (update.type === 'js-update') { + return queueUpdate(fetchUpdate(update)) + } + // css-update // this is only sent when a css file referenced with is updated const { path, timestamp } = update @@ -171,27 +173,36 @@ async function handleMessage(payload: HMRPayload) { (e) => !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl) ) - if (el) { - const newPath = `${base}${searchUrl.slice(1)}${ - searchUrl.includes('?') ? '&' : '?' - }t=${timestamp}` - - // rather than swapping the href on the existing tag, we will - // create a new link tag. Once the new stylesheet has loaded we - // will remove the existing link tag. This removes a Flash Of - // Unstyled Content that can occur when swapping out the tag href - // directly, as the new stylesheet has not yet been loaded. + + if (!el) { + return + } + + const newPath = `${base}${searchUrl.slice(1)}${ + searchUrl.includes('?') ? '&' : '?' + }t=${timestamp}` + + // rather than swapping the href on the existing tag, we will + // create a new link tag. Once the new stylesheet has loaded we + // will remove the existing link tag. This removes a Flash Of + // Unstyled Content that can occur when swapping out the tag href + // directly, as the new stylesheet has not yet been loaded. + return new Promise((resolve) => { const newLinkTag = el.cloneNode() as HTMLLinkElement newLinkTag.href = new URL(newPath, el.href).href - const removeOldEl = () => el.remove() + const removeOldEl = () => { + el.remove() + console.debug(`[vite] css hot updated: ${searchUrl}`) + resolve() + } newLinkTag.addEventListener('load', removeOldEl) newLinkTag.addEventListener('error', removeOldEl) outdatedLinkTags.add(el) el.after(newLinkTag) - } - console.debug(`[vite] css hot updated: ${searchUrl}`) - } - }) + }) + }) + ) + notifyListeners('vite:afterUpdate', payload) break case 'custom': { notifyListeners(payload.event, payload.data) diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 839e17dd729eda..64d0c19d1e4aa1 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -7,6 +7,7 @@ import type { export interface CustomEventMap { 'vite:beforeUpdate': UpdatePayload + 'vite:afterUpdate': UpdatePayload 'vite:beforePrune': PrunePayload 'vite:beforeFullReload': FullReloadPayload 'vite:error': ErrorPayload diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index c5d1950408ff04..30d0b4da05b177 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -34,7 +34,8 @@ if (!isBuild) { 'foo was: 1', '(self-accepting 1) foo is now: 2', '(self-accepting 2) foo is now: 2', - '[vite] hot updated: /hmr.ts' + '[vite] hot updated: /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 @@ -46,7 +47,8 @@ if (!isBuild) { 'foo was: 2', '(self-accepting 1) foo is now: 3', '(self-accepting 2) foo is now: 3', - '[vite] hot updated: /hmr.ts' + '[vite] hot updated: /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 }) @@ -67,7 +69,8 @@ if (!isBuild) { '(single dep) nested foo is now: 1', '(multi deps) foo is now: 2', '(multi deps) nested foo is now: 1', - '[vite] hot updated: /hmrDep.js via /hmr.ts' + '[vite] hot updated: /hmrDep.js via /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 @@ -84,7 +87,8 @@ if (!isBuild) { '(single dep) nested foo is now: 1', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 1', - '[vite] hot updated: /hmrDep.js via /hmr.ts' + '[vite] hot updated: /hmrDep.js via /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 }) @@ -106,7 +110,8 @@ if (!isBuild) { '(single dep) nested foo is now: 2', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 2', - '[vite] hot updated: /hmrDep.js via /hmr.ts' + '[vite] hot updated: /hmrDep.js via /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 @@ -123,7 +128,8 @@ if (!isBuild) { '(single dep) nested foo is now: 3', '(multi deps) foo is now: 3', '(multi deps) nested foo is now: 3', - '[vite] hot updated: /hmrDep.js via /hmr.ts' + '[vite] hot updated: /hmrDep.js via /hmr.ts', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 }) @@ -140,9 +146,11 @@ if (!isBuild) { '>>> vite:beforeUpdate -- update', '>>> vite:invalidate -- /invalidation/child.js', '[vite] hot updated: /invalidation/child.js', + '>>> vite:afterUpdate -- update', '>>> vite:beforeUpdate -- update', '(invalidation) parent is executing', - '[vite] hot updated: /invalidation/parent.js' + '[vite] hot updated: /invalidation/parent.js', + '>>> vite:afterUpdate -- update' ]) browserLogs.length = 0 }) diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 4af73ee48489f0..dcfa7692272187 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -45,6 +45,10 @@ if (import.meta.hot) { console.log(`foo was:`, foo) }) + import.meta.hot.on('vite:afterUpdate', (event) => { + console.log(`>>> vite:afterUpdate -- ${event.type}`) + }) + import.meta.hot.on('vite:beforeUpdate', (event) => { console.log(`>>> vite:beforeUpdate -- ${event.type}`) @@ -58,9 +62,8 @@ if (import.meta.hot) { (document.querySelector('.global-css') as HTMLLinkElement).href ) - // We don't have a vite:afterUpdate event. - // We need to wait until the tag has been swapped out, which - // includes the time taken to download and parse the new stylesheet. + // Wait until the tag has been swapped out, which includes the time taken + // to download and parse the new stylesheet. Assert the swapped link. const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { diff --git a/playground/tailwind/__test__/tailwind.spec.ts b/playground/tailwind/__test__/tailwind.spec.ts index 7698ca2573387d..444110de59bd37 100644 --- a/playground/tailwind/__test__/tailwind.spec.ts +++ b/playground/tailwind/__test__/tailwind.spec.ts @@ -79,10 +79,10 @@ if (!isBuild) { await untilUpdated(() => getBgColor(el), 'rgb(220, 38, 38)') - expect(browserLogs).toMatchObject([ - '[vite] css hot updated: /index.css', + expect(browserLogs).toContain('[vite] css hot updated: /index.css') + expect(browserLogs).toContain( '[vite] hot updated: /src/components/PugTemplate.vue?vue&type=template&lang.js' - ]) + ) browserLogs.length = 0 })