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
26 changes: 26 additions & 0 deletions docs/errors/RDDT0003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
outline: deep
---

# RDDT0003: Rolldown Build Process Failed to Start

## Message

> Failed to start the Rolldown build process: `{error}`

## Cause

The Rolldown DevTools "Run build with devtools" button spawns a `vite build` child process (with Rolldown's `devtools` output forced on) so it can surface the resulting session. This diagnostic is thrown when that child process cannot be spawned — for example, when the `vite` binary cannot be resolved from the project root, or the terminal host fails to launch it.

## Example

Clicking "Run build with devtools" in a project where `vite` is not installed, or where the project root cannot run `vite build`.

## Fix

1. Ensure `vite` is installed in the project and `vite build` runs from the project root.
2. Run the build once from a terminal to confirm it succeeds, then retry from DevTools.

## Source

- [`packages/rolldown/src/node/rolldown/build-runner.ts`](https://github.com/vitejs/devtools/blob/main/packages/rolldown/src/node/rolldown/build-runner.ts) — `startBuild()` throws this when `ctx.terminals.startChildProcess` fails to spawn `vite build`.
1 change: 1 addition & 0 deletions docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Emitted by `@vitejs/devtools-rolldown`.
|------|-------|-------|
| [RDDT0001](./RDDT0001) | warn | Rolldown Logs Directory Not Found |
| [RDDT0002](./RDDT0002) | warn | Rolldown Log Reader Bad Line |
| [RDDT0003](./RDDT0003) | error | Rolldown Build Process Failed to Start |

## Vite DevTools (VDT)

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 @@ -34,6 +34,9 @@ function createMockRpc(entries: DevToolsDockEntry[] = []): DevToolsRpcClient {
throw new Error(`Unexpected shared state key: ${key}`)
},
},
client: {
register: () => {},
},
} as unknown as DevToolsRpcClient
}

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/client/webcomponents/state/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,18 @@ export async function createDocksContext(
return switchEntry(entry.id)
})

// Interim dock-activation bridge: a mounted plugin iframe can ask the shell
// to switch to another dock via the `devtoolskit:internal:navigate` server
// RPC, which broadcasts this client function. Focusing a specific terminal
// session (`options.sessionId`) awaits an upstream `@devframes/*` capability.
rpc.client.register({
name: 'vite:devtools:activate-dock',
type: 'action',
handler: (options: { dockId: string, sessionId?: string }) => {
switchEntry(options.dockId)
},
})

docksContextByRpc.set(rpc, docksContext)
return docksContext
}
11 changes: 10 additions & 1 deletion packages/core/src/node/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { messagesClear } from './internal/messages-clear'
import { messagesList } from './internal/messages-list'
import { messagesRemove } from './internal/messages-remove'
import { messagesUpdate } from './internal/messages-update'
import { navigate } from './internal/navigate'
import { rpcServerList } from './internal/rpc-server-list'
import { openInEditor } from './public/open-in-editor'
import { openInFinder } from './public/open-in-finder'
Expand All @@ -31,6 +32,7 @@ export const builtinInternalRpcDeclarations = [
messagesList,
messagesRemove,
messagesUpdate,
navigate,
rpcServerList,
] as const

Expand All @@ -56,7 +58,14 @@ declare module '@vitejs/devtools-kit' {
// `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 {}
export interface DevToolsRpcClientFunctions {
/**
* Ask the host shell to activate a dock by id (interim dock-activation
* bridge — see `rpc/internal/navigate.ts`). `sessionId` is reserved for a
* future upstream terminal-session-focus capability.
*/
'vite:devtools:activate-dock': (options: { dockId: string, sessionId?: string }) => void
}

// @keep-sorted
export interface DevToolsRpcSharedStates {
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/node/rpc/internal/navigate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineRpcFunction } from '@vitejs/devtools-kit'

/**
* Interim, Vite-side dock-activation bridge.
*
* A mounted plugin iframe (e.g. the Rolldown analyzer) has no direct handle on
* the host shell's active dock — that selection is client-local state. This RPC
* lets any client ask the shell to activate another dock by broadcasting the
* `vite:devtools:activate-dock` client function, which the shell handles by
* calling `switchEntry`.
*
* `sessionId` is carried through for a future upstream `@devframes/*` capability
* that focuses a specific terminal session; today the shell only switches docks.
*/
export const navigate = defineRpcFunction({
name: 'devtoolskit:internal:navigate',
type: 'action',
setup: (context) => {
return {
async handler(options: { dockId: string, sessionId?: string }): Promise<void> {
context.rpc.broadcast({
method: 'vite:devtools:activate-dock',
args: [options],
})
},
}
},
})
74 changes: 71 additions & 3 deletions packages/rolldown/src/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,50 @@ const normalizedSelectedSessions = computed(() => {
})

const rpc = useRpc()
const sessions = await rpc.value.call('vite:rolldown:list-sessions')
const sessions = ref<BuildInfo[]>(await rpc.value.call('vite:rolldown:list-sessions'))

const building = ref(false)
const buildError = ref<string | null>(null)
const buildSessionId = ref<string | null>(null)

async function refreshSessions() {
sessions.value = await rpc.value.call('vite:rolldown:list-sessions')
}

async function navigateToBuildTerminal() {
if (!buildSessionId.value)
return
await rpc.value.call('devtoolskit:internal:navigate', {
dockId: 'devframes-plugin-terminals',
sessionId: buildSessionId.value,
})
}

async function runBuild() {
// While a build is running, clicking opens its Terminals session to watch it
// stream, rather than starting another build.
if (building.value) {
await navigateToBuildTerminal()
return
}
building.value = true
buildError.value = null
try {
const { sessionId } = await rpc.value.call('vite:rolldown:run-build')
buildSessionId.value = sessionId
const { exitCode } = await rpc.value.call('vite:rolldown:wait-for-build')
if (exitCode != null && exitCode !== 0)
buildError.value = `Build exited with code ${exitCode}.`
// Refresh regardless — a failed build may still have emitted partial data.
await refreshSessions()
}
catch (error) {
buildError.value = error instanceof Error ? error.message : String(error)
}
finally {
building.value = false
}
}

function selectSession(session: BuildInfo) {
if (selectedSessionIds.value.includes(session.id)) {
Expand All @@ -67,7 +110,21 @@ function selectSession(session: BuildInfo) {
<p m0 op50 text-center>
No sessions yet.
<br>
Enable devtools output in your Rolldown config, then run a build:
Run a build with devtools output enabled to get started:
</p>
<button
btn-action rounded-8 text-3 flex="~ gap2 items-center justify-center" h9 px4
:title="building ? 'View this build in the Terminals tab' : 'Run a build with devtools output'"
@click="runBuild()"
>
<span :class="building ? 'i-ph-circle-notch-duotone animate-spin' : 'i-ph-play-duotone'" text-sm />
{{ building ? 'Building… (view terminal)' : 'Run build with devtools' }}
</button>
<p v-if="buildError" m0 text-sm text-center text-red>
{{ buildError }}
</p>
<p m0 op40 text-sm text-center>
Or enable it manually in your Rolldown config:
</p>
<div relative w-full>
<pre m0 p3 pr10 rounded-lg border="~ base" bg-code font-mono text-sm of-auto text-left><code>{{ ENABLE_DEVTOOLS_SNIPPET }}</code></pre>
Expand All @@ -92,12 +149,23 @@ function selectSession(session: BuildInfo) {
@select="selectSession"
/>
</div>
<div v-if="sessions.length" fixed top-5 right-5 flex="~ col gap2">
<div v-if="sessions.length" fixed top-5 right-5 flex="~ col gap2 items-end">
<div flex="~ row justify-around" w20 h8 border="~ base rounded-8" of-hidden>
<button v-for="mode in modeList" :key="mode.value" :title="mode.label" flex-1 op50 flex="~ items-center justify-center" :class="{ 'bg-active text-base op100!': sessionMode === mode.value }" hover="bg-active text-base op100!" @click="sessionMode = mode.value">
<span :class="mode.icon" class="text-sm" />
</button>
</div>
<button
btn-action rounded-8 text-3 flex="~ gap2 items-center justify-center" h8 px3
:title="building ? 'View this build in the Terminals tab' : 'Run a build with devtools output'"
@click="runBuild()"
>
<span :class="building ? 'i-ph-circle-notch-duotone animate-spin' : 'i-ph-play-duotone'" text-sm />
{{ building ? 'Building…' : 'Run build' }}
</button>
<p v-if="buildError" m0 text-xs text-right text-red max-w-60>
{{ buildError }}
</p>
</div>
<div v-if="selectedSessions.length > 0 && sessionMode === 'compare'" fixed bottom-5 right-5 border="~ base rounded-2" w100 max-lg:w85 bg-glass z-panel-content>
<CompareSessionMeta :sessions="normalizedSelectedSessions" class="flex-col gap0 [&>div]:border-none! [&>first-child]:border-b!" />
Expand Down
4 changes: 4 additions & 0 deletions packages/rolldown/src/node/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({
RDDT0002: {
why: (p: { line: number, error: string, preview: string }) => `Rolldown log reader skipped bad line ${p.line}: ${p.error}\n${p.preview}`,
},
RDDT0003: {
why: (p: { error: string }) => `Failed to start the Rolldown build process: ${p.error}`,
fix: 'Ensure `vite` is installed in this project and can run `vite build` from the project root.',
},
},
})
14 changes: 14 additions & 0 deletions packages/rolldown/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import type { PluginWithDevTools } from '@vitejs/devtools-kit'
import process from 'node:process'
import { DEVTOOLS_VITEPLUS_GROUP_ID } from '@vitejs/devtools-kit/constants'
import { clientPublicDir } from '../dirs'
import { ROLLDOWN_DEVTOOLS_ENV } from './rolldown/build-runner'
import { rpcFunctions } from './rpc/index'

const ROLLDOWN_DEVTOOLS_BASE = '/__devtools-rolldown/'

export function DevToolsRolldownUI(): PluginWithDevTools {
return {
name: 'vite:devtools:rolldown-ui',
// When the "Run build with devtools" button spawns a `vite build`, it sets
// `VITE_DEVTOOLS_ROLLDOWN` on the child. This plugin is in the build's
// plugin pipeline (mounted by core whenever this package is installed), so
// it forces Rolldown's `devtools` output on for that build — no manual
// `DevToolsIntegration` wiring needed. A normal `vite build` (without the
// env var) is left untouched.
configResolved(config) {
if (process.env[ROLLDOWN_DEVTOOLS_ENV] !== 'true')
return
for (const environment of Object.values(config.environments))
environment.build.rolldownOptions.devtools ??= {}
},
devtools: {
setup(ctx) {
for (const fn of rpcFunctions) {
Expand Down
81 changes: 81 additions & 0 deletions packages/rolldown/src/node/rolldown/build-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import process from 'node:process'
import { diagnostics } from '../diagnostics'

/** Fixed terminal-session id so repeat clicks reuse one session. */
export const BUILD_SESSION_ID = 'vite:rolldown:build'
/**
* Env var the spawned build carries so the Rolldown DevTools plugin forces
* `rolldownOptions.devtools` on for that build (see `plugin.ts`).
*/
export const ROLLDOWN_DEVTOOLS_ENV = 'VITE_DEVTOOLS_ROLLDOWN'

type BuildSession = Awaited<ReturnType<ViteDevToolsNodeContext['terminals']['startChildProcess']>>

/** The single in-flight build session (module-scoped, like the Vitest launcher). */
let current: BuildSession | undefined

export interface RunBuildResult {
sessionId: string
/** True when a build was already running and we reused its session. */
alreadyRunning: boolean
}

export interface WaitBuildResult {
/** Process exit code, or `null` when there is no build to await. */
exitCode: number | null
}

/**
* Spawn `vite build` with Rolldown's devtools output forced on. Returns
* immediately once the child is spawned and its terminal session registered —
* completion is awaited separately via {@link waitForBuild}.
*/
export async function startBuild(context: ViteDevToolsNodeContext): Promise<RunBuildResult> {
const cwd = context.cwd ?? process.cwd()

// Idempotent: a still-running build just reuses its session.
const existing = context.terminals.sessions.get(BUILD_SESSION_ID)
if (existing?.status === 'running')
return { sessionId: BUILD_SESSION_ID, alreadyRunning: true }

// A finished/dead session may linger — terminate it before re-spawning.
if (current)
await current.terminate().catch(() => {})

try {
current = await context.terminals.startChildProcess(
{
command: 'vite',
args: ['build'],
cwd,
env: { [ROLLDOWN_DEVTOOLS_ENV]: 'true' },
},
{
id: BUILD_SESSION_ID,
title: 'Rolldown build',
// The Terminals panel only renders icons its SPA statically ships;
// use a terminal icon it bundles.
icon: 'ph:terminal-window-duotone',
},
)
}
catch (error) {
throw diagnostics.RDDT0003({ error: error instanceof Error ? error.message : String(error) })
}

return { sessionId: BUILD_SESSION_ID, alreadyRunning: false }
}

/** Resolve once the spawned build exits. */
export async function waitForBuild(): Promise<WaitBuildResult> {
if (!current)
return { exitCode: null }
try {
const result = await current.getResult()
return { exitCode: result.exitCode ?? null }
}
catch {
return { exitCode: null }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { getLogsManager } from '../utils'

export const rolldownListSessions = defineRpcFunction({
name: 'vite:rolldown:list-sessions',
cacheable: true,
// Not client-cached: the "Run build" button re-fetches this to surface the
// session a fresh build just produced, so the list must stay live.
type: 'static',
jsonSerializable: true,
setup: (context) => {
Expand Down
13 changes: 13 additions & 0 deletions packages/rolldown/src/node/rpc/functions/rolldown-run-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { RunBuildResult } from '../../rolldown/build-runner'
import { defineRpcFunction } from '@vitejs/devtools-kit'
import { startBuild } from '../../rolldown/build-runner'

export const rolldownRunBuild = defineRpcFunction({
name: 'vite:rolldown:run-build',
type: 'action',
setup: (context) => {
return {
handler: (): Promise<RunBuildResult> => startBuild(context),
}
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { WaitBuildResult } from '../../rolldown/build-runner'
import { defineRpcFunction } from '@vitejs/devtools-kit'
import { waitForBuild } from '../../rolldown/build-runner'

export const rolldownWaitForBuild = defineRpcFunction({
name: 'vite:rolldown:wait-for-build',
type: 'action',
setup: () => {
return {
handler: (): Promise<WaitBuildResult> => waitForBuild(),
}
},
})
Loading
Loading