diff --git a/packages/core/src/node/__tests__/build-static-renderers.test.ts b/packages/core/src/node/__tests__/build-static-renderers.test.ts new file mode 100644 index 000000000..d1282cf9a --- /dev/null +++ b/packages/core/src/node/__tests__/build-static-renderers.test.ts @@ -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' }, + }) + }) +}) diff --git a/packages/core/src/node/__tests__/server-client-module-resolution.test.ts b/packages/core/src/node/__tests__/server-client-module-resolution.test.ts index 24b8cd306..faa0b3404 100644 --- a/packages/core/src/node/__tests__/server-client-module-resolution.test.ts +++ b/packages/core/src/node/__tests__/server-client-module-resolution.test.ts @@ -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', () => ({ @@ -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' }, + ]) + }) }) diff --git a/packages/core/src/node/build-static.ts b/packages/core/src/node/build-static.ts index 64a3615e1..7e51a067e 100644 --- a/packages/core/src/node/build-static.ts +++ b/packages/core/src/node/build-static.ts @@ -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, @@ -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 @@ -77,7 +79,7 @@ export async function buildStaticDevTools(options: BuildStaticOptions): Promise< const rendererManifest: Record = {} 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`, diff --git a/packages/core/src/node/plugins/build.ts b/packages/core/src/node/plugins/build.ts index 80bd851eb..02669938e 100644 --- a/packages/core/src/node/plugins/build.ts +++ b/packages/core/src/node/plugins/build.ts @@ -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' @@ -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 } @@ -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 }) }, } } diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts index 0d3b4f5cf..b2b122e77 100644 --- a/packages/core/src/node/plugins/index.ts +++ b/packages/core/src/node/plugins/index.ts @@ -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' @@ -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. * @@ -71,11 +75,11 @@ export async function DevTools(options: DevToolsOptions = {}): Promise 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( diff --git a/packages/core/src/node/plugins/server.ts b/packages/core/src/node/plugins/server.ts index 048804b71..6586062ae 100644 --- a/packages/core/src/node/plugins/server.ts +++ b/packages/core/src/node/plugins/server.ts @@ -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' @@ -37,7 +37,10 @@ export function renderDockImportsMap(docks: Iterable): string ].join('\n') } -export function DevToolsServer(options: ViteDevToolsUiOptions = {}): Plugin { +export function DevToolsServer( + options: ViteDevToolsUiOptions = {}, + renderers?: readonly DockRendererRegistration[], +): Plugin { let context: ViteDevToolsNodeContext let close: (() => Promise) | undefined return { @@ -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 diff --git a/packages/core/src/node/renderers.ts b/packages/core/src/node/renderers.ts new file mode 100644 index 000000000..f023f808e --- /dev/null +++ b/packages/core/src/node/renderers.ts @@ -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() + const registrations = [jsonRenderUiRenderer(), ...configuredRenderers] + for (const registration of registrations) + renderersByType.set(registration.type, registration) + return [...renderersByType.values()] +} diff --git a/packages/core/src/node/server.ts b/packages/core/src/node/server.ts index 1c4158ded..200d4e083 100644 --- a/packages/core/src/node/server.ts +++ b/packages/core/src/node/server.ts @@ -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. @@ -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 diff --git a/packages/kit/src/define.ts b/packages/kit/src/define.ts index 7a37c2765..8ab87e131 100644 --- a/packages/kit/src/define.ts +++ b/packages/kit/src/define.ts @@ -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(spec: SpecType): SpecType { return spec } diff --git a/packages/kit/src/node/context.ts b/packages/kit/src/node/context.ts index 728e147cc..75d2ee78e 100644 --- a/packages/kit/src/node/context.ts +++ b/packages/kit/src/node/context.ts @@ -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' @@ -22,7 +23,10 @@ export interface KitNodeContext extends DevframeHubContext { * (`docks.register({ type: 'json-render', view: renderer.view, … })`) and * call `updateSpec` / `updateState` on the handle to drive it reactively. */ - createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer + createJsonRenderer: ( + spec: SpecType, + options?: Pick, 'schema'>, + ) => JsonRenderer } export interface CreateKitContextOptions extends CreateHubContextOptions { @@ -48,7 +52,10 @@ export async function createKitContext(options: CreateKitContextOptions): Promis Object.defineProperty(context, 'viteServer', { value: options.viteServer, enumerable: true }) Object.defineProperty(context, 'createJsonRenderer', { - value: (spec: JsonRenderSpec) => createJsonRenderer(context, spec), + value: ( + spec: SpecType, + rendererOptions?: Pick, 'schema'>, + ) => createJsonRenderer(context, spec, rendererOptions), enumerable: true, }) @@ -62,17 +69,21 @@ export async function createKitContext(options: CreateKitContextOptions): Promis * 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( + context: KitNodeContext, + spec: SpecType, + options: Pick, 'schema'> = {}, +): JsonRenderer { + const view = createJsonRenderView(context, { id: `kit-${nanoid()}`, spec, ...options }) const handle = { _stateKey: view.ref.stateKey, view: view.ref, - } as JsonRenderer + } as JsonRenderer Object.defineProperties(handle, { updateSpec: { - value: (next: JsonRenderSpec) => view.update(next), + value: (next: SpecType) => view.update(next), enumerable: false, }, updateState: { diff --git a/packages/kit/src/types/docks.ts b/packages/kit/src/types/docks.ts index 7336487e6..dc503e494 100644 --- a/packages/kit/src/types/docks.ts +++ b/packages/kit/src/types/docks.ts @@ -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, diff --git a/packages/kit/src/types/json-render.ts b/packages/kit/src/types/json-render.ts index 4e890bc8e..da1e23690 100644 --- a/packages/kit/src/types/json-render.ts +++ b/packages/kit/src/types/json-render.ts @@ -11,7 +11,7 @@ import type { // `@devframes/json-render` package. /** A json-render spec — the declarative UI description a plugin authors. */ -export type JsonRenderSpec = DevframeJsonRenderSpec +export type JsonRenderSpec = DevframeJsonRenderSpec /** A single element within a spec's `elements` map. */ export type JsonRenderElement = UIElement @@ -28,9 +28,9 @@ export type { JsonRenderView, JsonRenderViewRef } * 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 { /** Replace the entire spec. */ - updateSpec: (spec: JsonRenderSpec) => void + updateSpec: (spec: SpecType) => void /** Shallow-merge values into the view's `state`. */ updateState: (state: Record) => void /** Unregister the underlying view's shared state and listeners. */ @@ -38,5 +38,5 @@ export interface JsonRenderer { /** 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 } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts index f5c922551..d2811cbce 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts @@ -26,17 +26,17 @@ export interface DevToolsViewLauncher extends DevframeViewLauncher { roots?: DevToolsLaunchRoot[]; }; } -export interface JsonRenderer { - updateSpec: (_: JsonRenderSpec) => void; +export interface JsonRenderer { + updateSpec: (_: SpecType) => void; updateState: (_: Record) => void; dispose: () => void; readonly _stateKey: string; - readonly view: JsonRenderViewRef; + readonly view: JsonRenderViewRef; } export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; - createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; + createJsonRenderer: (_: SpecType, _?: Pick, 'schema'>) => JsonRenderer; } export interface PluginWithDevTools extends Plugin { devtools?: DevToolsPluginOptions; @@ -50,11 +50,11 @@ export interface ViteDevToolsNodeContext extends KitNodeContext { // #region Types export type DevToolsDockEntryCategory = DevframeDockEntryCategory; export type JsonRenderElement = UIElement; -export type JsonRenderSpec = DevframeJsonRenderSpec; +export type JsonRenderSpec = DevframeJsonRenderSpec; // #endregion // #region Functions -export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; +export declare function defineJsonRenderSpec(_: SpecType): SpecType; // #endregion // #region Variables @@ -120,6 +120,7 @@ export { DevToolsViewHost } export { DevToolsViewIframe } export { DevToolsViewJsonRender } export { DevToolsViewLauncherStatus } +export { DockRendererRegistration } export { EntriesToObject } export { EventEmitter } export { EventsMap } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts index 7233dac49..e1864a75a 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts @@ -35,7 +35,7 @@ export interface InstallLauncherOptions { export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; - createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; + createJsonRenderer: (_: SpecType, _?: Pick, 'schema'>) => JsonRenderer; } export interface ProcessLauncherOptions { id: string; diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts index 1f8ad935e..c574801b6 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts @@ -4,6 +4,7 @@ // #region Interfaces export interface CreateDevToolsHubOptions { context: ViteDevToolsNodeContext; + renderers?: readonly DockRendererRegistration[]; ui?: ViteDevToolsUiOptions; server?: Server; host?: string;