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
23 changes: 23 additions & 0 deletions docs/content/2.module/1.utils-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,29 @@ to the [migration guide](/module/migration-v4). See
[`extendServerRpc()`](/module/migration-v4#ndt_dep_0003) and
[`startSubprocess()`](/module/migration-v4#ndt_dep_0004).

### `devtools:notify` hook

Push a notification into the unified devframe **Messages** system — the same
system that powers the Vite DevTools **Messages** dock and its toast overlay.
The notification is forwarded to `ctx.messages`, so no direct kit access is
needed, and calls made before the kit connects are buffered and replayed once
it does:

```ts
// server-side (module setup)
nuxt.callHook('devtools:notify', {
message: 'Something happened',
level: 'error', // 'info' | 'warn' | 'error' | 'success' | 'debug' (default 'info')
description: 'More details about what happened.',
notify: true, // also show a transient toast
})
```

Omit `autoDelete` for a **persistent**, leveled entry that builds history in the
Messages dock; set `notify` + `autoDismiss` + `autoDelete` for an **ephemeral**,
toast-only notification. From the client, the same input is available on the
injected client as `client.devtools.notify(...)`.

## `@nuxt/devtools-kit/iframe-client`

To provide complex interactions for your module integrations, we recommend to host your own view and display it in devtools via iframe.
Expand Down
11 changes: 11 additions & 0 deletions packages/devtools-kit/src/_types/client-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { $Fetch } from 'ofetch'
import type { BuiltinLanguage } from 'shiki'
import type { Ref } from 'vue'
import type { HookInfo, LoadingTimeMetric, PluginMetric } from './integrations'
import type { NuxtDevtoolsNotifyInput } from './notify'
import type { ServerFunctions } from './rpc'
import type { TimelineMetrics } from './timeline-metrics'

Expand Down Expand Up @@ -119,6 +120,16 @@ export interface NuxtDevtoolsClient {
renderMarkdown: (markdown: string) => string
colorMode: string

/**
* Push a notification through the devframe Messages system.
*
* Routes to the shared Messages host, so it surfaces in the Vite DevTools
* **Messages** dock and/or as a transient toast (when `notify` is set — the
* default). Use this from custom tabs / in-client code to give feedback
* through the same notification system as the rest of DevTools.
*/
notify: (input: NuxtDevtoolsNotifyInput) => Promise<void>

/**
* The connected Vite DevTools RPC client, mirroring `nuxt.devtools.devtoolsKit`
* on the server. Use it for full devframe-native access — register client RPC
Expand Down
16 changes: 16 additions & 0 deletions packages/devtools-kit/src/_types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ModuleCustomTab } from './custom-tabs'
import type { NuxtDevtoolsNotifyInput } from './notify'
import type { NuxtDevtoolsInfo } from './server-ctx'
import type { TerminalState } from './terminals'

Expand All @@ -26,6 +27,21 @@ declare module '@nuxt/schema' {
*/
'devtools:ready': (ctx: ViteDevToolsNodeContext) => void | Promise<void>

/**
* Push a notification through the devframe Messages system.
*
* Forwarded to the connected `ctx.messages` host, so it surfaces in the
* Vite DevTools **Messages** dock (persistent, when leveled) and/or as a
* transient toast (when `notify` is set). Calls made before the kit connects
* are buffered and replayed once it does.
*
* @example
* ```ts
* nuxt.callHook('devtools:notify', { message: 'Build failed', level: 'error' })
* ```
*/
'devtools:notify': (input: NuxtDevtoolsNotifyInput) => void

/**
* Hooks to extend devtools tabs.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-kit/src/_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './client-api'
export * from './common'
export * from './custom-tabs'
export * from './integrations'
export * from './notify'
export * from './options'
export * from './rpc'
export * from './server-ctx'
Expand Down
47 changes: 47 additions & 0 deletions packages/devtools-kit/src/_types/notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { DevToolsMessageFilePosition, DevToolsMessageLevel } from '@vitejs/devtools-kit'

/**
* Severity level of a notification, mirroring devframe's message levels.
*
* Determines the color/icon of the entry in the Vite DevTools **Messages** dock
* and its toast.
*/
export type NuxtDevtoolsNotifyLevel = DevToolsMessageLevel

/**
* A Nuxt-friendly subset of devframe's `DevframeMessageEntryInput`.
*
* This is the input accepted by the `devtools:notify` Nuxt hook, the `notify`
* RPC function and the injected client's `notify()` — all of which forward to
* the connected `ctx.messages` host so notifications flow through the single
* devframe Messages system (persistent dock list + toast overlay).
*
* Tiers are expressed through the flags below:
* - **Ephemeral** (toast-only feedback like "Copied!"): `notify: true` with an
* `autoDismiss` (toast lifetime) and `autoDelete` (entry lifetime) so it never
* builds up history in the Messages dock.
* - **Persistent** (server-originated, leveled): omit `autoDelete` so the entry
* is kept in the Messages dock list.
*/
export interface NuxtDevtoolsNotifyInput {
/** Short title / summary of the message. */
message: string
/** Severity level. Defaults to `'info'`. */
level?: NuxtDevtoolsNotifyLevel
/** Optional detailed description or explanation. */
description?: string
/** Optional tags/labels for filtering in the Messages dock. */
labels?: string[]
/** Optional grouping category (e.g. `'build'`, `'lint'`, `'runtime'`). */
category?: string
/** Optional source file position (e.g. for a build/lint error). */
filePosition?: DevToolsMessageFilePosition
/** Optional stack trace string. */
stacktrace?: string
/** Whether this message should also appear as a transient toast. */
notify?: boolean
/** Time in ms to auto-dismiss the toast (client-side). */
autoDismiss?: number
/** Time in ms to auto-delete the entry from the persistent list (server-side). */
autoDelete?: number
}
4 changes: 4 additions & 0 deletions packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ResolvedConfig } from 'vite'
import type { AnalyzeBuildsInfo } from './analyze-build'
import type { ModuleCustomTab } from './custom-tabs'
import type { AssetEntry, AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageUpdateInfo, ScannedNitroTasks, ServerRouteInfo } from './integrations'
import type { NuxtDevtoolsNotifyInput } from './notify'
import type { ModuleOptions, NuxtDevToolsOptions } from './options'
import type { InstallModuleReturn, ServerDebugContext } from './server-ctx'
import type { TerminalAction, TerminalInfo } from './terminals'
Expand Down Expand Up @@ -63,6 +64,9 @@ export interface ServerFunctions {
deleteStaticAsset: (filepath: string) => Promise<void>
renameStaticAsset: (oldPath: string, newPath: string) => Promise<void>

// Notifications
notify: (input: NuxtDevtoolsNotifyInput) => Promise<void>

// Actions
telemetryEvent: (payload: object, immediate?: boolean) => void
customTabAction: (name: string, action: number) => Promise<boolean>
Expand Down
11 changes: 11 additions & 0 deletions packages/devtools-kit/src/_types/server-ctx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { BirpcGroup } from 'birpc'
import type { Nuxt, NuxtDebugModuleMutationRecord } from 'nuxt/schema'
import type { NuxtDevtoolsNotifyInput } from './notify'
import type { ModuleOptions } from './options'
import type { ClientFunctions, ServerFunctions } from './rpc'

Expand Down Expand Up @@ -55,6 +56,16 @@ export interface NuxtDevtoolsServerContext {
*/
refresh: (event: keyof ServerFunctions) => void

/**
* Push a notification through the devframe Messages system (`ctx.messages`).
*
* The connected messages host surfaces it in the Vite DevTools **Messages**
* dock and/or as a toast. Calls made before the kit connects are buffered and
* replayed on connect. Used by the `devtools:notify` hook, the `notify` RPC
* function and the curated built-in notification sources.
*/
notify: (input: NuxtDevtoolsNotifyInput) => void

/**
* @deprecated Use the Vite DevTools RPC registration instead:
* `nuxt.devtools.rpc.register(defineRpcFunction(...))`. Kept working as a shim.
Expand Down
51 changes: 8 additions & 43 deletions packages/devtools-ui-kit/src/components/NNotification.vue
Original file line number Diff line number Diff line change
@@ -1,48 +1,13 @@
<script setup lang='ts'>
import type { DevtoolsUiShowNotificationPosition } from '../composables/notification'

import { ref } from 'vue'
import { devtoolsUiProvideNotificationFn } from '../composables/notification'

const show = ref(false)
const icon = ref<string>()
const text = ref<string>()
const classes = ref<string>()
const position = ref<DevtoolsUiShowNotificationPosition>('top-center')

devtoolsUiProvideNotificationFn((data) => {
text.value = data.message
icon.value = data.icon
classes.value = data.classes ?? 'text-primary border-primary'
icon.value = data.icon
show.value = true
position.value = data.position ?? 'top-center'
setTimeout(() => {
show.value = false
}, data.duration ?? 1500)
})
/**
* @deprecated The bespoke Nuxt DevTools toast has been retired. Notifications
* now flow through the devframe Messages system (Vite DevTools' built-in
* Messages dock + toast overlay). Use `devtoolsUiShowNotification(...)`, which
* is re-pointed onto that system — this component is a no-op shim kept only so
* existing `<NNotification />` mounts don't break, and renders nothing.
*/
</script>

<template>
<div
fixed left-0 right-0 z-999 text-center
:class="[
{ 'pointer-events-none overflow-hidden': !show },
{ 'top-0': position.startsWith('top') },
{ 'bottom-0': position.startsWith('bottom') },
]"
>
<div flex :style="{ justifyContent: position.includes('right') ? 'right' : position.includes('left') ? 'left' : 'center' }">
<div
border="~ base"
flex="~ inline gap2"
m-3 inline-block items-center rounded bg-base px-4 py-1 transition-all duration-300
:style="show ? {} : { transform: `translateY(${position.startsWith('top') ? '-' : ''}300%)` }"
:class="[show ? 'shadow' : 'shadow-none', classes]"
>
<div v-if="icon" :class="icon" />
<div>{{ text }}</div>
</div>
</div>
</div>
<div hidden />
</template>
35 changes: 33 additions & 2 deletions packages/devtools-ui-kit/src/composables/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,44 @@ let _devtoolsUiShowNotification: typeof devtoolsUiShowNotification

export type DevtoolsUiShowNotificationPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'

export function devtoolsUiShowNotification(data: {
/**
* Severity of an ephemeral notification. Mirrors devframe's message levels; the
* concrete implementation (provided by the Nuxt DevTools client) maps this onto
* a devframe message so the toast picks up the matching color/icon.
*/
export type DevtoolsUiShowNotificationLevel = 'info' | 'warn' | 'error' | 'success' | 'debug'

export interface DevtoolsUiShowNotificationData {
message: string
icon?: string
classes?: string
/**
* Severity of the notification. When omitted the implementation infers it
* from `classes`/`icon` (best-effort), defaulting to `info`.
*/
level?: DevtoolsUiShowNotificationLevel
/**
* Toast lifetime, in ms.
*
* @remarks Best-effort — the concrete implementation may forward this to the
* host toast, which ultimately owns placement and timing.
*/
duration?: number
/**
* @deprecated Placement is owned by the Vite DevTools chrome toast; this is a
* no-op and kept only for backward compatibility.
*/
position?: DevtoolsUiShowNotificationPosition
}) {
}

/**
* Show an ephemeral toast notification.
*
* This is a thin proxy: the Nuxt DevTools client provides the concrete
* implementation via {@link devtoolsUiProvideNotificationFn} at startup, which
* routes the toast through the devframe Messages system. Fire-and-forget.
*/
export function devtoolsUiShowNotification(data: DevtoolsUiShowNotificationData) {
_devtoolsUiShowNotification?.(data)
}

Expand Down
1 change: 0 additions & 1 deletion packages/devtools/client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ registerCommands(() => [
<template>
<div fixed inset-0 h-screen w-screen font-sans>
<NuxtLoadingIndicator />
<NNotification />
<NLoading v-if="waiting">
Connecting....
</NLoading>
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/composables/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { computed, ref } from 'vue'
import { useState } from '#imports'
import { renderMarkdown } from './client-services/markdown'
import { renderCodeHighlight } from './client-services/shiki'
import { notify } from './notify'
import { connectPromise, rpc, rpcClient, upsertClientFunction } from './rpc'

let warnedExtendClientRpc = false
Expand Down Expand Up @@ -67,6 +68,7 @@ export function useInjectionClient(): ComputedRef<NuxtDevtoolsIframeClient> {
host: client.value,
devtools: <NuxtDevtoolsClient>{
rpc,
notify,
devtoolsKit: rpcClient.value,
colorMode: mode.value,
renderCodeHighlight(code, lang) {
Expand Down
39 changes: 39 additions & 0 deletions packages/devtools/client/composables/notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { NuxtDevtoolsNotifyInput } from '../../src/types'
import { connectPromise, rpcClient } from './rpc'

/**
* The devframe Messages plugin RPC that adds an entry to the shared Messages
* host — the single notification system used across Nuxt + Vite DevTools. An
* entry with `notify: true` also renders a transient toast in the Vite DevTools
* chrome.
*/
const MESSAGES_ADD_RPC = 'devframes-plugin-messages:add'

/**
* Push a notification through the devframe Messages system from the client.
*
* Backs both the ephemeral toast adapter (`devtoolsUiShowNotification`) and the
* public injected client `notify()`. Defaults `level` to `info` and `notify` to
* `true`. Fire-and-forget: notifications are best-effort UI feedback, so any
* RPC error is swallowed.
*/
export async function notify(input: NuxtDevtoolsNotifyInput): Promise<void> {
try {
const client = rpcClient.value || await connectPromise
await client.call(MESSAGES_ADD_RPC as any, {
message: input.message,
level: input.level ?? 'info',
description: input.description,
labels: input.labels,
category: input.category,
filePosition: input.filePosition,
stacktrace: input.stacktrace,
notify: input.notify ?? true,
autoDismiss: input.autoDismiss,
autoDelete: input.autoDelete,
})
}
catch (error) {
console.error('[nuxt-devtools] Failed to push notification', error)
}
}
47 changes: 47 additions & 0 deletions packages/devtools/client/plugins/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { NuxtDevtoolsNotifyInput } from '../../src/types'
import { defineNuxtPlugin, devtoolsUiProvideNotificationFn } from '#imports'
import { notify } from '../composables/notify'

/** Default toast lifetime, in ms, when a caller does not specify a duration. */
const DEFAULT_TOAST_DURATION = 3000
/** Extra time, in ms, the persistent entry lingers past the toast before it is auto-deleted. */
const ENTRY_LINGER = 2000

/**
* Best-effort severity inference for legacy `devtoolsUiShowNotification` callers
* that convey intent through `classes`/`icon` rather than an explicit `level`.
*/
function inferLevel(data: { level?: NuxtDevtoolsNotifyInput['level'], classes?: string, icon?: string }): NonNullable<NuxtDevtoolsNotifyInput['level']> {
if (data.level)
return data.level

const hint = `${data.classes ?? ''} ${data.icon ?? ''}`
if (/red|error|danger/.test(hint))
return 'error'
if (/orange|yellow|warn/.test(hint))
return 'warn'
if (/green|success|checkmark/.test(hint))
return 'success'
return 'info'
}

/**
* Wire the Nuxt DevTools ephemeral toast onto the devframe Messages system.
*
* Registers the concrete implementation behind the UI kit's
* `devtoolsUiShowNotification` proxy: every call becomes a toast-only devframe
* message (`notify: true`, with `autoDismiss` + `autoDelete`) so ephemeral
* feedback never builds up history in the Messages dock.
*/
export default defineNuxtPlugin(() => {
devtoolsUiProvideNotificationFn((data) => {
const duration = data.duration ?? DEFAULT_TOAST_DURATION
void notify({
message: data.message,
level: inferLevel(data),
notify: true,
autoDismiss: duration,
autoDelete: duration + ENTRY_LINGER,
})
})
})
Loading
Loading