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
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 @@ -45,6 +45,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
195 changes: 195 additions & 0 deletions packages/rolldown/src/app/components/RunBuildDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<script setup lang="ts">
import type { BuildInfo } from '~~/node/rolldown/logs-manager'
import { DEVTOOLS_TERMINALS_DOCK_ID } from '@vitejs/devtools-kit/constants'
import ActionButton from '@vitejs/devtools-ui/components/Action/ActionButton.vue'
import DisplayBadge from '@vitejs/devtools-ui/components/Display/DisplayBadge.vue'
import DisplayTimestamp from '@vitejs/devtools-ui/components/Display/DisplayTimestamp.vue'
import OverlayModal from '@vitejs/devtools-ui/components/Overlay/OverlayModal.vue'
import VisualEmptyState from '@vitejs/devtools-ui/components/Visual/VisualEmptyState.vue'
import VisualLoading from '@vitejs/devtools-ui/components/Visual/VisualLoading.vue'
import { ref, watch } from 'vue'
import { NuxtLink } from '#components'
import { useRpc } from '#imports'

// Bubble the refreshed session list up so the parent's list stays in sync
// whether the build finished with the modal open or dismissed to the background.
const emit = defineEmits<{ refresh: [BuildInfo[]] }>()
const open = defineModel<boolean>('open', { default: false })

const rpc = useRpc()

type Stage = 'confirm' | 'running' | 'success' | 'error'
const stage = ref<Stage>('confirm')
const commandLine = ref('vite build')
const buildSessionId = ref<string | null>(null)
const errorMessage = ref<string | null>(null)
const newSessions = ref<BuildInfo[]>([])

// Reset to the confirmation step and load the exact command each time the
// dialog is opened afresh.
watch(open, async (isOpen) => {
if (!isOpen)
return
stage.value = 'confirm'
errorMessage.value = null
newSessions.value = []
buildSessionId.value = null
try {
const cmd = await rpc.value.call('vite:rolldown:get-build-command')
const env = Object.entries(cmd.env ?? {}).map(([k, v]) => `${k}=${v}`).join(' ')
commandLine.value = `${env ? `${env} ` : ''}${cmd.command} ${(cmd.args ?? []).join(' ')}`.trim()
}
catch {
commandLine.value = 'vite build'
}
})

async function confirmRun() {
stage.value = 'running'
errorMessage.value = null
// Snapshot existing session ids to surface only the ones this build produces.
const before = new Set((await rpc.value.call('vite:rolldown:list-sessions')).map(s => s.id))
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')
const list = await rpc.value.call('vite:rolldown:list-sessions')
emit('refresh', list)
newSessions.value = list.filter(s => !before.has(s.id))
// If the user dismissed the modal while it ran, leave it closed — the list
// was already refreshed above.
if (!open.value)
return
if (exitCode != null && exitCode !== 0) {
stage.value = 'error'
errorMessage.value = `Build exited with code ${exitCode}.`
}
else {
stage.value = 'success'
}
}
catch (error) {
if (!open.value)
return
stage.value = 'error'
errorMessage.value = error instanceof Error ? error.message : String(error)
}
}

async function viewInTerminal() {
if (buildSessionId.value) {
await rpc.value.call('hub:docks:activate', {
dockId: DEVTOOLS_TERMINALS_DOCK_ID,
params: { sessionId: buildSessionId.value },
})
}
open.value = false
}
</script>

<template>
<OverlayModal v-model:open="open">
<template #title>
Run build with devtools
</template>

<!-- Fixed min-height keeps the panel from jumping as stages swap. -->
<div class="flex flex-col gap-4 w-140 max-w-full min-h-64">
<!-- Confirm -->
<template v-if="stage === 'confirm'">
<p class="m0 op70 text-sm">
This runs a production build with Rolldown's devtools output enabled, then adds the resulting session below.
</p>
<pre class="m0 p3 rounded-lg border border-base bg-code font-mono text-sm of-auto text-left"><code>{{ commandLine }}</code></pre>
<div class="flex-auto" />
<div class="flex justify-end gap-2">
<ActionButton @click="open = false">
Cancel
</ActionButton>
<ActionButton variant="primary" icon="i-ph-play-duotone" @click="confirmRun()">
Run build
</ActionButton>
</div>
</template>

<!-- Running -->
<template v-else-if="stage === 'running'">
<VisualLoading class="flex-auto" text="Build running…" />
<div class="flex justify-end gap-2">
<ActionButton @click="open = false">
Dismiss
</ActionButton>
<ActionButton variant="primary" icon="i-ph-terminal-window-duotone" @click="viewInTerminal()">
View in terminals
</ActionButton>
</div>
</template>

<!-- Success -->
<template v-else-if="stage === 'success'">
<div class="flex gap-2 items-center text-green">
<span class="i-ph-check-circle-duotone" />
Build finished successfully.
</div>
<template v-if="newSessions.length">
<p class="m0 op60 text-sm">
New session{{ newSessions.length > 1 ? 's' : '' }} — open one to inspect it:
</p>
<div class="flex flex-col gap-2 flex-auto of-auto">
<NuxtLink
v-for="session of newSessions"
:key="session.id"
:to="`/session/${session.id}`"
class="border border-base rounded-md color-base text-left flex flex-col gap-1 px3 py2 hover:bg-active"
@click="open = false"
>
<div class="flex gap-1 items-center font-mono op50 text-sm">
<div class="i-ph-hash-duotone" />
{{ session.id }}
</div>
<div v-if="session.meta.inputs?.[0]" class="flex gap-1 items-center">
<DisplayModuleId :id="session.meta.inputs[0].filename" :cwd="session.meta.cwd" />
<DisplayBadge :text="session.meta.inputs[0].name || 'entry'" />
</div>
<DisplayTimestamp :timestamp="session.timestamp" class="text-xs op50" />
</NuxtLink>
</div>
</template>
<VisualEmptyState
v-else
class="flex-auto"
icon="i-ph-package-duotone"
description="The build produced no new devtools session."
/>
<div class="flex justify-end gap-2">
<ActionButton @click="viewInTerminal()">
View in terminals
</ActionButton>
<ActionButton variant="primary" @click="open = false">
Close
</ActionButton>
</div>
</template>

<!-- Error -->
<template v-else>
<div class="flex gap-2 items-center text-red">
<span class="i-ph-x-circle-duotone" />
Build failed.
</div>
<p v-if="errorMessage" class="m0 op70 text-sm">
{{ errorMessage }}
</p>
<div class="flex-auto" />
<div class="flex justify-end gap-2">
<ActionButton @click="open = false">
Close
</ActionButton>
<ActionButton variant="primary" icon="i-ph-terminal-window-duotone" @click="viewInTerminal()">
View in terminals
</ActionButton>
</div>
</template>
</div>
</OverlayModal>
</template>
40 changes: 32 additions & 8 deletions packages/rolldown/src/app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { BuildInfo } from '~~/node/rolldown/logs-manager'
import ActionButton from '@vitejs/devtools-ui/components/Action/ActionButton.vue'
import ActionIconButton from '@vitejs/devtools-ui/components/Action/ActionIconButton.vue'
import BannerRolldownDevTools from '@vitejs/devtools-ui/components/Banner/BannerRolldownDevTools.vue'
import DisplayIconButton from '@vitejs/devtools-ui/components/Display/DisplayIconButton.vue'
import { useClipboard } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useRpc } from '#imports'
Expand Down Expand Up @@ -45,7 +46,10 @@ 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'))

// Drives the confirm → run → result modal (see RunBuildDialog).
const runBuildOpen = ref(false)

function selectSession(session: BuildInfo) {
if (selectedSessionIds.value.includes(session.id)) {
Expand All @@ -67,15 +71,27 @@ function selectSession(session: BuildInfo) {
<p class="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>
<ActionButton
variant="primary"
icon="i-ph-play-duotone"
title="Run a build with devtools output"
@click="runBuildOpen = true"
>
Run build with devtools
</ActionButton>
<p class="m0 op40 text-sm text-center">
Or enable it manually in your Rolldown config:
</p>
<div class="relative w-full">
<pre class="m0 p3 pr10 rounded-lg border border-base bg-code font-mono text-sm of-auto text-left"><code>{{ ENABLE_DEVTOOLS_SNIPPET }}</code></pre>
<DisplayIconButton
class="absolute" top2 right2
title="Copy snippet"
class-icon="i-ph-copy-duotone"
<ActionIconButton
class="absolute top2 right2"
icon="i-ph-copy-duotone"
:tooltip="copied ? 'Copied!' : 'Copy snippet'"
:active="copied"
active-class="text-green bg-active op100"
@click="copy()"
/>
</div>
Expand All @@ -92,12 +108,19 @@ function selectSession(session: BuildInfo) {
@select="selectSession"
/>
</div>
<div v-if="sessions.length" class="fixed top-5 right-5 flex flex-col gap2">
<div v-if="sessions.length" class="fixed top-5 right-5 flex flex-col gap2 items-end">
<div class="flex flex-row justify-around w20 h8 border border-base rounded-8 of-hidden">
<button v-for="mode in modeList" :key="mode.value" :title="mode.label" class="flex-1 op50 flex items-center justify-center hover:bg-active hover:text-base hover:op100!" :class="{ 'bg-active text-base op100!': sessionMode === mode.value }" @click="sessionMode = mode.value">
<span :class="mode.icon" class="text-sm" />
</button>
</div>
<ActionButton
icon="i-ph-play-duotone"
title="Run a build with devtools output"
@click="runBuildOpen = true"
>
Run build
</ActionButton>
</div>
<div v-if="selectedSessions.length > 0 && sessionMode === 'compare'" class="fixed bottom-5 right-5 border 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 All @@ -110,5 +133,6 @@ function selectSession(session: BuildInfo) {
</div>
</div>
</div>
<RunBuildDialog v-model:open="runBuildOpen" @refresh="sessions = $event" />
</div>
</template>
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
Loading
Loading