Proxy API
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Proxy API
Section titled “Proxy API”Child apps and web components communicate with the Wippy host through the proxy runtime (proxy.js). Your code never talks to that runtime directly — you import named getters from @wippy-fe/proxy, a thin synchronous facade over it. The same import works for both surfaces:
- Micro Frontend Apps (
view.page) run inside a srcdoc iframe where the host injectsproxy.js. - Web components (
view.component) run as ESM modules in the host page; the host provides@wippy-fe/proxythrough the import map.
For how the runtime is loaded into each context, see Proxy & Isolation.
Initialization
Section titled “Initialization”@wippy-fe/proxy exports synchronous getters — host, api, on, config, state, ws, logger, sanitize, html, loadCss, loadWebComponent, loadByTagName, hostCss, define, classifyLink, installVueWarnSuppressor, addIcons, tailwindConfig. Import what you need and use it directly. There is no getWippyApi, no instance, and no GetConfig/SetConfig handshake to wait on.
The canonical pattern is identical for micro frontend apps and web components:
import { host, api, on, config, state, ws, logger } from '@wippy-fe/proxy'
host.navigate('/dashboard')const agents = await api.get('/api/v1/agents') // api is axios; the await is the HTTP call, not obtaining `api`const off = on('@history', ({ path }) => router.replace(path))const token = config.auth.tokenThese getters are synchronous — host, api, on, config, etc. are available the moment your code runs. The host injects the child config synchronously, before the runtime loads (for both view.page apps and view.component web components), so the runtime initializes before your script executes. You never await to obtain a getter, and there is no GetConfig/SetConfig handshake. The only await you write is for an actual async operation (an HTTP call via api, a state read, etc.).
Mark @wippy-fe/proxy as external in your Vite config — the host provides it through the import map:
export default defineConfig({ build: { rollupOptions: { external: ['@wippy-fe/proxy'], }, },})TypeScript types
Section titled “TypeScript types”The proxy types — AppConfig, ProxyApiInstance, StateApi, ProxyWsApi, and the WebSocket message types — ship as ambient declarations in @wippy-fe/types-global-proxy, not as named exports of any package. Add it to your tsconfig.json types (or use a triple-slash reference) and they are available globally — no import:
{ "compilerOptions": { "types": ["@wippy-fe/types-global-proxy"] } }// AppConfig, ProxyApiInstance, … are ambient globals — annotate with them directly, no import:function render(cfg: AppConfig) { /* … */ }type HostApi = ProxyApiInstance['host'] // HostApi is this indexed type, not a separate exportThere is no import … from '@wippy-fe/shared' for these — @wippy-fe/shared only carries the layout-bus types and the GLOBAL_* name constants.
Internals (do not use)
Section titled “Internals (do not use)”The runtime installs a handful of globals for its own use — window.$W, window.getWippyApi, window.initWippyApi, and the window.__WIPPY_* set. Application and component code must never read or override them. Always go through @wippy-fe/proxy instead. They are listed only so you do not accidentally clobber them — see Proxy & Isolation § Internals.
@wippy-fe/proxy(documented here) is the API your child code uses. The host’s own bootstrap,initWippyApp(config, rootContainer?), mounts the whole Web Host on the module-embed / facade path — child app code never calls it.
Config
Section titled “Config”config
Section titled “config”The child application configuration delivered by the host. It is a plain object (not a function) — imported directly and ready to read synchronously. New docs target only the current wippy-context-2.0 contract.
import { config } from '@wippy-fe/proxy'
const token = config.auth.tokeninterface ChildAppConfig { $schema: 'wippy-context-2.0' auth: { token: string expiresAt: string } env: { APP_API_URL: string APP_AUTH_API_URL: string APP_WEBSOCKET_URL: string [key: string]: string | undefined } axiosDefaults?: Partial<AxiosDefaults> routePrefix?: string apiRoutes?: Record<string, string> theming: { global?: { customCSS?: string cssVariables?: Record<string, string> icons?: Record<string, unknown> iconSets?: Record<string, Record<string, unknown>> } } context: { resourceId: string resourceType: 'page' | 'artifact' route?: string [key: string]: unknown } selfPageId?: string mountRoutes?: Record<string, string>}For dynamic pages, if the host URL is /c/page-id/something/else?foo=1:
config.context?.routecarries/something/else?foo=1.config.pathis a deprecated compatibility field from pre-wippy-context-2.0payloads and should not be used in new code.
Host Control
Section titled “Host Control”The host communication API (HostApi). Imported directly and used synchronously.
import { host } from '@wippy-fe/proxy'host.startChat(agentToken, options?)
Section titled “host.startChat(agentToken, options?)”Opens a new chat session using the provided agent start token.
host.startChat(agentToken: string, options?: { sidebar?: boolean }): void| Parameter | Type | Default | Description |
|---|---|---|---|
agentToken | string | — | Token that identifies which agent to start |
options.sidebar | boolean | false | true opens the chat in the right sidebar panel; false opens in the main area |
host.startChat('my-agent-token') // Main areahost.startChat('my-agent-token', { sidebar: true }) // Right sidebarhost.openSession(sessionId, options?)
Section titled “host.openSession(sessionId, options?)”Opens an existing chat session by UUID.
host.openSession(sessionId: string, options?: { sidebar?: boolean }): voidhost.openSession('abc-123-uuid', { sidebar: false })host.navigate(url)
Section titled “host.navigate(url)”Requests SPA navigation from the host. Supported patterns:
/c/<page-id>— navigate to a dynamic page/c/<page-id>/<sub-path>— dynamic page with sub-path/chat/<session-id>— open a chat session- Any mount route claimed by a page with
mountRoutein its registry entry
host.navigate(url: string): voidhost.navigate('/c/my-page-id')host.navigate('/chat/session-uuid')host.navigate('/keeper')Managed-layout caveat.
startChat,openSession,openArtifact, andnavigatetarget the standard compat shell (the chat view, right panel, and root route). Infe_mode = managedthey still dispatch but have no built-in rendering surface — render chat, artifacts, and sub-routes through declared panels instead. See Multi-Panel Layout § What works in which mode.
host.onRouteChanged(internalRoute, navId?) — MANDATORY for SPA apps
Section titled “host.onRouteChanged(internalRoute, navId?) — MANDATORY for SPA apps”Notifies the host when the page’s internal route changes. The host updates the browser URL bar to include the child’s route. This call is required — without it the host URL stays on the page root and the browser back button does not work for child navigation.
host.onRouteChanged(internalRoute: string, navId?: number): void// Vue Router — call in every afterEachrouter.afterEach((to) => { host.onRouteChanged(to.fullPath)})host.confirm(options) → Promise<boolean>
Section titled “host.confirm(options) → Promise<boolean>”Shows a PrimeVue confirmation dialog. Resolves true if the user accepts, false if they reject or dismiss.
host.confirm(options: LimitedConfirmationOptions): Promise<boolean>const confirmed = await host.confirm({ message: 'Delete this item permanently?', header: 'Confirm Delete', icon: 'tabler:trash', acceptLabel: 'Delete', rejectLabel: 'Cancel', acceptClass: 'p-button-danger',})
if (confirmed) { await api.delete('/api/v1/items/123')}host.toast(options)
Section titled “host.toast(options)”Shows a PrimeVue toast notification.
host.toast(options: ToastMessageOptions): voidseverity | Appearance |
|---|---|
success | Green |
info | Blue |
warn | Yellow |
error | Red |
host.toast({ severity: 'success', summary: 'Saved', detail: 'Your changes have been saved.', life: 3000,})host.openArtifact(artifactUUID, options?)
Section titled “host.openArtifact(artifactUUID, options?)”Opens an artifact in the sidebar or a modal.
host.openArtifact( artifactUUID: string, options?: { target?: 'sidebar' | 'modal' }): voidThe default target is 'sidebar'.
host.openArtifact('artifact-uuid-123', { target: 'modal' })host.setContext(context, sessionUUID?, source?)
Section titled “host.setContext(context, sessionUUID?, source?)”Sends context data to the current chat session. If no session is open yet, the context is queued and applied to the next session opened via startChat or openSession. Optionally scope the context to a specific session UUID or mark it with a source descriptor.
host.setContext( context: Record<string, unknown>, sessionUUID?: string, source?: { type: 'page' | 'artifact', uuid: string, instanceUUID?: string }): voidhost.setContext({ currentPage: 'dashboard', selectedItemIds: [1, 2, 3],})host.classifyLink(url) → LinkClassification
Section titled “host.classifyLink(url) → LinkClassification”Classifies an href as host-nav, child-nav, external, or ignore. Uses mountRoutes and routePrefix from the child config plus baked-in system route segments. Pure function — no side effects.
host.classifyLink(href: string): LinkClassification
interface LinkClassification { kind: 'host-nav' | 'child-nav' | 'external' | 'ignore' href: string normalizedPath?: string targetPageId?: string // set when host-nav matched a specific mountRoute}// Classifier-aware anchor handlerimport { host } from '@wippy-fe/proxy'
document.addEventListener('click', (ev) => { const a = (ev.target as HTMLElement)?.closest('a') if (!a) return const cls = host.classifyLink(a.getAttribute('href') ?? '')
if (cls.kind === 'host-nav') { ev.preventDefault() host.navigate(cls.normalizedPath ?? cls.href) } // child-nav / external / ignore: let existing handlers run})For Vue apps, replace RouterLink from vue-router with RouterLink from @wippy-fe/router — it uses classifyLink internally and is prop-compatible with the real RouterLink.
host.handleError(code, error)
Section titled “host.handleError(code, error)”Reports an error to the host for centralized handling.
host.handleError( code: 'auth-expired' | 'other', error: Record<string, unknown>): void'auth-expired'— triggers the host re-authentication flow'other'— general error; logged and shown to the user if appropriate
try { await api.get('/protected-endpoint')} catch (error) { if ((error as any).response?.status === 401) { host.handleError('auth-expired', error as Record<string, unknown>) } else { host.handleError('other', error as Record<string, unknown>) }}host.logout()
Section titled “host.logout()”Signs the current user out and ends their session.
host.logout(): voidhost.bridge
Section titled “host.bridge”Channel-based parent-child messaging when the page is embedded inside a <w-iframe>. See Proxy & Isolation § Parent-child bridge for the full protocol.
// Fire-and-forget to parenthost.bridge.post(channel: string, payload?: unknown): void
// Request/response (resolves with parent handler's return value)host.bridge.request<T>( channel: string, payload?: unknown, options?: { timeoutMs?: number }): Promise<T>
// Register a handler for incoming messages from parenthost.bridge.on( channel: string, handler: (payload: unknown) => unknown | Promise<unknown>): () => void // returns unsubscribeIf you omit options.timeoutMs, host.bridge.request() defaults to a 10-second (10000 ms) deadline. On timeout the returned promise rejects with an Error whose message is Bridge request <id> timed out after <ms>ms. A request to a channel the parent has no handler for rejects immediately with No handler registered for channel "<channel>" rather than waiting out the deadline.
host.layout
Section titled “host.layout”Access to the managed-layout API. Only available when hostConfig.layout is set (i.e., fe_mode = managed). Outside that context, host.layout.snapshot is null and mutation calls are no-ops.
const layout = host.layout
// Read current snapshotif (layout.snapshot) { console.log(layout.snapshot.activeBreakpoint) // 'default' | 'sm' | ... console.log(layout.snapshot.panels) // panel definition map console.log(layout.snapshot.layouts) // breakpoint-keyed panel trees}
// Subscribe to changes (the fresh snapshot is passed to the handler)import { on } from '@wippy-fe/proxy'
on('@layout-change', (snapshot) => { console.log(snapshot.activeBreakpoint)})
// Mutationslayout.resizePanel('right', '40%')layout.collapsePanel('nav')layout.expandPanel('nav')layout.movePanel('right', { relativeTo: 'main', position: 'after' })layout.removePanel('right')layout.updatePanel('right', { kind: 'page', id: 'chat-panel' }) // {kind,id} replaces content wholesalelayout.updatePanel('right', { props: { artifactId: 'abc-123' } }) // {props} shallow-merges into existing props
layout.addFloating('flap', { kind: 'component', tagName: 'w-right-flap', position: { x: 0, y: 200 }, size: { width: 48, height: 80 }, dismissable: false,})layout.removeFloating('flap')layout.closeModal('confirm-discard')
// In-tab buslayout.broadcast('open-chat', { token: 'abc' }) // 1:N (sender excluded)layout.send('right', 'open-chat', { token: 'abc' }) // 1:1 to named panel
const off = layout.on('open-chat', ({ payload, sourcePanelId, targetPanelId }) => { // handle})off() // unsubscribeFor the full managed-layout model, see Multi-Panel Layout.
A pre-configured axios instance with:
- Base URL from the deployment environment
- Automatic
Authorization: Bearer <token>injection on every request
import { api } from '@wippy-fe/proxy'
const response = await api.get('/api/v1/users')const result = await api.post('/api/v1/items', { name: 'New item' })File upload
Section titled “File upload”import { api, on } from '@wippy-fe/proxy'
const formData = new FormData()formData.append('file', file)
const abort = new AbortController()
const response = await api.post('/api/v1/uploads', formData, { signal: abort.signal, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (evt) => { if (!evt.total) return const pct = Math.round((evt.loaded * 100) / evt.total) uploadProgress.value = pct },})
const uploadedUuid = response.data.uuid // { success: boolean, uuid: string }
// Track processing status via WebSocketon(`upload:${uploadedUuid}`, (msg) => { // msg.data.status: 'uploaded' | 'completed' | 'error' | 'processing'})
// Cancel in-flight uploadabort.abort()Maximum file size: 100 MB.
File download
Section titled “File download”const response = await api.get('/api/v1/uploads/{uuid}/download', { responseType: 'blob',})
const url = URL.createObjectURL(response.data)const a = document.createElement('a')a.href = urla.download = 'filename.pdf'a.click()URL.revokeObjectURL(url)Retrieve upload info
Section titled “Retrieve upload info”// Paginated listconst list = await api.get('/api/v1/uploads/list', { params: { limit: 10, offset: 0 },})// list.data.uploads: Array<{ uuid, mime_type, size, status, meta: { filename } }>
// Single uploadconst upload = await api.get(`/api/v1/uploads/${uuid}`)// upload.data: { uuid, mime_type, size, status, meta: { filename, content_sample? } }SSE streaming
Section titled “SSE streaming”The proxy api supports server-sent event streams via the fetch adapter. Use this for token-by-token LLM completions, long-running progress streams, or any text/event-stream response.
Do not use the browser’s native
EventSource— it cannot attach custom headers and therefore cannot carry the proxy’sAuthorization: Bearertoken.
import { api } from '@wippy-fe/proxy'
const abort = new AbortController()
const response = await api.post('/api/v1/agents/stream', { prompt: 'Hello' }, { adapter: 'fetch', // required — the default xhr adapter buffers the full body responseType: 'stream', headers: { Accept: 'text/event-stream' }, signal: abort.signal,})
const reader = (response.data as ReadableStream<Uint8Array>).getReader()const decoder = new TextDecoder()let buffer = ''
try { while (true) { const { done, value } = await reader.read() if (done) break
buffer += decoder.decode(value, { stream: true })
while (true) { const sep = buffer.indexOf('\n\n') if (sep === -1) break const rawEvent = buffer.slice(0, sep) buffer = buffer.slice(sep + 2)
const dataLines = rawEvent .split('\n') .filter(line => line.startsWith('data:')) .map(line => line.slice(5).trimStart())
if (dataLines.length === 0) continue const payload = dataLines.join('\n') if (payload === '[DONE]') return
try { const evt = JSON.parse(payload) handleEvent(evt) } catch { handleText(payload) } } }} finally { reader.releaseLock()}
// Cancel the streamabort.abort()To default all requests to the fetch adapter:
// In package.json → wippy.configOverrides, or window.__WIPPY_CONFIG_OVERRIDES__{ "axiosDefaults": { "adapter": "fetch" }}Events
Section titled “Events”on(topic, handler) → () => void
Section titled “on(topic, handler) → () => void”on subscribes to events from the host’s WebSocket layer or internal proxy events. Returns an unsubscribe function.
on(topic: string, handler: (event: unknown) => void): () => voidTopics use colon-separated segments. * is a single-segment wildcard. The pattern must have the same number of segments as the topic it matches.
import { on } from '@wippy-fe/proxy'
// Unsubscribe when doneconst unsub = on('session:abc:message:*', (msg) => { console.log(msg.data)})unsub()Every on() call returns an unsubscribe function. Always call it when the component unmounts to prevent leaks. On iframe unload, remaining subscriptions are auto-cleaned, but explicit cleanup is still required for components that mount and unmount within a long-lived iframe.
// Vue Composition APIimport { onUnmounted } from 'vue'
const unsub1 = on('session:*:message:*', handler)const unsub2 = on('artifact:*', handler)
onUnmounted(() => { unsub1() unsub2()})// Vanilla / Web Componentimport { on } from '@wippy-fe/proxy'
class MyEl extends HTMLElement { private unsubs: Array<() => void> = []
connectedCallback() { this.unsubs.push(on('session:*:message:*', handler)) }
disconnectedCallback() { this.unsubs.forEach(fn => fn()) this.unsubs = [] }}Built-in topics
Section titled “Built-in topics”| Topic | Handler payload | Description |
|---|---|---|
@history | { path: string } | Host URL changed (SPA navigation). Fires when the parent pushes a new route. |
@visibility | boolean | Iframe visibility changed. true = visible, false = hidden. |
@message | Full WS message | All WebSocket messages. Internally subscribes to *, *:*, *:*:*, *:*:*:*. |
@state-error | { error: string, key?: string } | State save operation failed (quota exceeded, serialization error). |
@layout-change | LayoutSnapshot | Managed-layout snapshot updated; the fresh snapshot is passed to the handler. Equivalent to reading host.layout.snapshot. |
@layout-breakpoint | { name: string, width: number } | Active managed-layout breakpoint changed; name is the new breakpoint, width its threshold (px). |
Wildcard patterns
Section titled “Wildcard patterns”on('@history', ({ path }) => { /* host URL changed */ })on('@visibility', (visible: boolean) => { /* shown or hidden */ })
// All session messages in a specific sessionon('session:abc-123:message:*', (msg) => { /* ... */ })
// All messages across all sessionson('@message', (msg) => { /* ... */ })
// Topics whose parts contain ':' must be encodedon(`session:${encodeURIComponent('id:with:colons')}:message:*`, handler)Subscribing to the same topic multiple times from the same frame is safe. The proxy deduplicates at the host level. Each on() call still gets its own independent unsubscribe handle.
state — cross-iframe key-value persistence
Section titled “state — cross-iframe key-value persistence”state provides host-mediated storage that survives iframe destruction. State is scoped per page or artifact UUID; each app gets an isolated namespace.
All methods accept an optional { scope?: string } option to override the default scope. Use scope when multiple instances of the same component need separate state buckets.
Scope uniqueness: scope values are passed as-is by the raw
stateAPI and must be globally unique across your application. The@wippy-fe/pinia-persistplugin automatically prefixes custom scopes with@custom:to prevent collisions with system scopes.
import { state } from '@wippy-fe/proxy'
// Write (fire-and-forget; @state-error fires on quota exceeded)await state.set('filters', { search: 'john', status: 'active' })
// Read (returns null if key not found)const filters = await state.get<{ search: string, status: string }>('filters')
// Delete a keyawait state.remove('filters')
// Clear all state for this pageawait state.clear()
// Read all at once (useful for bulk hydration)const all = await state.getAll()
// Custom scopeawait state.set('count', 42, { scope: 'my-widget-instance-1' })const count = await state.get<number>('count', { scope: 'my-widget-instance-1' })Method signatures:
state.get<T = unknown>(key: string, options?: { scope?: string }): Promise<T | null>state.set(key: string, value: unknown, options?: { scope?: string }): Promise<void>state.remove(key: string, options?: { scope?: string }): Promise<void>state.clear(options?: { scope?: string }): Promise<void>state.getAll(options?: { scope?: string }): Promise<Record<string, unknown>>Recommended save pattern — save when the page goes to background rather than on every change:
on('@visibility', async (visible) => { if (!visible) { await state.set('scrollY', document.documentElement.scrollTop) await state.set('formData', currentFormData) }})Limits: 2 MB per page (JSON-serialized, configurable by the host through hostConfig.stateCache). State lives in host memory — survives iframe reload but not a full browser page refresh.
Pinia integration
Section titled “Pinia integration”For Vue apps using Pinia, @wippy-fe/pinia-persist automates persistence:
import { createWippyPersist, preloadWippyState } from '@wippy-fe/pinia-persist'
const preloaded = await preloadWippyState()const pinia = createPinia()pinia.use(createWippyPersist(preloaded))app.use(pinia)Then mark stores:
const useMyStore = defineStore('my-store', () => { const filters = ref({ search: '' }) return { filters }}, { wippyPersist: true, // or: wippyPersist: { pick: ['filters'], debounce: 500 }})WebSocket
Section titled “WebSocket”ws sends commands through the host’s WebSocket connection. Responses arrive via on() topic subscriptions.
ws.send(command)
Section titled “ws.send(command)”Fire-and-forget. No response delivery — subscribe to the relevant topic first.
ws.send(command: WsCommand): voidimport { ws, on } from '@wippy-fe/proxy'
on('session:my-session:message:*', (msg) => { console.log('Response:', msg.data)})
ws.send({ type: 'session_message', session_id: 'my-session', message_id: crypto.randomUUID(), data: { text: 'Hello from child app' },})ws.sendWithResponse(command) → Promise<WsMessage>
Section titled “ws.sendWithResponse(command) → Promise<WsMessage>”Sends a command and waits for the matching server response. Times out after 30 seconds.
ws.sendWithResponse(command: WsCommand): Promise<WsMessage>const response = await ws.sendWithResponse({ type: 'session_open', start_token: 'my-token',})console.log('Session opened:', response.data)ws.sendCommand(sessionId, data)
Section titled “ws.sendCommand(sessionId, data)”Convenience wrapper for session control commands.
ws.sendCommand(sessionId: string, data: { command: string, [key: string]: unknown }): voidws.sendCommand('session-uuid', { command: 'stop' })ws.sendCommand('session-uuid', { command: 'model', name: 'gpt-4' })ws.sendCommand('session-uuid', { command: 'agent', name: 'my-agent' })Logger
Section titled “Logger”logger
Section titled “logger”Structured logging that traverses iframe boundaries. Logs flow child → host → parent website where transports (Sentry, Graylog, console) process them. Each child’s context (resourceId, resourceType, nesting depth) is automatically attached to every log entry.
Use logger instead of console.log/error for anything you want to appear in production monitoring.
import { logger } from '@wippy-fe/proxy'
logger.debug('Component mounted', { pageId: 'abc' })logger.info('User loaded page', { pageId: 'abc' })logger.warn('Slow API response', { ms: 3200 })logger.error('Failed to save', { endpoint: '/api/save' })logger.captureException(error, context?)
Section titled “logger.captureException(error, context?)”Captures and forwards an exception. Unhandled errors (window.onerror, unhandledrejection) are automatically captured when ProxyConfig.injections.errorCapture is true.
try { await riskyOperation()} catch (error) { logger.captureException(error, { operation: 'riskyOperation' })}Breadcrumbs and context
Section titled “Breadcrumbs and context”// Breadcrumbs attach to the next exception for debugging contextlogger.addBreadcrumb({ category: 'navigation', message: 'Navigated to /settings' })logger.addBreadcrumb({ category: 'ui', message: 'Clicked Save button' })
// Persistent context — attached to all subsequent logs from this childlogger.setContext('user', { id: 'user-123', role: 'admin' })
// Tags — key/value pairs for filtering and searchlogger.setTag('version', '1.2.0')logger.setTag('feature', 'dashboard')Web Components
Section titled “Web Components”loadByTagName(tagName, options?) → Promise<void>
Section titled “loadByTagName(tagName, options?) → Promise<void>”Loads and registers a peer web component by its HTML tag name. Resolves after customElements.define fires — it is safe to document.createElement(tagName) immediately after. The tag is added to the sanitize allowlist automatically on success.
import { loadByTagName } from '@wippy-fe/proxy'
await loadByTagName('wc-thread-picker')await loadByTagName('wc-slow-pkg', { timeoutMs: 60_000 })
// Safe to use immediatelydocument.body.appendChild(document.createElement('wc-thread-picker'))options.timeoutMs overrides the default 30-second deadline for waiting on customElements.define after the script is appended. Surfaces stuck or broken components (404, parse error, missing define call) as a rejection rather than an indefinite hang.
loadWebComponent(componentId, tagName?) → Promise<void>
Section titled “loadWebComponent(componentId, tagName?) → Promise<void>”Loads a web component by its Wippy registry artifact id rather than its tag name. Useful when you have a registry id from a config value or backend response.
import { loadWebComponent } from '@wippy-fe/proxy'
await loadWebComponent('wippy.components:my-chart')DOM-scan loader (<script type="wippy-components-loader">)
Section titled “DOM-scan loader (<script type="wippy-components-loader">)”For pages that need multiple components, the proxy scans for these script tags on init and loads each entry through loadWebComponent:
<script type="wippy-components-loader">{ "wc-foo": "wippy.components:foo", "wc-bar": "wippy.components:bar" }</script>Same deduplication and allowlist auto-update behavior as loadByTagName.
Utilities
Section titled “Utilities”sanitize(html, options?) → string
Section titled “sanitize(html, options?) → string”Default-allowlisted HTML sanitizer scoped to the current proxy context. Combines the chat-rendering defaults (<p>, <a>, <code>, <table>, etc.) with every web component tag currently registered in this runtime.
import { sanitize, loadByTagName } from '@wippy-fe/proxy'
const safe = sanitize('<p>hi</p><script>alert(1)</script>')// → '<p>hi</p>'
// After loadByTagName, the tag is automatically allowed:await loadByTagName('wc-thread-picker')sanitize('<wc-thread-picker thread-id="42"></wc-thread-picker>')// → '<wc-thread-picker thread-id="42"></wc-thread-picker>'
// One-off extra tagssanitize(dialogBody, { extraTags: { 'iconify-icon': ['icon'] } })sanitize re-reads the tag allowlist on every call, so tags registered after import are still picked up.
html.inject(sourceHtml, options) → Promise<string>
Section titled “html.inject(sourceHtml, options) → Promise<string>”Applies the source-HTML-to-srcdoc transform without mounting an element. Prefer <w-iframe> for normal use; use this only when building custom hosting infrastructure.
import { html } from '@wippy-fe/proxy'
const processed = await html.inject(sourceHtml, { baseUrl: 'https://example.com/app/', resourceId: 'child-id', resourceType: 'page', route: '/initial',})Config Overrides
Section titled “Config Overrides”Pages can override selected child-facing config fields per page without a separate deployment. The override shape still uses customization for compatibility, and the host projects those values into the current child theming.global result before the page receives wippy-context-2.0 config.
Setting overrides
Section titled “Setting overrides”Registry pages (recommended): Set meta.config_overrides in the page’s _index.yaml. The host includes it in the content API response and injects it automatically.
Standalone packages: Set wippy.configOverrides in the page’s package.json.
Manual / testing: Set window.__WIPPY_CONFIG_OVERRIDES__ in a <script> tag that runs before proxy.js.
window.__WIPPY_CONFIG_OVERRIDES__ = { customization: { cssVariables: { '--p-primary': '#ff6b00', }, customCSS: '.my-page-header { border-radius: 12px; }', },}Merge rules
Section titled “Merge rules”| Field | Merge behavior |
|---|---|
cssVariables | Replaces the host’s values — the page provides its own theme |
customCSS | Replaces the host’s value |
iconSets | Merged additively |
axiosDefaults | Deep merged |
routePrefix | Replaced |
apiRoutes | Deep merged |
Every nested child the page embeds — <w-iframe>, <w-artifact>, and html.inject content — is built from the page’s already-merged config and inherits it automatically, recursively down the sub-tree. So a page’s overrides (especially theming) propagate to everything beneath it, not just to the page itself.
Vue Utilities
Section titled “Vue Utilities”installVueWarnSuppressor(app)
Section titled “installVueWarnSuppressor(app)”Available from @wippy-fe/proxy 0.0.33. Silences [Vue warn]: Failed to resolve component: foo-bar for tags registered via customElements.define(...) rather than app.component(...). Vue’s template compiler emits these warnings for web component tags it does not recognize — the elements render correctly, but the console fills with noise.
import { installVueWarnSuppressor } from '@wippy-fe/proxy'import { createApp } from 'vue'import App from './App.vue'
const app = createApp(App)installVueWarnSuppressor(app)app.use(router)app.mount('#app')What it suppresses:
- Tags already registered via
customElements.define(...)— system tags (w-iframe,w-artifact,wippy-loading,wippy-error) and every tag registered by the autoload pipeline (loadByTagName, scanner). - Tags matching the custom element naming shape (
^[a-z][a-z0-9]*-[a-z0-9-]*$) that are not yet registered — covers the race window where Vue renders before the autoload script lands.
What still warns:
- PascalCase component typos (
<UsreCard />). The suppressor does not match these against the kebab pattern andcustomElements.getreturnsundefined, so they pass through to the console — preserving the signal that distinguishes real bugs from noise.
The function is idempotent: a second call on the same app is a true no-op. A Symbol.for('@wippy-fe/proxy/vue-warn-suppressor-installed') marker is planted on app.config; the marker is exported as VUE_WARN_SUPPRESSOR_INSTALLED_MARKER for test setups that need to clear it across reloads.
If a warnHandler was already installed, it is preserved as previous and called for warnings the suppressor does not silence.
createAppRouter(routes, options?) from @wippy-fe/router
Section titled “createAppRouter(routes, options?) from @wippy-fe/router”Canonical memory-router factory for srcdoc subapps. Replaces the boilerplate every subapp currently duplicates (memory history, afterEach route sync to host, @history subscription):
import { createAppRouter } from '@wippy-fe/router'import { config } from '@wippy-fe/proxy'import { routes } from './routes'
const router = createAppRouter(routes, { initialPath: config.context?.route,})app.use(router)Loading and Error Components
Section titled “Loading and Error Components”Two web components are auto-registered via loading.js (injected before proxy.js). No imports or manual registration needed.
<wippy-loading>
Section titled “<wippy-loading>”Fullscreen loading spinner with theme-aware colors.
| Attribute | Description |
|---|---|
title | Main text (e.g., “Loading…”) |
subtitle | Secondary text |
no-bg | Boolean — transparent background for overlay use |
<wippy-loading title="Loading..." subtitle="Please wait"></wippy-loading><wippy-loading no-bg title="Loading page content..."></wippy-loading><wippy-error>
Section titled “<wippy-error>”Fullscreen error display with severity-based coloring.
| Attribute | Values | Default |
|---|---|---|
title | Any string | ”Something went wrong” |
message | Any string | (empty) |
icon | circle, triangle, sad | circle |
severity | danger, warning | danger |
no-bg | Boolean | (absent) |
<wippy-error title="Failed to load" message="Server returned 500" severity="danger"></wippy-error><wippy-error title="Connection Lost" message="Retrying..." icon="triangle" severity="warning"></wippy-error>Both components use Shadow DOM with CSS variables from @wippy-fe/theme and include hardcoded fallbacks for pre-theme contexts.
Recommended pattern for vanilla HTML pages:
<body> <wippy-loading id="loader" title="Loading..."></wippy-loading> <div id="content" style="display:none"><!-- content --></div>
<script type="module"> import { api, host } from '@wippy-fe/proxy'
async function init() { try { // fetch data, set up page... document.getElementById('loader').remove() document.getElementById('content').style.display = 'block' } catch (error) { const errorEl = document.createElement('wippy-error') errorEl.setAttribute('title', 'Initialization failed') errorEl.setAttribute('message', error.message) document.getElementById('loader').replaceWith(errorEl) } } init() </script></body>Vue 3 — app.html entry:
<div id="app"> <wippy-loading title="Loading..."></wippy-loading></div><script type="module" src="./src/app.ts"></script>When Vue mounts into #app it replaces the <wippy-loading> element automatically.