From 3bede9125e76ba613a54c002615ba8c39365496f Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 22 Aug 2026 08:10:41 +0900 Subject: [PATCH] fix: resolve devtools client runtime errors --- packages/devtools/build.config.ts | 14 ++++++++ packages/devtools/client/app.vue | 2 +- packages/devtools/client/composables/npm.ts | 2 +- packages/devtools/client/nuxt.config.ts | 8 +++++ packages/devtools/client/pages/index.vue | 11 ++++++ packages/devtools/package.json | 1 + .../src/runtime/plugins/view/client.ts | 36 +++++++++---------- pnpm-lock.yaml | 3 ++ pnpm-workspace.yaml | 1 + tests/e2e/fixtures/devtools.ts | 21 ++++++----- tests/e2e/specs/runtime-console.spec.ts | 31 ++++++++++++++++ tests/e2e/specs/tabs.spec.ts | 17 ++------- 12 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 packages/devtools/client/pages/index.vue create mode 100644 tests/e2e/specs/runtime-console.spec.ts diff --git a/packages/devtools/build.config.ts b/packages/devtools/build.config.ts index 52b79012c6..3d5cfe654a 100644 --- a/packages/devtools/build.config.ts +++ b/packages/devtools/build.config.ts @@ -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', @@ -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`) + }, }, }) diff --git a/packages/devtools/client/app.vue b/packages/devtools/client/app.vue index 283940546f..8daaa69ef5 100644 --- a/packages/devtools/client/app.vue +++ b/packages/devtools/client/app.vue @@ -152,7 +152,7 @@ registerCommands(() => [ Connecting....
rpc.checkForUpdateFor('nuxt')) + return usePackageUpdate('nuxt').info } export function satisfyNuxtVersion(range: string) { diff --git a/packages/devtools/client/nuxt.config.ts b/packages/devtools/client/nuxt.config.ts index 8a283a50cc..ba35199106 100644 --- a/packages/devtools/client/nuxt.config.ts +++ b/packages/devtools/client/nuxt.config.ts @@ -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', diff --git a/packages/devtools/client/pages/index.vue b/packages/devtools/client/pages/index.vue new file mode 100644 index 0000000000..e5fe991372 --- /dev/null +++ b/packages/devtools/client/pages/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 35facea51b..a930a7fdee 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -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:*", diff --git a/packages/devtools/src/runtime/plugins/view/client.ts b/packages/devtools/src/runtime/plugins/view/client.ts index 0a7420e121..0c5084cfbe 100644 --- a/packages/devtools/src/runtime/plugins/view/client.ts +++ b/packages/devtools/src/runtime/plugins/view/client.ts @@ -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' @@ -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() @@ -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) }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ae42e62af..9ad2600536 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -452,6 +452,9 @@ importers: packages/devtools: dependencies: + '@devframes/hub': + specifier: ^0.9.4 + version: 0.9.4(crossws@0.4.10(srvx@0.11.22))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)) '@devframes/plugin-code-server': specifier: ^0.9.4 version: 0.9.4(@devframes/hub-ui@0.9.4(@devframes/hub@0.9.4(crossws@0.4.10(srvx@0.11.22))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)))(@devframes/json-render@0.9.4(@devframes/hub@0.9.4(crossws@0.4.10(srvx@0.11.22))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)))(@devframes/hub@0.9.4(crossws@0.4.10(srvx@0.11.22))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22)))(devframe@0.9.4(cac@7.0.0)(srvx@0.11.22))(vite@8.2.2) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 66224f1b15..18ac603210 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/tests/e2e/fixtures/devtools.ts b/tests/e2e/fixtures/devtools.ts index b9fc30ee91..876bc0962f 100644 --- a/tests/e2e/fixtures/devtools.ts +++ b/tests/e2e/fixtures/devtools.ts @@ -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 { await page.waitForFunction( () => Boolean((globalThis as any).__NUXT_DEVTOOLS_HOST__?.devtools), @@ -32,11 +32,16 @@ async function ensureDockReady(page: Page): Promise { 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, diff --git a/tests/e2e/specs/runtime-console.spec.ts b/tests/e2e/specs/runtime-console.spec.ts new file mode 100644 index 0000000000..b15ab2d0b9 --- /dev/null +++ b/tests/e2e/specs/runtime-console.spec.ts @@ -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([]) +}) diff --git a/tests/e2e/specs/tabs.spec.ts b/tests/e2e/specs/tabs.spec.ts index ef047013c3..0085607e84 100644 --- a/tests/e2e/specs/tabs.spec.ts +++ b/tests/e2e/specs/tabs.spec.ts @@ -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 - // `` 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 - // `