# Architecture — `ui-extensions-sdk` This repository builds the **App SDK** and publishes it to npm as `@contentful/app-sdk` (preferred) and `contentful-ui-extensions-sdk` (legacy alias). Everything below describes the codebase in this repo and the runtime it ships. ## Overview The App SDK is the **bridge** between an app running in a sandboxed iframe (the *child frame*, customer code) and the Contentful Web App (the *parent frame*, host). It exposes a typed, Promise-based JavaScript API that wraps PostMessage interactions with the host. Apps call `init()`, receive a location-specific SDK object, and use it to read/write content, open dialogs, navigate, and (in newer surfaces) interact with AI agents. Inception: 2015-11-02 (`d21d1f9` `Initial commit`). Core transport pattern is unchanged; new locations and APIs are layered on top. ## System Context ```mermaid graph LR CWA["Contentful Web App
(parent frame)"] WR["Widget Renderer"] IF["Customer App iframe
(child frame)"] SDK["@contentful/app-sdk
(this repo)"] CMA["Contentful Management API"] IF -.uses.-> SDK CWA --> WR WR -- "postMessage(connect)" --> IF IF -- "postMessage(call/send)" --> CWA CWA -- "postMessage(response/event)" --> IF SDK -- "via channel" --> CMA ``` The SDK never talks to Contentful APIs directly. CMA calls flow through the channel: `app code → sdk.cma.() → cmaAdapter → channel.call('CMAAdapterCall', opts) → web app → CMA`. This indirection lets the host inject auth, environment, and release context without the app handling tokens. ## Module Map (`lib/`) | Module | Role | Key surface | |---|---|---| | `index.ts` | Public entry: re-exports types and exposes `init` + `locations` | `init`, `locations`, all public types | | `initialize.ts` | Listens for the host's `connect` message, builds the SDK, and hands it to the user's callback | `createInitializer` | | `channel.ts` | PostMessage send/receive; correlates request IDs to response promises; dispatches host events to handlers | `Channel`, `connect`, `sendInitMessage` | | `signal.ts` | Tiny event emitter; `MemoizedSignal` replays the latest value to new subscribers | `Signal`, `MemoizedSignal` | | `api.ts` | Composes the location-specific SDK by reducing a list of "producer" functions | `createAPI`, `LOCATION_TO_API_PRODUCERS` | | `locations.ts` | The `locations` runtime enum exported to consumers | `LOCATION_*` keys | | `entry.ts` | Entry-level API (`getSys`, `publish`, `save`, `onSysChanged`, fields, tasks, metadata) | `createEntry` | | `field.ts` | Per-content-type field; multi-locale wrapper | `makeField` | | `field-locale.ts` | Single field+locale: `getValue`, `setValue`, `onValueChanged`, validation/disabled signals | `makeFieldLocale` | | `editor.ts` | Entry editor settings: locale settings, hidden-field visibility | `createEditor` | | `space.ts` | Legacy CRUD wrappers (deprecated since v4.0.0) — every method warns and delegates to channel | `createSpace` | | `dialogs.ts` | System dialogs (alert/confirm/prompt) and entity selectors (entries, assets, experiences, patterns, component definitions) | `createDialogs` | | `navigator.ts` | Open/navigate to entries, assets, app pages, app config; release-aware | `createNavigator` | | `app.ts` | App lifecycle hooks for the config screen: `setReady`, `onConfigure` (preInstall), `onConfigurationCompleted` (postInstall) | `createApp` | | `agent.ts` | AI agent location: context updates, toolbar actions, layout variant control | `createAgent` | | `asset.ts` | Asset-side methods on the asset sidebar location | `createAsset` | | `cma.ts` | Wraps `contentful-management` `createClient` with the channel-backed adapter | `createCMAClient` | | `cmaAdapter.ts` | The channel-backed `Adapter` for `contentful-management` | `createAdapter` | | `window.ts` | iframe height management: `MutationObserver`-driven auto-resize + manual `updateHeight` | `createWindow` | | `types/` | Public TypeScript surface — no runtime code | All `*AppSDK` types, `ConnectMessage`, `IdsAPI`, etc. | | `utils/deferred.ts` | Promise-with-external-resolve helper used by `initialize.ts` | `createDeferred` | ## Data Flow: Initialization Handshake ``` [Host] [App iframe] │ │ │ ◄─── connect listener ──────│ channel.ts:waitForConnect │ │ installs message listener │ ◄── postMessage(init) ──────│ channel.ts:sendInitMessage │ │ │── postMessage("connect", [params, queue]) ─► │ initialize.ts:connectDeferred resolves │ │ api.ts:createAPI builds SDK │ │ user's init callback invoked │ │ │── postMessage() ────────────────►│ channel.ts:_handleMessage routes ``` The SDK *connects immediately on import*, before `init()` is called, so it doesn't lose host messages that arrive before the app is ready (`initialize.ts:21-32`). User callback fires after the connect handshake completes. ## Data Flow: A Field Read ``` app code field-locale.ts channel.ts host │ getValue() ────►│ │ │ │ │ returns memoized value │ │ │◄────────────────│ │ │ ... host emits valueChanged event │ │ ◄── addHandler ─────────│ │ │ onValueChanged ─│ │ │ │ (handler) │ │ ◄── postMessage ─│ │ │ signal.dispatch │ │ │◄── handler ─────│ │ │ ``` Reads of "current value" are local (memoized in `MemoizedSignal`); writes (`setValue`) and most other operations round-trip through the channel. ## Build Pipeline ``` lib/**/*.ts │ ├─► tsc --noEmit (npm run check-types — gate) │ └─► rollup (rollup.config.js) ├─► dist/cf-extension-api.js UMD, target=ES5, terser-minified ├─► dist/cf-extension-api.bundled.js UMD with deps inlined for CDN consumption ├─► dist/index.d.ts tsc-emitted ambient types └─► dist/cf-extension.css (legacy stylesheet, kept for back-compat) ``` - **Target ES5** for broadest browser compatibility (`tsconfig.json#target`); customers loading via `