@u-devtools/kit
v0.2.4
Published
SDK for creating Universal DevTools plugins
Maintainers
Readme
@u-devtools/kit
SDK for creating Universal DevTools plugins. Provides helper functions for plugin development and Web Components adapter to use Vue components from @u-devtools/ui in any framework.
Installation
npm install -D @u-devtools/kitUsage
Creating a Plugin
import { definePlugin } from '@u-devtools/kit/define-plugin';
export const myPlugin = () => definePlugin({
name: 'My Plugin',
root: import.meta.url,
client: './client',
app: './app',
server: './server',
});Important: definePlugin must be imported from @u-devtools/kit/define-plugin (not from @u-devtools/kit) because it uses Node.js APIs (node:url, node:path) and should only be used in server-side code (Vite plugin context).
Web Components Integration
Use defineVueElement to register Vue components as standard Web Components. This allows you to use Vue components from @u-devtools/ui in React, Angular, plain HTML, or any other framework.
Key Features:
- Attribute & Property Sync: Automatically maps HTML attributes to Vue props
- Complex Data Support: Use
.propsproperty for objects/arrays/functions - Event Forwarding: Vue events become standard DOM CustomEvents
- Slot Bridge: Initial HTML content becomes Vue default slot
- Light DOM: No Shadow DOM, ensuring Tailwind CSS works perfectly
Registration:
import { defineVueElement } from '@u-devtools/kit';
import { UButton, UCard } from '@u-devtools/ui';
// Register components
defineVueElement('u-button', UButton, {
attributes: ['label', 'variant', 'icon'],
emits: ['click']
});
defineVueElement('u-card', UCard, {
attributes: ['title', 'subtitle']
});Batch Registration:
import { defineVueElements } from '@u-devtools/kit';
defineVueElements([
{
tagName: 'u-button',
component: UButton,
options: { attributes: ['label'], emits: ['click'] }
},
{
tagName: 'u-card',
component: UCard,
options: { attributes: ['title'] }
},
]);Usage in Plain HTML / CMS / PHP:
<!-- Props via attributes -->
<u-card title="Web Component Demo">
<p class="text-gray-400 mb-4">
This is standard HTML using Vue components via Custom Elements!
</p>
<!-- Events via standard listener -->
<u-button
label="Click Me"
variant="primary"
id="my-btn"
></u-button>
</u-card>
<script>
const btn = document.getElementById('my-btn');
// Listen to Vue event as standard DOM event
btn.addEventListener('click', (e) => {
console.log('Vue button clicked!', e.detail);
// Update attribute (reflects to Vue prop)
btn.setAttribute('label', 'Clicked!');
btn.setAttribute('variant', 'success');
});
</script>Passing Complex Data (JSON/Arrays):
<u-table id="users-table"></u-table>
<script>
const table = document.getElementById('users-table');
// Use the .props setter for complex data
table.props = {
columns: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'User' }
],
rows: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
};
</script>Usage in React:
React passes data to Custom Elements as attributes (strings) by default. For events and complex data, use useRef and .props setter.
import React, { useEffect, useRef, useState } from 'react';
import { defineVueElements } from '@u-devtools/kit';
import { UButton, UCard, UInput } from '@u-devtools/ui';
// Register components once
defineVueElements([
{
tagName: 'u-button',
component: UButton,
options: { attributes: ['label', 'variant'], emits: ['click'] }
},
{
tagName: 'u-card',
component: UCard,
options: { attributes: ['title'] }
},
{
tagName: 'u-input',
component: UInput,
options: { attributes: ['model-value', 'placeholder'], emits: ['update:modelValue'] }
},
]);
export const ReactApp = () => {
const [text, setText] = useState('');
const inputRef = useRef<HTMLElement>(null);
const buttonRef = useRef<HTMLElement>(null);
useEffect(() => {
// Pass event handlers via .props setter
if (inputRef.current) {
(inputRef.current as any).props = {
onUpdate:modelValue: (value: string) => setText(value)
};
}
if (buttonRef.current) {
(buttonRef.current as any).props = {
onClick: () => alert(`Text: ${text}`)
};
}
}, [text]);
return (
<div className="p-4">
<u-card title="React Component">
<div className="p-4 space-y-4">
<u-input
ref={inputRef}
model-value={text}
placeholder="Type something..."
/>
<div>React State: {text}</div>
<u-button
ref={buttonRef}
label="Submit"
variant="primary"
/>
</div>
</u-card>
</div>
);
};Usage in Angular:
Angular has excellent support for Custom Elements. You just need to enable the schema.
App Module:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA] // <-- Required for custom elements
})
export class AppModule {}Component:
import { Component } from '@angular/core';
import { defineVueElement } from '@u-devtools/kit';
import { UButton } from '@u-devtools/ui';
defineVueElement('u-button', UButton, {
attributes: ['label'],
emits: ['click']
});
@Component({
selector: 'app-root',
template: `
<u-button
[label]="buttonText"
(click)="handleClick($event)">
</u-button>
`
})
export class AppComponent {
buttonText = 'Click Me';
handleClick(event: CustomEvent) {
console.log('Clicked!', event.detail);
}
}App Context Plugin Definition
defineApp(definition)
Declaratively defines app-side plugin logic that runs in the main window context.
Parameters:
definition(AppPluginDefinition): Plugin definition object
AppPluginDefinition:
component(Component | undefined): Optional Vue component to render in overlay layersetup(function): Setup function that receives{ bridge, onCleanup }menu(object | undefined): Declarative menu item configurationcommands(array | undefined): Declarative command definitions
Example:
import { defineApp } from '@u-devtools/kit';
import type { AppBridge } from '@u-devtools/core';
import type { MyPluginProtocol } from './types';
import MyOverlay from './app/MyOverlay.vue';
export default defineApp({
// Optional: Vue component rendered in overlay plugins layer
component: MyOverlay,
// Declarative menu registration
menu: {
id: 'my-plugin:quick-action',
label: 'Quick Action',
icon: 'Bolt',
order: 10,
action: (ctx) => {
if (!ctx.isOpen) {
ctx.open();
}
ctx.switchPlugin('My Plugin');
},
},
setup({ bridge, onCleanup }) {
const typedBridge = bridge as AppBridge<MyPluginProtocol>;
// Bridge is automatically created and managed by overlay
typedBridge.send('plugin-ready', { message: 'Hello from app context' });
typedBridge.on('action', (data) => {
// data is automatically typed based on Protocol
console.log('Action received:', data);
});
// Register cleanup function
// Bridge is automatically closed by overlay
onCleanup(() => {
// Remove event listeners, restore patches, etc.
console.log('Plugin cleanup');
});
},
});Key Benefits:
- Automatic
AppBridgelifecycle management - Built-in HMR cleanup support via
onCleanup - Declarative component rendering in overlay
- Declarative menu and command registration
- Fully typed RPC communication via Protocol
- No manual
import.meta.hothandling required
Plugin Context
Module Scope Singleton Pattern
Each plugin has its own isolated context using the Module Scope Singleton pattern. This works everywhere: Vue, React, Svelte, Solid, Vanilla JS, and Node.js.
Key Benefits:
- ✅ Zero Dependencies: Core context is pure JavaScript, no Vue or React required
- ✅ Universal: Works in any framework or vanilla JavaScript
- ✅ No Boilerplate: No need for
<Context.Provider>components - ✅ No Prop Drilling: Context is available in any file via import
- ✅ Isolation: Each plugin has its own closed context
Setup
1. Create context.ts in your plugin:
// src/context.ts
import { createDevToolsContext } from '@u-devtools/kit';
import type { AppBridge, ClientApi } from '@u-devtools/core';
import type { MyPluginProtocol } from './types';
import type { Toast } from '@u-devtools/kit';
import { createToast } from '@u-devtools/overlay';
// 1. Create "raw" context
const { setupDevTools, useBridge: useRawBridge, useToast: useRawToast, useApi: useRawApi } = createDevToolsContext();
// 2. Export setup (used in client.ts and app.ts)
export { setupDevTools };
// 3. Export separate typed hooks
export function useBridge(): AppBridge<MyPluginProtocol> {
return useRawBridge() as AppBridge<MyPluginProtocol>;
}
export function useToast(): Toast {
return useRawToast();
}
export function useApi(): ClientApi {
const api = useRawApi();
if (!api) {
throw new Error('[u-devtools] API not available in my-plugin context');
}
return api;
}2. Initialize in client.ts:
// src/client.ts
import { AppBridge } from '@u-devtools/core';
import { createToast } from '@u-devtools/overlay';
import { setupDevTools } from './context';
renderMain(container, api) {
const bridge = new AppBridge('my-plugin');
// Initialize context (once!)
setupDevTools({ api, bridge, toast: createToast() });
// ... render UI
}3. Initialize in app.ts (for app context):
// src/app.ts
import { defineApp } from '@u-devtools/kit';
import { setupDevTools } from './context';
export default defineApp({
setup({ bridge, onCleanup }) {
// Initialize context (api is not available in app context)
setupDevTools({ bridge });
// ... setup logic
},
});4. Use in components:
// In any component or composable
import { useBridge, useApi, useToast } from './context';
// Use separate hooks - import only what you need
const bridge = useBridge();
const api = useApi();
const toast = useToast();
// Use anywhere, no prop drilling needed
bridge.send('event', { data: 'test' });
toast.success('Done!');
api.storage.set('key', 'value');Framework Adapters
Vue Adapter
useBridgeState<T>(syncedState: SyncedState<T>): Ref<T>
Vue adapter for SyncedState that converts it to a Vue ref with bidirectional synchronization.
Import:
import { useBridgeState } from '@u-devtools/kit/vue';Example:
import { useBridgeState } from '@u-devtools/kit/vue';
import { useBridge } from './context';
const bridge = useBridge();
const isOpen = bridge.state('isOpen', false);
// Convert to Vue ref
const isOpenRef = useBridgeState(isOpen);
// Use as normal Vue ref
watch(isOpenRef, (val) => {
console.log('State changed:', val);
});
// Update from Vue
isOpenRef.value = true; // Automatically syncs to App contextReact Adapter
useBridgeState<T>(syncedState: SyncedState<T>): [T, (value: T) => void]
React adapter for SyncedState that returns a tuple [value, setValue] compatible with React state.
Import:
import { useBridgeState } from '@u-devtools/kit/react';Example:
import { useBridgeState } from '@u-devtools/kit/react';
import { useBridge } from './context';
const bridge = useBridge();
const isOpen = bridge.state('isOpen', false);
// Convert to React state
const [isOpenValue, setIsOpen] = useBridgeState(isOpen);
// Use as normal React state
useEffect(() => {
console.log('State changed:', isOpenValue);
}, [isOpenValue]);
// Update from React
setIsOpen(true); // Automatically syncs to App contextSolid Adapter
useBridgeState<T>(syncedState: SyncedState<T>): [() => T, (value: T) => void]
Solid adapter for SyncedState that returns a Solid signal.
Import:
import { useBridgeState } from '@u-devtools/kit/solid';Example:
import { useBridgeState } from '@u-devtools/kit/solid';
import { useBridge } from './context';
const bridge = useBridge();
const isOpen = bridge.state('isOpen', false);
// Convert to Solid signal
const [isOpenValue, setIsOpen] = useBridgeState(isOpen);
// Use as normal Solid signal
createEffect(() => {
console.log('State changed:', isOpenValue());
});
// Update from Solid
setIsOpen(true); // Automatically syncs to App contextSvelte Adapter
useBridgeState<T>(syncedState: SyncedState<T>): SvelteStore<T>
Svelte adapter for SyncedState that converts it to a Svelte Writable Store compatible with Svelte's store contract.
Import:
import { useBridgeState } from '@u-devtools/kit/svelte';Example:
<script>
import { useBridgeState } from '@u-devtools/kit/svelte';
import { useBridge } from './context';
const bridge = useBridge();
const isOpen = bridge.state('isOpen', false);
const isOpenStore = useBridgeState(isOpen);
</script>
<button on:click={() => $isOpenStore = !$isOpenStore}>
Is Open: {$isOpenStore}
</button>Lit Adapter
useBridgeState<T>(host: ReactiveControllerHost, syncedState: SyncedState<T>): BridgeStateController<T>
Lit adapter for SyncedState that works as a Reactive Controller. Automatically calls requestUpdate() when state changes.
Import:
import { useBridgeState } from '@u-devtools/kit/lit';Example:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { useBridgeState } from '@u-devtools/kit/lit';
import { useBridge } from './context';
@customElement('my-element')
export class MyElement extends LitElement {
private bridge = useBridge();
private isOpen = useBridgeState(this, this.bridge.state('isOpen', false));
render() {
return html`
<button @click=${() => this.isOpen.value = !this.isOpen.value}>
Is Open: ${this.isOpen.value}
</button>
`;
}
}Vanilla JavaScript Adapter
Vanilla adapter provides DOM binding utilities and reactive state management without any framework.
Import:
import { bindText, bindInput, bindClass, bindVisible, bindAttr, bindStyle, bindHtml, useBridgeState } from '@u-devtools/kit/vanilla';Available Bindings:
bindText(element, state)- Binds text contentbindHtml(element, state)- Binds HTML contentbindClass(element, state, className)- Toggles CSS classbindVisible(element, state)- Controls visibility (display: none)bindAttr(element, state, attrName)- Binds HTML attributebindInput(element, state)- Two-way binding for inputsbindStyle(element, state, property)- Binds CSS style property
useBridgeState for Vanilla:
useBridgeState(state, onChange?)- Creates reactive reference with optional effect callback
Example:
import { setupDevTools, useBridge } from './context';
import { bindText, bindInput, bindClass } from '@u-devtools/kit/vanilla';
import { AppBridge } from '@u-devtools/core';
const plugin = {
renderMain(container, api) {
const bridge = new AppBridge('vanilla');
setupDevTools({ api, bridge });
// Now you can use useBridge() anywhere in this context
// Create reactive states
const counter = bridge.state('counter', 0);
const userName = bridge.state('user', 'Guest');
const isDark = bridge.state('isDark', false);
// Create markup
container.innerHTML = `
<div class="p-4">
<span id="count-display"></span>
<input id="name-input" />
<button id="toggle-theme">Toggle</button>
</div>
`;
// Bind states to DOM
const disposables = [
bindText(container.querySelector('#count-display')!, counter),
bindInput(container.querySelector('#name-input')!, userName),
bindClass(container as HTMLElement, isDark, 'dark-theme'),
];
// Alternative: use useBridgeState with effect
const countRef = useBridgeState(counter, (val) => {
container.querySelector('#count-display')!.textContent = String(val);
});
// Event handlers
container.querySelector('#toggle-theme')!.onclick = () => {
isDark.value = !isDark.value;
};
container.querySelector('#btn-inc')!.onclick = () => {
countRef.value++; // Updates both local and bridge state
};
// Cleanup
return () => {
disposables.forEach(fn => fn());
countRef.dispose();
bridge.close();
};
}
};Benefits:
- ✅ Pure JavaScript - no framework dependencies
- ✅ Reactive - DOM updates automatically when state changes
- ✅ Two-way binding - input changes sync to state
- ✅ Automatic cleanup - all bindings return cleanup functions
Toast Notifications
Toast is automatically included in the context. Access it via useToast():
Example:
import { useToast } from './context';
const toast = useToast();
// Show notifications
toast.success('Operation completed!');
toast.error('Something went wrong');
toast.info('Processing...');Features:
- Automatically detects context (iframe or overlay)
- Uses
postMessagefor cross-iframe communication - Direct rendering in overlay context
- Consistent API across all contexts
API Reference
definePlugin(options)
Creates a DevTools plugin definition.
⚠️ Important: definePlugin must be imported from @u-devtools/kit/define-plugin (not from @u-devtools/kit) because it uses Node.js APIs (node:url, node:path) and should only be used in server-side code (Vite plugin context).
Import:
import { definePlugin } from '@u-devtools/kit/define-plugin';Why separate import?
definePluginuses Node.js APIs (node:url,node:path) that cannot be bundled in browser code- By importing from
@u-devtools/kit/define-plugin, you ensure it's only used server-side - The main
@u-devtools/kitpackage is browser-safe and doesn't include Node.js dependencies
Options:
name(string): Plugin nameroot(string): Must passimport.meta.urlfor path resolutionclient(string | null): Relative path to client file (default:'./client')app(string | null): Relative path to app fileserver(string | null): Relative path to server file (default:'./server')useDist(boolean): Force use production paths even in dev mode
defineVueElement(tagName, VueComponent, options)
Registers a Vue component as a Web Component.
Parameters:
tagName(string): Custom element tag name (must contain a hyphen)VueComponent(Component): Vue component from@u-devtools/uioptions(DefineElementOptions): Configuration options
Options:
attributes(string[]): List of HTML attributes to observe and sync with Vue propsemits(string[]): List of Vue events to forward as DOM CustomEvents
defineVueElements(definitions)
Batch registration helper for multiple components.
Parameters:
definitions(Array): Array of{ tagName, component, options }objects
Examples
See the plugins/react-test plugin for a complete React integration example.
