Skip to content

Commit

Permalink
feat: extendable rpc (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Mar 16, 2023
1 parent e6d8938 commit 96080a8
Show file tree
Hide file tree
Showing 31 changed files with 405 additions and 122 deletions.
64 changes: 57 additions & 7 deletions docs/content/2.module/0.guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,72 @@ When you need to refresh the custom tabs, you can call `nuxt.callHook('devtools:

## API for Custom View

To provide complex interactions for your module integrations, we recommend to host your own view and display it in devtools via iframe.
Please refer to [Iframe Client](/module/utils-kit#nuxtdevtools-kitiframe-client).

To get the infomation from the devtools and the client app, you can do this in your client app:
## Custom RPC Functions

Nuxt DevTools uses Remote Procedure Call (RPC) to communicate between the server and client. For modules you can also leverage that to communicate your server code.

To do that, we recommend to define your types in a shared TypeScript file first:

```ts
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'
// rpc-types.ts

export const devtoolsClient = useDevtoolsClient()
export interface ServerFunctions {
getMyModuleOptions(): MyModuleOptions
}

export interface ClientFunctions {
showNotification(message: string): void
}
```

When the iframe been served with the same origin (CORS limitation), devtools will automatically inject `__NUXT_DEVTOOLS__` to the iframe's window object. You can access it as a ref using `useDevtoolsClient()` utility.
And then in your module code:

`devtoolsClient.value.host` contains APIs to communicate with the client app, and `devtoolsClient.value.devtools` contains APIs to communicate with the devtools. For example, you can get the router instance from the client app:
```ts
import { defineNuxtModule } from '@nuxt/kit'
import { extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit'
import type { ClientFunctions, ServerFunctions } from './rpc-types'

const RPC_NAMESPACE = 'my-module-rpc'

export default defineNuxtModule({
setup(options, nuxt) {
// wait for DevTools to be initialized
onDevToolsInitialized(async () => {
const rpc = extendServerRpc<ClientFunctions, ServerFunctions>(RPC_NAMESPACE, {
// register server RPC functions
getMyModuleOptions() {
return options
},
})

// call client RPC functions
// since it might have multiple clients connected, we use `broadcast` to call all of them
await rpc.broadcast.showNotification('Hello from My Module!')
})
}
})
```

And on the client side, you can do:

```ts
const router = computed(() => devtoolsClient.value?.host?.nuxt.vueApp.config.globalProperties?.$router)
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
import type { ClientFunctions, ServerFunctions } from './rpc-types'

const RPC_NAMESPACE = 'my-module-rpc'

onDevtoolsClientConnected(async (client) => {
const rpc = client.devtools.extendClientRpc(RPC_NAMESPACE, {
showNotification(message) {
console.log(message)
},
})

// call server RPC functions
const options = await rpc.getMyModuleOptions()
})
```

## Trying Local Changes
Expand Down
76 changes: 71 additions & 5 deletions docs/content/2.module/1.utils-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ APIs are subject to change.

Since v0.3.0, we are now providing a utility kit for easier DevTools integrations, similar to `@nuxt/kit`.

It can be access via `@nuxt/devtools-kit`:
```bash
npm i @nuxt/devtools-kit
```

```ts
import { addCustomTab } from '@nuxt/devtools-kit'
```

Generally, we recommend to module authors to install `@nuxt/devtools` as a dev dependency and bundled `@nuxt/devtools-kit` into your module.
We recommend module authors to install `@nuxt/devtools-kit` as a dependency and `@nuxt/devtools` as a dev dependency.

## `@nuxt/devtools-kit`

## `addCustomTab()`
### `addCustomTab()`

A shorthand for calling the hook `devtools:customTabs`.

Expand All @@ -36,11 +40,11 @@ addCustomTab(() => ({
}))
```

## `refreshCustomTabs()`
### `refreshCustomTabs()`

A shorthand for call hook `devtools:customTabs:refresh`. It will refresh all custom tabs.

## `startSubprocess()`
### `startSubprocess()`

Start a sub process using `execa` and create a terminal tab in DevTools.

Expand Down Expand Up @@ -69,3 +73,65 @@ const subprocess = startSubprocess(
subprocess.restart()
subprocess.terminate()
```

### `extendServerRpc()`

Extend the server RPC with your own methods.

```ts
import { extendServerRpc } from '@nuxt/devtools-kit'

const rpc = extendServerRpc('my-module', {
async myMethod() {
return 'hello'
},
})
```

Learn more about [Custom RPC functions](/module/guide#custom-rpc-functions).

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

To provide complex interactions for your module integrations, we recommend to host your own view and display it in devtools via iframe.

To get the infomation from the devtools and the client app, you can do this in your client app:

```ts
import { useDevtoolsClient } from '@nuxt/devtools-kit/iframe-client'

export const devtoolsClient = useDevtoolsClient()
```

When the iframe been served with the same origin (CORS limitation), devtools will automatically inject `__NUXT_DEVTOOLS__` to the iframe's window object. You can access it as a ref using `useDevtoolsClient()` utility.

### `useDevtoolsClient()`

It will return a ref of `NuxtDevtoolsIframeClient` object that are intially `null` and will be updated when the connection is ready.

`NuxtDevtoolsIframeClient` contains two properties:

- `host`: APIs to communicate with the client app
- `devtools`: APIs to communicate with the devtools

`host` can be undefined when devtools are accessed standalone or from a different origin.

For example, you can get the router instance from the client app:

```ts
const router = computed(() => devtoolsClient.value?.host?.nuxt.vueApp.config.globalProperties?.$router)
```

### `onDevtoolsClientConnected()`

Similiar to `useDevtoolsClient()` but as a callback style:

```ts
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'

onDevtoolsClientConnected(async (client) => {
// client is NuxtDevtoolsIframeClient

const config = client.devtools.rpc.getServerConfig()
// ...
})
```
3 changes: 3 additions & 0 deletions docs/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export default defineNuxtConfig({
'@nuxtjs/plausible',
...(process.env.CI ? [] : ['../local']),
],
css: [
'~/style.css',
],
})
7 changes: 7 additions & 0 deletions docs/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:root {
--prose-code-inline-color: #325b27;
}

:root.dark {
--prose-code-inline-color: #c0f0ad;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "0.2.5",
"private": false,
"packageManager": "[email protected].1",
"packageManager": "[email protected].3",
"scripts": {
"build": "pnpm -r --filter=\"./packages/**/*\" run build",
"stub": "pnpm -r run stub",
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"execa": "^7.1.1"
},
"devDependencies": {
"birpc": "^0.2.8",
"birpc": "^0.2.10",
"hookable": "^5.5.1",
"unbuild": "^1.1.2",
"unimport": "^3.0.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools-kit/src/_types/client-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface NuxtDevtoolsClient {
renderCodeHighlight: (code: string, lang: string, lines?: boolean, theme?: string) => string
renderMarkdown: (markdown: string) => string
colorMode: string

extendClientRpc: <ServerFunctions = {}, ClientFunctions = {}>(name: string, functions: ClientFunctions) => BirpcReturn<ServerFunctions, ClientFunctions>
}

export interface NuxtDevtoolsIframeClient {
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools-kit/src/_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from './client-api'
export * from './integrations'
export * from './wizard'
export * from './rpc'
export * from './server-ctx'
export * from './module-options'
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {} from '@nuxt/schema'
import type { ModuleCustomTab } from '@nuxt/devtools-kit/types'
import type { ModuleCustomTab } from './custom-tabs'

export interface ModuleOptions {
/**
Expand Down
21 changes: 21 additions & 0 deletions packages/devtools-kit/src/_types/server-ctx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { BirpcGroup } from 'birpc'
import type { Nuxt } from 'nuxt/schema'
import type { ClientFunctions, ServerFunctions } from './rpc'
import type { ModuleOptions } from './module-options'

/**
* @internal
*/
export interface NuxtDevtoolsServerContext {
nuxt: Nuxt
options: ModuleOptions

rpc: BirpcGroup<ClientFunctions, ServerFunctions>

/**
* Invalidate client cache for a function and ask for re-fetching
*/
refresh: (event: keyof ServerFunctions) => void

extendServerRpc: <ClientFunctions = {}, ServerFunctions = {}>(name: string, functions: ServerFunctions) => BirpcGroup<ClientFunctions, ServerFunctions>
}
53 changes: 34 additions & 19 deletions packages/devtools-kit/src/iframe-client.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
import type { Ref } from 'vue'
import { shallowRef, triggerRef } from 'vue'
import type { NuxtDevtoolsIframeClient } from './types/client-api'
import type { NuxtDevtoolsIframeClient } from './_types/client-api'

let clientRef: Ref<NuxtDevtoolsIframeClient | undefined> | undefined
const hasSetup = false
const fns = [] as ((client: NuxtDevtoolsIframeClient) => void)[]

export function useDevtoolsClient() {
if (!clientRef) {
clientRef = shallowRef<NuxtDevtoolsIframeClient | undefined>()
export function onDevtoolsClientConnected(fn: (client: NuxtDevtoolsIframeClient) => void) {
fns.push(fn)

if (hasSetup)
return

// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore injection
if (window.__NUXT_DEVTOOLS__) {
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore injection
if (window.__NUXT_DEVTOOLS__) {
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore injection
setup(window.__NUXT_DEVTOOLS__)
}
fns.forEach(fn => fn(window.__NUXT_DEVTOOLS__))
}

Object.defineProperty(window, '__NUXT_DEVTOOLS__', {
set(value) {
if (value)
fns.forEach(fn => fn(value))
},
get() {
return clientRef!.value
},
configurable: true,
})

return () => {
fns.splice(fns.indexOf(fn), 1)
}
}

export function useDevtoolsClient() {
if (!clientRef) {
clientRef = shallowRef<NuxtDevtoolsIframeClient | undefined>()

Object.defineProperty(window, '__NUXT_DEVTOOLS__', {
set(value) {
if (value)
setup(value)
},
get() {
return clientRef!.value
},
configurable: true,
})
onDevtoolsClientConnected(setup)
}

function setup(client: NuxtDevtoolsIframeClient) {
Expand Down
24 changes: 23 additions & 1 deletion packages/devtools-kit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useNuxt } from '@nuxt/kit'
import type { BirpcGroup } from 'birpc'
import type { Options as ExecaOptions } from 'execa'
import { execa } from 'execa'
import type { ModuleCustomTab, TerminalState } from './types'
import type { ModuleCustomTab, NuxtDevtoolsServerContext, TerminalState } from './types'

/**
* Hooks to extend a custom tab in devtools.
Expand Down Expand Up @@ -110,3 +111,24 @@ export function startSubprocess(
clear,
}
}

export function extendServerRpc<ClientFunctions = {}, ServerFunctions = {}>(
namespace: string,
functions: ServerFunctions,
nuxt = useNuxt(),
): BirpcGroup<ClientFunctions, ServerFunctions> {
const ctx = _getContext(nuxt)
if (!ctx)
throw new Error('Failed to get devtools context.')

return ctx.extendServerRpc<ClientFunctions, ServerFunctions>(namespace, functions)
}

export function onDevToolsInitialized(fn: () => void, nuxt = useNuxt()) {
nuxt.hook('devtools:initialized', fn)
}

function _getContext(nuxt = useNuxt()): NuxtDevtoolsServerContext | undefined {
// @ts-expect-error - internal
return nuxt?.devtools
}
16 changes: 14 additions & 2 deletions packages/devtools/client/composables/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Lang } from 'shiki-es'
import type { NuxtDevtoolsClient, NuxtDevtoolsHostClient, NuxtDevtoolsIframeClient, VueInspectorData } from '@nuxt/devtools-kit/types'
import { renderMarkdown } from './client-services/markdown'
import { renderCodeHighlight } from './client-services/shiki'
import type { NuxtDevtoolsHostClient, NuxtDevtoolsIframeClient, VueInspectorData } from '~/../src/types'
import { extendedRpcMap, rpc } from './rpc'

export function useClient() {
return useState<NuxtDevtoolsHostClient>('devtools-client')
Expand Down Expand Up @@ -36,7 +37,7 @@ export function useInjectionClient(): ComputedRef<NuxtDevtoolsIframeClient> {

return computed(() => ({
host: client.value,
devtools: {
devtools: <NuxtDevtoolsClient>{
rpc,
colorMode: mode.value,
renderCodeHighlight(code, lang) {
Expand All @@ -45,6 +46,17 @@ export function useInjectionClient(): ComputedRef<NuxtDevtoolsIframeClient> {
renderMarkdown(code) {
return renderMarkdown(code)
},
extendClientRpc(namespace, functions) {
extendedRpcMap.set(namespace, functions)

return new Proxy({}, {
get(_, key) {
if (typeof key !== 'string')
return
return (rpc as any)[`${namespace}:${key}`]
},
})
},
},
}))
}
Loading

0 comments on commit 96080a8

Please sign in to comment.