diff --git a/plans/README.md b/plans/README.md index e0769aa5c4..3c1cbf1c94 100644 --- a/plans/README.md +++ b/plans/README.md @@ -6,6 +6,11 @@ executor with no prior context. Executor: read the whole plan before starting, honor its STOP conditions, run the drift check first, and update your row below when done. +> **Related plan set:** the numbered plans below are `improve`-skill audit fixes. +> The Vite DevTools 0.4 / devframe 0.6 *integration* RFCs (compat foundation, +> messages/terminals reuse, dock groups, ecosystem dogfooding) live in +> [`vite-devtools-integration/`](./vite-devtools-integration/README.md). + ## Execution order & status | Plan | Title | Priority | Effort | Depends on | Status | diff --git a/plans/vite-devtools-integration/00-compat-foundation.md b/plans/vite-devtools-integration/00-compat-foundation.md new file mode 100644 index 0000000000..6955f08047 --- /dev/null +++ b/plans/vite-devtools-integration/00-compat-foundation.md @@ -0,0 +1,209 @@ +# Plan 00 — Compatibility & deprecation foundation + +**Status:** ready to execute · **Risk:** medium · **Depends on:** nothing · +**Foundation for:** plans 01, 02, 03 build on the surfaces this plan creates. +**Outcome:** Nuxt DevTools keeps its existing public API working, **exposes the +devframe-native API alongside it** (connect-safe), and gains a **nostics-driven +soft-deprecation** system so migrations are minimal and self-discoverable. This +is the base layer for the long-term move to devframe-native APIs. + +> Self-contained: read this whole file. Shared API facts are repeated here. + +## Why this exists + +We're cutting a **new major** of Nuxt DevTools on top of Vite DevTools 0.4 / +devframe 0.6 (migration already merged, nuxt/devtools#1010). Breaking changes +are acceptable, but the migration for module authors/users must be **minimal** +and **self-discoverable**. Strategy: + +1. **Keep** the existing Nuxt DevTools API working. +2. **Expose** the devframe-native API (docks/terminals/messages/commands/ + diagnostics) so authors can start using it directly. +3. **Soft-deprecate** the Nuxt API where a devframe equivalent exists, via a + backward-compatible **shim** + a **nostics** deprecation diagnostic (code + + fix + doc link). Hard-break only where a faithful shim is infeasible. +4. Long term: gradually retire the shimmed Nuxt APIs (next major). + +Plans 01 (messages), 02 (terminals), 03 (dock groups) each *apply* this +foundation to their specific APIs. This plan builds the **mechanism** + policy + +a couple of pilot deprecations. + +## Context you need + +### The devframe-native context (target surface) + +`nuxt.devtools.devtoolsKit` is a `ViteDevToolsNodeContext` (= devframe +`DevframeNodeContext`) exposing `docks`, `terminals`, `messages`, `commands`, +and **`diagnostics`** (a `DevframeDiagnosticsHost`). It is assigned in +`packages/devtools/src/server-rpc/index.ts` (`connectDevToolsKit(ctx)`), and is +**`undefined` until the Vite DevTools plugin connects** — typed +`devtoolsKit: DevToolsNodeContext | undefined` in +`packages/devtools-kit/src/_types/server-ctx.ts`. `server-rpc/index.ts` already +demonstrates the **pre-connect queue** pattern (`pendingBroadcasts`, flushed in +`connectDevToolsKit`). + +### The current Nuxt DevTools public API surface (what modules use) + +- **Server context** `nuxt.devtools` (`NuxtDevtoolsServerContext`, + `packages/devtools-kit/src/_types/server-ctx.ts`): `rpc` (`rpc.broadcast`, + `rpc.functions`), `extendServerRpc(name, fns)`, `refresh(event)`, + `openInEditorHooks`, `devtoolsKit`. +- **`@nuxt/devtools-kit` root exports** (`packages/devtools-kit/src/index.ts`): + `addCustomTab`, `refreshCustomTabs`, `startSubprocess`, `extendServerRpc`, + `onDevToolsInitialized`. +- **Client kit subpaths**: `@nuxt/devtools-kit/iframe-client` + (`onDevtoolsClientConnected`, `useDevtoolsClient`), `@nuxt/devtools-kit/host-client` + (`onDevtoolsHostClientConnected`, `useDevtoolsHostClient`). `NuxtDevtoolsClient` + (`_types/client-api.ts`) with `rpc`, `extendClientRpc`, `renderMarkdown`, + `renderCodeHighlight`, `colorMode`. +- **Nuxt hooks** (`packages/devtools-kit/src/_types/hooks.ts`): `devtools:before`, + `devtools:initialized`, `devtools:customTabs`, `devtools:customTabs:refresh`, + `devtools:terminal:register|write|remove|exit`, runtime `devtools:terminal:data`. +- **Custom tabs**: `ModuleCustomTab` (`_types/custom-tabs.ts`) with iframe/launch/ + vnode views; `ModuleOptions.customTabs`. +- **Subprocess**: `startSubprocess(execaOptions, tabOptions, nuxt?)` + (`src/index.ts`), returns `{ getProcess (deprecated), getResult, terminate, + restart, clear }`. + +### nostics (diagnostics library) — the deprecation engine + +`nostics@1.1.4` (dep of devframe & devtools-kit). Two ways to use it: + +- **Standalone catalog** (works pre-connect, prints to terminal): + ```ts + import { createConsoleReporter, defineDiagnostics } from 'nostics' + const diagnostics = defineDiagnostics({ + docsBase: (code) => `https://devtools.nuxt.com/e/${String(code).toLowerCase()}`, // confirm canonical URL + reporters: [createConsoleReporter()], // default method: 'warn' + codes: { + NDT_DEP_0001: { + why: (p) => `\`${p.api}\` is deprecated.`, + fix: (p) => `Use \`${p.replacement}\` instead.`, + }, + }, + }) + diagnostics.NDT_DEP_0001({ api: 'x', replacement: 'y' }) // warns, returns a throwable Diagnostic + throw diagnostics.NDT_DEP_0001({ api: 'x', replacement: 'y' }) // hard-break variant (error path) + diagnostics.NDT_DEP_0001({ ... }, { method: 'error' }) // per-call severity override + ``` + - Definition shape: `{ why: string|((p)=>string), fix?: string|((p)=>string), docs?: string|false }`. **No `level`** field — severity is chosen per call via the reporter `method`. +- **Register into the DevTools host** (so deprecations also surface *inside* + DevTools) after connect, via `nuxt.devtools.devtoolsKit.diagnostics`: + ```ts + const cat = ctx.diagnostics.defineDiagnostics({ docsBase, codes }) // host supplies its ANSI reporter + ctx.diagnostics.register(cat) + // later: throw ctx.diagnostics.logger.NDT_DEP_0001({...}) / cat.NDT_DEP_0001({...}) + ``` + `DevframeDiagnosticsHost` = `{ logger, register(defs), defineDiagnostics(opts) }`. + +Existing ad-hoc deprecation warnings to fold into this: the `startSubprocess` +`getProcess()` `console.warn` (`packages/devtools-kit/src/index.ts` ~line 126) +and the `logger.warn` in `packages/devtools/src/server-rpc/index.ts` (the +`disableAuthorization` notice, ~lines 165‑168), and the `disableAuthorization` +warning in `module-main.ts`. + +## Decisions (locked) + +1. **Expose devframe-native API as first-class connect-safe hosts** on + `nuxt.devtools`: `nuxt.devtools.docks`, `.terminals`, `.messages`, + `.commands`, `.diagnostics` — proxies that resolve to `devtoolsKit.*` once + connected and **queue pre-connect calls** (mirroring `pendingBroadcasts`). + Keep the raw `devtoolsKit` as an escape hatch. +2. **Self-discoverable deprecations** via a Nuxt nostics catalog + (`defineDiagnostics` with `createConsoleReporter()` + `docsBase` → Nuxt docs), + **also registered into the DevTools host diagnostics after connect** so they + appear in DevTools too. Warn-only by default; each code carries `why` + `fix` + + doc link. +3. **Shim-first** compatibility: every existing API keeps working this major via + shims forwarding to devframe; deprecations are warn-only; removal deferred to + the next major. **Hard-break only where a faithful shim is infeasible** — + those emit an **error-level** diagnostic naming the exact replacement. + +## Implementation + +### Step 1 — connect-safe devframe host accessors on `nuxt.devtools` + +In `packages/devtools/src/server-rpc/index.ts` (`setupRPC`) — where the context +and `pendingBroadcasts` queue already live: + +- Add `docks`, `terminals`, `messages`, `commands`, `diagnostics` to the + `NuxtDevtoolsServerContext` (extend the type in + `packages/devtools-kit/src/_types/server-ctx.ts`). +- Implement each as a small proxy/wrapper: when `devtoolsKitCtx` is set, forward + to `devtoolsKitCtx.`; when not, either (a) buffer mutating calls + (register/add/start…) into a queue flushed in `connectDevToolsKit`, or (b) + return a thenable/guarded stub. Prefer buffering for the register/add/start + methods and reads returning empty until connected. +- Keep `devtoolsKit` getter as the raw escape hatch. +- Document these as the **forward path** in `docs/`. + +### Step 2 — the Nuxt deprecation diagnostics catalog + +- Add a module, e.g. `packages/devtools-kit/src/diagnostics.ts` (exported so both + the kit and the app can emit), that builds the standalone catalog with + `defineDiagnostics({ docsBase, reporters:[createConsoleReporter()], codes })`. + Reserve a code range/prefix: `NDT_DEP_xxxx` for deprecations (and maybe + `NDT_xxxx` for other diagnostics later). +- Provide a tiny `deprecate(code, params, opts?)` helper wrapping the handle call + (default warn; `opts.method:'error'` for hard-breaks). +- After connect (in `connectDevToolsKit`), also `ctx.diagnostics.register(cat)` + (or re-define via `ctx.diagnostics.defineDiagnostics` and register) so codes + are known to DevTools and post-connect emissions surface in the DevTools + diagnostics UI. Guard for `diagnostics` host availability. +- Confirm the canonical docs URL scheme with maintainers (e.g. + `https://devtools.nuxt.com/e/` or a migration guide anchor); wire it into + `docsBase`. + +### Step 3 — shim pattern + deprecation classification map + +Establish the pattern (used by plans 01/02/03): keep the old API; internally +forward to the devframe host; emit the deprecation diagnostic once per unique +call site (dedupe by code+key to avoid noise). Ship this **classification map** +(implemented incrementally by the owning plans): + +| Nuxt API | devframe-native path | Policy | Owner | +|---|---|---|---| +| `devtoolsUiShowNotification` | `messages` host (`add`/`info`/…) | shim + deprecate | Plan 01 | +| `startSubprocess`, `devtools:terminal:*` hooks | `terminals` host (`register`/`startChildProcess`/`startPtySession`) | shim + deprecate | Plan 02 | +| `startSubprocess().getProcess()` | `getResult()` | already deprecated → move to nostics code | Plan 00 (pilot) | +| `addCustomTab` / `devtools:customTabs` / `ModuleCustomTab` | stays (Nuxt-native); gains `dock:true` → `docks` | **keep** (extend, not deprecate) | Plan 03 | +| `extendServerRpc`, `rpc.broadcast`, `rpc.functions` | `ctx.rpc` (`register`/`defineRpcFunction`/`broadcast`) | keep + expose; low-urgency soft-deprecate later | Plan 00 exposes; future | +| client `extendClientRpc`, `useDevtoolsClient`, `onDevtoolsClientConnected` | devframe client RPC | keep + shim | future | +| `onDevToolsInitialized`, `devtools:before/initialized` | Nuxt lifecycle | **keep** (no devframe equivalent) | — | +| `disableAuthorization` option | handled by Vite DevTools auth | already warned → move to nostics code (**breaking**, error-level) | Plan 00 (pilot) | + +### Step 4 — pilot deprecations (prove the mechanism) + +- Replace the `getProcess()` `console.warn` (`devtools-kit/src/index.ts`) with a + `NDT_DEP_xxxx` warn diagnostic. +- Replace the `disableAuthorization` `logger.warn` (`server-rpc/index.ts` / + `module-main.ts`) with a diagnostic (error-level — it no longer does anything). +- These validate both reporting paths (pre-connect console + post-connect host). + +## Acceptance criteria + +- `nuxt.devtools.terminals` / `.messages` / `.docks` / `.commands` / + `.diagnostics` are usable from a module's setup (calls made before the kit + connects are queued and applied after connect; no crashes when `devtoolsKit` + is still undefined). +- Using a deprecated API prints a terminal warning with a **code**, a **fix**, + and a **doc link**, and the same deprecation is visible inside DevTools' + diagnostics after connect. +- The two pilot deprecations fire correctly (getProcess → warn; disableAuthorization → error). +- `pnpm lint && pnpm build && pnpm typecheck` pass. +- No existing module integration breaks solely due to this plan (shims in place; + plans 01/02/03 attach their specific shims). + +## Risks / gotchas + +- **Connect timing.** The hosts are undefined pre-connect; buffer mutating calls + and flush on `connectDevToolsKit` (reuse the `pendingBroadcasts` pattern). +- **Diagnostic noise.** Dedupe deprecation emissions (once per code+key) so a + hot path doesn't spam the console. +- **Docs URL.** `docsBase` must point at real pages or the "see:" link is dead — + confirm the scheme and create stubs/redirects before shipping. +- **Prod stripping.** nostics supports `defineProdDiagnostics` + a strip plugin; + DevTools is dev-only so this is optional, but keep `why`/`fix` out of any + shipped runtime if size matters. +- **Don't over-deprecate.** Custom tabs and the Nuxt lifecycle hooks have no + clean devframe equivalent yet — keep them; only deprecate where the map says so. diff --git a/plans/vite-devtools-integration/01-messages-unification.md b/plans/vite-devtools-integration/01-messages-unification.md new file mode 100644 index 0000000000..40bc7ab26a --- /dev/null +++ b/plans/vite-devtools-integration/01-messages-unification.md @@ -0,0 +1,190 @@ +# Plan 01 — Unify notifications on the devframe Messages system + +**Status:** ready to execute · **Risk:** low · **Depends on:** nothing +**Outcome:** one notification system across Nuxt DevTools — ephemeral toasts and +persistent, leveled notifications both flow through devframe's Messages +subsystem, surfaced by Vite DevTools' built-in **Messages** dock and its toast +overlay. Nuxt's bespoke `devtoolsUiShowNotification` toast is retired (or reduced +to a thin adapter). + +> Self-contained: read this whole file; you don't need other plans. Shared API +> facts are repeated here. + +## Relationship to Plan 00 (foundation) + +If Plan 00 (compat foundation) has landed, prefer its **connect-safe** +`nuxt.devtools.messages` host over reaching into `nuxt.devtools.devtoolsKit.messages` +directly (it queues pre-connect calls for you), and emit the toast +soft-deprecation through Plan 00's **nostics** catalog (a `NDT_DEP_xxxx` code +with `fix` + doc link) rather than an ad-hoc `console.warn`. If Plan 00 is not +yet in place, use `devtoolsKit.messages` guarded for the undefined-until-connect +window (queue + flush like `server-rpc/index.ts`'s `pendingBroadcasts`). + +## Context you need + +Repo: `nuxt/devtools` monorepo (this checkout). Nuxt DevTools v4 renders inside +**Vite DevTools** `@vitejs/devtools@0.4` on the **devframe 0.6** runtime. The +built-in **Messages** dock (`@devframes/plugin-messages`) is already mounted by +`DevTools()` (Nuxt uses default options) and auto-hides when there are no +persisted entries. PR nuxt/devtools#1010 (already on `main`) kept the Vite +DevTools core toast working: a message with `notify: true` shows a floating +toast in the Vite DevTools chrome (rendered above the panel iframe). + +### Current Nuxt notification system (to be replaced) + +- `packages/devtools-ui-kit/src/composables/notification.ts` — defines + `devtoolsUiShowNotification({ message, icon?, classes?, duration?, position? })` + which proxies to a fn registered by `devtoolsUiProvideNotificationFn`. +- `packages/devtools-ui-kit/src/components/NNotification.vue` — the single + transient toast component (one message at a time, default 1500ms, default + `top-center`); registers the provider fn; mounted once in + `packages/devtools/client/app.vue` (``, ~line 146). +- Callers of `devtoolsUiShowNotification` (all client-side, fire-and-forget): + - `packages/devtools/client/components/AssetDetails.vue` (multiple) + - `packages/devtools/client/components/AssetDropZone.vue` (multiple) + - `packages/devtools/client/composables/editor.ts` + - `packages/devtools/client/pages/modules/timeline.vue` + +There is **no** message history / severity / server-originated notification +today. + +### devframe Messages API (target) + +**Node side** — `ctx.messages` (a `DevframeMessagesHost`) where `ctx` is the +`ViteDevToolsNodeContext` available in `module-main.ts`'s +`devtools.setup(ctx)` and stored as `devtoolsKit` on the Nuxt server context +(see `packages/devtools/src/server-rpc/index.ts` → `connectDevToolsKit`, and the +`devtoolsKit` field typed in `packages/devtools-kit/src/_types/server-ctx.ts`): + +```ts +ctx.messages.add({ message, level, description?, notify?, autoDismiss?, autoDelete?, labels?, category?, filePosition?, stacktrace? }) +ctx.messages.info|warn|error|success|debug(message, extra?) // Promise +ctx.messages.update(id, patch); ctx.messages.remove(id); ctx.messages.clear() +``` + +Key entry fields: +- `level`: `'info' | 'warn' | 'error' | 'success' | 'debug'`. +- `notify`: show a transient toast in the Vite DevTools chrome. +- `autoDismiss`: toast auto-hides. +- `autoDelete`: entry is not kept in the persistent list (toast-only). + +**Client side** — the Nuxt DevTools client already holds a devframe RPC client +(`packages/devtools/client/composables/rpc.ts` → `rpcClient` / +`getDevToolsRpcClient()`, and the `rpc` proxy that does `client.call(method, …)`). +Messages can be added from the client by calling the messages RPC method +(`devframes-plugin-messages:add`) — the client is trusted and the method is +registered by the plugin. Confirm the exact method name at build time by +inspecting `node_modules/.pnpm/@devframes+plugin-messages@0.6.0*/…/dist/rpc` +(`devframes-plugin-messages:list|add|update|remove|clear`). There is also a core +kit surface (`devtoolskit:internal:messages:*` / `hub:messages:*`) visible in +the connection meta; prefer the plugin method, and fall back to whichever one +actually drives the core toast if the plugin one does not (verify with a manual +`rpc.call(...)` in the running playground). + +## Decisions (locked) + +1. **Unify** everything onto the devframe Messages system — no parallel toast. +2. **Tiered by intent**: + - Ephemeral client feedback (e.g. "Copied!") → `notify: true`, + `autoDismiss: true`, `autoDelete: true` (toast-only, never persisted). + - Server-originated notifications → persisted, with a real `level`, no + `autoDelete`, so they build history in the Messages dock. +3. **Public notify API** for Nuxt internals + ecosystem modules, forwarded to + `ctx.messages`, plus a curated set of built-in sources. + +## Implementation + +### Step 1 — server: a notify path into `ctx.messages` + +- In the server RPC layer (`packages/devtools/src/server-rpc/`), add a small + module (e.g. `messages.ts`) wired in `server-rpc/index.ts` alongside the other + `setup*RPC` calls. It should: + - Expose a helper `notify(input)` that calls `ctx.devtoolsKit?.messages.add(…)` + (guard for `devtoolsKit` being `undefined` before the kit connects — queue + and flush on connect, mirroring how `server-rpc/index.ts` already queues + `pendingBroadcasts` until `connectDevToolsKit` runs). + - Register a Nuxt hook **`devtools:notify`** (add it to the hooks type in + `packages/devtools-kit/src/_types/hooks.ts`) so modules can push: + `nuxt.callHook('devtools:notify', { message, level?, description?, … })`. + - Expose an RPC function `notify(input)` on the server functions so the client + can push server-persisted messages when appropriate. +- Type the notify input as a Nuxt-friendly subset of `DevframeMessageEntryInput` + (`message`, `level?`, `description?`, `labels?`, `filePosition?`, `notify?`, + `autoDismiss?`, `autoDelete?`). + +### Step 2 — curated built-in sources + +Wire an initial, deliberately small set of internal producers to `notify(...)` +(persisted, leveled — no `autoDelete`): + +- **Build / module errors & warnings**: hook Vite/Nuxt build errors and module + setup warnings. Start with the ones Nuxt DevTools already knows about + (e.g. analyze-build failures in `packages/devtools/src/integrations/analyze-build.ts`, + and module-install failures surfaced via the terminal/exit path). +- **Server task results**: on server-task completion/failure (see + `packages/devtools/src/server-rpc/server-tasks.ts`) push a `success`/`error`. + +Keep this curated — do **not** blanket-mirror consola. Each source should be an +intentional, leveled message. + +### Step 3 — client: re-point the ephemeral toast + +- Reimplement `devtoolsUiShowNotification` (in + `packages/devtools-ui-kit/src/composables/notification.ts`) so that, instead of + driving the local `NNotification` component, it emits a devframe message via + the client RPC with `notify:true, autoDismiss:true, autoDelete:true` and a + `level` derived from intent (default `info`; allow callers to pass one). + - The UI kit must not hard-depend on the devtools client RPC. Keep the + `devtoolsUiProvideNotificationFn` seam: the **client** provides the concrete + implementation (which calls the devframe RPC) at startup; the UI kit stays a + thin proxy. So: keep `notification.ts` as the proxy, and register the + devframe-backed implementation from the Nuxt DevTools client (e.g. in + `packages/devtools/client/plugins/` or `app.vue` setup) via + `devtoolsUiProvideNotificationFn`. +- Map the existing callers' options (`message`, `icon`, `duration`, `position`) + onto message fields where they have an equivalent; `position`/`duration` become + no-ops or best-effort (the chrome toast owns placement/timing). Audit each + caller (`AssetDetails.vue`, `AssetDropZone.vue`, `editor.ts`, `timeline.vue`) + and keep the call sites unchanged in shape if possible. + +### Step 4 — retire the bespoke toast + +- Remove `` from `packages/devtools/client/app.vue` and delete + (or empty) `packages/devtools-ui-kit/src/components/NNotification.vue` once no + caller renders it. If the UI kit exports `NNotification` publicly, keep an + export shim or do a deprecation cycle (see risks). + +### Step 5 — expose `notify` in the public client API + +- Add `notify(...)` to the injected client (`NuxtDevtoolsClient` / + `useNuxtDevTools()` — see `packages/devtools/src/runtime/use-nuxt-devtools.ts` + and the client injection in `packages/devtools/client/composables/client.ts`), + so custom tabs and host code can push notifications through the same system. + +## Acceptance criteria + +- Triggering a "Copied!"-style action shows a transient toast in the Vite + DevTools chrome and leaves **no** entry in the Messages dock list. +- `nuxt.callHook('devtools:notify', { message, level: 'error' })` (and a build + error from a curated source) shows a toast **and** a persisted entry with the + correct severity in the Messages dock. +- The old `NNotification` component is no longer mounted; no dead + `devtoolsUiShowNotification` behavior remains. +- `pnpm lint && pnpm build && pnpm typecheck` pass. +- Manual: run `pnpm -C playgrounds/tab-pinia exec nuxt dev`; open Vite DevTools; + confirm toast placement and Messages-dock persistence for both tiers. + +## Risks / gotchas + +- **Which RPC method drives the core toast.** Verify empirically in the running + playground (`rpc.call('devframes-plugin-messages:add', …)` vs the + `devtoolskit:internal:messages:*` / `hub:messages:*` methods). Whichever makes + a floating toast appear is the one to use for the ephemeral tier. +- **Toast location.** The chrome toast renders in the Vite DevTools host layer, + not inside the (possibly promoted, separate) Nuxt iframe where the action + happened. This is expected under "unify"; confirm it renders above the panel. +- **UI-kit dependency direction.** Don't make `@nuxt/devtools-ui-kit` depend on + the devtools client RPC. Preserve the `devtoolsUiProvideNotificationFn` seam. +- **Public API break.** If `NNotification` / `devtoolsUiShowNotification` are + part of the UI kit's public surface, keep a compatibility shim and deprecate + rather than hard-remove. diff --git a/plans/vite-devtools-integration/02-terminals-reuse.md b/plans/vite-devtools-integration/02-terminals-reuse.md new file mode 100644 index 0000000000..a10cc4c255 --- /dev/null +++ b/plans/vite-devtools-integration/02-terminals-reuse.md @@ -0,0 +1,210 @@ +# Plan 02 — Reuse the built-in devframe Terminals dock + +**Status:** ready to execute · **Risk:** medium · **Depends on:** nothing +**Outcome:** Nuxt DevTools stops shipping its own `@xterm` terminal UI + RPC and +instead surfaces terminal sessions through devframe's `ctx.terminals`, so they +render in Vite DevTools' built-in **Terminals** dock. Existing modules that use +the `devtools:terminal:register` hook keep working via a compat shim. A new +opt-in interactive **PTY** path is added. + +> Self-contained: read this whole file; shared API facts are repeated here. + +## Relationship to Plan 00 (foundation) + +If Plan 00 (compat foundation) has landed, prefer its **connect-safe** +`nuxt.devtools.terminals` host over `nuxt.devtools.devtoolsKit.terminals` +(it queues pre-connect `register`/`start…` calls), and emit the +`devtools:terminal:register` / `startSubprocess` soft-deprecations through Plan +00's **nostics** catalog (`NDT_DEP_xxxx` with `fix` + doc link). The +`getProcess()`→`getResult()` deprecation is already a Plan 00 pilot. If Plan 00 +is not yet in place, use `devtoolsKit.terminals` guarded for the +undefined-until-connect window (queue + flush like `pendingBroadcasts`). + +## Context you need + +Nuxt DevTools v4 renders inside **Vite DevTools** `@vitejs/devtools@0.4` on the +**devframe 0.6** runtime. `DevTools()` (Nuxt uses default options) already mounts +the built-in **Terminals** dock (`@devframes/plugin-terminals`), which auto-hides +when there are no sessions and **surfaces sessions registered on the hub +terminals host read-only** (in addition to ones it spawns itself). + +### Current Nuxt terminals system (to be replaced) + +Model today = "a module spawns/owns its own process and streams output to +DevTools; DevTools is a passive viewer" (output-only; no stdin, no resize). + +- **Server RPC:** `packages/devtools/src/server-rpc/terminals.ts` — + `setupTerminalRPC({ nuxt, rpc, refresh })` keeps an in-memory + `Map` and listens on Nuxt hooks: + - `devtools:terminal:register` → add + `refresh('getTerminals')` + - `devtools:terminal:remove` → delete + - `devtools:terminal:write` → append to `terminal.buffer` + + `rpc.broadcast.onTerminalData.asEvent({ id, data })` + - `devtools:terminal:exit` → mark `isTerminated` + + `rpc.broadcast.onTerminalExit.asEvent({ id, code })` + - RPC fns: `getTerminals()`, `getTerminalDetail(id)` (with buffer), + `runTerminalAction(id, 'restart'|'terminate'|'remove'|'clear')`. + - Wired in `packages/devtools/src/server-rpc/index.ts` via `setupTerminalRPC(ctx)`. +- **Process lifecycle helper:** `packages/devtools-kit/src/index.ts` (~lines + 46‑121) — `start`/`restart`/`clear`/`terminate`/`register` spawn via + `tinyexec`'s `x()`, force `COLORS`/`FORCE_COLOR`, pipe stdout/stderr through + `devtools:terminal:write`, and fire `devtools:terminal:exit` on exit. +- **Client:** + - `packages/devtools/client/pages/modules/terminals.vue` (the "Terminals" tab, + inside ``). + - `packages/devtools/client/components/TerminalPage.vue` (tab bar / selection / + remove). + - `packages/devtools/client/components/TerminalView.vue` — `new Terminal()` + from `@xterm/xterm` + `FitAddon` from `@xterm/addon-fit` + `@xterm/xterm/css/xterm.css`; + seeds from `getTerminalDetail`, streams via the `devtools:terminal:data` + client hook; footer buttons call `runTerminalAction`. + - Client broadcast handlers in `packages/devtools/client/setup/client-rpc.ts` + (`onTerminalData` → `devtools:terminal:data` hook; `onTerminalExit` → cleanup + of installing-modules / analyze-build subprocess entries). +- **Types:** broadcast signatures in `packages/devtools-kit/src/_types/rpc.ts` + (`onTerminalData`, `onTerminalExit`); hook signatures in + `packages/devtools-kit/src/_types/hooks.ts` (`devtools:terminal:register/write/…`). +- **Deps:** `@xterm/addon-fit`, `@xterm/xterm` in `packages/devtools/package.json`. +- **Internal consumers of the terminal pipeline** (must keep working): module + installation and analyze-build spawn terminals and rely on + `devtools:terminal:exit` to clean up their UI state (see + `state-subprocess` usage in `client-rpc.ts`, and + `packages/devtools/src/integrations/analyze-build.ts` / + the npm/module-install RPC). + +### devframe Terminals API (target) + +`ctx.terminals` is a `DevframeTerminalsHost` on the `ViteDevToolsNodeContext` +available in `packages/devtools/src/module-main.ts`'s `devtools.setup(ctx)` and +stored as `devtoolsKit` on the Nuxt server context +(`packages/devtools/src/server-rpc/index.ts` → `connectDevToolsKit`; +`packages/devtools-kit/src/_types/server-ctx.ts` `devtoolsKit` field): + +```ts +interface DevframeTerminalsHost { + readonly sessions: Map + readonly events: EventEmitter<{ 'terminal:session:updated': (s) => void }> + register: (session: DevframeTerminalSession) => DevframeTerminalSession // surface an externally-owned session (read-only) + update: (session: DevframeTerminalSession) => void + startChildProcess: (exec, terminal) => Promise // output-only, devframe owns process + startPtySession: (exec, terminal) => Promise // interactive: write(data)+resize(cols,rows) +} +// DevframeTerminalSession: { id, title, status:'running'|'stopped'|'error', icon?, interactive?, buffer?: string[], stream?: ReadableStream } +``` + +Two ways to feed it: +- **`register(session)`** — for the current model where *the module owns the + process*. Provide a session carrying a `stream` (or `buffer`) you push into. + This is the mapping for the compat shim. +- **`startChildProcess` / `startPtySession`** — for the *new* model where + devframe spawns and owns the process from `{ command, args, cwd?, env? }`. + +## Decisions (locked) + +1. **Full replacement + compat shim**: delete Nuxt's terminal UI (`@xterm`), + `server-rpc/terminals.ts`, and the Terminals tab; route sessions through + `ctx.terminals` (built-in Terminals dock). **Keep the + `devtools:terminal:register` hook** as a shim forwarding to `ctx.terminals` + so existing modules keep working unchanged. +2. **Capability**: preserve **output-only** for existing module terminals (map to + read-only registered sessions via `ctx.terminals.register` + a stream fed by + `devtools:terminal:write`), and **add opt-in interactive PTY** via + `ctx.terminals.startPtySession` for new use cases. + +## Implementation + +### Step 1 — bridge the Nuxt terminal hooks onto `ctx.terminals` (compat shim) + +Rewrite `packages/devtools/src/server-rpc/terminals.ts` (or replace it with a +`terminals-bridge` integration) so that instead of maintaining its own map + RPC +it: + +- On `devtools:terminal:register`: create a `ReadableStream` (keep its + controller in a `Map`), build a `DevframeTerminalSession` + `{ id, title: terminal.name, status: 'running', icon?, stream }`, and call + `devtoolsKit.terminals.register(session)`. Guard for `devtoolsKit` being + `undefined` before the kit connects — queue registrations and flush them in + `connectDevToolsKit` (mirror the existing `pendingBroadcasts` pattern in + `server-rpc/index.ts`). +- On `devtools:terminal:write`: `controller.enqueue(data)` for that id (and keep + a rolling buffer if the session's initial `buffer` needs seeding). +- On `devtools:terminal:exit`: `controller.close()`, set the session `status` to + `'stopped'`/`'error'` and `devtoolsKit.terminals.update(session)`. +- On `devtools:terminal:remove` + `runTerminalAction` semantics: map + `restart`/`terminate`/`remove`/`clear` onto the module-provided + `onActionRestart`/`onActionTerminate` callbacks (still owned by the module) and + the session lifecycle. The built-in Terminals dock provides the UI controls; if + a control has no counterpart on a read-only registered session, hide/disable it + (registered read-only sessions have no `write`/`resize`). + +Keep the `TerminalState`/`TerminalInfo`/`TerminalAction` types and the +`devtools:terminal:*` hook signatures (`packages/devtools-kit/src/_types/`) so +module-facing contracts don't change. + +### Step 2 — internal producers keep working + +- The module-install and analyze-build flows currently spawn via the + devtools-kit `start()` helper and rely on `devtools:terminal:exit`. Keep + `packages/devtools-kit/src/index.ts`'s `start()`/`register()` API (it fires the + same hooks), so those producers are unchanged; they now surface via the bridge + in the built-in dock. +- The client-side cleanup in `client-rpc.ts` (`onTerminalExit` → prune + installing-modules / analyze-build state) depends on the exit broadcast. Since + we're removing the Nuxt terminals RPC, **re-home that cleanup**: subscribe to + terminal exit via the devframe terminals host events (or keep a minimal + server→client broadcast for exit) so subprocess UI state still clears. + Do **not** lose this behavior. + +### Step 3 — add an opt-in PTY path + +- Add a server helper (e.g. on the Nuxt server context / a new + `devtools:terminal:spawn` hook option `{ interactive: true }`) that calls + `devtoolsKit.terminals.startPtySession({ command, args, cwd, env, cols, rows }, { id, title })`. + Interactive sessions get stdin `write` + `resize` handled entirely by the + built-in Terminals dock UI (no Nuxt client code needed). +- Note PTY uses `zigpty` native bindings with graceful pipe fallback + (`isPtyAvailable()`); the fallback degrades TUI fidelity but still runs — no + hard failure if bindings are missing. Do **not** make PTY the default. + +### Step 4 — delete the bespoke UI + deps + +- Remove `packages/devtools/client/pages/modules/terminals.vue`, + `components/TerminalPage.vue`, `components/TerminalView.vue`. +- Remove the `@xterm/xterm` + `@xterm/addon-fit` deps from + `packages/devtools/package.json` (and the catalog entries in + `pnpm-workspace.yaml` if now unused), plus the optimizeDeps/prebundle mentions + of xterm if any. Run an install to refresh the lockfile. +- Remove the now-unused `onTerminalData`/`getTerminals`/`getTerminalDetail` + client and server surface (keep only what Step 2's cleanup needs). +- Drop the "Terminals" entry from the client tab list (the SideNav will no longer + show it; the built-in Terminals dock replaces it). + +## Acceptance criteria + +- A module registering a terminal via `nuxt.callHook('devtools:terminal:register', …)` + and streaming output via `devtools:terminal:write` shows up as a read-only + session in the built-in **Terminals** dock, with live output; the Nuxt + "Terminals" tab no longer exists. +- Module install / analyze-build still work end-to-end, and their transient UI + state clears on process exit. +- An opt-in interactive PTY session accepts keystrokes and resizes in the + built-in dock (or degrades to piped output if zigpty is unavailable). +- No `@xterm/*` deps remain; `pnpm lint && pnpm build && pnpm typecheck` pass. +- Manual: a playground module that registers a dev-server terminal shows it in + the Terminals dock. + +## Risks / gotchas + +- **Ownership-model mismatch.** The current hook model is "module owns the + process"; `startChildProcess`/`startPtySession` are "devframe owns it". Use + `register(session)` + a stream for the shim so module-owned processes keep + their lifecycle/`onActionRestart`/`onActionTerminate` semantics. +- **Don't drop exit-driven cleanup.** `client-rpc.ts` prunes installing-modules / + analyze-build state on `devtools:terminal:exit`. Preserve an exit signal path. +- **Timing before kit connect.** Terminals may register before + `connectDevToolsKit` runs; queue + flush (mirror `pendingBroadcasts`). +- **Read-only session controls.** Registered read-only sessions have no + `write`/`resize`; make sure the dock UI doesn't imply interactivity for them + (rely on the session's `interactive` flag being falsy). +- **PTY native bindings** may be absent in some environments — keep PTY opt-in + and rely on the documented pipe fallback. diff --git a/plans/vite-devtools-integration/03-dock-groups-presentation.md b/plans/vite-devtools-integration/03-dock-groups-presentation.md new file mode 100644 index 0000000000..1934d79828 --- /dev/null +++ b/plans/vite-devtools-integration/03-dock-groups-presentation.md @@ -0,0 +1,222 @@ +# Plan 03 — "Nuxt" dock group + promote-tab-to-dock capability + +**Status:** ready to execute · **Risk:** high (UX/architecture) · +**Depends on:** benefits from plans 01 & 02 landing first (not a hard dep) +**Outcome:** Nuxt DevTools presents as a **"Nuxt" dock group** in the Vite +DevTools dock bar. The full client stays reachable as the group's hub member, +and a curated set of tools is **promoted** to sibling dock buttons under the +group. Promotion is a general, opt-in capability any tab can request. + +> Self-contained: read this whole file; shared API facts are repeated here. +> This is the largest/most opinionated plan — do it after 01 & 02 if possible. + +## Relationship to Plan 00 (foundation) + +If Plan 00 (compat foundation) has landed, register the group + promoted entries +through its **connect-safe** `nuxt.devtools.docks` host (queues pre-connect +`register` calls) rather than reaching into `devtoolsKit.docks` directly. The +`dock:true` promotion flag is an **additive extension** of the existing custom +tabs API (`ModuleCustomTab`) — it is **not** a deprecation (per the Plan 00 +classification map, custom tabs stay Nuxt-native). If Plan 00 is not yet in +place, use `devtoolsKit.docks` guarded for the undefined-until-connect window. + +## Context you need + +Nuxt DevTools v4 renders inside **Vite DevTools** `@vitejs/devtools@0.4` on +**devframe 0.6**. Today it registers exactly **one** dock entry: + +`packages/devtools/src/module-main.ts` (~lines 92‑98), inside the plugin +`devtools.setup(ctx)` callback (`ctx` is a `ViteDevToolsNodeContext`): + +```ts +ctx.docks.register({ + id: 'nuxt:devtools', + type: 'iframe', + icon: 'https://nuxt.com/assets/design-kit/icon-green.svg', + title: 'Nuxt DevTools', + url: '/__nuxt_devtools__/client/', +}) +``` + +That iframe loads the whole client, whose tabs live behind an internal +`SideNav`. Tab model: +- Built-in tabs = Nuxt pages under `packages/devtools/client/pages/modules/*.vue`, + each declaring `definePageMeta({ icon, title, category?, order?, ... })`. +- Category logic: `packages/devtools/client/composables/state-tabs.ts` + (`getCategorizedTabs` / `getCategorizedRecord`); default category `'app'`. +- Custom tabs (module- and user-contributed): server side + `packages/devtools/src/server-rpc/custom-tabs.ts` (`setupCustomTabRPC`, seeded + from `options.customTabs`, extended via the `devtools:customTabs` Nuxt hook, + refreshed via `devtools:customTabs:refresh`); client side + `useCustomTabs()` (`client/composables/state.ts`), merged in `state-tabs.ts`. + A custom tab may be an **iframe** view (its own `src`) or an in-client view. + +### devframe dock/group API (target) + +`ctx.docks.register(entry, force?)` on the `ViteDevToolsNodeContext`: + +- A **group** is a first-class dock entry: `{ id, type: 'group', title, icon, + defaultOrder?, category?, defaultChildId? }`. `defaultChildId` = the member + auto-opened when the group button is activated. +- A **member** joins by setting `groupId: ''` on its own entry (any + entry type). Grouping is a flat pointer, **one level deep**, orphan-tolerant. +- `category` (`'app'|'framework'|'web'|'advanced'|'default'|'~builtin'|(string&{})`) + is a **separate** ordering axis, not the grouping mechanism. +- Entry types: `iframe` (`{ url, frameId?, clientScript?, remote? }`), `action`, + `custom-render`, `launcher`, `json-render`, `group`. +- Precedent to copy exactly: Vite DevTools core registers the `~viteplus` + ("Vite+") group and Rolldown joins it with `groupId: DEVTOOLS_VITEPLUS_GROUP_ID` + (constant exported from `@vitejs/devtools-kit/constants`). Find these in + `node_modules/.pnpm/@vitejs+devtools@0.4.0*/…/dist` (`server-*.js` registers the + group; the rolldown package's `index.mjs` joins it). + +### Hard constraint — the Vue DevTools bridge + +Only **one** iframe can hold the Vue DevTools messaging context at a time +(`setIframeServerContext(iframe)` is a single global; see +`packages/devtools/src/runtime/plugins/view/client.ts` — the `bindVueDevToolsIframe` +logic that fixed the "Connecting…" bug binds exactly one dock iframe). Therefore +the Vue-backed tools **Pinia** and **Render Tree** must stay inside the single +hub iframe and must **not** be promoted to their own iframe entries. + +## Decisions (locked) + +1. **Curated hybrid "Nuxt" group.** Register a `type:'group'` entry `id:'nuxt'`, + title "Nuxt". The existing `nuxt:devtools` iframe becomes its hub member + (`groupId:'nuxt'`, and the group's `defaultChildId:'nuxt:devtools'`). +2. **General opt-in promotion.** Any tab — core page, module custom tab, or user + custom tab — can request a dock button under the group via a flag + (e.g. `dock: true` in `definePageMeta` meta and in the custom-tab options + type). Ship a curated default set enabled. +3. **Type-aware realization** of a promoted tab into a dock entry: + - If the tab is an **iframe** custom tab → register a dock `iframe` entry + pointing at the tab's own `src` (`groupId:'nuxt'`). Zero Nuxt-client cost. + - Otherwise (core page / in-client custom tab) → register a dock `iframe` + entry whose `url` deep-links into the Nuxt client in a new **shell-less + "dock" mode** (SideNav hidden), e.g. `/__nuxt_devtools__/client/modules/components?dock=1`. + One Nuxt-client instance per promoted tool (lazily created by iframe-pane on + first open). +4. **Relocating** SideNav semantics: a promoted tool **leaves** the hub SideNav; + it lives only as a dock button. The hub SideNav shrinks to the non-promoted + tools + the Vue-bridge tools (Pinia, Render Tree) that can't be promoted. +5. **Default promoted set: Components, Server Routes, Pages.** (All RPC-driven, + none depend on the Vue bridge, so safe as separate instances.) + +## Implementation + +### Step 1 — register the "Nuxt" group + hub membership + +In `module-main.ts`'s `devtools.setup(ctx)`, before/around the existing +`ctx.docks.register({ id:'nuxt:devtools', … })`: + +```ts +ctx.docks.register({ + id: 'nuxt', + type: 'group', + title: 'Nuxt', + icon: 'https://nuxt.com/assets/design-kit/icon-green.svg', + defaultOrder: -900, // sits near the framework tools; tune vs ~viteplus (-1000) + defaultChildId: 'nuxt:devtools', +}) +ctx.docks.register({ + id: 'nuxt:devtools', + type: 'iframe', + icon: '…', + title: 'Nuxt DevTools', + url: '/__nuxt_devtools__/client/', + groupId: 'nuxt', +}) +``` + +### Step 2 — a shell-less "dock" render mode in the client + +- Add a mode (query param `?dock=1` or a dedicated route prefix) that the client + reads to render a single tool **without the SideNav/shell**. In + `packages/devtools/client/app.vue` the shell already distinguishes + `isUtilityView` (hides `SideNav` for `/__`/`/` routes) — extend that to a + "dock" mode: when set, hide `SideNav` and render only `` (the target + tool). Pinia/render-tree already use `layout:'full'`; reuse that pattern. +- A promoted in-client tool's dock entry URL = + `/__nuxt_devtools__/client{route}?dock=1` (e.g. `/modules/components?dock=1`). +- Each such iframe is its own client instance; it connects to the host client + via the existing `connectParent()` path + (`packages/devtools/client/plugins/global.ts` reads + `window.parent.__NUXT_DEVTOOLS_HOST__`), so host metrics/hooks still work. + **Do not** rely on the Vue DevTools bridge in these (that's hub-only). + +### Step 3 — the promotion capability (server + registration) + +- Extend the tab meta contract with an opt-in `dock` flag: + - Core pages: read `dock` from `definePageMeta` (surface it through the tab + collection in `state-tabs.ts` / the client tab type). + - Custom tabs: add `dock?: boolean` to the custom-tab options type (the + `devtools:customTabs` contributions and `options.customTabs`), threaded + through `server-rpc/custom-tabs.ts` (`getCustomTabs`). +- Server side, in `module-main.ts`/`server-rpc`, collect the set of tabs with + `dock: true` and register a dock `iframe` entry per tab under `groupId:'nuxt'`, + choosing the URL per the **type-aware** rule in Decision 3: + - iframe custom tab → its own `src`. + - everything else → `/__nuxt_devtools__/client{route}?dock=1`. + - Use the tab's `icon`/`title` for the dock entry; derive a stable dock id + (e.g. `nuxt:tab:`). + - Custom tabs arrive/refresh dynamically (`devtools:customTabs:refresh`) — keep + the promoted dock entries in sync (register on add, update on change; use the + `ctx.docks.register(...)` return `{ update }` handle, and re-evaluate on + refresh). Guard for `devtoolsKit` not yet connected (queue + flush, mirroring + `server-rpc/index.ts` `pendingBroadcasts`). +- Ship the curated default: mark **Components, Server Routes, Pages** with + `dock: true` in their `definePageMeta`. + +### Step 4 — relocating SideNav + +- In `state-tabs.ts` (the SideNav's tab source), **exclude** tabs that are + promoted-to-dock so they no longer appear in the hub SideNav. Keep Vue-bridge + tools (Pinia, Render Tree) and all non-promoted tools in the SideNav. +- Make sure the promoted tool is still reachable programmatically (e.g. the + command palette / deep links) even though it's not in the SideNav. +- Consider what the hub shows by default (Overview) now that Components/Server + Routes/Pages are gone from its SideNav. + +### Step 5 — docs & extensibility surface + +- Document the `dock: true` flag for module authors (custom tab option) so the + ecosystem (Content, i18n, …) can surface a Nuxt-group dock button. +- Ensure orphan tolerance: if a module marks `dock:true` but the group somehow + isn't registered, the entry still renders top-level (devframe handles this). + +## Acceptance criteria + +- The dock bar shows a **"Nuxt"** group button; expanding it lists the hub + ("Nuxt DevTools") plus **Components**, **Server Routes**, **Pages** as members. +- Opening a promoted tool shows just that tool (no SideNav), in its own iframe, + fully functional (RPC works; host metrics available). +- Those three tools **no longer appear** in the hub SideNav; Pinia + Render Tree + still do and still connect (Vue bridge intact — no "Connecting…" regression). +- A playground module contributing a custom tab with `dock: true` gets its own + dock button under the Nuxt group (iframe tab → its own src; in-client tab → + `?dock=1`). +- `pnpm lint && pnpm build && pnpm typecheck` and the e2e suite + (`pnpm test:e2e`) pass; extend/adjust the dock/iframe e2e fixtures + (`tests/e2e/`) for the group + promoted entries. + +## Risks / gotchas + +- **Per-entry iframe cost.** Each promoted in-client tool is a full, separate + Nuxt DevTools client instance (lazy, then kept alive by iframe-pane). Keep the + default promoted set small; that's why it's curated (3 tools). +- **Vue bridge is single-context.** Never promote Pinia/Render Tree as their own + iframe; they must stay in the hub. Guard against a module marking a Vue-bridge + tool `dock:true`. +- **Bridge binding targets the hub.** `bindVueDevToolsIframe` in + `view/client.ts` binds one dock iframe. With multiple Nuxt iframes now present + (hub + promoted), ensure it binds the **hub** iframe (id `nuxt:devtools`), not + a promoted one. Review/adjust that logic (it currently binds by dock entry id + `nuxt:devtools`, which is correct — verify it still resolves to the hub). +- **Dynamic custom tabs.** Promoted dock entries must track the async + `devtools:customTabs` lifecycle (add/update/remove + refresh) and the + pre-connect queue. +- **"Relocating" UX surprise.** Tools vanish from the SideNav; make sure they're + discoverable via the dock group and the command palette. Consider a one-time + note/changelog for users. +- **e2e fixtures.** `tests/e2e/fixtures/devtools.ts` currently drives the single + `nuxt:devtools` dock entry; add coverage for the group + a promoted entry. diff --git a/plans/vite-devtools-integration/04-ecosystem-playgrounds.md b/plans/vite-devtools-integration/04-ecosystem-playgrounds.md new file mode 100644 index 0000000000..e90909b00f --- /dev/null +++ b/plans/vite-devtools-integration/04-ecosystem-playgrounds.md @@ -0,0 +1,130 @@ +# Plan 04 — Ecosystem dogfooding playgrounds + +**Status:** ready to execute · **Risk:** low · **Depends on:** nothing (but most +useful once plans 00–03 are in progress, to verify real integrations). +**Outcome:** a `playgrounds-ecosystem/` folder with one playground per popular +Nuxt module that ships a Nuxt DevTools integration, wired to the **local** +`@nuxt/devtools`, for verification + dogfooding — plus a short per-module +**compatibility/migration report** that later seeds an issue/PR to that module. + +> Self-contained: read this whole file. + +## Why + +Popular modules register Nuxt DevTools tabs/RPC (custom iframe tabs, RPC +functions, launch views, etc.). As we (a) migrate to Vite DevTools 0.4 / +devframe 0.6, (b) add the compat foundation (Plan 00), and (c) reshape the dock +(Plan 03), we need to see real integrations render and behave. These playgrounds +are the dogfooding surface and the basis for helping the ecosystem migrate. + +## Modules to cover (initial set) + +Create one playground per module: + +- `nuxt-og-image` / `@nuxtjs/seo` — OG Image playground tab (iframe + interactive) +- `@nuxt/scripts` — Scripts DevTools tab (first-party) +- `@nuxt/content` — Content DevTools tab (collections/DB viewer) +- `@nuxtjs/tailwindcss` — the classic "Tailwind Viewer" iframe custom tab +- `@nuxthub/core` — Hub viewers (DB/KV/Blob), heavier RPC-driven integration +- `@nuxt/fonts` — Fonts module DevTools surface +- `@nuxt/image` — Image module DevTools surface + +(Chosen because each exercises a real DevTools integration; `@nuxtjs/tailwindcss` +and `nuxt-og-image` are canonical iframe-tab integrations, `@nuxthub/core` is a +rich RPC one.) + +## Decisions (locked) + +1. **One playground per module** under `playgrounds-ecosystem//`. +2. Each links the **local** `@nuxt/devtools` (workspace) so it tests the new + version — reuse the existing playground convention + (`const devtoolsModule = process.env.NUXT_DEVTOOLS_LOCAL ? '../../local' : '@nuxt/devtools'`); + note the relative path is `'../../local'` from `playgrounds-ecosystem//` + — verify/point it at the repo's `local` devtools entry used by `playgrounds/*`. +3. **Opt-in install / out of main CI.** These are NOT part of the default + `pnpm install` or the main CI e2e — they pull heavy third-party deps and + would bloat install + flake CI. Keep the dep graph isolated so a normal repo + install is unaffected. +4. **Per-module compatibility report** (a markdown doc) capturing what works / + breaks / needs migration; later seeds an upstream issue/PR. + +## Implementation + +### Step 1 — isolate the workspace so the main install stays lean + +- Do **not** add `playgrounds-ecosystem/**` to the default `packages:` globs in + `pnpm-workspace.yaml` (which currently lists `packages/**` and `playgrounds/**`). + Options, pick one: + - **Separate opt-in workspace file / script** that installs the ecosystem set + on demand (e.g. a `pnpm -C playgrounds-ecosystem/ install` per + playground, each with its own lockfile), or + - a dedicated `pnpm-workspace.ecosystem.yaml` + an npm script + (`pnpm run ecosystem:install`) that a contributor runs explicitly. +- Each playground links the local devtools. Confirm how `playgrounds/*` resolve + `'../../local'` (the repo exposes a `local` Nuxt module entry for the built + `@nuxt/devtools`); replicate that resolution from the deeper + `playgrounds-ecosystem//` path (likely `'../../local'` still resolves + to `/local` — verify the depth). + +### Step 2 — scaffold each playground + +For each module, a minimal Nuxt app: + +- `playgrounds-ecosystem//package.json` — `nuxt`, the target module (pin + a known-good version), and a `dev`/`build` script. Bind dev to `0.0.0.0` for + remote preview (`nuxi dev --host 0.0.0.0`). +- `nuxt.config.ts` — `modules: [devtoolsModule, '']`, plus the minimal + module config needed to activate its DevTools integration (e.g. tailwind needs + a config; content needs a `content/` dir; og-image needs a route with OG tags; + nuxthub needs its bindings/config). +- Just enough app content to make the integration light up (a page, a component, + sample content, etc.). + +### Step 3 — a per-module compatibility report + +- `playgrounds-ecosystem//COMPAT.md` (or a shared + `playgrounds-ecosystem/REPORTS.md`) recording, per module: + - Does the module's DevTools tab/entry appear? (in the hub SideNav and/or, if + the module opts into `dock:true` from Plan 03, as a dock button) + - Does its RPC / iframe view load without console errors under Vite DevTools + 0.4 / devframe 0.6? + - Any use of deprecated Nuxt DevTools APIs (surfaced by the Plan 00 nostics + deprecation diagnostics) — capture the codes shown. + - Verdict: works as-is / works via shim (with deprecation) / broken + required + migration steps. +- These reports are the raw material for upstream issues/PRs to each module. + +### Step 4 — a dogfooding runbook + +- `playgrounds-ecosystem/README.md`: how to install (opt-in), run a single + playground against the local devtools, and where to record findings. Include + the `agent-browser`/manual steps to open Vite DevTools and exercise each + integration. + +## Acceptance criteria + +- `playgrounds-ecosystem//` exists for each of the 7 modules, each + runnable with the local `@nuxt/devtools` and its DevTools integration visibly + active. +- A normal `pnpm install` at the repo root is **unaffected** (ecosystem deps are + opt-in, not pulled by default; main CI unchanged). +- Each playground has a compatibility report with a clear verdict. +- Running any one playground surfaces (via Plan 00 diagnostics) whether the + module uses deprecated APIs. + +## Risks / gotchas + +- **Dependency bloat / lockfile churn.** Keep ecosystem deps out of the default + workspace + lockfile; isolate per playground so the core repo install stays + lean and reproducible. +- **Module config specifics.** Some integrations only activate with real config + (tailwind config, content dir, nuxthub bindings, og-image routes) — scaffold + the minimum to trigger each DevTools surface. +- **Version drift.** Pin each module to a known version in its playground; note + the version in the report so upstream fixes can be tracked. +- **Local-devtools resolution.** Verify the `'../../local'` link resolves from + the extra directory depth; adjust the relative path if + `playgrounds-ecosystem//` is one level deeper than `playgrounds//`. +- **Not a CI gate (yet).** These are manual dogfooding surfaces; if we later want + automated smoke coverage, add a separate optional workflow (kept off the + critical path). diff --git a/plans/vite-devtools-integration/README.md b/plans/vite-devtools-integration/README.md new file mode 100644 index 0000000000..78f09e33a9 --- /dev/null +++ b/plans/vite-devtools-integration/README.md @@ -0,0 +1,203 @@ +# Nuxt DevTools × Vite DevTools 0.4 / devframe 0.6 — integration plans + +This folder holds three **independently executable** implementation plans. Each +plan is self-contained: a fresh agent with no prior context can pick up any one +file and execute it. Read this overview once for shared context, then follow the +individual plan. + +## Background + +Nuxt DevTools v4 renders inside **Vite DevTools** (`@vitejs/devtools` 0.4.x), +which is built on the **devframe 0.6** runtime (`@devframes/hub`, plus the +official `@devframes/plugin-terminals` / `@devframes/plugin-messages` / +`@devframes/plugin-inspect` built-ins). The migration to those versions has +already landed on `main` (nuxt/devtools#1010). + +Today Nuxt DevTools: + +- Registers **one** `type:'iframe'` dock entry (`nuxt:devtools`) that loads the + whole client (`/__nuxt_devtools__/client/`), with ~25 tabs behind an internal + `SideNav`. Registration lives in `packages/devtools/src/module-main.ts` + (~lines 92‑98), inside a Vite DevTools plugin `devtools.setup(ctx)` callback. +- Ships its **own** terminals system (`@xterm` UI, `server-rpc/terminals.ts`, + `devtools:terminal:*` Nuxt hooks, output-only child processes). +- Ships its **own** ephemeral toast (`devtoolsUiShowNotification` in + `packages/devtools-ui-kit`), client-only, no history. + +These plans make Nuxt DevTools *use the platform* instead of duplicating it. + +## Strategy (this major) + +We're cutting a **new major**. Breaking changes are acceptable, but migration +for module authors/users must be **minimal and self-discoverable**, with a +gradual long-term move to devframe-native APIs. Concretely: + +1. **Keep** the existing Nuxt DevTools API working. +2. **Expose** the devframe-native API alongside it (connect-safe hosts on + `nuxt.devtools`). +3. **Soft-deprecate** the Nuxt API where a devframe equivalent exists — via a + backward-compatible **shim** + a **nostics** deprecation diagnostic (code + + fix + doc link). **Hard-break only where a faithful shim is infeasible.** +4. Build the feature work (Messages / Terminals / Dock groups) **on top of** + that foundation. +5. **Dogfood** against real ecosystem modules and produce migration reports. + +## Workstreams + +| # | Plan | Scope | Risk | Depends on | +|---|------|-------|------|------------| +| 00 | [`00-compat-foundation.md`](./00-compat-foundation.md) | Expose devframe-native API as connect-safe hosts on `nuxt.devtools`; add nostics-driven soft-deprecation (catalog + host registration); establish the shim-first policy + deprecation map. | Medium | none | +| 01 | [`01-messages-unification.md`](./01-messages-unification.md) | Route all notifications through the devframe Messages system (`messages` host + built-in Messages dock); retire the bespoke toast. | Low | 00 | +| 02 | [`02-terminals-reuse.md`](./02-terminals-reuse.md) | Retire Nuxt's `@xterm` terminals; surface sessions in the built-in Terminals dock via the `terminals` host; keep a compat shim for the `devtools:terminal:register` hook. | Medium | 00 | +| 03 | [`03-dock-groups-presentation.md`](./03-dock-groups-presentation.md) | Introduce a **"Nuxt" dock group** and a general **promote‑tab‑to‑dock** capability; relocate a curated set of tools onto the dock bar. | High (UX) | 00 | +| 04 | [`04-ecosystem-playgrounds.md`](./04-ecosystem-playgrounds.md) | `playgrounds-ecosystem/` per popular module (og-image/SEO, scripts, content, tailwindcss, nuxthub, fonts, image) linked to local devtools; opt-in install; per-module compat report. | Low | cross-cutting (verifies 00–03) | + +Recommended order: **00 → 01 → 02 → 03**, with **04** running alongside as the +dogfooding/verification surface. Each is technically buildable/reviewable as a +**separate PR** (00 first, since 01/02/03 use the surfaces it creates). + +## Shared facts every plan relies on + +**Versions** (already in `pnpm-workspace.yaml` catalogs): `@vitejs/devtools` +`^0.4.0`, `@vitejs/devtools-kit` `^0.4.0`, `vite-plugin-inspect` `^12.0.2`, +`vue` `^3.5.39`; transitively `devframe`/`@devframes/hub`/`@devframes/plugin-*` +`0.6.0`. + +**Where Nuxt gets the Vite DevTools node context (`ctx`)** — a +`ViteDevToolsNodeContext`, which exposes `ctx.docks`, `ctx.terminals`, +`ctx.messages`, `ctx.commands`, `ctx.rpc`: + +- In `packages/devtools/src/module-main.ts`, inside + `defineViteDevToolsPlugin({ name: 'nuxt:devtools', devtools: { setup(ctx) { … } } })`. + That callback also calls `connectDevToolsKit?.(ctx)`. +- `connectDevToolsKit` is defined in `packages/devtools/src/server-rpc/index.ts` + (`setupRPC`), which stores the ctx as `devtoolsKitCtx` and exposes it on the + Nuxt server context as `ctx.devtoolsKit` (typed in + `packages/devtools-kit/src/_types/server-ctx.ts` as `DevToolsNodeContext | undefined`). + So server-side integrations receive `{ devtoolsKit }` and can call + `devtoolsKit.terminals.*` / `devtoolsKit.messages.*`. + +**devframe host APIs (node side)** — from `@devframes/hub` (re-exported by +`@vitejs/devtools-kit`): + +```ts +// ctx.terminals: DevframeTerminalsHost +interface DevframeTerminalsHost { + readonly sessions: Map + readonly events: EventEmitter<{ 'terminal:session:updated': (s) => void }> + register: (session: DevframeTerminalSession) => DevframeTerminalSession + update: (session: DevframeTerminalSession) => void + startChildProcess: (exec: { command; args; cwd?; env? }, + terminal: Omit) => Promise // output-only + startPtySession: (exec: { command; args?; cwd?; env?; cols?; rows? }, + terminal: Omit) => Promise // interactive (zigpty; pipe fallback) +} +// A registered session may carry `buffer?: string[]` and/or `stream?: ReadableStream`. +// child-process sessions are output-only (no write/resize); pty sessions add write(data)+resize(cols,rows). + +// ctx.messages: DevframeMessagesHost +interface DevframeMessagesHost { + readonly entries: Map + info|warn|error|success|debug: (message: string, extra?) => Promise + add: (entry: DevframeMessageEntryInput) => Promise + update: (id, patch) => Promise + remove: (id) => Promise + clear: () => Promise +} +// DevframeMessageEntry fields incl: message, description?, level('info'|'warn'|'error'|'success'|'debug'), +// stacktrace?, filePosition?, notify?, autoDismiss?, autoDelete?, labels?, category?, from, timestamp, status? +``` + +**devframe dock API (node side)** — `ctx.docks.register(entry, force?)`: + +```ts +// A GROUP is a first-class dock entry: +ctx.docks.register({ id: 'nuxt', type: 'group', title: 'Nuxt', icon: '…', defaultOrder: -900, defaultChildId: 'nuxt:devtools' }) +// A member joins by pointing groupId at the group id: +ctx.docks.register({ id: 'nuxt:devtools', type: 'iframe', title: 'Nuxt DevTools', icon: '…', url: '…', groupId: 'nuxt' }) +``` + +- Grouping is a **flat pointer** (`groupId`), one level deep, orphan-tolerant + (a member whose group never registers renders top-level). +- `category` (`'app' | 'framework' | 'web' | 'advanced' | 'default' | '~builtin' | (string&{})`) + is a **separate** ordering/bucketing axis, not the grouping mechanism. +- Precedent to copy: Vite DevTools core registers the `~viteplus` group + (`DEVTOOLS_VITEPLUS_GROUP_ID`, "Vite+") and Rolldown joins it via + `groupId: DEVTOOLS_VITEPLUS_GROUP_ID`. + +**Built-in docks already mounted by `DevTools()`** (Nuxt calls `DevTools()` with +default options, so `builtinDevTools` is on): **Terminals**, **Messages**, +**Inspect** — each `category:'~builtin'`, auto-hidden when empty. Sessions/ +messages Nuxt pushes into `ctx.terminals` / `ctx.messages` surface in those +docks for free. + +**Hard constraint (Vue DevTools bridge):** only **one** iframe can hold the +Vue DevTools messaging context at a time (`setIframeServerContext(iframe)` is a +single global). The Vue-backed tools (**Pinia**, **Render Tree**) must therefore +stay inside the single hub iframe; they cannot each be a separately-promoted +iframe. (This is the same mechanism as the "Connecting…" fix in +`packages/devtools/src/runtime/plugins/view/client.ts`.) + +## Design decisions (locked via a grilling session) + +North star: pursue **all** of — discoverability/UX parity, maintenance +reduction, behavioral consistency, and capability upgrade. + +Foundation (plan 00): +- **Expose devframe-native API** as first-class **connect-safe** hosts on + `nuxt.devtools` (`docks`/`terminals`/`messages`/`commands`/`diagnostics`), + queuing pre-connect calls; keep raw `devtoolsKit` as an escape hatch. +- **Self-discoverable deprecations** via a Nuxt **nostics** catalog + (`defineDiagnostics` + `createConsoleReporter()` + `docsBase` → Nuxt docs), + **also registered into the DevTools host diagnostics** so they surface inside + DevTools. Warn-only by default; every code carries `why` + `fix` + doc link. +- **Shim-first**: keep every existing API working this major via shims; + deprecations are warnings; removal deferred to the next major. **Hard-break + only where a faithful shim is infeasible** (error-level diagnostic naming the + replacement). + +Ecosystem (plan 04): +- **One playground per module** under `playgrounds-ecosystem/`, linked to the + **local** `@nuxt/devtools`; initial set: og-image/SEO, scripts, content, + tailwindcss, nuxthub, fonts, image. +- **Opt-in install / out of main CI** (avoid dep bloat + flake). +- **Per-module compatibility report** that later seeds an upstream issue/PR. + +Presentation (plan 03): +- **Curated hybrid "Nuxt" dock group**: the hub iframe stays the primary member; + a small curated set is promoted as sibling dock entries. +- Promotion is a **general opt-in capability** — any tab (core, module, user + custom) can request a dock button via a flag; ship curated defaults. +- **Type-aware realization**: iframe custom tabs → dock iframe at their own + `src`; core pages + in-client custom tabs → dock iframe deep-linking into the + Nuxt client in a **shell-less "dock" mode** (SideNav hidden), one instance per + promoted tool. +- **Relocating** SideNav semantics: a promoted tool leaves the hub SideNav and + lives only as a dock button; the SideNav shrinks to non-promoted + Vue tools. +- Vue-bridge tools (Pinia, Render Tree) stay in the hub only. +- Default promoted set: **Components, Server Routes, Pages**. + +Terminals (plan 02): +- **Full replacement + compat shim**: retire the `@xterm` UI + `server-rpc/terminals` + + terminals tab; route sessions through `ctx.terminals` (built-in Terminals + dock); keep the `devtools:terminal:register` hook as a shim forwarding to + `ctx.terminals`. +- Capability: **preserve output-only** for existing module terminals (map to + read-only registered sessions), **add opt-in PTY** for new interactive use. + +Messages (plan 01): +- **Unify everything into the devframe Messages system** (one notification + system). The client toast is re-implemented to push into it. +- **Tiered by intent**: ephemeral client feedback → `notify + autoDismiss + + autoDelete` (toast-only, no history); server-originated → persisted + leveled. +- **Public notify API** (a `devtools:notify` Nuxt hook and/or + `useNuxtDevTools().notify()`) forwarded to `ctx.messages`, plus a curated set + of built-in sources (build/module errors & warnings, server-task results). + +## Working agreement + +- Base each PR on a fresh branch off `origin/main`. +- Follow the repo's `AGENTS.md`; run `pnpm lint`, `pnpm build`, `pnpm typecheck`, + and the relevant `pnpm test:e2e` before opening a PR. +- Conventional Commits; one PR per plan; note in the PR body that it was created + with the help of an agent.