Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/core/src/node/__tests__/build-static-renderers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { buildStaticDevTools } from '../build-static'

const mutateRendererManifest = vi.hoisted(() => vi.fn())

vi.mock('@devframes/json-render-ui/hub', () => ({
jsonRenderUiRenderer: () => ({ type: 'json-render', file: '/unused-builtin.mjs' }),
}))

vi.mock('devframe/rpc/dump', () => ({
collectStaticRpcDump: () => ({ files: {}, manifest: {} }),
}))

vi.mock('../ui', () => ({
createViteDevToolsUi: () => ({}),
}))

function fakeContext(): ViteDevToolsNodeContext {
return {
cwd: process.cwd(),
host: { getStorageDir: () => tmpdir() },
views: { buildStaticDirs: [] },
docks: { values: () => [] },
services: { ready: vi.fn() },
rpc: {
definitions: { values: () => [] },
sharedState: {
get: vi.fn(() => ({ mutate: mutateRendererManifest })),
},
},
} as unknown as ViteDevToolsNodeContext
}

describe('buildStaticDevTools renderers', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('copies the resolved configured renderers and publishes one manifest entry per type', async () => {
expect.assertions(4)
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'vite-devtools-renderers-'))
const replacementFile = join(temporaryDirectory, 'replacement.mjs')
const customFile = join(temporaryDirectory, 'custom.mjs')
const outputDirectory = join(temporaryDirectory, 'output')
await writeFile(replacementFile, 'export const replacement = true')
await writeFile(customFile, 'export const custom = true')

await buildStaticDevTools({
context: fakeContext(),
outDir: outputDirectory,
renderers: [
{ type: 'json-render', file: replacementFile },
{ type: 'custom-render', file: customFile },
],
})

expect(await readFile(join(outputDirectory, '__devtools', '__renderers', 'json-render.mjs'), 'utf8')).toBe('export const replacement = true')
expect(await readFile(join(outputDirectory, '__devtools', '__renderers', 'custom-render.mjs'), 'utf8')).toBe('export const custom = true')
expect(mutateRendererManifest).toHaveBeenCalledOnce()
expect(mutateRendererManifest.mock.calls[0]![0]({})).toEqual({
'json-render': { importFrom: '/__devtools/__renderers/json-render.mjs' },
'custom-render': { importFrom: '/__devtools/__renderers/custom-render.mjs' },
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ vi.mock('@devframes/hub/initiate', () => ({
}))

vi.mock('@devframes/json-render-ui/hub', () => ({
jsonRenderUiRenderer: () => ({}),
jsonRenderUiRenderer: () => ({ type: 'json-render', file: '/builtin-json-render.mjs' }),
}))

vi.mock('../ui', () => ({
Expand Down Expand Up @@ -57,4 +57,29 @@ describe('createDevToolsHub client module resolution', () => {
expect(initHub).toHaveBeenCalledOnce()
expect(initHub.mock.calls[0]![0]).not.toHaveProperty('clientModuleResolution')
})

it('uses the built-in renderer list by default', async () => {
expect.assertions(1)
await createDevToolsHub({ context: fakeContext() })

expect(initHub.mock.calls[0]![0].renderers).toEqual([
{ type: 'json-render', file: '/builtin-json-render.mjs' },
])
})

it('replaces matching built-ins and appends new configured renderers', async () => {
expect.assertions(1)
await createDevToolsHub({
context: fakeContext(),
renderers: [
{ type: 'json-render', file: '/replacement.mjs' },
{ type: 'custom-render', file: '/custom.mjs' },
],
})

expect(initHub.mock.calls[0]![0].renderers).toEqual([
{ type: 'json-render', file: '/replacement.mjs' },
{ type: 'custom-render', file: '/custom.mjs' },
])
})
})
8 changes: 5 additions & 3 deletions packages/core/src/node/build-static.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable no-console */

import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { DockRendererRegistration, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ViteDevToolsUiOptions } from './ui'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import { DOCK_RENDERERS_STATE_KEY } from '@devframes/hub/constants'
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import {
DEVTOOLS_CONNECTION_META_FILENAME,
DEVTOOLS_DIRNAME,
Expand All @@ -18,11 +17,14 @@ import { colors as c } from 'devframe/utils/colors'
import { resolveStaticAssetsSource } from 'devframe/utils/remote-assets'
import { dirname, join, relative, resolve } from 'pathe'
import { MARK_NODE } from './constants'
import { resolveDockRendererRegistrations } from './renderers'
import { createViteDevToolsUi } from './ui'

export interface BuildStaticOptions {
context: ViteDevToolsNodeContext
outDir: string
/** Dock renderer modules copied into the static output, replacing built-ins by matching type. */
renderers?: readonly DockRendererRegistration[]
withApp?: boolean
/** Reference-UI options forwarded to `createUi`. */
ui?: ViteDevToolsUiOptions
Expand Down Expand Up @@ -77,7 +79,7 @@ export async function buildStaticDevTools(options: BuildStaticOptions): Promise<
const rendererManifest: Record<string, { importFrom: string, importName?: string }> = {}
const renderersRoot = resolve(devToolsRoot, '__renderers')
await fs.mkdir(renderersRoot, { recursive: true })
for (const registration of [jsonRenderUiRenderer()]) {
for (const registration of resolveDockRendererRegistrations(options.renderers)) {
await fs.cp(registration.file, resolve(renderersRoot, `${registration.type}.mjs`))
rendererManifest[registration.type] = {
importFrom: `${DEVTOOLS_MOUNT_PATH}__renderers/${registration.type}.mjs`,
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/node/plugins/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */

import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { DockRendererRegistration, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { Plugin, ResolvedConfig } from 'vite'
import type { ViteDevToolsUiOptions } from '../ui'
import { colors as c } from 'devframe/utils/colors'
Expand All @@ -9,6 +9,7 @@ import { MARK_NODE } from '../constants'

export interface DevToolsBuildOptions {
outDir?: string
renderers?: readonly DockRendererRegistration[]
/** Reference-UI options forwarded to the static snapshot's `createUi`. */
ui?: ViteDevToolsUiOptions
}
Expand Down Expand Up @@ -38,7 +39,7 @@ export function DevToolsBuild(options: DevToolsBuildOptions = {}): Plugin {
: resolve(resolvedConfig.root, resolvedConfig.build.outDir)

const { buildStaticDevTools } = await import('../build-static')
await buildStaticDevTools({ context, outDir, withApp: true, ui: options.ui })
await buildStaticDevTools({ context, outDir, withApp: true, ui: options.ui, renderers: options.renderers })
},
}
}
8 changes: 6 additions & 2 deletions packages/core/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DockRendererRegistration } from '@vitejs/devtools-kit'
import type { Plugin } from 'vite'
import type { ViteDevToolsUiOptions } from '../ui'
import { DevToolsBuild } from './build'
Expand All @@ -15,6 +16,9 @@ export interface DevToolsOptions {
*/
builtinDevTools?: boolean

/** Dock renderer modules, replacing built-ins with the same type and appending new types. */
renderers?: readonly DockRendererRegistration[]

/**
* How the embedded floating dock reveals itself on a fresh page.
*
Expand Down Expand Up @@ -71,11 +75,11 @@ export async function DevTools(options: DevToolsOptions = {}): Promise<Plugin[]>

const plugins = [
DevToolsInjection(),
DevToolsServer(ui),
DevToolsServer(ui, options.renderers),
]

if (build?.withApp) {
plugins.push(DevToolsBuild({ outDir: build.outDir, ui }))
plugins.push(DevToolsBuild({ outDir: build.outDir, ui, renderers: options.renderers }))
}

plugins.unshift(
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/node/plugins/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientScriptEntry, DevToolsDockEntry, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ClientScriptEntry, DevToolsDockEntry, DockRendererRegistration, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { Server as NodeHttpServer } from 'node:http'
import type { Plugin } from 'vite'
import type { ViteDevToolsUiOptions } from '../ui'
Expand Down Expand Up @@ -37,7 +37,10 @@ export function renderDockImportsMap(docks: Iterable<DevToolsDockEntry>): string
].join('\n')
}

export function DevToolsServer(options: ViteDevToolsUiOptions = {}): Plugin {
export function DevToolsServer(
options: ViteDevToolsUiOptions = {},
renderers?: readonly DockRendererRegistration[],
): Plugin {
let context: ViteDevToolsNodeContext
let close: (() => Promise<void>) | undefined
return {
Expand All @@ -54,6 +57,7 @@ export function DevToolsServer(options: ViteDevToolsUiOptions = {}): Plugin {
const devtools = await createDevToolsHub({
context,
ui: options,
renderers,
// Share Vite's HTTP server for a route-bound WS upgrade; fall back to a
// side-car when Vite runs in middleware mode without its own server.
// Vite types `httpServer` as a broader union (incl. http2); at dev
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/node/renderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { DockRendererRegistration } from '@vitejs/devtools-kit'
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'

/** Merge configured renderers over the built-ins while keeping one entry per type. */
export function resolveDockRendererRegistrations(
configuredRenderers: readonly DockRendererRegistration[] = [],
): DockRendererRegistration[] {
const renderersByType = new Map<string, DockRendererRegistration>()
const registrations = [jsonRenderUiRenderer(), ...configuredRenderers]
for (const registration of registrations)
renderersByType.set(registration.type, registration)
return [...renderersByType.values()]
}
8 changes: 5 additions & 3 deletions packages/core/src/node/server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import type { HubInstance } from '@devframes/hub/initiate'
import type { ConnectionMeta, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ConnectionMeta, DockRendererRegistration, ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ViteDevToolsHost } from '@vitejs/devtools-kit/node'
import type { Server as NodeHttpServer } from 'node:http'
import type { DevToolsConfig } from './config'
import type { ViteDevToolsUiOptions } from './ui'
import { initHub } from '@devframes/hub/initiate'
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants'
import { getAuthHandler, isClientAuthDisabled } from './auth-handler'
import { resolveDockRendererRegistrations } from './renderers'
import { createViteDevToolsUi } from './ui'

export interface CreateDevToolsHubOptions {
context: ViteDevToolsNodeContext
/** Dock renderer modules served by the hub, replacing built-ins by matching type. */
renderers?: readonly DockRendererRegistration[]
/**
* Reference-UI options forwarded to `createUi` — the embedded dock's
* reveal policy and the dock-bar rendering preferences.
Expand Down Expand Up @@ -67,7 +69,7 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom
// Serve + advertise the reference json-render frontend so `json-render`
// docks (kit's `createJsonRenderer`, the git/data-inspector devframes)
// render instead of hub-ui's missing-renderer fallback.
renderers: [jsonRenderUiRenderer()],
renderers: resolveDockRendererRegistrations(options.renderers),
// With a live Vite dev server, route bare-specifier dock client scripts
// (`ClientScriptEntry.importFrom` naming an npm module, e.g.
// vue-tracer's `vite-plugin-vue-tracer/client/vite-devtools`) through
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { defineCommand, defineDockEntry } from '@devframes/hub'
* package, whose spec is a plain `@json-render/core` `Spec`), so the kit keeps
* the convenience helper for authoring specs with inference.
*/
export function defineJsonRenderSpec(spec: JsonRenderSpec): JsonRenderSpec {
export function defineJsonRenderSpec<SpecType extends JsonRenderSpec>(spec: SpecType): SpecType {
return spec
}

Expand Down
23 changes: 17 additions & 6 deletions packages/kit/src/node/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CreateHubContextOptions, DevframeHubContext } from '@devframes/hub/node'
import type { CreateJsonRenderViewOptions } from '@devframes/json-render/node'
import type { ResolvedConfig, ViteDevServer } from 'vite'
import type { JsonRenderer, JsonRenderSpec } from '../types/json-render'
import { createHubContext } from '@devframes/hub/node'
Expand All @@ -22,7 +23,10 @@
* (`docks.register({ type: 'json-render', view: renderer.view, … })`) and
* call `updateSpec` / `updateState` on the handle to drive it reactively.
*/
createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer
createJsonRenderer: <SpecType extends JsonRenderSpec>(
spec: SpecType,
options?: Pick<CreateJsonRenderViewOptions<SpecType>, 'schema'>,

Check failure on line 28 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.

Check failure on line 28 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.
) => JsonRenderer<SpecType>
}

export interface CreateKitContextOptions extends CreateHubContextOptions {
Expand All @@ -48,7 +52,10 @@
Object.defineProperty(context, 'viteServer', { value: options.viteServer, enumerable: true })

Object.defineProperty(context, 'createJsonRenderer', {
value: (spec: JsonRenderSpec) => createJsonRenderer(context, spec),
value: <SpecType extends JsonRenderSpec>(
spec: SpecType,
rendererOptions?: Pick<CreateJsonRenderViewOptions<SpecType>, 'schema'>,

Check failure on line 57 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.

Check failure on line 57 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.
) => createJsonRenderer(context, spec, rendererOptions),
enumerable: true,
})

Expand All @@ -62,17 +69,21 @@
* shared-state projection walks only enumerable own keys, so the live
* closures never reach the wire while `_stateKey` does.
*/
function createJsonRenderer(context: KitNodeContext, spec: JsonRenderSpec): JsonRenderer {
const view = createJsonRenderView(context, { id: `kit-${nanoid()}`, spec })
function createJsonRenderer<SpecType extends JsonRenderSpec>(
context: KitNodeContext,
spec: SpecType,
options: Pick<CreateJsonRenderViewOptions<SpecType>, 'schema'> = {},

Check failure on line 75 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.

Check failure on line 75 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Property 'schema' is missing in type '{}' but required in type 'Pick<any, "schema">'.

Check failure on line 75 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'CreateJsonRenderViewOptions' is not generic.

Check failure on line 75 in packages/kit/src/node/context.ts

View workflow job for this annotation

GitHub Actions / lint

Property 'schema' is missing in type '{}' but required in type 'Pick<any, "schema">'.
): JsonRenderer<SpecType> {
const view = createJsonRenderView(context, { id: `kit-${nanoid()}`, spec, ...options })

const handle = {
_stateKey: view.ref.stateKey,
view: view.ref,
} as JsonRenderer
} as JsonRenderer<SpecType>

Object.defineProperties(handle, {
updateSpec: {
value: (next: JsonRenderSpec) => view.update(next),
value: (next: SpecType) => view.update(next),
enumerable: false,
},
updateState: {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/docks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DevframeDockEntryCategory, DevframeViewLauncher } from '@devframes/hub/types'

export type { DockRendererRegistration } from '@devframes/hub/initiate'

export type {
ClientScriptEntry,
DevframeDockActivation as DevToolsDockActivation,
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/types/json-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// `@devframes/json-render` package.

/** A json-render spec — the declarative UI description a plugin authors. */
export type JsonRenderSpec = DevframeJsonRenderSpec
export type JsonRenderSpec<Element extends UIElement = UIElement> = DevframeJsonRenderSpec<Element>

Check failure on line 14 in packages/kit/src/types/json-render.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'DevframeJsonRenderSpec' is not generic.

/** A single element within a spec's `elements` map. */
export type JsonRenderElement = UIElement
Expand All @@ -28,15 +28,15 @@
* view: renderer.view, … })`); the client subscribes through its `stateKey`.
* Drive the panel by calling `updateSpec` / `updateState` on the handle.
*/
export interface JsonRenderer {
export interface JsonRenderer<SpecType extends JsonRenderSpec = JsonRenderSpec> {
/** Replace the entire spec. */
updateSpec: (spec: JsonRenderSpec) => void
updateSpec: (spec: SpecType) => void
/** Shallow-merge values into the view's `state`. */
updateState: (state: Record<string, unknown>) => void
/** Unregister the underlying view's shared state and listeners. */
dispose: () => void
/** Shared-state key the client subscribes to for the live spec + state. */
readonly _stateKey: string
/** The serializable reference to the underlying view. */
readonly view: JsonRenderViewRef
readonly view: JsonRenderViewRef<SpecType>

Check failure on line 41 in packages/kit/src/types/json-render.ts

View workflow job for this annotation

GitHub Actions / lint

Type 'JsonRenderViewRef' is not generic.
}
Loading
Loading