Skip to content
Open
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
24 changes: 24 additions & 0 deletions docs/errors/DTK0052.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
outline: deep
---
# DTK0052: Launcher Process Exited Before Ready

## Message

> The "`{id}`" launcher process exited with code `{exitCode}` before its server was ready.

## Cause

A server launcher built with [`createProcessLauncher`](/kit/dock-system) spawns a child process and waits for its server to become reachable before swapping the dock to an iframe. This diagnostic is raised when the process **exits first** — the launcher races the readiness probe against the process's own exit, so a process that dies on startup (a build error, a missing config, no matching files) fails fast instead of blocking on a probe that can never succeed.

## Example

Starting the Vitest UI in a project with no test files: `vitest --ui` prints `No test files found` and exits with code 1 before its server ever binds.

## Fix

Open the launcher's terminal session to read the process output, resolve the underlying failure, then use **Retry** on the launcher card.

## Source

- [`packages/kit/src/node/create-process-launcher.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/create-process-launcher.ts) — the `serve` launch flow throws this when the process's `getResult()` settles before `onReady` resolves.
1 change: 1 addition & 0 deletions docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Emitted by `@vitejs/devtools` and `@vitejs/devtools-kit`.
| [DTK0032](./DTK0032) | error | Dock Launch Error |
| [DTK0050](./DTK0050) | error | Integration Install Failed |
| [DTK0051](./DTK0051) | warn | Connection Meta Serve Failed |
| [DTK0052](./DTK0052) | error | Launcher Process Exited Before Ready |

Hub-side diagnostics for docks, terminals, messages, and commands live upstream in `@devframes/hub` under the `DF8xxx` range — see the [Devframe error reference](https://devfra.me/errors/).

Expand Down
92 changes: 92 additions & 0 deletions docs/kit/dock-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ interface DockEntry {
description?: string
buttonStart?: string
buttonLoading?: string
/** Bound command id — the launch button, palette, and keybinding share it */
command?: string
/** Terminal session this launcher tracks (enables "View in Terminal") */
terminalSessionId?: string
/** Author-set single line of progress/status, shown inline on the card */
digest?: string
}
/** JsonRenderer handle created by ctx.createJsonRenderer() (for type: 'json-render') */
ui?: JsonRenderer
Expand Down Expand Up @@ -301,6 +307,92 @@ ctx.docks.register({
})
```

### Binding a command

Point `launcher.command` at a registered command so the launch button, the command palette, and any keybinding all run one handler. `command` is the serializable launch path, so `onLaunch` is optional:

```ts
const COMMAND_ID = 'my-plugin:start'
ctx.commands.register({ id: COMMAND_ID, title: 'Start My App', handler: start })

ctx.docks.register({
id: 'my-launcher',
title: 'My App',
icon: 'ph:rocket-launch-duotone',
type: 'launcher',
launcher: {
title: 'Start My App',
command: COMMAND_ID,
},
})
```

### Tracking a terminal session

When the launch spawns a process, tie the launcher to its terminal session. Set `terminalSessionId` to surface a **View in Terminal** action (which opens the Terminals dock focused on that session), and set `digest` to a short status of the process — its progress, not its raw output:

```ts
const session = await ctx.terminals.startChildProcess(
{ command: 'vite', args: ['dev'], cwd },
{ id: 'my-app:dev', title: 'Dev Server' },
)

ctx.docks.update({
id: 'my-launcher',
title: 'My App',
icon: 'ph:rocket-launch-duotone',
type: 'launcher',
launcher: {
title: 'Start My App',
status: 'loading',
terminalSessionId: 'my-app:dev',
digest: 'Waiting for the server…',
},
})
```

The **View in Terminal** action calls the hub's `hub:docks:activate` RPC (devframe 0.7.3+), which switches the host shell to the Terminals dock and focuses the tracked session — where the full output lives.

`createProcessLauncher` composes all of the above (register + command binding + `prepare` + spawn + digest + session navigation) in one call. A plain **terminal launcher** stays a launcher while a long-running process runs:

```ts
import { createProcessLauncher } from '@vitejs/devtools-kit/node'

createProcessLauncher({
id: 'my-app',
title: 'My App',
icon: 'ph:rocket-launch-duotone',
process: { command: 'vite', args: ['dev'], cwd: process.cwd() },
})
```

Pass `serve.onReady` for the common **server launcher** shape — run some commands, start a server, then replace the card with an iframe embedding it. The card shows a status while `onReady` resolves the URL, then the dock swaps to the iframe:

```ts
let url: string

createProcessLauncher({
id: 'my-ui',
title: 'My UI',
icon: 'ph:browser-duotone',
// Optional: run setup (e.g. install an optional dep) before spawning.
prepare: async () => {
/* install-on-demand */
},
process: async () => {
const port = await getPort()
url = `http://localhost:${port}/`
return { command: 'my-ui', args: ['--port', String(port)], cwd: process.cwd() }
},
serve: {
onReady: async () => {
await waitForServer(url)
return url
},
},
})
```

## JSON render panels

JSON render panels describe a UI as a JSON spec on the server — the client renders it from a built-in component library. This is the shortest path to a DevTools panel: server-side TypeScript only.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/client/webcomponents/.generated/css.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { h } from 'vue'
import { mountWithContext } from '../../stories/story-helpers'
import ViewLauncher from './ViewLauncher.vue'

function launcher(status: 'idle' | 'loading' | 'error' | 'success'): DevToolsViewLauncher {
function launcher(
status: 'idle' | 'loading' | 'error' | 'success',
extras: Partial<DevToolsViewLauncher['launcher']> = {},
): DevToolsViewLauncher {
return {
id: 'launcher',
type: 'launcher',
Expand All @@ -14,6 +17,7 @@ function launcher(status: 'idle' | 'loading' | 'error' | 'success'): DevToolsVie
title: 'Launch My Cool App',
description: 'Start the dev server and open it here.',
status,
...extras,
},
} as DevToolsViewLauncher
}
Expand All @@ -38,15 +42,30 @@ const meta = {
export default meta
type Story = StoryObj

function launcherStory(status: 'idle' | 'loading' | 'error' | 'success'): Story {
function launcherStory(
status: 'idle' | 'loading' | 'error' | 'success',
extras: Partial<DevToolsViewLauncher['launcher']> = {},
): Story {
return {
render: () => ({
setup: () => mountWithContext({}, ctx => stage(h(ViewLauncher, { context: ctx, entry: launcher(status) }))),
setup: () => mountWithContext({}, ctx => stage(h(ViewLauncher, { context: ctx, entry: launcher(status, extras) }))),
}),
}
}

export const Idle: Story = launcherStory('idle')
export const Loading: Story = launcherStory('loading')
export const Success: Story = launcherStory('success')
export const Error: Story = launcherStory('error')

// A failed launch surfaces the reason and offers a clickable Retry.
export const Error: Story = launcherStory('error', {
error: 'No test files found, exiting with code 1',
})

// A launcher tracking a terminal session: it shows the process's progress and
// offers to jump to that session in the Terminals dock.
export const WithProgress: Story = launcherStory('loading', {
buttonLoading: 'Starting…',
terminalSessionId: 'my-app:dev',
digest: 'Waiting for the server…',
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { DevToolsViewLauncher, DevToolsViewLauncherStatus } from '@vitejs/devtools-kit'
import type { DocksContext } from '@vitejs/devtools-kit/client'
import { DEVTOOLS_TERMINALS_DOCK_ID } from '@vitejs/devtools-kit/constants'
import { computed } from 'vue'
import Button from '../display/Button.vue'
import DockIcon from '../dock/DockIcon.vue'
Expand All @@ -15,25 +16,46 @@ function onLaunch() {
}

const status = computed(() => props.entry.launcher.status || 'idle')
// Hub's author-set `digest` — a short line of progress/status.
const progress = computed(() => props.entry.launcher.digest)
const terminalSessionId = computed(() => props.entry.launcher.terminalSessionId)

// Ask the host shell to switch to the Terminals dock focused on this
// launcher's session (devframe 0.7.3 `hub:docks:activate`). Our shell converges
// on the mirrored `devframe:docks:active` slot to perform the switch.
function viewInTerminal() {
if (!terminalSessionId.value)
return
props.context.rpc.call('hub:docks:activate', {
dockId: DEVTOOLS_TERMINALS_DOCK_ID,
params: { sessionId: terminalSessionId.value },
})
}

const iconsMap: Record<DevToolsViewLauncherStatus, string> = {
error: 'i-ph-warning-duotone',
error: 'i-ph-arrow-clockwise-duotone',
idle: 'i-ph-rocket-launch-duotone',
loading: 'i-svg-spinners-8-dots-rotate',
success: 'i-ph-check-duotone',
}

const error = computed(() => props.entry.launcher.error)

const buttonText = computed(() => {
if (status.value === 'idle')
return props.entry.launcher.buttonStart || 'Launch'
else if (status.value === 'loading')
return props.entry.launcher.buttonLoading || 'Loading...'
else if (status.value === 'error')
return 'ERROR'
return 'Retry'
else if (status.value === 'success')
return 'Success'
else
return `UNKNOWN STATUS: ${status.value}`
})

// Idle and error are actionable (error = Retry); loading and success are not.
const canLaunch = computed(() => status.value === 'idle' || status.value === 'error')
</script>

<template>
Expand All @@ -43,16 +65,38 @@ const buttonText = computed(() => {
{{ entry.launcher.title }}
</h1>
<p>{{ entry.launcher.description }}</p>
<!-- Failure reason, shown above the Retry button. -->
<p v-if="status === 'error' && error" class="max-w-full text-sm text-red-600 dark:text-red-400 text-center text-balance">
{{ error }}
</p>
<Button
variant="primary"
:variant="status === 'error' ? 'danger' : 'primary'"
:loading="status === 'loading'"
:disabled="status !== 'idle'"
:disabled="!canLaunch"
@click="onLaunch"
>
<template #icon>
<div class="w-4.5 h-4.5" :class="iconsMap[status]" />
</template>
{{ buttonText }}
</Button>

<!-- Short progress/status of the process (not its raw output). -->
<div v-if="status !== 'error' && progress" class="inline-flex items-center gap-2 text-sm color-base op70">
<div v-if="status === 'loading'" class="i-svg-spinners-3-dots-fade flex-none" />
<span>{{ progress }}</span>
</div>

<Button
v-if="terminalSessionId"
variant="ghost"
size="sm"
@click="viewInTerminal"
>
<template #icon>
<div class="w-3.5 h-3.5 i-ph-arrow-square-out-duotone" />
</template>
View in Terminal
</Button>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function createMockRpc(entries: DevToolsDockEntry[] = []): DevToolsRpcClient {
})

return {
client: {
register: () => () => {},
},
sharedState: {
get: async (key: string) => {
if (key === 'devframe:docks')
Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/client/webcomponents/state/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DevToolsClientCommand, DevToolsDockEntry } from '@vitejs/devtools-kit'
import type { DevToolsClientCommand, DevToolsDockEntry, DevToolsRpcClientFunctions } from '@vitejs/devtools-kit'
import type { CommandsContext, DevToolsRpcClient, DockClientScriptContext, DockEntryState, DockPanelStorage, DocksContext } from '@vitejs/devtools-kit/client'
import type { SharedState } from 'devframe/utils/shared-state'
import type { WhenContext } from 'devframe/utils/when'
Expand Down Expand Up @@ -113,6 +113,22 @@ export async function createDocksContext(
return switchEntry(id)
}

// Honor cross-iframe dock-activation requests (devframe 0.7.3). A mounted
// plugin — or our own launcher's "View in Terminal" action — calls the
// `hub:docks:activate` RPC; the hub broadcasts `devframe:docks:activate` to
// every client. Our shell runs its own dock machinery rather than hub's
// client host, so we handle the broadcast here and switch the active dock
// ourselves. The target dock (e.g. Terminals) reads `activation.params` to
// focus a specific session.
rpc.client.register({
name: 'devframe:docks:activate' satisfies keyof DevToolsRpcClientFunctions,
type: 'action',
handler: (activation: { dockId: string, params?: Record<string, unknown> }) => {
if (activation?.dockId)
switchEntry(activation.dockId)
},
})

let _settingsStorePromise: Promise<SharedState<DevToolsDocksUserSettings>> | undefined
const getSettingsStore = async () => {
if (!_settingsStorePromise) {
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/node/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,22 @@ export type BuiltinServerFunctionsDump = {
[K in keyof BuiltinServerFunctionsStatic]: Awaited<ReturnType<BuiltinServerFunctionsStatic[K]>>
}

declare module '@vitejs/devtools-kit' {
export interface DevToolsRpcServerFunctions extends BuiltinServerFunctions {}
// devframe ≥0.7.4 declares its RPC name maps inside a bundled chunk that the
// public entrypoints re-export, so augmentation must target `devframe/types`
// directly — a renamed re-export (the kit's `DevTools*` alias) no longer
// merges. `@devframes/hub` augments the same module. `hub:docks:activate` is
// now declared by the hub itself, so we no longer declare it here.
declare module 'devframe/types' {
interface DevframeRpcServerFunctions extends BuiltinServerFunctions {}

// @keep-sorted
// `devframe:auth:revoked` and `devframe:rpc:client-state:*` are declared
// upstream by devframe; `devframe:messages:updated` / `devframe:terminals:updated`
// by `@devframes/hub`. We only declare what is Vite-DevTools-specific here.
export interface DevToolsRpcClientFunctions {}
interface DevframeRpcClientFunctions {}

// @keep-sorted
export interface DevToolsRpcSharedStates {
interface DevframeRpcSharedStates {
'devframe:commands': DevToolsServerCommandEntry[]
'devframe:docks': DevToolsDockEntry[]
'devframe:user-settings': DevToolsDocksUserSettings
Expand Down
Loading
Loading