Wippy FE Compliance Checklist
A single, exhaustive checklist for shipping Wippy child apps (view.page) and web components (view.component) — covering YAML registration, FE source, build pipeline, theming, proxy API, router/host integration, and host-less mode. Every rule cites a source (canonical doc, host contract, gold-standard reference, or real-world incident) and most have a copy-paste verification command.
This doc supersedes the older app-checklist.md for new work. The older checklist remains valid for the micro-frontend-app subset; this one extends it with web components, host-less mode, real-world fixes, and verification recipes.
Reference templates (must be validated against this checklist):
- Micro Frontend App:
app-template/frontend/applications/main/ - Web component:
app-template/frontend/web-components/mermaid/
Templates are examples, not authority. When a template disagrees with this contract, fix or mark the template; do not weaken the contract to make stale example code pass. See §14.
Conventions
- MUST — REJECT the module if not satisfied
- SHOULD — WARN; document the deviation if you keep it
- MAY — informational, no compliance gate
- VERIFY — copy-paste shell command that confirms the rule
Source references use:
docs:<filename>:<line>— canonical FE docsgold:<path>— a file in the gold-standard app-template repo,wippyai/app. Path aliases:gold:main/…=frontend/applications/main/…;gold:mermaid/…=frontend/web-components/mermaid/…. Any othergold:path is repo-root-relative (e.g.gold:src/app/views/_index.yaml)kb:<topic>— Wippy KBincident:<id>— bug found during a real audit
Table of contents
- The FE isolation paradigm — read this first
- Decide what you're shipping
- YAML registration (host contract)
- Micro Frontend Apps — manifest, build, runtime
- Web components — manifest, build, runtime
- Theming
- Proxy API & subscriptions
- Router & host integration
- Build pipeline & Makefile
- Host-less mode
- Verification recipes
- Acceptance criteria (REJECT rules)
- Known intentional deviations
- Tooling gotchas
- Template validation policy
- Appendix A — Window globals & DOM markers
- Appendix B — HostApi method signatures
- Appendix C — ProxyConfig.injections reference
0. The FE isolation paradigm
Cite as: "check FE isolation paradigm is followed".
A Wippy FE module (view.page or view.component) is a standalone, universal build artifact that has ZERO knowledge of where or how it is served. The _index.yaml registry entries on the BE side are the serving facade — they declare, for THIS Wippy deployment, where the bundle is mounted (meta.url), how it's reached (meta.entry_point / meta.base_path), and where the bytes come from (fs.directory + http.static, or any other source: fs.embed, in-memory FS, upstream proxy, DB-backed FS).
┌──────────────────────┐ built once, served anywhere ┌──────────────────────┐
│ FE module (universal)│ ──────────────────────────────► │ _index.yaml (per-BE) │
│ │ │ │
│ - vite base: '' │ │ - meta.url │
│ - relative imports │ │ - meta.entry_point │
│ - package.json: │ │ - meta.tag_name │
│ tag, props, events │ │ - fs.directory OR │
│ - NO mount URL │ │ fs.embed OR any │
│ - NO peer-module URL │ │ filesystem source │
└──────────────────────┘ └──────────────────────┘
▲ │
│ built bundle (index.js, sourcemaps, optional │
│ static assets) lands wherever the BE expects ──────┘
│ (configurable via Makefile / build script's `--outDir`)
What the FE module MUST NOT know
- The URL prefix where it will be mounted on this deployment.
- Whether it's served from a filesystem, in-memory FS, CDN, or remote registry.
- Which other modules are co-located on the same Wippy instance, or at what URLs they live.
- Whether the BE uses one centralized
fs.directoryor one per module.
What the YAML facade decides (and can override per deployment)
meta.url— the URL prefix at which a router serving this bundle is mounted. Not a physical path. May be backed by/static/..., an in-memory FS, or a remote server.meta.entry_point— the URL relative to the bundle root pointing at the entry (app.htmlfor pages,index.jsfor components).meta.tag_name(WCs only) — the custom-element tag the bundle registers itself as.fs.directory+http.static(or any equivalent) — where the bytes come from on this deployment. Single-shared-FS, per-module-FS, embedded-FS — all valid, FE doesn't care.
Corollaries (the rules this paradigm produces)
Bundle portability:
vite.config.tsMUST havebase: ''so all asset URLs in the build are relative. Hardcodingbase: '/app/keeper/'is REJECT — it bakes a deployment-specific URL prefix into a universal artifact. See §3.4 and §9.5.
No cross-module URL hardcoding:
- A FE module MUST NEVER
import('/components/x/dist/index.js')orfetch('/wc/x/...')to reach another WC's physical URL. REJECT. Such a URL is a presumption about THIS deployment's serving layout — it breaks the moment the BE relocates the peer module, or runs it from an embedded FS. Consume another WC via the proxy / host pipeline instead:- Registry-declared WCs (the common case) — if the peer is
auto_register: true+announced: truein itsview.componententry, the host eagerly registers it in every iframe at boot. Consumer code just doesawait customElements.whenDefined('wc-x')and then uses the tag. No URL knowledge required. - Artifact-based / dynamically-loaded WCs — call
loadWebComponent(componentId, tagName?)from@wippy-fe/proxy.componentIdis the artifact UUID (the host fetches the artifact's package.json via the artifact API, validates it as a Wippy package, then dynamic-imports it). This is the path for user-generated / AI-generated WCs delivered as artifacts.
- Registry-declared WCs (the common case) — if the peer is
Build script owns the output path, not vite.config:
- The deployment's build script (Makefile / make.ps1 / npm script) passes
--outDir <wherever-the-BE-expects>per module. Thevite.config.tsdoes NOT hardcodeoutDir.
package.json describes the module, not its mount point:
- Carry
wippy.tagName,wippy.type,wippy.props,wippy.events. Do NOT carrywippy.url,wippy.host,wippy.mountPath— those are deployment-specific and belong in the BE's_index.yamlmeta.*. - Source-of-truth precedence: the registry entry always wins over
package.json.meta.tag_name,meta.props,meta.eventson theview.componententry override the correspondingwippy.tagName/wippy.props/wippy.eventsinpackage.jsonfor THIS deployment.
Re-mounting under a different URL must work without a rebuild:
- Because
base: '', the bundle's relative imports resolve against whatever<base>the host injects frommeta.url+meta.base_pathat serve time. Test in dev by changingmeta.url, reloading — no rebuild needed. If it breaks, the paradigm is being violated somewhere.
Audit checklist — "is FE isolation paradigm followed?"
-
vite.config.tshasbase: ''(and not'/something/'). -
vite.config.tsdoes NOT hardcodeoutDir— the build script passes--outDirper deployment. -
package.jsoncarrieswippy.tagName/wippy.type/ props / events. It does NOT carry any deployment URL or mount path. - Source tree has zero hardcoded references to peer modules' physical URLs.
- If the module consumes another WC, it does so via
await customElements.whenDefined('wc-x')(for registry-declared peers withauto_register: true+announced: true) orloadWebComponent(artifactUUID)(for artifact-delivered peers) — never by hardcoding a URL. - No
import.meta.url-based path math that presumes a specific serving layout. The patterndefine(import.meta.url, …)is fine;import.meta.url.split('/components/')[1]is not.
1. Decide what you're shipping
| Question | Use |
|---|---|
Will it be loaded in its own iframe with its own app.html? |
view.page |
Will it be inserted as a <custom-tag> inside another page's DOM? |
view.component |
| Both? | ship two registry entries — one for each role |
Runtime difference (kb:view-page-vs-component):
view.page→ host loads it in an iframe with its ownapp.html. Code runs in iframe document.view.component→ host registers a custom tag and inserts the element directly in the host DOM (typically with shadow DOM). Code runs in the same document as the host.
view.page does not imply nav presence. A view.page entry with meta.announced: false is reachable via:
- a backend
mountRoutesomeone navigates to, - direct
host.openSession/host.openArtifactinvocation, - being used inside a managed-layout panel,
- being loaded inside a
<w-artifact>element by another app.
A "page in iframe with no nav-owner" — common in artifact viewers, embedded demos, sub-tools — is an explicit and supported pattern. Set announced: false to keep it out of nav while leaving it routable.
Conversely, a view.component entry with auto_register: true and announced: true will appear in tag-explorer registries; with announced: false it's an internal building block.
For view.component entries, announced: true is a HARD requirement to participate in the host's autoload. The /api/public/components/list endpoint filters server-side by announced == true; auto_register: true alone is not enough to make the host inject the WC's <script type="module"> tag at boot. Symptom of getting this wrong: customElements.get('your-tag') stays undefined, Vue silently renders an empty <your-tag></your-tag> with no shadow root content, no console error.
2. YAML registration (host contract)
A registry.entry is what the host actually reads at navigation/render time. The wippy.* block in your package.json is the host-less mirror. YAML is canonical; package.json is for embedded fallback.
2.1 view.page meta fields
- name: main
kind: registry.entry
meta:
type: view.page
name: main
title: Admin Panel
icon: tabler:layout-dashboard
order: 0
announced: true
secure: false
url: /app
base_path: app/main
entry_point: app.html
mountRoute: /home/:part(.*)*
proxy:
enabled: true
injections:
css:
themeConfig: true
iframe: true
primevue: true
customCss: true
customVariables: true
tailwindConfig: true
iconifyIcons: true
Field-by-field (gold:src/app/views/_index.yaml, kb:view-page-fields):
| Field | Required | Purpose |
|---|---|---|
meta.type |
MUST | "view.page" literal |
meta.name |
MUST | Page id (combines with namespace into <ns>:<name>, e.g. app.views:main) |
meta.title |
MUST | Human-readable title (used in nav and page tabs) |
meta.icon |
SHOULD | Iconify icon code (e.g. tabler:home); only relevant if announced: true |
meta.url |
MUST | URL prefix at which the FS+http.router serving this bundle is mounted. Not a physical path. |
meta.base_path |
MUST | URL path appended to url to reach the bundle root. Combined with url, becomes the HTML <base> injected into entry_point, so relative module imports inside app.html resolve against the bundle root. |
meta.entry_point |
MUST | URL path relative to the bundle root pointing at the entry HTML file. e.g. app.html (most common). |
meta.mountRoute |
MAY | Current compatibility spelling for a backend casing bug; the planned backend field is meta.mount_route. Use mountRoute until that correction ships. |
meta.secure |
SHOULD | Default false. Set true to enforce auth. |
meta.announced |
SHOULD | Default false. Set true to appear in nav. Pages without nav-owner keep false. |
meta.hidden |
MAY | Soft-hide from announced nav. |
meta.order |
MAY | Sort position in nav (when announced). |
meta.group / meta.group_icon / meta.group_order |
MAY | Nav grouping. |
meta.config_overrides |
MAY | Per-page + sub-tree override. The facade owns top-level backend css_variables / custom_css parameters for the app shell. Under config_overrides, nested keys retain their defined frontend casing, such as customization.customCSS. |
meta.proxy |
SHOULD | Per-entry proxy injection config nested under meta. Registry fields follow their documented schema; nested injections keys retain lower camelCase. The host deep-merges the YAML over bundled wippy.proxy. |
VERIFY at runtime that the host registry recognizes your entry:
curl -fsS http://<host>/api/public/pages/list | jq '.pages[] | select(.id=="<namespace>:<name>")'
2.2 view.component meta fields
- name: mermaid
kind: registry.entry
meta:
type: view.component
name: mermaid
title: Mermaid Diagram
tag_name: example-mermaid
entry_point: index.js
announced: true
secure: false
auto_register: true
url: /app/wc/mermaid
props:
type: object
properties:
definition:
type: string
default: ""
description: Mermaid diagram definition string
transparent:
type: boolean
default: true
description: Whether the diagram background is transparent
Field-by-field (gold:src/app/views/_index.yaml, kb:view-component-fields):
| Field | Required | Purpose |
|---|---|---|
meta.type |
MUST | "view.component" literal |
meta.name |
MUST | Component logical name |
meta.tag_name |
MUST | Custom-element tag. Must contain a hyphen (Custom Elements spec). |
meta.entry_point |
MUST | URL relative to bundle root pointing at the entry JS file (e.g. index.js). |
meta.url |
MUST | URL prefix where the bundle is served. Same semantics as for view.page. |
meta.base_path |
MAY | Supported for components too; composes the same way (<url>/<base_path>/<entry_point>). Current app-template WC entries omit it, so they are served as meta.url + meta.entry_point. |
meta.announced |
MUST per kb:view-component-fields |
Default false (falls back to meta.public). MUST be true for the component to appear in /api/public/components/list. |
meta.secure |
MUST per kb | Default false. |
meta.auto_register |
MUST per kb | Default false. MUST be true for the host to autoload it; leave/set false for lazy-loaded WCs. |
meta.props |
SHOULD | JSON Schema mirroring wippy.props from package.json. Each property MUST have type, default, description. |
meta.events |
MAY | JSON Schema mirroring wippy.events. Omit if the WC has no custom events. |
meta.description |
SHOULD | Verbose AI/human-readable usage explanation. Not a one-line label — a paragraph or two that explains: what the WC renders, the intended call shape, what input forms are supported, what edge cases or fallbacks exist, and notable performance characteristics. |
meta.title, meta.icon |
MAY | For browse/registry UIs. |
No proxy: block for WCs — they run in the host's document, not in their own iframe, so proxy injections don't apply.
Hyphenated prop names (e.g. allow-multiple) are camelCased in Vue (allowMultiple). Non-string props are JSON-encoded in attributes; the WC must JSON.parse them or use WippyVueElement/WippyElement (which handle this automatically).
2.3 Backend config_overrides and frontend AppConfig shape
The top-level backend field is config_overrides; its nested configuration
object is passed through and retains the defined lower-camel-case keys:
config_overrides:
customization:
cssVariables: {}
customCSS: ""
iconSets: {}
axiosDefaults: {}
routePrefix: /admin
apiRoutes: {}
The host projects that backend data into the lower-camel-case frontend shape:
interface AppConfigOverrides {
customization?: Partial<AppCustomization> // compatibility-shaped override; projected into theming.global for children
axiosDefaults?: Partial<AxiosDefaults> // MERGED into config.axiosDefaults
routePrefix?: string // REPLACES config.routePrefix
apiRoutes?: ApiRoutesOverride // REPLACES config.apiRoutes
}
Frontend customization merge semantics (mergeChildCustomization):
cssVariables→ REPLACE (override map fully replaces parent map)customCSS→ REPLACE (new string replaces parent; not concatenated)icons→ MERGE shallow (additive)iconSets→ MERGE per-prefix (additive)
Isolation depends on the field. The projected overrides are not uniformly "isolation-only"; behaviour is field-specific:
cssVariables/customCSS(REPLACE) → the page's theme replaces the inherited one and then propagates to everything the page embeds (it is merged into the page'stheming.global, which all nested children inherit). Use it to theme a sub-tree, not just one iframe: a module shipping pages with their own palette (e.g. an admin UI whose theme cascades to its artifacts/sub-apps), demo pages with divergent themes, artifact viewers with a fixed brand, debug pages on alternate API routes.icons/iconSets(MERGE) → additive, NON-isolating. Adding icons via backendconfig_overrides.customization.iconsoriconSetsaugments the child-projected icon set without isolating the iframe. Frontend code reads the result fromconfig.theming.global.icons/iconSets.
2.4 The meta.proxy: entry-level block (page only)
For view.page entries, the registry entry's proxy: block nested under
meta: configures host-side proxy injection per page. The top-level registry
field is proxy; nested injections keys retain their defined lower-camel-case
shape, matching frontend package.json and runtime AppConfig. The host
deep-merges the YAML over bundled wippy.proxy.
meta:
type: view.page
# ...
proxy:
enabled: true
injections:
css:
themeConfig: true
iframe: true
primevue: true
customCss: true
customVariables: true
tailwindConfig: true
iconifyIcons: true
All injection flags are technically MAY — the host has sane defaults — but micro frontend apps SHOULD declare them explicitly to avoid invisible drift.
3. Micro Frontend Apps — manifest, build, runtime
3.1 package.json
Reference: gold:main/package.json.
{
"name": "@wippy/app-main",
"version": "1.0.0",
"specification": "wippy-component-1.0",
"title": "Wippy App",
"description": "...",
"files": ["dist/", "src/", "package.json"],
"browser": "dist/app.js",
"wippy": {
"type": "page",
"title": "Wippy App",
"icon": "tabler:home",
"path": "dist/app.html",
"proxy": {
"enabled": true,
"injections": {
"css": {
"themeConfig": true,
"iframe": true,
"primevue": true,
"markdown": true,
"customCss": true,
"customVariables": true
},
"tailwindConfig": false,
"resizeObserver": false,
"preventLinkClicks": false,
"iconifyIcons": false,
"errorCapture": true
}
},
"scripts": {
"build": "build",
"debug": "build:debug",
"test": "lint"
}
},
"scripts": {
"build": "vite build",
"build:debug": "vite build --mode development",
"dev": "vite build --watch",
"type-check": "vue-tsc --build --force",
"lint": "eslint src --ext .ts,.vue",
"lint:fix": "eslint src --ext .ts,.vue --fix"
},
"dependencies": {
"@wippy-fe/theme": "^0.0.46"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"@wippy-fe/types-global-proxy": "^0.0.46",
"@wippy-fe/vite-plugin": "^0.0.46",
"autoprefixer": "^10.4.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.0.0",
"postcss": "^8.4.0",
"primevue": "^4.3.3",
"tailwindcss": "3",
"typescript": "^5.0.0",
"vite": "^6.0.0",
"vue": "^3.5.0",
"vue-eslint-parser": "^9.4.3",
"vue-router": "^4.6.4",
"vue-tsc": "^2.0.0"
},
"peerDependencies": {
"@iconify/vue": "^5.0.0",
"@wippy-fe/pinia-persist": "^0.0.46",
"@wippy-fe/proxy": "^0.0.46",
"@wippy-fe/router": "^0.0.46",
"axios": "^1.0.0",
"luxon": "^3.5.0",
"pinia": "^2.1.0",
"primevue": "^4.3.3",
"vue": "^3.5.0",
"vue-router": "^4.0.0"
}
}
Specification — docs:app-checklist.md:39, gold:main/package.json:4:
- MUST set
"specification": "wippy-component-1.0". - MUST set package
nameto@<org>/app-<short>(e.g.@wippy/app-main). - MUST set top-level
"title"(matcheswippy.title).
wippy block:
- MUST set
wippy.type: "page". - MUST set
wippy.title(typically equals top-leveltitle). - SHOULD set
wippy.iconto an Iconify code (only relevant if the page isannounced: true). - MUST set
wippy.pathto the built HTML entry inside the artifact the registry actually serves. dist/app.htmlis only the default local Vite example.
wippy.scripts map — entry → npm script binding:
- MUST set
wippy.scripts.build(typically"build"). - MAY set
wippy.scripts.debug(typically"build:debug"). - MAY set
wippy.scripts.test(typically"lint"or"test").
wippy.proxy.injections — full reference: Appendix C.
All flags are technically MAY (host has defaults). The recommended set for a typical micro frontend app is:
| Flag | Recommended | When to set otherwise |
|---|---|---|
proxy.enabled |
true |
false for pages with no proxy needs (rare) |
injections.css.themeConfig |
true |
false only if you don't use Wippy theming |
injections.css.iframe |
true |
Never disable for a view.page; this historically named asset supplies default themed scrollbar consistency |
injections.css.primevue |
true for product UI |
false only while the artifact has no standard control or surface that PrimeVue provides |
injections.css.markdown |
true if app renders any markdown |
false if you have no markdown anywhere |
injections.css.customCss |
true |
false if you don't read child-projected theming.global.customCSS |
injections.css.customVariables |
true |
false if you don't read child-projected theming.global.cssVariables |
injections.tailwindConfig |
false |
true if using Tailwind Play CDN runtime |
injections.resizeObserver |
false |
true for widget-style pages needing reported size |
injections.preventLinkClicks |
false |
true if you don't have your own router |
injections.iconifyIcons |
false |
true if using CDN Iconify |
injections.errorCapture |
true |
false if you handle errors fully internally |
injections.refreshWhenVisible |
false (or omit) |
true for pages that need stale-data refresh |
injections.historyPolyfill |
true (or omit) |
leave on; host installs always-stub |
injections.css.iframe is mandatory for every view.page, even if the page
does not currently overflow. This preserves scrollbar consistency when content
or browser behavior later introduces scrolling.
Dependency hygiene:
dependencies: only what's bundled into the page (e.g.@wippy-fe/theme, app-specific libs likechart.js).devDependencies: build toolchain (vite,vue-tsc,typescript,@vitejs/plugin-vue,eslint*,tailwindcss@3,postcss,autoprefixer,primevuefor build-time,vuefor build-time,vue-routerif you build-time-import it,@wippy-fe/types-global-proxy,@wippy-fe/vite-plugin).- Rollup externals: every key in the
importsobject fetched from the target Web Host release, including keys the artifact does not currently import. Bundle an imported exact specifier only when it is absent from that map. peerDependencies: package roots the artifact actually imports and expects the host to provide. They are not a copy of the full external list because import-map subpaths are not independent npm peer packages.- Adding to
peerDependenciesdoes NOT bundle the package — it is a subscription to a verified target-host import-map entry.
Version alignment: use one coherent @wippy-fe/* package family compatible with the target host release (the current published family is 0.0.46). Ecosystem mismatch causes silent ABI drift.
VERIFY required wippy fields:
node -e 'const p=require("./package.json");const m=["specification","title"].filter(k=>!p[k]).concat(p.wippy?[]:["wippy"]);if(m.length)throw new Error("missing: "+m.join(","));console.log("OK")'
3.2 vite.config.ts
Reference: gold:main/vite.config.ts.
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import vue from '@vitejs/plugin-vue'
import { wippyPagePlugin } from '@wippy-fe/vite-plugin'
import { defineConfig } from 'vite'
const hostImportMap = JSON.parse(
readFileSync(new URL('./import-map.json', import.meta.url), 'utf8'),
)
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('example-'),
},
},
}),
wippyPagePlugin(),
],
base: '',
resolve: {
alias: { '@': resolve(__dirname, './src') },
},
build: {
target: 'esnext',
cssCodeSplit: false,
sourcemap: true,
rollupOptions: {
input: { app: resolve(__dirname, 'app.html') },
external: Object.keys(hostImportMap.imports),
output: {
entryFileNames: '[name].js',
assetFileNames: '[name]-[hash][extname]',
},
},
},
})
Rules:
- MUST set
base: ''(relative paths; bundle is portable to any URL prefix). A hardcoded absolute base (e.g./app/keeper/) is REJECT — see §9.5. - MUST include
vue()plugin. - SHOULD include
wippyPagePlugin()from@wippy-fe/vite-plugin(default; opt out only if your team explicitly does NOT want host-less mode support — rare). - SHOULD pass
template.compilerOptions.isCustomElementif your templates render custom-element tags. - MUST set
build.target: 'esnext'. - MAY set
build.cssCodeSplit: falseto inline all CSS into a single bundle. - MAY set
build.sourcemap: truefor production. - MUST set
build.rollupOptions.inputto yourapp.html. - MUST list every key in the fetched target-host import map, not only currently imported specifiers. Bundle a used exact specifier when it is absent.
- MUST NOT set
build.assetsInlineLimitto a large value (incident:1I); leave at the 4 KB default. - MUST NOT force
define: { 'process.env.NODE_ENV': '"production"' }(incident:7C); it overrides--mode development.
VERIFY base + canonical externals + plugin:
grep -E "base:\s*['\"]" path/to/vite.config.ts # must show: base: ''
grep -F "Object.keys(hostImportMap.imports)" path/to/vite.config.ts
grep -E "wippyPagePlugin" path/to/vite.config.ts # SHOULD be present
3.3 app.html
Reference: gold:main/app.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wippy</title>
<!-- Verified Web Host webcomponents-1.0.44 example. Re-fetch for another tag. -->
<script type="importmap">
{
"imports": {
"vue": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/vue.js",
"pinia": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/pinia.js",
"vue-router": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/vue-router.js",
"axios": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/axios.js",
"nanoevents": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/nanoevents.js",
"luxon": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/luxon.js",
"@iconify/vue": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/@iconify---vue.js",
"iconify-icon": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/iconify-icon.js",
"@tanstack/vue-query": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/@tanstack---vue-query.js",
"sanitize-html": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/sanitize-html.js",
"markdown-it": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/markdown-it.js",
"markdown-it-async": "https://web-host.wippy.ai/webcomponents-1.0.44/@vendor/markdown-it-async.js",
"@wippy-fe/proxy": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/proxy.js",
"@wippy-fe/markdown-iframe": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/markdown-iframe.js",
"@wippy-fe/log": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log.js",
"@wippy-fe/log/default-receiver": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log/default-receiver.js",
"@wippy-fe/log/logger": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log/logger.js",
"@wippy-fe/log/sentry-transport": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log/sentry-transport.js",
"@wippy-fe/log/console-transport": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log/console-transport.js",
"@wippy-fe/log/gelf-transport": "https://web-host.wippy.ai/webcomponents-1.0.44/@wippy-fe/log/gelf-transport.js"
}
}
</script>
<script
src="https://web-host.wippy.ai/webcomponents-1.0.44/dev-proxy.js"
data-role="@wippy/scripts"
></script>
</head>
<body>
<div id="app">
<wippy-loading title="Loading..."></wippy-loading>
</div>
<script type="module" src="./src/app.ts"></script>
</body>
</html>
Rules:
- MUST contain
<!DOCTYPE html>,<html lang="...">, charset, viewport. - MUST contain a
<title>. - MUST contain
<script type="importmap">with the completeimportsobject fetched from the same Web Host release tag. - MUST re-fetch that map only when the Web Host tag changes or when adding a dependency to check whether its exact specifier can be external.
- MUST contain exactly one
<script data-role="@wippy/scripts" src="https://web-host.wippy.ai/<release-tag>/dev-proxy.js">. The URL always requires a release-tag segment. - MUST contain
<div id="app"></div>mount point. - MUST contain
<wippy-loading title="...">inside the mount instead of a hand-rolled spinner. - MUST contain
<script type="module" src="./src/app.ts">(or your entry path) at end of body.
VERIFY:
grep -c '<script type="importmap">' app.html # must = 1
grep -c 'data-role="@wippy/scripts"' app.html # must = 1
grep -c '<wippy-loading' app.html # must >= 1
3.4 src/app.ts (bootstrap)
Reference: gold:main/src/app.ts.
import { addCollection } from '@iconify/vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createWippyPersist, preloadWippyState } from '@wippy-fe/pinia-persist'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import { PrimeVuePlugin } from '@wippy-fe/theme/primevue-plugin'
// Sync getters from @wippy-fe/proxy — available immediately, never awaited to obtain.
import { config, host, api, on } from '@wippy-fe/proxy'
import App from './app/app.vue'
import { AXIOS_INSTANCE, HOST_API, WIPPY_INSTANCE } from './constants'
import { createAppRouter } from '@wippy-fe/router'
import { routes } from './router'
import './styles.css'
import './tailwind.css'
export async function createMainApp() {
const routePath = config.context?.route
const initialPath = routePath
? (routePath.startsWith('/') ? routePath : '/' + routePath)
: '/'
if (config.theming.global?.icons) {
addCollection({
prefix: 'custom',
icons: config.theming.global.icons,
})
}
for (const [prefix, icons] of Object.entries(config.theming.global?.iconSets ?? {})) {
addCollection({ prefix, icons })
}
const app = createApp(App)
const preloaded = await preloadWippyState()
const pinia = createPinia()
pinia.use(createWippyPersist(preloaded))
app.use(pinia)
app.use(VueQueryPlugin)
app.use(PrimeVuePlugin)
app.provide(HOST_API, host)
app.provide(AXIOS_INSTANCE, api)
app.provide(WIPPY_INSTANCE, { on })
const router = createAppRouter(routes, { initialPath })
app.use(router)
return app
}
export async function mountApp(elementId: string = '#app') {
const app = await createMainApp()
app.mount(elementId)
return app
}
mountApp()
config,host,api, andonare synchronous getters from@wippy-fe/proxy— the host injects the child config before the runtime loads, so they resolve the moment your code runs. You neverawaitto obtain them (the onlyawaitleft ispreloadWippyState(), an actual async op). Providing them viaapp.provide(...)is an ergonomics choice so the rest of the app caninject(...); a component can equallyimport { host, api, on } from '@wippy-fe/proxy'at its own call site. See Proxy API.
Rules:
- MUST obtain
config,host,api,onviaimport { ... } from '@wippy-fe/proxy'(sync getters — noawaitto obtain them; neverwindow.$W/getWippyApi). - MUST resolve initial path from
config.context?.route, then fall back to'/'. - MUST normalize the resolved path to start with
/. - MUST
app.provide(HOST_API, ...),app.provide(AXIOS_INSTANCE, ...),app.provide(WIPPY_INSTANCE, ...). - MUST
app.mount('#app')(or whatever id matches the<div id>in app.html). - MUST register the PrimeVue plugin if you use any PrimeVue component.
- SHOULD register
createWippyPersist(preloaded)on pinia for state persistence across iframe destructions. - SHOULD register
VueQueryPluginif you use TanStack Query. - MUST register
config.theming.global?.iconsandconfig.theming.global?.iconSetsduring bootstrap. - MUST NOT
console.logboot diagnostics in production (console.warn/console.errorallowed).
3.5 src/router/index.ts
Reference: gold:main/src/router/index.ts.
Canonical pattern — app.ts uses the package factory directly; the local
router module exports route records only:
import type { RouteRecordRaw } from 'vue-router'
export const routes: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: () => import('../pages/home.vue') },
{ path: '/users', name: 'users', component: () => import('../pages/users.vue') },
{ path: '/:pathMatch(.*)*', name: 'not-found', redirect: '/' },
]
Rules:
- MUST use
createAppRouter()for any page that can run in iframe orautomode. A directcreateWebHistory()router is allowed only for an explicitly Fragment-only page and makes that artifact non-portable. - MUST source the initial host route from
config.context?.route; if it is absent, use/or an application-owned default. - MUST NOT fall back to
window.locationorwindow.parent.location. - MUST NOT reproduce the factory's memory-history,
afterEach,@history,navId, orsetLocalRouterprotocol in application code. - MUST include catch-all route
/:pathMatch(.*)*withname: 'not-found'.
Use the current coherent @wippy-fe/router family (0.0.46 at publication). The package is the canonical home of portable memory routing, local-router registration, and @history synchronization. Do not hand-roll this protocol.
3.6 src/constants.ts and src/types.ts
Reference: gold:main/src/constants.ts, gold:main/src/types.ts.
// src/constants.ts
import type { InjectionKey } from 'vue'
import type { HostApi, ProxyApiInstance } from './types'
export const HOST_API = Symbol('host_api') as InjectionKey<HostApi>
export const AXIOS_INSTANCE = Symbol('axios') as InjectionKey<ProxyApiInstance['api']>
export const WIPPY_INSTANCE = Symbol('proxy') as InjectionKey<ProxyApiInstance>
// src/types.ts
// HostApi / ProxyApiInstance / AppConfig are not named exports of any @wippy-fe package.
// Derive them at the type level from $W (typeof only — no runtime access to the internal
// global). The $W typings ship with @wippy-fe/types-global-proxy (add it to tsconfig "types").
export type HostApi = Awaited<ReturnType<typeof window.$W.host>>
export type ProxyApiInstance = Awaited<ReturnType<typeof window.$W.instance>>
export type WippyConfig = Awaited<ReturnType<typeof window.$W.config>>
Both files are tiny and stable. Copy verbatim into new apps.
3.7 Styling
src/styles.css — 9-line boilerplate (gold:main/src/styles.css):
html, body {
height: 100%;
margin: 0;
background: transparent;
}
#app {
height: 100%;
}
Rules:
- MUST set
background: transparentso the host's iframe styles win. - MUST NOT set padding/margin on
html, body, #app. - MUST NOT redefine
--p-surface-N,--p-content-background,--p-text-color,--p-primary-color, etc. at module scope. Host owns them. - MUST NOT put raw PrimeVue component selectors (
.p-dialog,.p-button, etc.) in module-local source CSS. Shared facadecustom_cssand per-page YAMLconfig_overrides.customization.customCSSmay intentionally use global.p-*selectors as part of the shared PrimeVue theme. - MUST NOT write raw Tailwind color classes (
text-red-500,bg-green-100, etc.) for colors that have semantic meaning. Use severity classes (text-danger-500,bg-success-100) instead. (docs:theming.md) - DO put per-app theming in YAML
meta.config_overrides(or the package.jsonwippy.configOverridesmirror) — not in source CSS.
src/tailwind.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
tailwind.config.ts (gold:main/tailwind.config.ts):
import themePreset from '@wippy-fe/theme/tailwind.config'
export default {
presets: [themePreset],
content: ['./src/**/*.{vue,ts}', './app.html'],
}
Note: themePreset is a default import, not a named import.
postcss.config.js (CRITICAL):
module.exports = {
plugins: { tailwindcss: {}, autoprefixer: {} },
}
3.8 Vue / TypeScript hygiene
Reference: gold:main/tsconfig.json.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"types": ["vite/client", "@wippy-fe/types-global-proxy"]
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
Rules:
- MUST
target: "ES2020"(canonical).libMUST include"ES2020","DOM","DOM.Iterable". ES2022+ is allowed but ES2020 is the gold-standard floor. - MUST
module: "ESNext",moduleResolution: "bundler",strict: true,noEmit: true. - MUST include
vite/clientand@wippy-fe/types-global-proxyintypes. - MUST include
src/**/*.ts,src/**/*.vue, andvite.config.tsininclude. - ALL
.vuefiles MUST use<script setup lang="ts">at the top. Place the<script setup>block before<template>. - MUST type props with TS interface or generic:
defineProps<{ foo: string }>(). Untyped object syntax is REJECT. - MUST use Composition API. Use
refoverreactivewhere the semantics are equivalent. - Use
computedproperties for derived state; prefer early returns to reduce nesting. - File names SHOULD be kebab-case.
- MUST avoid
any; preferunknown+ guards. Each retainedanyMUST have a justifying comment. Aim ≤ 50 across an entire app. pages/<x>.vueMUST be lazy-loaded in router:() => import('../pages/x.vue').- MUST NOT use
console.login production code.console.warnandconsole.errorare allowed for error reporting. npm run type-checkMUST exit 0.- Tests SHOULD exist for non-trivial logic and MUST pass.
- Handle all states in UI components: loading, error, empty, success.
- Prefix event handlers with
handle(e.g.,handleClick,handleSubmit).
3.9 Subscription cleanup (the leak avoidance pattern)
instance.on(pattern, cb) returns an unsubscribe function. ALWAYS store it. ALWAYS call it in onUnmounted. (incident:3A-3I, kb:subscription-cleanup.)
Canonical pattern:
import { onMounted, onUnmounted, inject } from 'vue'
import { WIPPY_INSTANCE } from '../constants'
const instance = inject(WIPPY_INSTANCE)!
let unsub: (() => void) | null = null
onMounted(() => {
unsub = instance.on('keeper.task', () => load())
})
onUnmounted(() => {
unsub?.()
})
For multiple subscriptions:
let unsubs: Array<() => void> = []
onMounted(() => {
unsubs.push(instance.on('keeper.session:message', onMessage))
unsubs.push(instance.on('keeper.session:status', onStatus))
})
onUnmounted(() => {
unsubs.forEach(u => u?.())
unsubs = []
})
Anti-patterns (REJECT):
instance.on(...)at module top-level (outsideonMounted) — leaks for app lifetime (incident:3A).instance.on(...)with return value discarded — silent leak (incident:3B-3G).instance.off(...)— that method does NOT exist;// @ts-ignorewon't save you (incident:3D).- Loop-creating subscriptions without storing all unsubs (
incident:3C). window.addEventListener('message', ...)without matchingremoveEventListenerinonUnmounted(incident:3I).- Raw
new EventSource(...)— bypasses host auth bridge; useinstance.on(...)for an equivalent server-side topic (incident:3J). window.addEventListener('error' | 'unhandledrejection', ...)/window.onerror— installing window-global error handlers. The host owns global error capture (the host shell's error handler + the iframe proxy'serrorCaptureinjection); a child app or web component adding its own duplicates them. This bites web components especially: multiple instances share one realm, so a single error fires every instance's handler → doubled error reporting and toasts. For your own reporting useinstance.logger.captureException(...); for component-scoped failures use Vue'sonErrorCaptured. MUST NOT install globalerror/unhandledrejectionhandlers from a WC.
4. Web components — manifest, build, runtime
4.1 package.json
Reference: gold:mermaid/package.json.
{
"name": "@example/mermaid",
"version": "1.0.0",
"specification": "wippy-component-1.0",
"title": "Mermaid Diagram",
"description": "...",
"browser": "dist/index.js",
"files": ["dist/", "src/", "package.json"],
"dependencies": {
"@wippy-fe/theme": "^0.0.46",
"@wippy-fe/webcomponent-core": "^0.0.46",
"@wippy-fe/webcomponent-vue": "^0.0.46",
"mermaid": "^11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"@wippy-fe/proxy": "^0.0.46",
"@wippy-fe/vite-plugin": "^0.0.46",
"typescript": "^5.0.0",
"vite": "^6.0.0",
"vue": "^3.5.0",
"vue-tsc": "^2.0.0"
},
"peerDependencies": {
"@wippy-fe/proxy": "^0.0.46",
"vue": "^3.5.0"
},
"wippy": {
"tagName": "example-mermaid",
"type": "widget",
"description": "...",
"props": {
"type": "object",
"properties": {
"definition": { "type": "string", "default": "", "description": "..." },
"transparent": { "type": "boolean", "default": true, "description": "..." }
}
},
"scripts": {
"build": "build",
"debug": "build:debug",
"test": "lint"
}
},
"scripts": {
"build": "vite build",
"build:debug": "vite build --mode development",
"dev": "vite build --watch",
"lint": "eslint src --ext .ts,.vue",
"lint:fix": "eslint src --ext .ts,.vue --fix"
}
}
Specification & metadata:
- MUST
"specification": "wippy-component-1.0". - MUST
namefollow@<org>/<short>(e.g.@example/mermaid). - MUST set top-level
"title"and"description". - MUST set
"browser": "dist/index.js"pointing at the built entry. - MUST list
dist/,src/,package.jsoninfiles.
wippy block (WC-specific):
- MUST
wippy.type: "widget"OR"component"(NOT"page"or"web-component"). - MUST
wippy.tagName(camelCase) — the custom element tag. Must contain a hyphen. - MUST
wippy.description— a verbose AI/human-readable usage explanation (not a one-line label). Must explain HOW to use the WC: the expected call shape (which props vs children), supported input forms, fallback paths, notable perf characteristics. - MUST
wippy.propsJSON Schema. Every property MUST havetype,default,description. - MAY
wippy.eventsJSON Schema (omit if no custom events). - MUST NOT have
wippy.path(no HTML entry). - MUST NOT have
wippy.icon(no nav presence). - MUST NOT have
wippy.proxyblock (WCs run in host doc, not iframe). - MUST set
wippy.scripts.build. MAY setdebugandtest.
Dependency hygiene:
dependencies: bundled-into-WC packages. Canonical:@wippy-fe/theme,@wippy-fe/webcomponent-core,@wippy-fe/webcomponent-vue. Plus the WC's domain libs (e.g.mermaid,chart.js).devDependencies: build toolchain. Canonical:vite,@vitejs/plugin-vue,typescript,vue-tsc,vuefor build-time,eslint*,@wippy-fe/proxy(build-time type imports).peerDependencies: only imported npm package roots expected from the host. Rollup externals separately contain every key from the fetched import map.
Retained visibility (0.0.52+):
- MUST NOT use proxy
on('@visibility', ...)in a direct web component; that topic belongs to iframe/Web Fragment pages. - SHOULD use
useHostVisibilityRefresh(task)when data loaded on mount can become stale while the element remains mounted in an inactive buffer or closed drawer. - MUST refresh every remote dataset loaded by that ordinary mounted task, not an arbitrary subset.
- MUST keep local UI state and non-idempotent subscriptions outside the refresh task.
- MUST handle cancellation on unmount and stale responses when another code path can load the same data. The helper serializes its own task and coalesces intervening reveals.
- MUST test initial load, exact
false -> true, duplicate suppression, same-element identity, and local-state preservation.
4.2 vite.config.ts (web component, library mode)
Reference: gold:mermaid/vite.config.ts.
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import vue from '@vitejs/plugin-vue'
import { wippyComponentPlugin } from '@wippy-fe/vite-plugin'
import { defineConfig } from 'vite'
const hostImportMap = JSON.parse(
readFileSync(new URL('./import-map.json', import.meta.url), 'utf8'),
)
export default defineConfig({
plugins: [vue(), wippyComponentPlugin()],
build: {
target: 'esnext',
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MermaidDiagram',
fileName: 'index',
formats: ['es'],
},
rollupOptions: {
input: { index: resolve(__dirname, 'src/index.ts') },
external: Object.keys(hostImportMap.imports),
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name]-[hash][extname]',
},
// preserveEntrySignatures: false ensures define(import.meta.url, …) stays in
// the entry chunk — required so the ?declare-tag= query the host appends reaches
// import.meta.url and registration doesn't silently no-op.
preserveEntrySignatures: false,
},
sourcemap: true,
},
})
Rules:
- MUST set
build.target: 'esnext'. - MUST use
build.liblibrary mode withformats: ['es'](ESM only). - MUST set
entry(andinput.index) to yoursrc/index.ts. - MUST set
preserveEntrySignatures: false. - MUST set entry/chunk/asset file names:
[name].js,[name]-[hash].js,[name]-[hash][extname]. - MUST include
wippyComponentPlugin()from@wippy-fe/vite-plugininpluginsso the build emitswippy-meta.jsonin the actual output directory (see §9.3a). - MUST externalize every key in the fetched target-host import map.
- MUST bundle an imported exact specifier that is absent from that map. This
rule applies independently to
@wippy-fe/*and everyprimevue/*subpath. - DO NOT set
base(no HTML entry, base is irrelevant). - DO NOT set
cssCodeSplit(CSS is?inline-imported into the JS, see §4.3).
4.3 src/index.ts (entry)
Reference: gold:mermaid/src/index.ts.
import { WippyVueElement, define } from '@wippy-fe/webcomponent-vue'
import type { WippyElementConfig, WippyPropsSchema } from '@wippy-fe/webcomponent-vue'
import type { ComponentProps } from './types.ts'
import type { Events } from './constants.ts'
import MermaidDiagram from './app/mermaid-diagram.vue'
import stylesText from './styles.css?inline'
import pkg from '../package.json'
class MermaidElement extends WippyVueElement<ComponentProps, Events> {
static get wippyConfig(): WippyElementConfig<ComponentProps> {
return {
propsSchema: pkg.wippy.props as WippyPropsSchema,
hostCssKeys: ['themeConfigUrl'] as const,
inlineCss: stylesText,
contentTemplate: 'text/vnd.mermaid', // optional; reads text from a child <template data-type="text/vnd.mermaid"> element
}
}
static get vueConfig() {
return {
rootComponent: MermaidDiagram,
}
}
}
export async function webComponent() {
return MermaidElement
}
define(import.meta.url, MermaidElement)
Rules:
- MUST extend
WippyVueElement<ComponentProps, Events>(Vue) orWippyElement(vanilla). - MUST implement
static get wippyConfig()returning:propsSchema: pkg.wippy.props as WippyPropsSchema— single source of truth from package.json.hostCssKeys: [...]— which host-provided CSS bundles to inject into the shadow root. Use the const names from@wippy-fe/webcomponent-core:themeConfigUrl(theme tokens),iframeCssUrl(historically named default themed scrollbar styling),primeVueCssUrl(PrimeVue components),markdownCssUrl(markdown). Pick the minimal set you need. (preflightCssUrlis not a member of theHostCssKeyunion — Tailwind v3 preflight is reachable only imperatively vialoadCss(hostCss.preflightCssUrl).)inlineCss: stylesText— your WC-specific CSS imported via?inline.contentTemplate?: 'text/vnd.foo'— optional MIME type; when set, the WC reads text from a child<template data-type="<mime>">element, e.g.<example-mermaid><template data-type="text/vnd.mermaid">graph TD; A --> B</template></example-mermaid>(rare).
- MUST implement
static get vueConfig()returning{ rootComponent }. Addplugins: [PrimeVuePlugin, ...]if you use PrimeVue components. - MUST export async
webComponent()factory function so the host loader can call it. - MUST
define(import.meta.url, ElementClass)at module level.
4.4 Theme compatibility
- WC root element MUST NOT have padding or margin. Host controls outer spacing.
- WC MUST use semantic CSS vars for theme-dependent colors:
--p-text-color,--p-content-background,--p-content-border-color,--p-text-muted-color,--p-content-hover-background,--p-primary-color. - WC MUST NOT use raw
--p-surface-Nfor theme-dependent purposes — that scale is fixed. - For derived shades, use
color-mix(in srgb, var(--semantic) X%, transparent). - For severity colors, use
--p-danger-*,--p-success-*,--p-warn-*,--p-info-*,--p-help-*,--p-accent-*— never raw Tailwind color names. - Use
<Icon icon="tabler:icon-name" />from@iconify/vuefor all icons — never inline<svg>for reusable iconography. - Use semantic HTML elements where possible; include proper ARIA roles and attributes on interactive elements.
4.5 src/styles.css
@import "@wippy-fe/theme/theme-config.css";
.my-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
box-sizing: border-box;
}
The ?inline import in index.ts reads this file as a string and bakes it into the bundle. Combined with hostCssKeys, the shadow root gets host CSS + your WC-specific CSS.
4.6 src/constants.ts (events typing)
Reference: gold:mermaid/src/constants.ts.
import { useProps, useEvents, usePropsErrors } from '@wippy-fe/webcomponent-vue'
import type { ComponentProps } from './types.ts'
export interface Events {
load: undefined
unload: undefined
error: { message: string, error: unknown }
invalid: { message: string }
}
export const useComponentProps = () => useProps<ComponentProps>()
export const useComponentEvents = () => useEvents<Events>()
export const useComponentPropsErrors = usePropsErrors
Use useComponentProps() and useComponentEvents() in your Vue components instead of plain defineProps / defineEmits — they integrate with the WC's prop/event marshalling.
4.7 tsconfig.json (WC variant)
Reference: gold:mermaid/tsconfig.json.
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["vite/client", "@wippy-fe/proxy"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Differences vs micro frontend apps:
typesuses@wippy-fe/proxy(not@wippy-fe/types-global-proxy).- Adds
useDefineForClassFields,noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch,allowImportingTsExtensions. referencesto atsconfig.node.json.
4.8 Runtime caching / state persistence
For state that must survive WC unmount or iframe destruction, use @wippy-fe/pinia-persist:
persist-keyprop values MUST be globally unique across the app.- Bundle
@wippy-fe/pinia-persistwhen its exact specifier is absent from the pinned target-host import map; externalize it when the exact key is present.
5. Theming
5.0 Visual-matching escalation (HEAVILY recommended)
To match a visual design, escalate in this strict order. Do not skip ahead — most "I want it to look like X" work fits at level 1 or 2.
| Level | What | Where |
|---|---|---|
| 1 — CSS variables | Override existing --p-* semantic vars (primary/content/text/severity) and override the surface scale if the brand needs a different neutral palette. Use Playwright + DevTools getComputedStyle(document.documentElement) to enumerate every --p-* already defined; pick from that menu first. |
Facade theming.global / theming.children, or per-page YAML config_overrides.customization.cssVariables for isolation. NEVER :root in .css files. |
2 — facade custom_css / frontend customCSS for PrimeVue components |
Add design-token overrides (--p-button-border-radius, --p-dialog-shadow, etc.) and selector tweaks (.p-button.p-button-xs { … }, .p-accordionheader::before { … }) when level 1 vars don't reach. |
Facade theming.global / theming.children, or per-page YAML config_overrides.customization.customCSS. NEVER raw .p-* rules in .css files. |
| 3 — Custom Vue components | Build your own component. Reserved for things PrimeVue genuinely doesn't offer: novel visualizations (force graph, custom chart), domain-specific layouts, interactions outside PrimeVue's catalog. | Vue source in your app. |
REJECT level-3 work that could have been done at level 1 or 2. Examples of "should have been level 1/2":
- Custom dropdown when
<Select>exists. - Custom modal when
<Dialog>+useDialogexists. - Custom toast when
<Toast>+useToastexists (orhost.toast()). - Custom confirm prompt when
<ConfirmDialog>+useConfirmexists (orhost.confirm()). - Custom tooltip when the
v-tooltipdirective exists. - Custom inline button styled to look like a primary button when a styled
<Button>exists.
Examples where level 3 IS legitimate:
- Force graph for dataflow visualization (no PrimeVue equivalent).
- Token-bar charts (Chart.js wrapper).
- Markdown/rich-text renderers (markdown-it / shiki wrappers).
- Code editor (Monaco WC).
- Domain-specific shell components in managed-layout panels.
5.1 Facade-first: the main way to theme a Wippy app
A Wippy module composes itself from ns.dependency entries. One of those is wippy/facade — the dependency that parameterises the host shell (top bar, nav, login page, layout) and ALL the global theming. The facade is where the main customization lives.
Set theming on the facade dependency, not on individual pages. Parameters of interest:
wippy/facade parameter |
Purpose |
|---|---|
app_title, app_name, app_icon |
brand identity |
custom_css |
shared facade-theme CSS — reaches the host chrome, view.page documents, and view.component shadow roots on supported hosts. Put shared PrimeVue appearance here; keep necessary domain layout and novel structure in module CSS. |
css_variables |
JSON map of arbitrary custom-property overrides. The compiler emits effective Auto/forced mode blocks, and WippyElement bridges every configured name into its inner theme root before customCSS. |
host_custom_css |
host-chrome-only CSS (not delivered to children — scope class rules to .wippy-host-app). Use children_custom_css for CSS that should reach those children but not the host chrome. |
hide_nav_bar, show_admin, history_mode, session_type, login_path |
UX shell behaviour |
fe_mode, host_config_layout |
managed-layout mode + layout declaration |
5.1.1 Three levels of override (priority, low → high)
- Facade global — set in the host's
wippy/facadens.dependencyparameters. Affects the whole user shell + every page inheriting from the facade. Shared PrimeVue appearance and brand theming belong here. - Page overrides — registry YAML uses
meta.config_overrides.customization.cssVariables/customCSS; frontendpackage.jsonuseswippy.configOverrides.customization.cssVariables/customCSSas the host-less mirror. The projected frontend values replace the inherited page theme and cascade to its nested subtree. Backend nestedicons/iconSetsremain frontendicons/iconSetsand merge additively. - Runtime overlay —
window.__WIPPY_CONFIG_OVERRIDES__set BEFORE proxy.js loads. Rare; for query-string or feature-flag theming.
See theming.md for the full three-level guide with examples, escalation criteria, and anti-patterns.
5.1.2 Where each override lives — STRICT placement rule
Mismatched placement is the #1 source of theme drift. The rule:
| Override target | Where it goes | Where it MUST NOT go |
|---|---|---|
Existing host var (--p-*) — change its value |
Facade theming, or YAML config_overrides.customization.cssVariables for per-page isolation |
NEVER :root { --p-* } in src/styles.css |
| New derived var your project owns — needed for project use | Same place as above; compute via color-mix() or var() referencing host vars |
NEVER :root { --my-* } in src/styles.css |
HOST-owned selector override (.p-button, .p-dialog, .p-inputtext, etc.) |
Facade custom_css, or YAML config_overrides.customization.customCSS for per-page isolation |
NEVER raw .p-* rules in src/styles.css |
Domain layout or genuinely novel structure (.search-layout, .diagram-node) |
src/styles.css |
facade theme unless the rule is intentionally shared |
| Project-scoped non-theme constant (chart bar color, fixed spacing tag) | src/styles.css with a clear project prefix (e.g., --keeper-chart-bar-*) |
n/a |
Rationale: theme is a host concern; the host's CSS pipeline composes facade global + per-page customization in a defined order. CSS files inside the bundle ship AFTER the host's pipeline and shadow it, breaking the override semantics.
REJECT 42b: any :root { --p-* } (or :root { --<other-host-var> }) redefinition in a child app's .css file. Move to facade theming or per-page YAML config_overrides.customization.cssVariables.
REJECT 43a: any raw .p-<component> rule in a child app's .css file. Move to facade custom_css or per-page YAML config_overrides.customization.customCSS.
5.2 Semantic vs fixed CSS variables
| Variable | Flips in dark mode? | Use for |
|---|---|---|
--p-text-color |
yes | body text |
--p-content-background |
yes | container / page background |
--p-content-border-color |
yes | borders |
--p-text-muted-color |
yes | secondary text |
--p-content-hover-background |
yes | hover states |
--p-primary-color |
yes | primary action color |
--p-surface-0 … --p-surface-950 |
NO (fixed scale) | only as anchors for color-mix(); avoid for theme-dependent UI |
--p-primary-500 … --p-primary-950 |
NO (fixed scale) | only when you need a specific primary shade |
--p-danger-color, --p-success-color, --p-warn-color, --p-info-color, --p-help-color, --p-accent-color |
yes | severity colors. Use these, NOT raw Tailwind names. |
Anti-pattern (REJECT):
.card { background: var(--p-surface-100); } /* fixed; doesn't flip */
.card { background: var(--p-primary); } /* valid palette seed, wrong semantic consumer; use --p-primary-color */
Canonical:
.card {
background: var(--p-content-background);
border: 1px solid var(--p-content-border-color);
color: var(--p-text-color);
}
.muted-card {
background: color-mix(in srgb, var(--p-content-background) 92%, var(--p-text-color) 8%);
}
.danger-banner { background: var(--p-danger-color); }
5.3 REPLACE vs MERGE per field
Field in customization |
Per-page semantics |
|---|---|
cssVariables |
REPLACE — your map fully replaces parent's |
customCSS |
REPLACE — your string fully replaces parent's |
icons |
MERGE shallow — additive |
iconSets |
MERGE per-prefix — additive |
AppConfigOverrides top-level:
| Field | Semantics |
|---|---|
customization |
merged via mergeChildCustomization (above) |
axiosDefaults |
MERGE shallow |
routePrefix |
REPLACE |
apiRoutes |
REPLACE |
5.4 @light / @dark blocks
The host supports @light and @dark keys in cssVariables. Top-level variables apply to every mode; mode maps replace only their named entries. The compiler emits effective Auto-light/Auto-dark media blocks and forced .w-theme-light / .w-theme-dark selectors, with shadow-aware anchors for Web Fragments.
Example:
css_variables:
--p-primary-color: var(--p-primary-500)
--kp-bg: var(--p-content-background)
'@light':
--p-content-background: '#ffffff'
--p-text-color: '#18181b'
'@dark':
--p-content-background: '#1c1a19'
--p-text-color: '#fafafa'
Applications must use the host theme mode and .w-theme-light / .w-theme-dark protocol; do not invent data-theme selectors. Pages that may also render as Web Fragments pair :root.w-theme-* with :host(.w-theme-*); a pure shadow-DOM component may use :host(.w-theme-*) alone. See also micro-frontend-app-theming.md and host-less-mode.md.
5.4.1 Exact propagation acceptance
For any configured palette, static presence checks are insufficient. Drive the inventory from @wippy-fe/theme/tokens.json and verify primary, secondary, accent, danger, success, warn, info, and help independently. For each family assert its base, every 50–950 shade, the four semantic aliases, and one direct shade/alias override. Also cover a surface token and an arbitrary sentinel property.
Run the same assertions in Auto-light, Auto-dark, forced Light, and forced Dark. Compare exact values at page root, WC host, and WC inner [data-wippy-theme-root], then use rendered color probes so the browser physically resolves color-mix(). Changing primary must not change any severity family. Reject a primary-only bridge, a hand-maintained family whitelist, or advice to duplicate complete palettes in application CSS.
5.5 customCSS scoping
- Scope selectors to
.wippy-host-apponly when they target host chrome. Shared facade-theme selectors such as.p-drawer-contentmay intentionally remain unscoped so the same PrimeVue theme reaches pages and injected component shadow roots. - Scope to a specific child/page boundary only when isolation is intentional. Per-page overrides can use top-level selectors within their delivered page scope.
5.6 Iconify discipline
Icons in Wippy apps follow a single workflow:
- Use
@iconify/vue<Icon>for ALL icons. Don't inline<svg>for reusable iconography. Don't ship icon-font CSS (Tabler-icons-font, Material Icons font). The proxy and the build assume Iconify; mixing systems creates a/b drift. - Prefer permissive packs. All free for commercial use, all available via Iconify:
tabler(MIT, ~5,400 icons) — broad UI coverage; the gold-standard default for keeper-class apps.lucide(ISC, ~1,500 icons) — clean line style.phosphor(MIT, ~7,000 icons) — six weight variants.material-symbols(Apache 2.0, ~3,000+ icons) — Google's modern set.mdi(Apache 2.0, ~7,000 icons) — Material Design Icons community pack.heroicons(MIT, ~300 icons) — Tailwind team's set, outline + solid.
- Don't use commercial-licensed packs (FontAwesome Pro, etc.) without licence verification per developer seat. Iconify hosts MIT/CC-BY subsets of FontAwesome (
fa6-solid/fa6-regular/fa6-brands) — use those instead. - Custom icons — when no permissive pack covers a symbol:
- Declare them in
theming.global.icons/iconSetswhen shared, orconfig_overrides.customization.iconsfor per-page additions — safe becauseiconsMERGES, not replaces. - The bootstrap path (
config.theming.global?.icons → addCollection({ prefix: 'custom', icons })plusiconSets) wires them automatically. - NEVER call
addCollection()from arbitrary application code. The bootstrap path is canonical; everything else fragments the registry. - Mint custom icons sparingly. If you find yourself adding more than a dozen, consider whether a permissive pack already has the symbol.
- Declare them in
- At call sites, prefer
<Icon icon="tabler:home" />over hardcoded SVG. Use Iconify's pack:name format consistently. Usearia-hidden="true"for decorative icons,aria-labelfor meaningful ones.
REJECT (5.6.r): any .vue file that registers icons via addCollection() outside app.ts's canonical bootstrap. REJECT raw <svg> for reusable iconography (one-off illustrations are OK).
6. Proxy API & subscriptions
6.1 Injection keys (apps)
In src/constants.ts (micro frontend apps):
| Key | Provides | Use |
|---|---|---|
HOST_API |
HostApi |
inject(HOST_API) |
WIPPY_INSTANCE |
ProxyApiInstance |
inject(WIPPY_INSTANCE) |
AXIOS_INSTANCE |
pre-configured axios (auth + baseURL) |
inject(AXIOS_INSTANCE) |
For web components, import from @wippy-fe/proxy directly:
import { host, api, on } from '@wippy-fe/proxy'
6.2 host.* methods (full reference: Appendix B)
| Method | Use |
|---|---|
host.toast |
replaces PrimeVue ToastService |
host.confirm |
replaces window.confirm |
host.startChat |
open a new chat |
host.openSession |
navigate to session |
host.openArtifact |
open artifact |
host.setContext |
set chat context |
host.navigate |
host-side navigation |
host.onRouteChanged |
report router change |
host.handleError |
report error |
host.formatUrl |
prepend routePrefix |
host.classifyLink |
classify nav target |
host.layout |
managed-layout API (always present; host.layout.snapshot is null outside managed mode) |
host.logout |
sign out |
Rules:
- MUST use
host.toastnot PrimeVue ToastService. - MUST use
host.confirmnotwindow.confirm. - MUST use injected
useApi()/AXIOS_INSTANCEnot rawaxios.create(). - MUST NOT call
sendIframeMessage()directly — go throughhost.*methods.
VERIFY:
grep -r "axios.create" src/ # should = 0
grep -r "window.confirm" src/ # should = 0
6.3 instance.on(pattern, cb) reserved patterns
| Pattern | Payload | Meaning |
|---|---|---|
@history |
{ path?, navId? } |
host pushed a route |
@visibility |
boolean | iframe/Web Fragment visibility changed; not the retained direct-WC signal |
@layout-change |
LayoutSnapshot |
layout tree changed |
@layout-panel-changed |
{ panelId, ... } |
single panel changed |
@layout-breakpoint |
{ name, width } |
breakpoint changed (name = new breakpoint, width = threshold px) |
@message |
wildcard | catch all WebSocket messages |
@state-error |
{ error, key } |
state save failed |
Custom topics use colon-separated parts; * is wildcard.
6.4 Layout API
host.layout is always present (a LayoutApi object). Outside managed-layout mode, host.layout.snapshot is null and all mutation/bus calls are silent no-ops — gate on host.layout.snapshot (or isManaged from useWippyLayout) before mutating, not a null-check on host.layout.
7. Router & host integration
(Source body in §3.5; this section is verification-focused.)
| # | Rule | REJECT? |
|---|---|---|
| 7-1 | imports and uses createAppRouter from @wippy-fe/router |
yes |
| 7-2 | initial path = config.context?.route ?? '/' |
yes |
| 7-3 | no window.location / window.parent.location host-route fallback |
yes |
| 7-4 | no application-owned createMemoryHistory, afterEach, @history, navId, or setLocalRouter synchronization protocol |
yes |
| 7-5 | catch-all /:pathMatch(.*)* route with name: 'not-found' |
yes |
| 7-6 | direct createWebHistory appears only in a documented Fragment-only artifact |
yes |
If you persist last-route to localStorage, EXCLUDE ID-bearing routes (/session/:id, /changes/:id, etc.) — reload-after-delete lands on stale 404s otherwise (incident:2H).
Prefer the proxy, AppConfig, and router APIs over raw cross-frame messaging. A documented low-level embedding integration that truly needs message MUST validate both event.origin against an explicit configured allowlist and event.source against the expected window, then remove the listener on unmount. A source comparison alone is not an origin check.
8. Build pipeline & Makefile
8.1 Canonical Makefile recipe
build-<app>-frontend:
cd <path-to-app> && npm install --no-audit --no-fund --prefer-offline && npm run build -- --outDir <dest> --emptyOutDir
Rules:
- MUST identify the registry owner, module/static mount, build owner, output directory, and emitted entry before rebuilding.
- MUST pass both
--outDirand--emptyOutDir; resolve and verify the exact target before running the build. - MUST NOT use the
rm + mkdir + cpdance — pollutes source tree withdist/and is not atomic. - MUST
cdinto the app dir. - The output directory MUST match the path actually mounted by the deployment;
static/<embed-name>is a common layout, not a universal contract.
Each module that publishes a frontend MUST have its own build-<app>-frontend target. Add to publish-* chains.
8.2 make.bat + make.ps1 are required (every module, every time)
Every module that ships a Makefile MUST also ship make.bat + make.ps1 next to it. No "if your team runs on Windows" carve-out — Wippy modules are written by mixed teams and audited on mixed machines, and the wrapper is small enough that there is no reason not to have it.
make.batis a thin shim that invokesmake.ps1viapowershell -NoProfile -NonInteractive -ExecutionPolicy Bypass.make.ps1mirrors every Makefile target one-for-one —build-*,lint*,publish*,dev,clean, etc. — so the same workflow runs on Linux, macOS, and Windows.- The exact
npm run build -- --outDir <abs-or-relative> --emptyOutDircommand belongs in bothMakefileandmake.ps1;make.batcontains no duplicated build logic. See Build System for complete runnable examples. - Keep
make.ps1pure ASCII (no em-dashes, smart quotes) — Windows PowerShell 5.1 reads BOM-less files as Windows-1252 and corrupts non-ASCII chars on read.
REJECT a module that ships Makefile without matching make.bat + make.ps1.
8.3 Fetched import-map snapshot and dependency roles
Fetch <fe_facade_url>/import-map.json once during development:
curl.exe -fsS "https://web-host.wippy.ai/<version-tag>/import-map.json" -o import-map.json
- Vite externals MUST be
Object.keys(hostImportMap.imports). - Host-less
app.htmlMUST contain the same completeimportsobject. peerDependenciescontain only actually imported npm package roots expected from the host; they cannot mirror import-map subpath keys.- Re-fetch when the Web Host tag changes or when adding a dependency to check whether its exact specifier can be external.
- Bundle a used exact specifier when it is absent from the fetched map.
Mismatch symptom: Failed to resolve module specifier 'pinia' (incident:8A).
8.4 Pre-publish gates
npm run type-checkMUST exit 0.npm testMUST pass if any tests exist.npm run build -- --outDir <abs-or-relative> --emptyOutDirMUST succeed.npm run lintSHOULD exit 0 if you have eslint configured.
9. Host-less mode
Host-less = boot the SPA via a static HTTP server with no real Wippy host running. dev-proxy.js provides a host shim plus a "dev overlay" UI for accepting/editing the config. Host-less mode is the default supported workflow for new apps; the wippyPagePlugin() and importmap+<wippy-loading> patterns described below should be present unless a team has a very good reason to opt out (rare). (See host-less-mode.md for full detail.)
9.1 Complete pinned Web Host import map
Use the complete, valid JSON script shown in §3.3. It is a verified
webcomponents-1.0.44 example, not a hand-maintained canonical package list.
For another tag, replace its complete JSON body with the fetched response;
never put comments inside the JSON itself.
Rules:
- MUST exist in
app.html. - MUST copy the complete
importsobject from the same pinned Web Host release, including@wippy-fe/proxyand unused keys. - MUST bundle an imported exact specifier that is absent from the fetched map.
- Re-fetch only when the Web Host tag changes or when adding a dependency.
9.2 dev-proxy.js + @wippy/scripts data-role
<script
src="https://web-host.wippy.ai/<release-tag>/dev-proxy.js"
data-role="@wippy/scripts"
></script>
Production CDN form:
<script
src="https://web-host.wippy.ai/<release-tag>/dev-proxy.js"
data-role="@wippy/scripts"
></script>
Rules:
- MUST have
data-role="@wippy/scripts"so the host (when running) can find and replace it. - MUST have
src=set; raw<script data-role="@wippy/scripts"></script>placeholder is acceptable only when a real host injects the src at boot.
9.3 wippyPagePlugin (default)
// vite.config.ts
import { wippyPagePlugin } from '@wippy-fe/vite-plugin'
export default defineConfig({
plugins: [vue(), wippyPagePlugin(), /* … */],
/* … */
})
The plugin's transformIndexHtml hook injects the package.json wippy block into the built HTML at the top of <head>:
<script type="application/json" data-role="@wippy/package">
{
"name": "...",
"wippy": { "proxy": { "injections": { ... } }, "configOverrides": { ... } }
}
</script>
Dev-proxy reads the JSON synchronously at boot and seeds:
- proxy injection defaults from
wippy.proxy.injections - per-page customization from
wippy.configOverrides.customization
so the dev-overlay shows the correct values pre-populated.
Rules:
- SHOULD include
wippyPagePlugin()invite.config.ts. This is the default for new apps; opt out only with a very good reason (e.g. shipping a host-only bundle that explicitly does not support host-less dev), and record the reason in maintained project guidance. - MUST install the coherent current
@wippy-fe/vite-pluginfamily (0.0.46at publication) in devDependencies. - The plugin is harmless under a real host (the host ignores the
@wippy/packagescript tag).
VERIFY the script is in the built HTML:
OUTPUT_DIR="${OUTPUT_DIR:-dist}"
npm run build -- --outDir "$OUTPUT_DIR" --emptyOutDir &&
grep -c 'data-role="@wippy/package"' "$OUTPUT_DIR/app.html" # SHOULD = 1
9.3a wippyComponentPlugin (web components)
view.component packages have no HTML entry to inject into, so the page plugin doesn't apply. Use wippyComponentPlugin() from the same @wippy-fe/vite-plugin package — it's emit-only:
// vite.config.ts (web component)
import { wippyComponentPlugin } from '@wippy-fe/vite-plugin'
export default defineConfig({
plugins: [vue(), wippyComponentPlugin(), /* … */],
/* … */
})
The component plugin emits wippy-meta.json in the actual Vite output directory (the resolved wippy block) only — no HTML transform, no inline script tag.
Rules:
- MUST be present in every current
view.componentbuild. - MUST install the coherent current
@wippy-fe/vite-pluginfamily.
VERIFY the meta file is in the dist:
OUTPUT_DIR="${OUTPUT_DIR:-dist}"
npm run build -- --outDir "$OUTPUT_DIR" --emptyOutDir &&
test -f "$OUTPUT_DIR/wippy-meta.json" &&
echo "OK: wippy-meta emitted" || echo "MISSING: wippy-meta.json"
9.3b The wippy-meta.json contract + version correlation
The presence of wippy-meta.json next to the served entry is a hard requirement for the current contract (wippy/views 1.0.31 or newer with the coherent @wippy-fe/vite-plugin family). The output directory is deployment-defined; verify the file beside the artifact that the registry actually serves. The file is the resolved wippy block from package.json, with every "file://<rel>" string replaced by the referenced file's UTF-8 contents at build time.
Two endpoints read it:
| Endpoint | What it serves |
|---|---|
GET /api/public/pages/content/{id} |
resolved wippy-meta.json next to the served app.html (view.page) |
GET /api/public/components/list + /components/by-tag/{tag} |
resolved wippy-meta.json next to each index.js (view.component). {tag} is the WC's custom-element tag name (e.g. example-mermaid), resolved via loadByTagName() — not the CDN git release tag. |
YAML-first priority: the operator's _index.yaml registry entry overlays the bundled meta per-field. If meta.tag_name, meta.title, meta.description, meta.props, meta.events, or meta.entry_point is set in YAML, that wins. Otherwise the bundled meta fills in.
Fallback when missing: if wippy-meta.json is absent next to the entry, views falls back to a deprecated YAML-synthesis path AND emits a per-process deprecation warning. Treat the warning as a release-blocker.
Current compatibility contract
Use wippy/views 1.0.31 or newer with one coherent current @wippy-fe/* package family (0.0.46 at publication). The Vite plugin validates package metadata and emits wippy-meta.json; the registry entry may overlay deployment fields. Historical migration combinations are not an authoring target.
VERIFY the meta file is in the dist and contains resolved content (no file:// strings):
OUTPUT_DIR="${OUTPUT_DIR:-dist}"
npm run build -- --outDir "$OUTPUT_DIR" --emptyOutDir
test -f "$OUTPUT_DIR/wippy-meta.json" && echo "OK: emitted" || echo "REJECT: missing"
grep -c 'file://' "$OUTPUT_DIR/wippy-meta.json" | { read n; [ "$n" = "0" ] && echo "OK: all file:// resolved" || echo "REJECT: $n unresolved file:// refs"; }
9.4 wippy-loading
<div id="app">
<wippy-loading title="Loading..."></wippy-loading>
</div>
The <wippy-loading> element is auto-registered by dev-proxy (and by the real host) before the body parses.
REJECT custom hand-rolled spinners.
9.5 base: '' (relative paths) — REJECT if hardcoded
base: '' in vite.config produces relative ./app.js, ./assets/... paths in the built HTML/JS. The bundle is portable to any URL prefix and any mount point — host-managed, host-less dev, or moved between projects.
A hardcoded absolute base (e.g. base: '/app/keeper/') ties the bundle to a specific mount point and breaks portability. This is a 100% REJECT — there is no acceptable "documented exception". A child app must not assume its own URL prefix; the prefix is a host-side meta.url + meta.base_path concern, and the host injects the appropriate <base> into the HTML at serve time. Set base: '' and let the host do its job.
9.6 Dev-overlay accept flow
<wippy-dev-overlay>shadow-DOM web component, FAB in bottom-right.- Manual mode blocks boot until "Accept config" clicked.
- Auto-accept via
localStorage['@wippy-dev/auto-accept'] === 'true'. - Stored config:
localStorage['@wippy-dev/config'],localStorage['@wippy-dev/proxy-config']. - Reset clears all
@wippy-dev/*keys + reloads.
10. Verification recipes
Run these before submitting. Each maps to a section.
10.1 Bootstrap & build
# 10.1.1 — wippy.specification + wippy.type
node -e 'const p=require("./package.json"); if(p.specification!=="wippy-component-1.0") throw new Error("bad specification"); const t=p.wippy?.type; if(t!=="page" && t!=="widget" && t!=="component") throw new Error("bad wippy.type"); console.log("OK")'
# 10.1.2 — markdown injection if app uses markdown (micro frontend apps only)
grep -A 10 'wippy.proxy.injections.css' package.json | grep -c '"markdown": true' # 1 if uses markdown, 0 otherwise
# 10.1.3 — base relative
grep -E "base:\s*['\"]" vite.config.ts # must show: base: ''
# 10.1.4 — wippyPagePlugin present (host-less default)
grep -c "wippyPagePlugin" vite.config.ts # SHOULD = 1
# 10.1.5 — type-check
npx vue-tsc --build --force || npx vue-tsc --noEmit # exit 0
10.2 Router & host integration (micro frontend apps)
# 10.2.1 — package factory is the only portable routing implementation
grep -c "from '@wippy-fe/router'" src/app.ts # >= 1
grep -c "createAppRouter(routes, { initialPath })" src/app.ts # >= 1
# 10.2.2 — portable app code does not reproduce the package protocol
grep -E "createMemoryHistory|router\.afterEach|on\(['\"]@history|setLocalRouter|window(?:\.parent)?\.location" src/{app.ts,router/index.ts} # empty
grep -c "createWebHistory" src/router/index.ts # 0 unless explicitly Fragment-only
# 10.2.3 — catch-all + name
grep -E "pathMatch.*not-found|name:.*not-found" src/router/index.ts # >= 1
10.3 Proxy API & subscription cleanup
# 10.3.1 — no module-scope instance.on
grep -n "^instance\.on" src/**/*.{ts,vue} # should be empty
# 10.3.2 — every instance.on has matching onUnmounted in same file
for f in $(grep -rl "instance\.on(" src --include="*.vue"); do
o=$(grep -c "instance\.on(" "$f"); u=$(grep -c "onUnmounted" "$f")
[ "$o" -gt 0 ] && [ "$u" -eq 0 ] && echo "FAIL: $f"
done
# 10.3.3 — no instance.off
grep -r "instance\.off" src # should be empty
# 10.3.4 — no raw axios.create
grep -r "axios.create" src # should be empty
# 10.3.5 — no raw EventSource
grep -r "new EventSource" src # should be empty
# 10.3.6 — no window.confirm
grep -r "window\.confirm" src # should be empty
# 10.3.7 — addEventListener pairs with removeEventListener
for f in $(grep -rl "addEventListener" src --include="*.vue"); do
a=$(grep -c "addEventListener" "$f"); r=$(grep -c "removeEventListener" "$f")
[ "$a" -ne "$r" ] && echo "FAIL: $f add=$a remove=$r"
done
10.4 Styling & theming
# 10.4.1 — no theme-dependent --p-surface-N use (informational; document exceptions)
grep -r "var(--p-surface-[0-9]" src/**/*.vue | wc -l # aim for 0; project minimum acceptable
# 10.4.2 — no invalid --p-primary token
grep -rE "var\(--p-primary\)[^-]" src/**/*.vue | wc -l # must = 0
# 10.4.3 — no module-level redefinition of host tokens
grep -rE ":root\s*\{[^}]*--p-(content-background|text-color|primary)" src/styles.css # must = 0
10.5 Vue/TS hygiene
# 10.5.1 — every .vue starts with <script setup lang="ts">
find src -name "*.vue" | while read f; do
grep -q '<script setup lang="ts">' "$f" || echo "FAIL: $f"
done
# 10.5.2 — count any-casts (informational; aim ≤ 50)
grep -rE ":\s*any|as\s*any" src | wc -l
# 10.5.3 — no console.log
grep -rE "console\.log" src # should be empty
10.6 Host-less boot
# dist is Vite's default; set OUTPUT_DIR to the build owner's verified output.
OUTPUT_DIR="${OUTPUT_DIR:-dist}"
npm run build -- --outDir "$OUTPUT_DIR" --emptyOutDir
grep -c 'data-role="@wippy/scripts"' "$OUTPUT_DIR/app.html" # must = 1
grep -c 'data-role="@wippy/package"' "$OUTPUT_DIR/app.html" # SHOULD = 1
grep -c '<script type="importmap">' "$OUTPUT_DIR/app.html" # must = 1
grep -c '<wippy-loading' "$OUTPUT_DIR/app.html" # must >= 1
grep 'src="./app.js"' "$OUTPUT_DIR/app.html" # match
# live boot test: serve "$OUTPUT_DIR/" with dev-proxy.js from the target Web Host release.
# Browser: see <wippy-loading>, then dev-overlay FAB → Accept → app boots.
10.7 Browser-emulator dark/light + contrast check (recommended)
Static checks catch token misuse but not actual rendering. Before shipping any non-trivial visual change, verify the app in a browser emulator (Playwright or equivalent) under both dark and light theme, and check contrast on both.
Recommended Playwright recipe:
// dark + light snapshot pair
for (const scheme of ['dark', 'light']) {
await page.emulateMedia({ colorScheme: scheme })
await page.goto('http://localhost:<port>/<route>')
await page.waitForLoadState('networkidle')
await page.screenshot({ path: `.local/snap-${scheme}.png`, fullPage: true })
}
// contrast smoke — flag any element with computed text vs background
// contrast ratio < 4.5 (WCAG AA body) or < 3 (WCAG AA large text).
// Use `axe-core`, `@axe-core/playwright`, or `pa11y` for a real audit.
Verify visually: text legibility on both schemes, no light-only assumptions (white-on-white panels), severity colours readable on both, hover/active states visible in both.
REJECT a page that renders correctly in dark mode but is broken in light mode (or vice versa).
10.8 Final gates
npx vue-tsc --build --force && \
npm test --if-present -- --run && \
npm run build -- --outDir dist --emptyOutDir && \
grep -c 'data-role="@wippy/scripts"' dist/app.html && \
grep -c '<wippy-loading' dist/app.html && \
echo "ALL GATES PASS"
11. Acceptance criteria (REJECT rules)
REJECT a submission if any of the following are true.
Manifest (§3.1, §4.1)
package.json.specificationis not"wippy-component-1.0".wippy.typeis not"page"(micro frontend apps),"widget"(web components, historical), or"component"(web components, newer alias accepted by the vite plugin validator).- Micro Frontend App:
wippy.pathdoes not point to the actual built artifact (e.g.dist/app.html). - WC:
wippy.tagNameis missing or does not contain a hyphen. - WC:
wippy.propsis missing OR has properties withouttype/default/description. 5a. WC:wippy.descriptionis missing OR is a one-line label. It MUST be a verbose usage explanation — see §4.1 / §2.2. peerDependenciesomits an npm package root that the artifact actually imports and expects the host to provide, or includes import-map subpaths as if they were separate npm packages.- A dependency is classified from a remembered package list instead of the pinned target-host import map.
- WC:
dependenciesis missing@wippy-fe/webcomponent-coreor@wippy-fe/webcomponent-vue.
vite.config.ts (§3.2, §4.2)
- Micro Frontend App:
baseis not''. Hardcoded absolute base (e.g./app/keeper/) is REJECT with no documented-exception escape hatch — see §9.5. build.rollupOptions.externalis not exactly every key in the fetched target-hostimportsobject, including currently unused keys.- WC:
build.liblibrary mode is missing ORformats: ['es']is missing. - WC:
build.rollupOptions.preserveEntrySignaturesis notfalse. - A used exact specifier present in the pinned map is bundled instead of externalized.
- A used exact specifier absent from the pinned map is externalized instead
of bundled. This applies to all
@wippy-fe/*andprimevue/*subpaths.
tsconfig.json (§3.8, §4.7)
strictis nottrue.targetis older thanES2020.- Micro Frontend App:
typesis missingvite/clientor@wippy-fe/types-global-proxy. - WC:
typesis missingvite/clientor@wippy-fe/proxy. vue-tscdoes not exit 0.
app.html (§3.3)
- No
<script data-role="@wippy/scripts">. - Host-less
app.htmldoes not contain the completeimportsobject fetched from the same pinned Web Host release. - No
<div id="app">mount. - No
<wippy-loading>(uses custom spinner instead).
Bootstrap (§3.4)
app.tsobtainsconfig/host/api/onfrom anything other than sync@wippy-fe/proxyimports (e.g. reaches forwindow.$W/getWippyApi, orawaits to obtain a getter).app.tsdoes not provideHOST_API,AXIOS_INSTANCE,WIPPY_INSTANCEinjections.app.tsresolves initial path from a non-canonical source (must beconfig.context?.route ?? '/', with documented project-specific extensions).
Router (§3.5, §7)
- An iframe-capable or
autopage bypasses@wippy-fe/router/ portable memory routing. DirectcreateWebHistoryis permitted only for a documented Fragment-only artifact. - Application code reproduces the package-owned memory-history,
afterEach,@history,navId, orsetLocalRoutersynchronization protocol. - Initial host route comes from browser location instead of
config.context?.route. - No catch-all
/:pathMatch(.*)*route OR catch-all has noname.
Proxy & subscriptions (§3.9, §6)
- Any
instance.on(...)at module scope (outsideonMounted). - Any
instance.on(...)whose return value is discarded. - Any
onUnmountedblock missing the matching unsubscribe call(s). - Any reference to
instance.off(...)(the method does not exist). - Any
window.addEventListener('message', ...)without matchingremoveEventListenerinonUnmounted. - Any raw
new EventSource(...). - Any raw
axios.create(...). - Any
window.confirm(...).
Styling (§3.7, §5, §4.4)
html, body, #appset non-zero padding/margin.styles.cssredefines--p-content-background,--p-text-color,--p-content-border-color,--p-primary-color, or--p-surface-*at module scope. 42b. ANY child-app.cssfile contains:root { --p-* … }or:root { --<other-host-var> … }redefinition (§5.1.2). Move to facade theming or per-page YAMLconfig_overrides.customization.cssVariables.- PrimeVue component tokens are restyled with
!importantinstyles.css. 43a. A child module duplicates shared PrimeVue appearance in local.p-*rules. Move shared appearance to facadecustom_css; retain module CSS only for justified domain layout or novel structure. - Any rendered UI uses palette seed
var(--p-primary)where the mode-aware semantic consumer--p-primary-coloris required. - Any Vue file uses raw Tailwind color names (
bg-red-*,bg-sky-*, etc.) for semantic meaning. - Any hardcoded hex/rgb in Vue source for semantic colors (use
--p-danger-*etc., orcolor-mix()). 46a. Page renders correctly in only one ofprefers-color-scheme: dark/light. Verify in a browser emulator before claiming the page is shippable (§10.7). 46b. Custom Vue component reimplements something PrimeVue already ships (e.g. custom dropdown when<Select>exists, custom modal when<Dialog>exists, custom toast when<Toast>exists, custom confirm when<ConfirmDialog>exists). Use the PrimeVue component, possibly with §5.0 level-1 / level-2 customization.
Iconography (§5.6)
46c. Reusable iconography uses raw <svg> instead of <Icon> from @iconify/vue.
46d. Custom icon collection registered via addCollection() outside the canonical app.ts bootstrap path.
46e. Icon font CSS (Tabler-icons-font, Material Icons font, FontAwesome CSS, etc.) shipped alongside Iconify.
Vue/TS hygiene (§3.8)
.vuefile does not use<script setup lang="ts">.definePropsuses untyped object syntax instead of TS generic.- Production code contains
console.log.
Web components (§4)
- WC root element has padding or margin.
- WC does not extend
WippyVueElementorWippyElement. - WC
static get wippyConfig()is missing OR doesn't returnpropsSchema/hostCssKeys/inlineCss. - WC
static get vueConfig()is missing OR doesn't returnrootComponent. - WC entry does not call
define(import.meta.url, ElementClass)at module level. - WC has
wippy.path(page-only field) orwippy.proxy(page-only block).
Build pipeline (§8)
55a. Module ships Makefile without matching make.bat + make.ps1 wrappers (§8.2).
Accessibility (§3.8)
- An icon-only native button or PrimeVue
<Button>lacks a stable accessible name such asaria-label. - A clickable non-interactive element is used as a control. In shipped Vue product UI use the applicable PrimeVue control with keyboard semantics and a stable accessible name; native controls are reserved for explicitly static/non-Vue examples or a documented semantic gap.
12. Known intentional deviations
When you knowingly diverge from canonical, record the reason in the project's maintained engineering guidance or audit record. Do not assume a specific agent-instruction filename. Real examples:
| Deviation | Reason | Acceptable? |
|---|---|---|
createPinia() registered but no defineStore yet |
Reserved for upcoming stores | BORDERLINE — clean up if no stores planned |
| Native control in a shipped Vue product surface | PrimeVue lacks the required semantics and the exception documents accessibility and design impact | RARELY YES |
Custom inlineCssPlugin in vite.config |
Single-file deployment | YES |
Raw localStorage.* for ad-hoc persistence keys |
Avoid pinia overhead for one or two keys | DISCOURAGED. Prefer the canonical stack: facade module owns theme; @wippy-fe/router factory owns route restoration; @wippy-fe/pinia-persist owns durable state. Raw localStorage should be a measured exception in a leaf component, not the default. |
Skip wippyPagePlugin() |
Want a very-good-reason: e.g. shipping a host-only bundle that explicitly does not support host-less dev | RARELY YES. Default is to include it. Record the reason in maintained project guidance. |
13. Tooling gotchas
13.1 Wippy CLI port already in use (:8080, :5173)
Symptom: EADDRINUSE when starting ./wippy.exe run -c.
Fix: override the gateway port via the -o flag. Examples:
# Pick a different port for the wippy gateway:
./wippy.exe run -c -o app:gateway:addr=:8086
# Combine multiple overrides — gateway port + facade fe_facade_url default:
./wippy.exe run -c -o app:gateway:addr=:9000 -o wippy.facade:fe_facade_url:default=http://localhost:5173
The -o <module>:<entry>:<property>=<value> form patches the registry entry's property at boot — no source edits required. To set a requirement default (rather than overriding a configured value), use the :default suffix on the property name.
For Vite (5173), kill the existing process or choose a different port via vite --port <n>.
13.2 Persistent app.db
Symptom: migration on first run succeeds, on second run fails with "table already exists".
Fix: delete .wippy/app.db* between fresh runs. For test harnesses, prefer :memory:.
13.3 npm ERESOLVE after @wippy-fe/* bump
Symptom: npm install fails with ERESOLVE after bumping one package in the @wippy-fe/* family without aligning its peers.
Fix: delete node_modules/ AND package-lock.json, then npm install.
13.4 Importmap drift
Symptom: Failed to resolve module specifier 'pinia'.
Fix: refresh the pinned target-host import map, use all of its keys as Vite
externals, copy its complete imports object into host-less app.html, and
bundle any imported exact specifier that is absent. Keep peer dependencies to
the imported npm roots actually expected from the host. See §8.3.
14. Template validation policy
Repository templates are examples, not an authority that can override this contract. Validate each template against the pinned Web Host import map and the current checklist before calling it compliant. A stale template must be fixed or explicitly marked non-compliant; the checklist must not grant it an automatic "gold standard" exception.
Appendix A — Window globals & DOM markers
Constants exported from @wippy-fe/shared:
| Constant | Value | Who writes | Who reads |
|---|---|---|---|
GLOBAL_CONFIG_VAR |
__WIPPY_APP_CONFIG__ |
host entry point | child app, dev-proxy |
GLOBAL_PROXY_CONFIG_VAR |
__WIPPY_PROXY_CONFIG__ |
host | dev-proxy boot |
GLOBAL_API_PROVIDER |
__WIPPY_APP_API__ |
host | child app |
GLOBAL_WEB_COMPONENT_CACHE |
__WIPPY_WEB_COMPONENT_CACHE__ |
wc loader | wc loader |
WIPPY_SCRIPTS_DATA_ROLE |
@wippy/scripts |
author (in app.html) |
host injects scripts adjacent to it |
WIPPY_PACKAGE_DATA_ROLE |
@wippy/package |
@wippy-fe/vite-plugin (build time) |
dev-proxy boot |
Authors should NEVER reference window.__WIPPY_* directly — always import from @wippy-fe/shared (constants) or use @wippy-fe/proxy API helpers.
Appendix B — HostApi method signatures
interface HostApi {
toast(opts: ToastMessageOptions): void
confirm(opts: LimitedConfirmationOptions): Promise<boolean>
startChat(token: string, opts?: { sidebar?: boolean }): void
openSession(uuid: string, opts?: { sidebar?: boolean }): void
openArtifact(uuid: string, opts?: { target: 'modal' | 'sidebar' }): void
setContext(
context: Record<string, unknown>,
sessionUUID?: string,
source?: { type: string; uuid: string; instanceUUID?: string },
): void
navigate(url: string): void
onRouteChanged(internalRoute: string, navId?: number): void
handleError(code: 'auth-expired' | 'other', error: Record<string, unknown>): void
formatUrl(relativeUrl: string): string
classifyLink(href: string | null | undefined): LinkClassification
layout: LayoutApi
logout(): void
}
LayoutApi:
interface LayoutApi {
readonly snapshot: LayoutSnapshot | null
resizePanel(panelId: string, size: SizeValue): void
collapsePanel(panelId: string): void
expandPanel(panelId: string): void
openDrawer(panelId: string): void
closeDrawer(panelId: string): void
toggleDrawer(panelId: string): void
movePanel(panelId: string, target: PanelTarget): void
removePanel(panelId: string): void
updatePanel(panelId: string, def: Partial<HostPanelDef>): void
openModal(id: string, def: HostModalDef): void
closeModal(modalId: string): void
addFloating(id: string, def: HostFloatingDef): void
removeFloating(floatingId: string): void
broadcast(channel: string, payload: unknown): void
send(target: string, channel: string, payload: unknown): void
on(channel: string, handler: (env: BroadcastEnvelope) => void): () => void
}
host.layout is always present (a LayoutApi object). Outside managed-layout mode, host.layout.snapshot is null and all mutation/bus calls are silent no-ops — gate on host.layout.snapshot (or isManaged from useWippyLayout) before mutating, not a null-check on host.layout.
Appendix C — ProxyConfig.injections reference
interface ProxyConfig {
enabled: boolean
injections: {
css: {
themeConfig: boolean // semantic CSS vars
iframe: boolean // default themed scrollbar consistency
primevue: boolean // PrimeVue component CSS
markdown: boolean // markdown typography
customCss: boolean // theming.global.customCSS
customVariables: boolean // theming.global.cssVariables → effective Auto/forced mode blocks
}
tailwindConfig: boolean // window.tailwind.config
resizeObserver: boolean // report iframe size
preventLinkClicks: boolean // intercept <a> clicks
iconifyIcons: boolean // register iconify-icon WC + icons
refreshWhenVisible: boolean // reload on @visibility(true)
historyPolyfill: boolean // history() stub (always installed)
errorCapture: boolean // unhandledrejection + onerror → host
}
}
The YAML registry-entry uses top-level meta.proxy, while nested keys under
the injections wrapper retain lower camelCase, matching frontend
package.json and runtime AppConfig. The host deep-merges the YAML over
bundled wippy.proxy:
meta:
type: view.page
# ...
proxy:
enabled: true
injections:
css:
themeConfig: true
iframe: true
primevue: true
customCss: true
customVariables: true
tailwindConfig: true
iconifyIcons: true
CSS is applied in this logical order: themeConfig → primevue/tailwind → iframe → markdown → customVariables → customCss. The custom variable and CSS layers use the runtime's override mechanism and win over ordinary document stylesheets; do not depend on a particular <head> insertion position.
Cross-references
- micro-frontend-app.md — detailed micro-frontend-app authoring guide
- web-component.md — web-component authoring guide
- proxy-api.md — full HostApi + instance.on() reference
- host-less-mode.md — host-less boot in detail
- theming.md — three-level theming guide
- build-system.md — build pipeline details