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
14 changes: 14 additions & 0 deletions packages/devtools/build.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { writeFile } from 'node:fs/promises'
import { defineBuildConfig } from 'unbuild'
import Vue from 'unplugin-vue/rollup'
import { buildCSS } from './src/webcomponents/scripts/build-css'

const WEB_COMPONENTS_STUB = new URL('./dist/webcomponents/index.mjs', import.meta.url)

export default defineBuildConfig({
entries: [
'src/module',
Expand Down Expand Up @@ -39,5 +42,16 @@ export default defineBuildConfig({
return
options.plugins.push(Vue())
},
'build:done': async (ctx) => {
if (!ctx.options.stub)
return

// unbuild's default stub loads TypeScript through jiti. This entry is
// imported by the browser-side inspector plugin, where that Node-only
// loader cannot run. Vite can transform the source entry directly while
// developing this workspace; published builds still receive the bundled
// web component above.
await writeFile(WEB_COMPONENTS_STUB, `export * from '../../src/webcomponents/index.ts'\n`)
},
},
})
2 changes: 1 addition & 1 deletion packages/devtools/client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ registerCommands(() => [
Connecting....
</NLoading>
<div
v-else
v-show="!waiting"
id="nuxt-devtools-app"
class="grid grid-cols-[1fr]"
h-full h-screen of-hidden rounded-xl bg-base font-sans
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/client/composables/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function usePackageUpdate(name: string, options?: NpmCommandOptions): Ret
}

export function useNuxtVersion() {
return useAsyncState('npm:check:nuxt', () => rpc.checkForUpdateFor('nuxt'))
return usePackageUpdate('nuxt').info
}

export function satisfyNuxtVersion(range: string) {
Expand Down
8 changes: 8 additions & 0 deletions packages/devtools/client/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export default defineNuxtConfig({

vite: {
warmupEntry: false,
vue: {
// floating-vue still implements its poppers with Options API mixins,
// computed properties, and methods. Keep that runtime enabled even when
// Nuxt's application defaults change.
features: {
optionsAPI: true,
},
},
$client: {
build: {
target: 'esnext',
Expand Down
11 changes: 11 additions & 0 deletions packages/devtools/client/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { definePageMeta } from '#imports'

definePageMeta({
redirect: '/modules/overview',
})
</script>

<template>
<div />
</template>
1 change: 1 addition & 0 deletions packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
}
},
"dependencies": {
"@devframes/hub": "catalog:prod",
"@devframes/plugin-code-server": "catalog:prod",
"@devframes/plugin-data-inspector": "catalog:prod",
"@nuxt/devtools-kit": "workspace:*",
Expand Down
36 changes: 18 additions & 18 deletions packages/devtools/src/runtime/plugins/view/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { $Fetch } from 'ofetch'
import type { Ref } from 'vue'

import type { Router } from 'vue-router'
import { getDevframeClientContext } from '@devframes/hub/client'
import { NuxtDevtoolsInspectPanel } from '@nuxt/devtools/webcomponents'
import { getDevToolsClientContext } from '@vitejs/devtools-kit/client'

import { createHooks } from 'hookable'
import { debounce } from 'perfect-debounce'
Expand All @@ -17,14 +17,14 @@ import { useAppConfig } from '#imports'

import { initTimelineMetrics } from '../../function-metrics-helpers'

// The `Nuxt` dock group id (see `NUXT_DEVTOOLS_GROUP_ID`). Activating the group
// auto-opens its `defaultChildId` (the shared-frame anchor). The anchor iframe
// dock (`nuxt:devtools`) hosts the one kept-alive client iframe that all tab
// members soft-navigate within.
const NUXT_DOCK_GROUP_ID = 'nuxt'
// Host controls must update the visible Devframe viewer context, rather than
// Vite's separate dock-registration context. Target the shared-frame anchor
// explicitly so open/navigate always mounts the one kept-alive client iframe
// used by every Nuxt tab.
const NUXT_DOCK_ANCHOR_ID = 'nuxt:devtools'

function getViteDevToolsContext() {
return getDevToolsClientContext() as any
function getDevframeContext() {
return getDevframeClientContext() as any
}

const clientRef = shallowRef<NuxtDevtoolsHostClient>()
Expand Down Expand Up @@ -56,27 +56,27 @@ export async function setupDevToolsClient({

devtools: {
toggle() {
const ctx = getViteDevToolsContext()
const ctx = getDevframeContext()
if (ctx)
ctx.docks.toggleEntry(NUXT_DOCK_GROUP_ID)
ctx.docks.toggleEntry(NUXT_DOCK_ANCHOR_ID)
},
close() {
const ctx = getViteDevToolsContext()
const ctx = getDevframeContext()
if (ctx)
ctx.panel.store.open = false
ctx.panel.session.open = false
},
open() {
const ctx = getViteDevToolsContext()
const ctx = getDevframeContext()
if (ctx) {
ctx.panel.store.open = true
ctx.docks.switchEntry(NUXT_DOCK_GROUP_ID)
ctx.panel.session.open = true
ctx.docks.switchEntry(NUXT_DOCK_ANCHOR_ID)
}
},
async navigate(path: string) {
const ctx = getViteDevToolsContext()
const ctx = getDevframeContext()
if (ctx) {
ctx.panel.store.open = true
ctx.docks.switchEntry(NUXT_DOCK_GROUP_ID)
ctx.panel.session.open = true
ctx.docks.switchEntry(NUXT_DOCK_ANCHOR_ID)
}
await client.hooks.callHook('host:action:navigate', path)
},
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ catalogs:
inlined:
package-manager-detector: ^1.8.0
prod:
'@devframes/hub': *devframe
'@devframes/plugin-code-server': *devframe
'@devframes/plugin-data-inspector': *devframe
'@nuxt/kit': *nuxt-kit
Expand Down
21 changes: 13 additions & 8 deletions tests/e2e/fixtures/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ interface DevToolsFixtures {
}

// e2e servers run with `VITE_DEVTOOLS_DISABLE_CLIENT_AUTH=true`, which trusts the
// *server* peer (so RPC is allowed) but never flips the *client-side* trust flag.
// Until it does, Vite DevTools never subscribes to the dock list, so no dock —
// and therefore no Nuxt group — ever appears. Nudge the flag here. This is purely
// test-environment plumbing; it is not something the tests assert on.
// *server* peer (so RPC is allowed), but the client can still initialize before
// that state is reflected locally. Complete the handshake through Devframe's
// public API before waiting for docks. This is purely test-environment plumbing;
// it is not something the tests assert on.
async function ensureDockReady(page: Page): Promise<void> {
await page.waitForFunction(
() => Boolean((globalThis as any).__NUXT_DEVTOOLS_HOST__?.devtools),
Expand All @@ -32,11 +32,16 @@ async function ensureDockReady(page: Page): Promise<void> {
null,
{ timeout: 30_000 },
)
await page.evaluate(() => {
const ctx = (globalThis as any).__DEVFRAME_HUB_CLIENT_CONTEXT__
if (ctx?.rpc && !ctx.rpc.isTrusted)
ctx.rpc.events?.emit?.('rpc:is-trusted:updated', true)
await page.evaluate(async () => {
const rpc = (globalThis as any).__DEVFRAME_HUB_CLIENT_CONTEXT__?.rpc
if (rpc && !rpc.isTrusted)
await rpc.requestTrust()
})
await page.waitForFunction(
() => Boolean((globalThis as any).__DEVFRAME_HUB_CLIENT_CONTEXT__?.rpc?.isTrusted),
null,
{ timeout: 30_000 },
)
await page.waitForFunction(
() => Boolean((globalThis as any).__DEVFRAME_HUB_CLIENT_CONTEXT__?.docks?.entries?.length),
null,
Expand Down
31 changes: 31 additions & 0 deletions tests/e2e/specs/runtime-console.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from '../fixtures/devtools'

test.skip(({ playground, mode }) => playground !== 'empty' || mode !== 'dev', 'runtime console: empty playground, dev mode')

test('DevTools renders interactive component rows without runtime diagnostics', async ({ page, openDevTools, navigateTab, devtoolsFrame }) => {
const diagnostics: string[] = []
const reportedRuntimeProblems = [
/does not provide an export named 'default'/i,
/NUXT_E(?:3003|3004|4007|4011)/,
/VUE_ROUTER_R0004/,
/Property "(?:finalTheme|getTargetNodes|themeClass|slotData)" was accessed during render but is not defined/,
/Cannot destructure property 'popperId'/,
]

page.on('pageerror', error => diagnostics.push(error.message))
page.on('console', (message) => {
const text = message.text()
if (reportedRuntimeProblems.some(pattern => pattern.test(text)))
diagnostics.push(text)
})

await page.goto('/')
await openDevTools()
await navigateTab('/modules/components')
const frame = devtoolsFrame()
await expect(frame.locator('body')).toContainText(/Built-in components/i, { timeout: 15_000 })
await page.waitForTimeout(500)

await expect.soft(frame.locator('body')).toContainText('NuxtLink', { timeout: 15_000 })
expect.soft(diagnostics, diagnostics.join('\n')).toEqual([])
})
17 changes: 2 additions & 15 deletions tests/e2e/specs/tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,10 @@ test('lists Nuxt built-in components even with no user components', async ({ pag
await page.goto('/')
await openDevTools()
await navigateTab('/modules/components')
// `empty` has no user components, but Nuxt always ships built-ins. Each
// row's name (`NuxtPage`/`NuxtLink`/...) would normally show inside a
// `<VDropdown>` trigger (`ComponentItem.vue`), but that dropdown's default
// slot currently fails to render under this stack: floating-vue's
// `Popper` component throws `Cannot destructure property 'popperId' of
// 'undefined'` invoking its own scoped slot (confirmed live — the row's
// `<button>`/`<ComponentName>` never mounts, leaving only the neighbouring
// `FilepathItem` badge, i.e. the shared `nuxt-nightly` package name,
// repeated once per row instead of the component's own name). This is the
// same "known remaining issue" `#1048` flagged but didn't fix — it affects
// every component/composable/route name rendered inside a dropdown
// trigger, not just this list. So assert on the section heading and count
// (rendered directly by `ComponentsList.vue`, not through the broken
// dropdown) rather than specific component names. Restore the name
// assertion once floating-vue renders `<VDropdown>`'s default slot again.
await expect(devtoolsFrame().locator('body'))
.toContainText(/Built-in components/i, { timeout: 15_000 })
await expect(devtoolsFrame().locator('body'))
.toContainText(/Total components: \d+/i, { timeout: 15_000 })
await expect(devtoolsFrame().locator('body'))
.toContainText('NuxtLink', { timeout: 15_000 })
})
Loading