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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/facade/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Run `make sync` after every Wippy Web Host version bump to pull fresh copies.

# Web Host CDN base — update version when Wippy Web Host releases
WEB_HOST_CDN = https://web-host.wippy.ai/webcomponents-1.0.30
WEB_HOST_CDN = https://web-host.wippy.ai/webcomponents-1.0.32

# Files to sync from CDN into public/@wippy-fe/
CDN_FILES = loading.js
Expand Down
6 changes: 3 additions & 3 deletions src/facade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ These fields are NOT configurable via requirements — they are computed at runt

| Requirement | Default | Description |
|---|---|---|
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.30` | CDN base URL for the Web Host frontend bundle |
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.32` | CDN base URL for the Web Host frontend bundle |
| `fe_entry_path` | `/iframe.html` | Iframe HTML entry point path (appended to `fe_facade_url`) |
| `fe_mode` | `compat` | `compat` (default — loads `module.js`) or `managed` (loads `managed-layout.js` for declarative multi-panel apps). See [Modes](#modes) above |

Expand Down Expand Up @@ -280,9 +280,9 @@ Scripts are fetched in parallel and awaited before the Web Host bundle is import

```json
{
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.30",
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.32",
"iframe_origin": "https://web-host.wippy.ai",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.30/iframe.html?waitForCustomConfig",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.32/iframe.html?waitForCustomConfig",
"login_path": "/login.html",
"login_redirect_param": null,
"env": {
Expand Down
2 changes: 1 addition & 1 deletion src/facade/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ entries:
targets:
- entry: wippy.facade:fe_facade_url
path: .default
default: https://web-host.wippy.ai/webcomponents-1.0.30
default: https://web-host.wippy.ai/webcomponents-1.0.32

- name: fe_entry_path
kind: ns.requirement
Expand Down
2 changes: 1 addition & 1 deletion src/facade/config_handler_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ local function define_tests()
end)

test.it("extracts iframe origin from facade URL", function()
local facade_url = "https://web-host.wippy.ai/webcomponents-1.0.30"
local facade_url = "https://web-host.wippy.ai/webcomponents-1.0.32"
local origin = facade_url:match("^(https?://[^/]+)")

test.eq(origin, "https://web-host.wippy.ai")
Expand Down
182 changes: 182 additions & 0 deletions src/views/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,185 @@
[wippy-documentation]: https://docs.wippy.ai
[releases-page]: https://github.com/wippyai/module-views/releases
[wippy-framework]: https://github.com/wippyai/framework

---

## Migrating to 0.4.32: bundled `wippy-meta.json` (Web Host ≥ 1.0.31)

Starting in `wippy/views@0.4.32`, the three view-metadata endpoints
(`/pages/content/{id}`, `/components/list`, `/components/by-tag/{tag}`)
read the consumer build's `wippy-meta.json` as the source for fields the
FE author owns. The YAML registry entry remains the **authoritative
abstraction**: it is read first, field by field, and bundled meta fills
only the gaps. The pre-0.4.32 YAML-only synthesis path stays in place as
a fallback — entries that ship no `wippy-meta.json` continue to work
unchanged.

### Resolution rule

**YAML always wins on a field-by-field basis.** Bundled meta is the
fallback for fields YAML omits. For every field below, the lookup order
is `YAML → bundled wippy-meta.json → legacy default`.

This means you can:
- **Slim a YAML entry** by removing fields the consumer's `package.json`
already declares — the bundled meta will fill them in transparently.
- **Override any field from YAML** by setting it explicitly in the
registry entry — operators always retain the last word.
- **Compose a themed variant** by re-pointing a registry entry at an
existing artifact's URL and adding `meta.config_overrides` for the
per-variant customization payload — the overlay deep-merges over the
bundled `wippy.configOverrides`.

### Bundled `wippy-meta.json` — how to produce it

Use [`@wippy-fe/vite-plugin`][wippy-fe-vite-plugin] ≥ 0.0.31 in the
consumer's `vite.config.ts`:

```ts
import { wippyPagePlugin } from '@wippy-fe/vite-plugin' // for view.page
// OR
import { wippyComponentPlugin } from '@wippy-fe/vite-plugin' // for view.component

export default defineConfig({
plugins: [/* ... */, wippyPagePlugin()],
})
```

Both plugins emit `dist/wippy-meta.json` — the consumer's `package.json`
with every `file://<relative-path>` reference resolved at build time.
Non-Vite consumers (rollup, esbuild, hand-built) can produce the same
file by any equivalent build step — see
[`web_components.spec.md`][spec] § "Package metadata: source of truth"
for the contract.

[wippy-fe-vite-plugin]: https://www.npmjs.com/package/@wippy-fe/vite-plugin
[spec]: https://github.com/wippyai/gen-2-chat/blob/main/web_components.spec.md

### Field-by-field mapping

#### `view.page` → `GET /pages/content/{id}` (wippy-component-1.0 spec response)

| Response field | YAML source (priority 1) | Bundled meta source (priority 2) | Legacy default (priority 3) |
|---|---|---|---|
| `name` | `meta.name` | `<pkg>.name` (e.g. `@org/my-page`) | entry id |
| `version` | — | `<pkg>.version` | `"1.0.0"` |
| `specification` | — | `<pkg>.specification` | `"wippy-component-1.0"` |
| `title` | `meta.title` | `<pkg>.wippy.title` → `<pkg>.title` | `""` |
| `baseUrl` | (always computed from `meta.url` + `meta.base_path` + `PUBLIC_API_URL` — operator/routing decision) | | |
| `wippy.type` | (always `"page"`) | | |
| `wippy.path` | `meta.entry_point` | `<pkg>.wippy.path` | `"index.html"` |
| `wippy.proxy` | — | `<pkg>.wippy.proxy` (whole block) | `synthesize_from_registry` builds from snake_case `data.proxy.*` |
| `wippy.configOverrides` | `meta.config_overrides` deep-merged ON TOP of bundled (YAML wins per nested key) | `<pkg>.wippy.configOverrides` | `nil` |

#### `view.component` → `GET /components/list` and `GET /components/by-tag/{tag}`

| Response field | YAML source (priority 1) | Bundled meta source (priority 2) | Legacy default (priority 3) |
|---|---|---|---|
| `id` | (always the registry entry id) | | |
| `name` | `meta.name` | `<pkg>.name` | `""` |
| `title` | `meta.title` | `<pkg>.wippy.title` → `<pkg>.title` | `""` |
| `tag_name` | `meta.tag_name` | `<pkg>.wippy.tagName` | `nil` |
| `base_url` | (always computed from `meta.url` + `meta.base_path` — operator/routing decision) | | |
| `entry_point` | `meta.entry_point` | `<pkg>.browser` (the wippy-component-1.0 spec field for ESM module / component packages) | `"index.js"` |
| `auto_register` | (always from `meta.auto_register` — deployment policy) | | |
| `props` | `meta.props` | `<pkg>.wippy.props` | `nil` |
| `events` | `meta.events` | `<pkg>.wippy.events` | `nil` |

### What to keep in YAML vs what to drop

#### YAML MUST keep (registry routing + deployment policy)

These are decisions the operator makes at registration time. They are not
in the consumer build:
- `name:` (the registry entry id — short slug, e.g. `iframe-demo`)
- `kind: registry.entry`
- `meta.type` (`view.page` / `view.component`)
- `meta.url`, `meta.base_path` (where to serve the bundle from)
- `meta.announced`, `meta.secure` (visibility + auth)
- `meta.auto_register` (component-only; deployment policy)
- `meta.mountRoute` (page-only; gen-2-chat routing)
- `meta.icon`, `meta.order`, `meta.group*` (UI metadata)
- `meta.config_overrides` (variant overlay payload — see below)

#### YAML CAN drop (covered by bundled `wippy-meta.json`)

If your build emits `wippy-meta.json`, these YAML fields are redundant
and can be removed:
- `meta.title` → comes from `package.json` `wippy.title` or top-level `title`
- `meta.tag_name` → comes from `package.json` `wippy.tagName`
- `meta.entry_point` → comes from `package.json` `wippy.path` (for `view.page`) or top-level `browser` (for `view.component`). Per [wippy-component-1.0 spec][spec], these are the canonical fields per package kind — `browser` is for ESM module / component entries, `wippy.path` is for HTML page entries. They are NOT interchangeable.
- `meta.props`, `meta.events` → come from `package.json` `wippy.props` / `wippy.events`
- `data.proxy` (the snake_case legacy proxy injection block at the top of the entry data) → comes from `package.json` `wippy.proxy` (camelCase). **Note**: there is NO camelCase YAML override for `wippy.proxy`. If you need a per-entry proxy override, declare it inside the consumer's `package.json` or fork the artifact.

#### YAML overlay pattern (variants)

To declare a themed variant that reuses an artifact, point the registry
entry at the same `url` + `base_path` and add `meta.config_overrides` with
the per-variant customization. The YAML keys MUST be camelCase to merge
correctly with the bundled `wippy.configOverrides`:

```yaml
- name: iframe-demo-themed
kind: registry.entry
meta:
type: view.page
name: iframe-demo-themed
title: Iframe Demo (Custom Palette)
url: /app
base_path: app/iframe-demo
mountRoute: /demo-themed/:part(.*)*
# Variant overlay — deep-merged ON TOP of bundled wippy.configOverrides.
# YAML wins per nested key (override-only the colors you care about).
config_overrides:
customization:
cssVariables:
"--p-primary": "#7c9ed9"
"--p-secondary": "#b4a7d6"
customCSS: |
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap');
:root, body, * { font-family: 'Quicksand', sans-serif !important; }
```

### Deprecation warnings

On the first request that observes an entry without a bundled
`wippy-meta.json`, the views module logs (once per `wippy.views`
process via a shared memory store):

> `[wippy.views] {kind} '{entry-id}' has no wippy-meta.json at {url} — falling back to YAML synthesis. Adopt @wippy-fe/vite-plugin so the consumer's package.json wippy block is the canonical source.`

The synthesis fallback is preserved indefinitely for legacy entries,
but the warning is your signal to migrate the consumer.

### Layer boundaries: who owns what

This is the principle the rest of the migration follows:

- **`package.json` is the FE-author's view. Source-relative, BE/router/FS agnostic.**
- `wippy.path: "dist/app.html"` (for pages) and `browser: "dist/index.js"` (for components) are CORRECT — they describe where the file lives in the build output, relative to the package root. The consumer doesn't know (and shouldn't need to know) where the operator will serve it.
- **YAML is the operator's view. Deployment-aware.**
- The operator knows the served layout (e.g. vite's `--outDir static/wc/foo/` flattens away the `dist/` prefix), and YAML's `meta.entry_point` is where the operator declares that adjustment. YAML overrides bundled meta on this field — the typical pattern is `meta.entry_point: app.html` when the package.json says `wippy.path: "dist/app.html"`.

This separation is what lets a single consumer build ship to multiple operator deployments with different served-layout shapes without rebuilding.

### Migration checklist (per registered entry)

1. Update the consumer's `vite.config.ts` to add `wippyPagePlugin()` (page) or `wippyComponentPlugin()` (component).
2. **Leave `package.json` entry paths source-relative** — `wippy.path: "dist/app.html"` for pages, top-level `browser: "dist/index.js"` for components. The FE author shouldn't encode the operator's deployment layout in their package manifest.
3. Build the consumer — confirm `dist/wippy-meta.json` is emitted.
4. Deploy the new build (the published wippy module or the served static dir).
5. **Set `meta.entry_point` in YAML to match the SERVED layout.** If your build pipeline strips the `dist/` prefix (typical with vite's `--outDir <served-path>`), declare it in YAML: `meta.entry_point: app.html` (for pages) or `meta.entry_point: index.js` (for components). YAML wins, bundled meta is the fallback for fields YAML omits — so this works regardless of what `package.json` declared.
6. Restart wippy and observe that the deprecation warning for this entry stops firing.
7. Slim the YAML registry entry — you can drop fields covered by bundled meta (`title`, `props`, `events`, `data.proxy`). Keep routing + policy fields, and keep `meta.entry_point` for the deployment-aware path adjustment. **`tag_name` for components**: optional — if omitted, views falls back to a scan of bundled meta files matching `wippy.tagName`. Keep it in YAML if you want the O(1) registry index lookup; drop it if the consumer's `wippy.tagName` is the only source of truth you want.
8. Restart wippy. Probe the API endpoint and confirm the response shape is unchanged from before slimming.

### What's NOT in scope for 0.4.32

- **No structural changes to YAML schema.** Existing YAML entries keep working without any edit.
- **No removal of the synthesize fallback.** Legacy entries (no `wippy-meta.json`) render exactly as before.
- **No automatic case conversion.** YAML `meta.config_overrides.*` and bundled `wippy.configOverrides.*` are merged key-for-key; both sides MUST use camelCase for the customization payload (already the convention in both layers).

[releases-page]: https://github.com/wippyai/module-views/releases
[wippy-documentation]: https://docs.wippy.ai
[wippy-framework]: https://github.com/wippyai/framework
30 changes: 30 additions & 0 deletions src/views/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,36 @@ entries:
source: file://resource_registry.lua
modules:
- registry
- name: bundled_meta
kind: library.lua
meta:
comment: Fetches consumer-built wippy-meta.json (file:// refs resolved at build time)
description: |
Reads the wippy-meta.json that @wippy-fe/vite-plugin emits next to a
consumer's app.html / index.js. Used by /pages/content/{id},
/components/list, and /components/by-tag/{tag} as the single source
of truth for view.page / view.component metadata when present.
Callers fall back to YAML-synthesis when fetch returns nil + nil.
A process-wide deprecation warning fires the FIRST time an entry is
seen without bundled meta (deduplicated via the shared `bundled_meta_warn`
memory store so each handler-pool worker doesn't re-warn the same id).
source: file://bundled_meta.lua
imports:
page_registry: wippy.views:page_registry
component_registry: wippy.views:component_registry
modules:
- http_client
- json
- store
- name: bundled_meta_warn
kind: store.memory
meta:
comment: |
Process-wide dedup of "no wippy-meta.json" deprecation warnings.
Each entry id is logged at most once per wippy process lifetime,
regardless of which handler-pool worker observes the miss.
max_size: 1000
cleanup_interval: "1h"
- name: dep.wippy.test
kind: ns.dependency
meta:
Expand Down
3 changes: 3 additions & 0 deletions src/views/api/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ entries:
source: file://list_components.lua
imports:
component_registry: wippy.views:component_registry
bundled_meta: wippy.views:bundled_meta
method: handler
modules:
- http
Expand All @@ -58,6 +59,7 @@ entries:
source: file://find_by_tag.lua
imports:
component_registry: wippy.views:component_registry
bundled_meta: wippy.views:bundled_meta
method: handler
modules:
- http
Expand All @@ -84,6 +86,7 @@ entries:
page_registry: wippy.views:page_registry
renderer: wippy.views:renderer
resource_registry: wippy.views:resource_registry
bundled_meta: wippy.views:bundled_meta
method: handler
modules:
- http
Expand Down
55 changes: 42 additions & 13 deletions src/views/api/find_by_tag.lua
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
local http = require("http")
local component_registry = require("component_registry")
local bundled_meta = require("bundled_meta")

-- GET /components/by-tag/{tag} — resolve a custom-element tag name to its
-- registered view.component metadata. Used by the host proxy SDK's
-- loadByTagName() to load a peer WC without the consumer knowing any URL.
--
-- Response shape mirrors a single item from /components/list (id, name,
-- title, tag_name, base_url, entry_point, auto_register, props, events).
-- Source-of-truth: prefer the bundled wippy-meta.json (the consumer's
-- package.json `wippy` block, emitted at build time by
-- @wippy-fe/vite-plugin) for `props`, `events`, `tag_name`, `entry_point`,
-- `name`, `title`. Falls back to the YAML registry entry's `meta.*` fields
-- when bundled meta is missing or omits the specific field. See
-- `bundled_meta.project_component_response` for the per-field mapping.

type ComponentResponse = {
id: string,
name: string,
title: string,
description: string?,
tag_name: string?,
base_url: string?,
entry_point: string?,
Expand All @@ -38,7 +44,33 @@ local function handler()
return
end

-- Fast path: registry index lookup by meta.tag_name. This is the
-- common case — every entry that declares its tag in YAML hits here
-- with a single registry query.
local component, err = component_registry.find_by_tag_name(tag)

-- Slow path: a consumer that ships `wippy.tagName` in its bundled
-- wippy-meta.json but does NOT declare `meta.tag_name` in the YAML
-- registry entry will miss the fast path (the registry has no key
-- to match against). Scan all view.components, fetch each one's
-- bundled meta, and find the entry whose `wippy.tagName` matches.
-- Linear in the number of view.components × one HTTP self-fetch
-- each — first miss for any tag, then served from the bundled_meta
-- cache on subsequent calls.
if not component then
local all, all_err = component_registry.find_all()
if not all_err and all then
for _, c in ipairs(all) do
local meta = bundled_meta.fetch_for_component(c)
if meta and type(meta.wippy) == "table" and meta.wippy.tagName == tag then
component = c
err = nil
break
end
end
end
end

if err or not component then
res:set_status(http.STATUS.NOT_FOUND)
res:write_json({
Expand Down Expand Up @@ -67,17 +99,14 @@ local function handler()
return
end

local component_info: ComponentResponse = {
id = type(component.id) == "string" and component.id or tostring(component.id),
name = type(component.name) == "string" and component.name or "",
title = type(component.title) == "string" and component.title or "",
tag_name = type(component.tag_name) == "string" and component.tag_name as string or nil,
base_url = component_registry.resolve_base_url(component),
entry_point = type(component.entry_point) == "string" and component.entry_point as string or nil,
auto_register = component.auto_register == true,
props = component.props,
events = component.events,
}
local meta, meta_err = bundled_meta.fetch_for_component(component)
if meta_err then
print("[views/find_by_tag] bundled_meta fetch error for tag=" .. tostring(tag) .. ": " .. tostring(meta_err))
meta = nil
end

local base_url = component_registry.resolve_base_url(component)
local component_info = bundled_meta.project_component_response(meta, component, base_url)

res:set_content_type(http.CONTENT.JSON)
res:set_status(http.STATUS.OK)
Expand Down
Loading
Loading