From a32cd63631245f90a6a5cb0f5301c4af1ad17de5 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 13 Mar 2023 11:30:45 +0100 Subject: [PATCH 1/6] wip: init terminal support --- local.ts | 18 +++++--- .../client/components/TerminalView.vue | 41 +++++++++++++++++++ .../client/pages/modules/terminals.vue | 28 +++++++++++++ packages/devtools/client/setup/client-rpc.ts | 4 ++ .../devtools/client/setup/unocss-runtime.ts | 2 +- packages/devtools/package.json | 4 +- packages/devtools/src/integrations/vscode.ts | 17 +++++--- packages/devtools/src/kit/index.ts | 41 ++++++++++++++++++- packages/devtools/src/server-rpc/index.ts | 2 + packages/devtools/src/server-rpc/terminal.ts | 35 ++++++++++++++++ packages/devtools/src/types/hooks.ts | 18 ++++++++ packages/devtools/src/types/integrations.ts | 13 ++++++ packages/devtools/src/types/rpc.ts | 8 +++- pnpm-lock.yaml | 16 ++++++++ 14 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 packages/devtools/client/components/TerminalView.vue create mode 100644 packages/devtools/client/pages/modules/terminals.vue create mode 100644 packages/devtools/src/server-rpc/terminal.ts diff --git a/local.ts b/local.ts index 3d90bf6f78..ef402e1c3d 100644 --- a/local.ts +++ b/local.ts @@ -18,7 +18,6 @@ * ``` */ import { defineNuxtModule, logger } from '@nuxt/kit' -import { execa } from 'execa' import { resolve } from 'pathe' import { getPort } from 'get-port-please' import { searchForWorkspaceRoot } from 'vite' @@ -26,6 +25,7 @@ import { ROUTE_CLIENT, defaultOptions } from './packages/devtools/src/constant' import type { ModuleOptions } from './packages/devtools/src/types' import { packageDir } from './packages/devtools/src/dirs' import { enableModule } from './packages/devtools/src/module-main' +import { startSubprocess } from './packages/devtools/src/kit' export type { ModuleOptions } @@ -40,10 +40,6 @@ export default defineNuxtModule({ const workspaceRoot = resolve(packageDir, '../..') const PORT = await getPort({ port: 12442 }) - // TODO: add embedded terminal and forward logs to it - const subprocess = execa('npx', ['nuxi', 'dev', '--port', PORT.toString()], { cwd: clientDir, stdio: 'pipe' }) - subprocess.stderr?.pipe(process.stderr) - nuxt.hook('vite:extendConfig', (config) => { config.server ||= {} // add proxy to client @@ -61,6 +57,18 @@ export default defineNuxtModule({ config.server.fs.allow.push(workspaceRoot) }) + nuxt.hook('app:resolve', () => { + const subprocess = startSubprocess({ + name: 'Nuxt Devtools Local', + command: 'npx', + args: ['nuxi', 'dev', '--port', PORT.toString()], + cwd: clientDir, + stdio: 'pipe', + }) + + subprocess.stderr?.pipe(process.stderr) + }) + logger.info(`Nuxt Devtools is using local client from \`${clientDir}\``) return enableModule(options, nuxt) diff --git a/packages/devtools/client/components/TerminalView.vue b/packages/devtools/client/components/TerminalView.vue new file mode 100644 index 0000000000..82ea656111 --- /dev/null +++ b/packages/devtools/client/components/TerminalView.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/devtools/client/pages/modules/terminals.vue b/packages/devtools/client/pages/modules/terminals.vue new file mode 100644 index 0000000000..19571429db --- /dev/null +++ b/packages/devtools/client/pages/modules/terminals.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/devtools/client/setup/client-rpc.ts b/packages/devtools/client/setup/client-rpc.ts index 31cb582520..28c8f3a046 100644 --- a/packages/devtools/client/setup/client-rpc.ts +++ b/packages/devtools/client/setup/client-rpc.ts @@ -1,4 +1,5 @@ import type { ClientFunctions } from '../../src/types' +import type {} from '../../src/types/hooks' export function setupClientRPC() { const nuxt = useNuxtApp() @@ -11,5 +12,8 @@ export function setupClientRPC() { async callHook(hook: string, ...args: any[]) { nuxt.hooks.callHookParallel(hook as any, ...args) }, + async onTerminalData(id: string, data: string) { + nuxt.hooks.callHookParallel('devtools:terminal:data', { id, data }) + }, } satisfies ClientFunctions) } diff --git a/packages/devtools/client/setup/unocss-runtime.ts b/packages/devtools/client/setup/unocss-runtime.ts index 305bd905db..75eac9ac03 100644 --- a/packages/devtools/client/setup/unocss-runtime.ts +++ b/packages/devtools/client/setup/unocss-runtime.ts @@ -1,6 +1,6 @@ +// @ts-expect-error missin types import presetIcons from '@unocss/preset-icons/browser' import initUnocssRuntime from '@unocss/runtime' -// @ts-expect-error missin types initUnocssRuntime({ defaults: { diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 9a91002919..05542e6236 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -110,6 +110,8 @@ "vue-tsc": "^1.2.0", "wait-on": "^7.0.1", "which": "^3.0.0", - "ws": "^8.12.1" + "ws": "^8.12.1", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" } } diff --git a/packages/devtools/src/integrations/vscode.ts b/packages/devtools/src/integrations/vscode.ts index 9fda9fea54..f871ea4063 100644 --- a/packages/devtools/src/integrations/vscode.ts +++ b/packages/devtools/src/integrations/vscode.ts @@ -6,6 +6,7 @@ import which from 'which' import waitOn from 'wait-on' import { LOG_PREFIX } from '../logger' import type { RPCContext } from '../server-rpc/types' +import { startSubprocess } from '../kit' export async function setup({ nuxt, options }: RPCContext) { const installed = !!await which('code-server').catch(() => null) @@ -32,12 +33,16 @@ export async function setup({ nuxt, options }: RPCContext) { logger.info(LOG_PREFIX, `Starting VS Code Server at ${url} ...`) - const command = execa('code-server', [ - 'serve-local', - '--accept-server-license-terms', - '--without-connection-token', - `--port=${port}`, - ]) + const command = startSubprocess({ + name: 'VS Code Server', + command: 'code-server', + args: [ + 'serve-local', + '--accept-server-license-terms', + '--without-connection-token', + `--port=${port}`, + ], + }) nuxt.hook('close', () => { command.kill() diff --git a/packages/devtools/src/kit/index.ts b/packages/devtools/src/kit/index.ts index 435ab5b21f..c719dd802a 100644 --- a/packages/devtools/src/kit/index.ts +++ b/packages/devtools/src/kit/index.ts @@ -1,6 +1,8 @@ import { useNuxt } from '@nuxt/kit' +import type { Options as ExecaOptions } from 'execa' +import { execa } from 'execa' import type { ModuleCustomTab } from '../types' -import '../types/hooks' +import type {} from '../types/hooks' /** * Hooks to extend a custom tab in devtools. @@ -21,3 +23,40 @@ export function addCustomTab(tab: ModuleCustomTab | (() => ModuleCustomTab | Pro export function refreshCustomTabs(nuxt = useNuxt()) { return nuxt.callHook('devtools:customTabs:refresh') } + +interface SubprocessOptions extends ExecaOptions { + name: string + command: string + args?: string[] +} + +/** + * Create a subprocess that handled by the DevTools. + */ +export function startSubprocess(options: SubprocessOptions, nuxt = useNuxt()) { + const process = execa( + options.command, + options.args, + { + ...options, + env: { + COLORS: 'true', + FORCE_COLOR: 'true', + ...options.env, + }, + }, + ) + + const id = (process.pid || options.command).toString() + nuxt.callHook('devtools:terminal:register', { + id, + name: options.name, + stream: process.stdout!, + }) + + process.on('exit', () => { + // TODO: unregister terminal + }) + + return process +} diff --git a/packages/devtools/src/server-rpc/index.ts b/packages/devtools/src/server-rpc/index.ts index 164eee309d..e8c69bc543 100644 --- a/packages/devtools/src/server-rpc/index.ts +++ b/packages/devtools/src/server-rpc/index.ts @@ -14,6 +14,7 @@ import { setupCustomTabRPC } from './custom-tabs' import { setupGeneralRPC } from './general' import { setupWizardRPC } from './wizard' import type { RPCContext } from './types' +import { setupTerminalRPC } from './terminal' export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { const serverFunctions = {} as ServerFunctions @@ -37,6 +38,7 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { ...setupAssetsRPC(ctx), ...setupNpmRPC(ctx), ...setupWizardRPC(ctx), + ...setupTerminalRPC(ctx), } satisfies ServerFunctions) const wsClients = new Set() diff --git a/packages/devtools/src/server-rpc/terminal.ts b/packages/devtools/src/server-rpc/terminal.ts new file mode 100644 index 0000000000..8f1b744650 --- /dev/null +++ b/packages/devtools/src/server-rpc/terminal.ts @@ -0,0 +1,35 @@ +import type { ServerFunctions, TerminalData } from '../types' +import type { RPCContext } from './types' + +export function setupTerminalRPC({ nuxt, birpc }: RPCContext) { + const terminals: TerminalData[] = [] + + nuxt.hook('devtools:terminal:register', (terminal) => { + const clone = { + ...terminal, + } + clone.id ||= Math.random().toString() // TODO: use random id + clone.content ||= '' + clone.stream?.on('data', (data) => { + const delta = data.toString() + clone.content += delta + birpc.broadcast.onTerminalData.asEvent(clone.id, delta) + }) + terminals.push(clone) + }) + + return { + listTerminals() { + return terminals.map(i => ({ + id: i.id, + name: i.name, + description: i.description, + icon: i.icon, + })) + }, + getTerminal(id: string) { + const terminal = terminals.find(t => t.id === id) + return terminal + }, + } satisfies Partial +} diff --git a/packages/devtools/src/types/hooks.ts b/packages/devtools/src/types/hooks.ts index 81f91217d5..af3a865b1b 100644 --- a/packages/devtools/src/types/hooks.ts +++ b/packages/devtools/src/types/hooks.ts @@ -1,4 +1,5 @@ import type { ModuleCustomTab } from './custom-tabs' +import type { TerminalData } from './integrations' declare module '@nuxt/schema' { interface NuxtHooks { @@ -21,6 +22,23 @@ declare module '@nuxt/schema' { * Retrigger update for custom tabs, `devtools:customTabs` will be called again. */ 'devtools:customTabs:refresh': () => void + + /** + * Register a terminal. + */ + 'devtools:terminal:register': (terminal: TerminalData) => void + } +} + +declare module '@nuxt/schema' { + /** + * Runtime Hooks + */ + interface RuntimeNuxtHooks { + /** + * On terminal data. + */ + 'devtools:terminal:data': (data: { id: string; data: string }) => void } } diff --git a/packages/devtools/src/types/integrations.ts b/packages/devtools/src/types/integrations.ts index 40b0fd05ba..d1cba7948b 100644 --- a/packages/devtools/src/types/integrations.ts +++ b/packages/devtools/src/types/integrations.ts @@ -1,3 +1,4 @@ +import type { Stream } from 'node:stream' import type { Import, UnimportMeta } from 'unimport' import type { VueInspectorClient } from 'vite-plugin-vue-inspector' import type { RouteRecordNormalized } from 'vue-router' @@ -105,3 +106,15 @@ export interface CodeSnippet { name: string docs?: string } + +export interface TerminalInfo { + id: string + name: string + description?: string + icon?: string +} + +export interface TerminalData extends TerminalInfo { + stream?: Stream + content?: string +} diff --git a/packages/devtools/src/types/rpc.ts b/packages/devtools/src/types/rpc.ts index b379c1c1cd..c44a3d5a5b 100644 --- a/packages/devtools/src/types/rpc.ts +++ b/packages/devtools/src/types/rpc.ts @@ -4,7 +4,7 @@ import type { StorageValue } from 'unstorage' import type { Component } from 'vue' import type { WizardActions, WizardArgs } from '../wizard' import type { ModuleCustomTab } from './custom-tabs' -import type { AssetInfo, AutoImportsWithMetadata, HookInfo, ImageMeta, UpdateInfo } from './integrations' +import type { AssetInfo, AutoImportsWithMetadata, HookInfo, ImageMeta, TerminalData, TerminalInfo, UpdateInfo } from './integrations' import type { ComponentRelationship } from './module' export interface ServerFunctions { @@ -20,6 +20,10 @@ export interface ServerFunctions { getStaticAssets(): Promise getPackageVersions(): UpdateInfo[] + // Terminal + listTerminals(): TerminalInfo[] + getTerminal(id: string): TerminalData | undefined + // Storage getStorageMounts(): Promise getStorageKeys(base?: string): Promise @@ -40,6 +44,8 @@ export interface ServerFunctions { export interface ClientFunctions { refresh(event: ClientUpdateEvent): void callHook(hook: string, ...args: any[]): Promise + + onTerminalData(id: string, data: string): void } export type ClientUpdateEvent = keyof ServerFunctions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93fe326534..33ae9911d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,8 @@ importers: wait-on: ^7.0.1 which: ^3.0.0 ws: ^8.12.1 + xterm: ^5.1.0 + xterm-addon-fit: ^0.7.0 dependencies: '@antfu/install-pkg': 0.1.1 '@nuxt/kit': 3.2.3 @@ -190,6 +192,8 @@ importers: wait-on: 7.0.1 which: 3.0.0 ws: 8.12.1 + xterm: 5.1.0 + xterm-addon-fit: 0.7.0_xterm@5.1.0 packages/devtools-ui-kit: specifiers: @@ -12427,6 +12431,18 @@ packages: engines: {node: '>=0.4'} dev: true + /xterm-addon-fit/0.7.0_xterm@5.1.0: + resolution: {integrity: sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.1.0 + dev: true + + /xterm/5.1.0: + resolution: {integrity: sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==} + dev: true + /xxhashjs/0.2.2: resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} dependencies: From 129ed2ac6f7c8203cef2e3c2f376fd16020c6f02 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 13 Mar 2023 12:09:15 +0100 Subject: [PATCH 2/6] refactor: `` --- local.ts | 20 +++++++++----- .../src/components/NDialog.vue | 4 +-- .../src/components/NIconButton.vue | 19 +++++++++++++ .../src/components/NSelect.vue | 4 +-- packages/devtools-ui-kit/src/unocss.ts | 3 +++ .../client/components/AssetDetails.vue | 27 ++++++++++++------- .../client/components/AssetGridItem.vue | 4 +-- .../client/components/AssetPreview.vue | 2 +- packages/devtools/client/components/Badge.vue | 2 +- .../client/components/ComponentDetails.vue | 4 +-- .../client/components/ComponentItem.vue | 6 ++--- .../client/components/DockingPanel.vue | 2 +- .../client/components/DrawerRight.vue | 6 ++--- .../devtools/client/components/HooksTable.vue | 2 +- .../devtools/client/components/ModuleItem.vue | 2 +- .../client/components/ModuleTreeNode.vue | 2 +- .../devtools/client/components/SideNav.vue | 2 +- .../client/components/SideNavItem.vue | 2 +- .../client/components/StateEditor.vue | 8 +++--- .../client/components/TerminalView.vue | 6 ++--- packages/devtools/client/layouts/default.vue | 4 +-- .../devtools/client/pages/modules/assets.vue | 2 +- .../devtools/client/pages/modules/imports.vue | 2 +- .../devtools/client/pages/modules/storage.vue | 8 +++--- .../client/pages/modules/terminals.vue | 15 ++++++++--- packages/devtools/client/unocss.config.ts | 2 -- packages/devtools/src/integrations/vscode.ts | 24 ++++++++++------- packages/devtools/src/kit/index.ts | 22 ++++++++------- packages/devtools/src/server-rpc/terminal.ts | 13 ++++----- packages/devtools/src/types/integrations.ts | 21 +++++++++++++-- 30 files changed, 149 insertions(+), 91 deletions(-) create mode 100644 packages/devtools-ui-kit/src/components/NIconButton.vue diff --git a/local.ts b/local.ts index ef402e1c3d..d66ca2b33f 100644 --- a/local.ts +++ b/local.ts @@ -58,13 +58,19 @@ export default defineNuxtModule({ }) nuxt.hook('app:resolve', () => { - const subprocess = startSubprocess({ - name: 'Nuxt Devtools Local', - command: 'npx', - args: ['nuxi', 'dev', '--port', PORT.toString()], - cwd: clientDir, - stdio: 'pipe', - }) + const subprocess = startSubprocess( + { + command: 'npx', + args: ['nuxi', 'dev', '--port', PORT.toString()], + cwd: clientDir, + stdio: 'pipe', + }, + { + id: 'devtools:local', + name: 'Nuxt Devtools Local', + icon: 'logos-nuxt-icon', + }, + ) subprocess.stderr?.pipe(process.stderr) }) diff --git a/packages/devtools-ui-kit/src/components/NDialog.vue b/packages/devtools-ui-kit/src/components/NDialog.vue index 1d44c8187d..8d31822e46 100644 --- a/packages/devtools-ui-kit/src/components/NDialog.vue +++ b/packages/devtools-ui-kit/src/components/NDialog.vue @@ -45,13 +45,13 @@ export default {