Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/kit/src/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export function createDevtoolsKit(options: DevtoolsKitOptions): DevtoolsKit {
plugins,
install() {
if (installed) return
// dispose() clears the runtime handlers and RPC; reinstalling would give a hook
// feeding a runtime that answers nothing. Create a new kit instead.
if (disposed) {
console.warn('[vue-devtools] install() called on a disposed kit; create a new kit instead')
return
}

const owners = ownerTarget[DEVTOOLS_KIT_OWNERS] ?? new Map<string, DevtoolsKit>()
const previousOwner = owners.get(clientName)
Expand All @@ -120,7 +126,6 @@ export function createDevtoolsKit(options: DevtoolsKitOptions): DevtoolsKit {
ownerTarget[DEVTOOLS_KIT_OWNERS] = owners

installed = true
disposed = false

if (enabled && options.hook?.install !== false) {
installedHook = installDevtoolsHook({
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/plugin/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export function createPluginController(runtime: DevtoolsRuntime): PluginControll
}
state.listeners.add(listener)
state.bufferedPlugins.forEach(listener)
// An unmounted app never comes back, so a later kit must not replay its setups.
const disposeUnmount = runtime.subscribe('app:unmount', (event) => {
if (event.type !== 'app:unmount' || event.app == null) return
state.bufferedPlugins = state.bufferedPlugins.filter(
([descriptor]) => descriptor.app !== event.app,
)
})

async function callAdapterHook(adapter: PluginApiAdapter, key: string, payload: unknown) {
if (!acceptsApp(adapter.descriptor, payload)) return
Expand All @@ -131,6 +138,7 @@ export function createPluginController(runtime: DevtoolsRuntime): PluginControll
},
dispose() {
state.listeners.delete(listener)
disposeUnmount()
plugins.clear()
adapters.clear()
},
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/plugin/kit-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { afterEach, expect, it, vi } from 'vitest'
import type { DevtoolsKit } from '../../../packages/kit/src/kit'
import { createDevtoolsKit } from '../../../packages/kit/src/kit'
import { setupDevToolsPlugin } from '../../../packages/kit/src/plugin'
import { createComponentTreeFixture } from '../../fixtures'

const kits: DevtoolsKit[] = []

afterEach(async () => {
for (const kit of kits.splice(0)) await kit.dispose()
})

function createKit(): DevtoolsKit {
const kit = createDevtoolsKit({ target: { name: 'Lifecycle Test' }, hook: { install: false } })
kit.install()
kits.push(kit)
return kit
}

it('does not reinstall a disposed kit', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const kit = createKit()
await kit.dispose()

kit.install()

expect(warn).toHaveBeenCalledOnce()
expect(kit.plugins.adapters.size).toBe(0)
})

it('does not replay plugin setups for an unmounted app into a later kit', async () => {
const first = createKit()
const fixture = createComponentTreeFixture(1)
first.runtime.dispatch({
type: 'app:init',
app: fixture.app,
time: 1,
version: '3.5.0',
vueTypes: {},
})
const setup = vi.fn()
setupDevToolsPlugin({ id: 'plugin:replay', app: fixture.app }, setup)
await vi.waitFor(() => expect(setup).toHaveBeenCalledOnce())

first.runtime.dispatch({ type: 'app:unmount', app: fixture.app, time: 2 })
await first.dispose()
createKit()
await new Promise((resolve) => setTimeout(resolve, 0))

expect(setup).toHaveBeenCalledOnce()
})

it('still replays plugin setups for a mounted app into a later kit', async () => {
const first = createKit()
const fixture = createComponentTreeFixture(1)
first.runtime.dispatch({
type: 'app:init',
app: fixture.app,
time: 1,
version: '3.5.0',
vueTypes: {},
})
const setup = vi.fn()
setupDevToolsPlugin({ id: 'plugin:kept', app: fixture.app }, setup)
await vi.waitFor(() => expect(setup).toHaveBeenCalledOnce())

await first.dispose()
createKit()
await vi.waitFor(() => expect(setup).toHaveBeenCalledTimes(2))
})
Loading