@persona/layout-editor-remote-module 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,8 +139,11 @@ The bridge's security model is intentionally narrow:
139
139
  first trusted origin for the lifetime of the bridge.
140
140
  - The iframe derives the ready target from `ancestorOrigins` or the referrer, and the host retries
141
141
  `init` until the frame answers.
142
- - Messages use a strict, versioned protocol. Credentials remain in the iframe URL and are never
143
- handled through bridge messages.
142
+ - Messages use a versioned protocol that stays compatible with frames built against older releases
143
+ (from the release after 0.5.1): receivers ignore message types they don't know and strip keys they
144
+ don't know, and fields are never removed, renamed or narrowed. `context.layoutType` and
145
+ `defaultConfig`'s `layoutType` can be layout types added later, so handle values you don't know.
146
+ Credentials remain in the iframe URL and are never handled through bridge messages.
144
147
  - `extraParams` rejects reserved names and credential-like keys.
145
148
 
146
149
  ### Consuming a manifest
@@ -1,4 +1,4 @@
1
- import { n as Json, t as RemoteModuleContext } from "./protocol-edoRdXpF.mjs";
1
+ import { n as Json, t as RemoteModuleContext } from "./protocol-BljV-Zhi.mjs";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/bridge/origin.d.ts
@@ -1,4 +1,4 @@
1
- import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-Dz3ZX59L.mjs";
1
+ import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-BgGlnuFn.mjs";
2
2
  import { Component, useEffect, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as Json, t as RemoteModuleContext } from "./protocol-edoRdXpF.mjs";
1
+ import { n as Json, t as RemoteModuleContext } from "./protocol-BljV-Zhi.mjs";
2
2
  import { CSSProperties, ReactNode } from "react";
3
3
 
4
4
  //#region src/bridge/host/buildModuleSrc.d.ts
@@ -1,4 +1,4 @@
1
- import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-Dz3ZX59L.mjs";
1
+ import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-BgGlnuFn.mjs";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
@@ -95,7 +95,8 @@ type ModuleCategory = z.infer<typeof moduleCategorySchema>;
95
95
  * modules.
96
96
  */
97
97
  interface RemoteModuleDefaultConfigOptions {
98
- layoutType: LayoutType;
98
+ /** Can be a layout type added after the module's release; handle values you don't know. */
99
+ layoutType: LayoutType | (string & {});
99
100
  position?: {
100
101
  x: number;
101
102
  y: number;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as JsonSchemaObject, c as ModuleCategory, d as RemoteModuleExports, f as RemoteModuleRegistrationManifestV1, i as Json, l as RemoteModuleDefaultConfigOptions, m as remoteModuleRegistrationManifestV1Schema, n as LayoutModuleEntry, o as LayoutType, p as RemoteModuleRegistrationManifestV1Schema, r as defineLayoutModule, s as ModuleBadge, t as LayoutModuleDefinitionInput, u as RemoteModuleDelivery } from "./define-C-3P4CFz.mjs";
1
+ import { a as JsonSchemaObject, c as ModuleCategory, d as RemoteModuleExports, f as RemoteModuleRegistrationManifestV1, i as Json, l as RemoteModuleDefaultConfigOptions, m as remoteModuleRegistrationManifestV1Schema, n as LayoutModuleEntry, o as LayoutType, p as RemoteModuleRegistrationManifestV1Schema, r as defineLayoutModule, s as ModuleBadge, t as LayoutModuleDefinitionInput, u as RemoteModuleDelivery } from "./define-D0nvEX_B.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/manifest-path.d.ts
@@ -122,41 +122,46 @@ const variableMapSchema = z.record(z.string(), z.union([z.string(), z.number()])
122
122
  const readySchema = z.object({
123
123
  ...envelopeSchema,
124
124
  type: z.literal("ready")
125
- }).strict();
125
+ });
126
126
  const requestDataSchema = z.object({
127
127
  ...envelopeSchema,
128
128
  type: z.literal("request-data"),
129
129
  requestId: requestIdSchema
130
- }).strict();
130
+ });
131
131
  const setHeightSchema = z.object({
132
132
  ...envelopeSchema,
133
133
  type: z.literal("set-height"),
134
134
  height: z.number()
135
- }).strict();
135
+ });
136
136
  const renderErrorSchema = z.object({
137
137
  ...envelopeSchema,
138
138
  type: z.literal("render-error"),
139
139
  message: z.string()
140
- }).strict();
140
+ });
141
141
  const resolveDataPathSchema = z.object({
142
142
  ...envelopeSchema,
143
143
  type: z.literal("resolve-data-path"),
144
144
  requestId: requestIdSchema,
145
145
  dataPath: z.string(),
146
146
  variableMap: variableMapSchema.optional()
147
- }).strict();
147
+ });
148
+ /**
149
+ * Accepts layout types added after this release, so a module should handle a `layoutType` it
150
+ * doesn't know.
151
+ */
152
+ const contextLayoutTypeSchema = z.union([layoutTypeSchema, z.string()]);
148
153
  /**
149
154
  * Runtime facts about where the module is rendered, owned by the host and not
150
155
  * derivable by the module itself. Distinct from `data`, which is the module's
151
156
  * persisted config.
152
157
  */
153
158
  const remoteModuleContextSchema = z.object({
154
- layoutType: layoutTypeSchema,
159
+ layoutType: contextLayoutTypeSchema,
155
160
  layoutObjectId: z.string().nullable(),
156
161
  isReadOnly: z.boolean(),
157
162
  isInEditor: z.boolean(),
158
163
  isUsingStubData: z.boolean()
159
- }).strict();
164
+ });
160
165
  const moduleToHostMessageSchema = z.discriminatedUnion("type", [
161
166
  readySchema,
162
167
  requestDataSchema,
@@ -169,25 +174,25 @@ const initSchema = z.object({
169
174
  type: z.literal("init"),
170
175
  data: jsonSchema,
171
176
  context: remoteModuleContextSchema
172
- }).strict();
177
+ });
173
178
  const dataSchema = z.object({
174
179
  ...envelopeSchema,
175
180
  type: z.literal("data"),
176
181
  requestId: requestIdSchema.optional(),
177
182
  data: jsonSchema
178
- }).strict();
183
+ });
179
184
  const contextSchema = z.object({
180
185
  ...envelopeSchema,
181
186
  type: z.literal("context"),
182
187
  context: remoteModuleContextSchema
183
- }).strict();
188
+ });
184
189
  const dataPathResultSchema = z.object({
185
190
  ...envelopeSchema,
186
191
  type: z.literal("data-path-result"),
187
192
  requestId: requestIdSchema,
188
193
  result: jsonSchema.optional(),
189
194
  error: z.string().optional()
190
- }).strict().refine(({ result, error }) => result === void 0 !== (error === void 0));
195
+ }).refine(({ result, error }) => result === void 0 !== (error === void 0));
191
196
  const hostToModuleMessageSchema = z.discriminatedUnion("type", [
192
197
  initSchema,
193
198
  dataSchema,
@@ -15,16 +15,12 @@ type JsonValue = Json;
15
15
  * persisted config.
16
16
  */
17
17
  declare const remoteModuleContextSchema: z.ZodObject<{
18
- layoutType: z.ZodEnum<{
19
- account: "account";
20
- case: "case";
21
- transaction: "transaction";
22
- }>;
18
+ layoutType: z.ZodType<"account" | "case" | "transaction" | (string & {}), unknown, z.core.$ZodTypeInternals<"account" | "case" | "transaction" | (string & {}), unknown>>;
23
19
  layoutObjectId: z.ZodNullable<z.ZodString>;
24
20
  isReadOnly: z.ZodBoolean;
25
21
  isInEditor: z.ZodBoolean;
26
22
  isUsingStubData: z.ZodBoolean;
27
- }, z.core.$strict>;
23
+ }, z.core.$strip>;
28
24
  type RemoteModuleContext = z.infer<typeof remoteModuleContextSchema>;
29
25
  //#endregion
30
26
  export { Json as n, RemoteModuleContext as t };
@@ -1,4 +1,4 @@
1
- import { n as LayoutModuleEntry, r as defineLayoutModule, t as LayoutModuleDefinitionInput } from "./define-C-3P4CFz.mjs";
1
+ import { n as LayoutModuleEntry, r as defineLayoutModule, t as LayoutModuleDefinitionInput } from "./define-D0nvEX_B.mjs";
2
2
  import { Plugin } from "vite";
3
3
 
4
4
  //#region src/vite-plugin.d.ts
@@ -135,15 +135,17 @@ function personaLayoutModules(options) {
135
135
  let definitions = [];
136
136
  return {
137
137
  async closeBundle() {
138
- if (definitions.length === 0 || resolvedConfig.command !== "build") return;
139
- const outDir = path.isAbsolute(resolvedConfig.build.outDir) ? resolvedConfig.build.outDir : path.resolve(root, resolvedConfig.build.outDir);
138
+ const envConfig = this.environment?.config;
139
+ if (definitions.length === 0 || envConfig?.command !== "build") return;
140
+ if (envConfig.consumer !== "client") return;
141
+ const outDir = path.isAbsolute(envConfig.build.outDir) ? envConfig.build.outDir : path.resolve(root, envConfig.build.outDir);
140
142
  const input = {};
141
143
  for (const definition of definitions) if (definition.render.kind === "code") input[entryNameFor(definition)] = definition.render.absolutePath;
142
144
  let emitted = [];
143
145
  if (Object.keys(input).length > 0) {
144
146
  const shimAlias = await ensureRemoteShimFiles();
145
147
  const result = await build$1({
146
- base: resolvedConfig.base,
148
+ base: envConfig.base,
147
149
  build: {
148
150
  emptyOutDir: false,
149
151
  outDir,
@@ -159,14 +161,14 @@ function personaLayoutModules(options) {
159
161
  mode: "production",
160
162
  plugins: options.modulePlugins?.() ?? [],
161
163
  resolve: { alias: {
162
- ...resolveAliasForEsbuild(resolvedConfig),
164
+ ...resolveAliasForEsbuild(envConfig),
163
165
  ...shimAlias
164
166
  } },
165
167
  root
166
168
  });
167
169
  emitted = (Array.isArray(result) ? result : [result]).flatMap((output) => "output" in output ? output.output : []);
168
170
  }
169
- const base = resolvedConfig.base ?? "/";
171
+ const base = envConfig.base ?? "/";
170
172
  const manifests = [];
171
173
  for (const definition of definitions) {
172
174
  const render = resolveManifestDelivery(definition, definition.render, emitted, root, base);
@@ -188,15 +190,18 @@ function personaLayoutModules(options) {
188
190
  await mkdir(path.dirname(manifestFilePath), { recursive: true });
189
191
  await writeFile(manifestFilePath, JSON.stringify({ modules: manifests }, null, 2), "utf8");
190
192
  },
191
- async config(config, env) {
193
+ async config(config) {
192
194
  root = config.root ? path.resolve(config.root) : process.cwd();
193
195
  definitions = options.modules.map((input) => ({
194
196
  definitionFilePath: input.key,
195
197
  input,
196
198
  render: resolveEntry(root, input.render)
197
199
  }));
200
+ },
201
+ configEnvironment(name, envConfig, env) {
198
202
  if (env.command !== "build") return;
199
- const existingInputs = absoluteInputPaths(config, root);
203
+ if ((envConfig.consumer ?? (name === "client" ? "client" : "server")) !== "client") return;
204
+ const existingInputs = absoluteInputPaths(envConfig, root);
200
205
  const toInject = {};
201
206
  for (const definition of definitions) {
202
207
  const delivery = definition.render;
@@ -205,14 +210,10 @@ function personaLayoutModules(options) {
205
210
  existingInputs.add(delivery.absolutePath);
206
211
  }
207
212
  }
208
- if (Object.keys(toInject).length > 0) {
209
- config.build ??= {};
210
- config.build.rollupOptions ??= {};
211
- config.build.rollupOptions.input = {
212
- ...toInputRecord(config.build.rollupOptions.input, root),
213
- ...toInject
214
- };
215
- }
213
+ if (Object.keys(toInject).length > 0) return { build: { rollupOptions: { input: {
214
+ ...toInputRecord(envConfig.build?.rollupOptions?.input, root),
215
+ ...toInject
216
+ } } } };
216
217
  },
217
218
  configResolved(config) {
218
219
  resolvedConfig = config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persona/layout-editor-remote-module",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.",
5
5
  "license": "SEE LICENSE",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "esbuild": ">=0.21.0 <1",
42
42
  "react": "^17 || ^18 || ^19",
43
43
  "react-dom": "^17 || ^18 || ^19",
44
- "vite": "^5 || ^6 || ^7 || ^8",
44
+ "vite": "^6 || ^7 || ^8",
45
45
  "zod": "^4.3"
46
46
  },
47
47
  "peerDependenciesMeta": {
@@ -3,13 +3,14 @@ name: getting-started
3
3
  description: >
4
4
  Wire up @persona/layout-editor-remote-module in a Persona dashboard app: register
5
5
  personaLayoutModules in a typical app's vite.config.ts, author modules with defineLayoutModule
6
- using a document (.html) render delivery, add a zod configSchema and an RJSF uiConfigSchema, and
7
- talk to the host from the iframed document with LayoutModuleBridge from the bridge-frame entry
8
- (layout data, host context such as layoutType / layoutObjectId / isReadOnly, resolveDataPath, and
9
- height). Activate when adding a new layout module to an app, integrating this package for the
10
- first time, wiring a module document to the dashboard host, or debugging a manifest that fails to
11
- build or validate (duplicate key, missing defaultSize entry, render path not found) or a module
12
- that never receives data from the host.
6
+ using a document (.html) render delivery, add a zod configSchema and an RJSF uiConfigSchema
7
+ (labels, field order, DataSourceField data-source pickers), and talk to the host from the iframed
8
+ document with LayoutModuleBridge from the bridge-frame entry (layout data, host context such as
9
+ layoutType / layoutObjectId / isReadOnly, resolveDataPath, and height). Activate when adding a new
10
+ layout module to an app, integrating this package for the first time, wiring a module document to
11
+ the dashboard host, or debugging a manifest that fails to build or validate (duplicate key,
12
+ missing defaultSize entry, render path not found) or a module that never receives data from the
13
+ host.
13
14
  metadata:
14
15
  type: core
15
16
  library: '@persona/layout-editor-remote-module'
@@ -123,9 +124,9 @@ as static JSON Schema, not re-executed as zod on the consuming side.
123
124
 
124
125
  ### `uiConfigSchema` — config panel layout
125
126
 
126
- The host renders the config panel with RJSF. Pass an RJSF `uiSchema` (widget choice, field order,
127
- labels) as `uiConfigSchema`; it is already plain data, so the plugin copies it into the manifest
128
- unchanged:
127
+ The host renders the config panel with RJSF. Pass an RJSF `uiSchema` as `uiConfigSchema`; it is
128
+ already plain data, so the plugin copies it into the manifest unchanged. Its top-level keys are the
129
+ `configSchema` property names, and each one holds that property's `ui:*` entries:
129
130
 
130
131
  ```ts
131
132
  defineLayoutModule({
@@ -141,6 +142,104 @@ defineLayoutModule({
141
142
  });
142
143
  ```
143
144
 
145
+ - **Labels and help text** come from the property's JSON Schema `title` / `description` — set them
146
+ with zod's `.meta({ title, description })` — and fall back to the property name. Override them per
147
+ field with `'ui:options': { title, description }`; the description shows as the label's tooltip.
148
+ - **Field order** is `'ui:order'` at the top level (use `'*'` for "everything else").
149
+ - **Controls are picked from the schema** without a `uiConfigSchema` entry: an enum renders as a
150
+ dropdown, an array of enum values with `uniqueItems: true` in its JSON Schema as a multi-select, a
151
+ boolean as a checkbox, and an array of objects as a list whose items open in an editing panel.
152
+ - **Everything is JSON.** The manifest is static JSON, so function-valued RJSF options
153
+ (`createItem`, `itemLabel`, …) and custom React fields/widgets cannot be supplied by a module —
154
+ only the `ui:field` names and options the host already registers, listed below.
155
+
156
+ #### `DataSourceField` — pick a value from the record
157
+
158
+ `'ui:field': 'DataSourceField'` turns a property into the host's data-source input: an
159
+ autocompleting method-chain input (e.g. `current_case.id`, or `query.<variable>` for a layout
160
+ variable) checked against the platform schema. Declare the property as a **string** — the field then
161
+ stores the chain itself as a string:
162
+
163
+ ```ts
164
+ defineLayoutModule({
165
+ // ...
166
+ layouts: ['account'],
167
+ defaultSize: { account: { width: 6, height: 24 } },
168
+ configSchema: z.object({
169
+ accountId: z.string().optional(),
170
+ }),
171
+ uiConfigSchema: {
172
+ accountId: {
173
+ 'ui:field': 'DataSourceField',
174
+ 'ui:options': {
175
+ title: 'Account ID',
176
+ description: 'Account whose transactions are monitored',
177
+ },
178
+ },
179
+ },
180
+ });
181
+ ```
182
+
183
+ `ui:options` it reads:
184
+
185
+ - `title`, `description` — label and tooltip; default to the schema's `title` / `description`.
186
+ - `expectedSchemaType` — the platform object type the chain must resolve to (e.g. `'case'`,
187
+ `'user'`); a chain resolving to another type is flagged. Unset means any chain is accepted. An
188
+ empty value is never validated.
189
+ - `rootPath` — limits validation to chains under one top-level property (e.g. `'current_case'`).
190
+ Only set it when `layouts` names a single layout that has that root; a chain outside `rootPath`
191
+ still saves but is not validated.
192
+
193
+ The config holds the **chain, not its value**. Resolve it at render time with `resolveDataPath` from
194
+ `LayoutModuleBridge` (below), skipping an unset field:
195
+
196
+ ```tsx
197
+ import { useEffect, useState } from 'react';
198
+ import {
199
+ type Json,
200
+ type LayoutModuleBridgeRenderProps,
201
+ } from '@persona/layout-editor-remote-module/bridge-frame';
202
+
203
+ function TransactionMonitor({
204
+ data,
205
+ resolveDataPath,
206
+ }: Pick<LayoutModuleBridgeRenderProps, 'data' | 'resolveDataPath'>) {
207
+ const chain = (data as { accountId?: string } | null)?.accountId;
208
+ const [accountId, setAccountId] = useState<Json>(null);
209
+
210
+ useEffect(() => {
211
+ setAccountId(null);
212
+ if (!chain) return;
213
+ let current = true;
214
+ resolveDataPath(chain).then(
215
+ (value) => current && setAccountId(value),
216
+ () => current && setAccountId(null),
217
+ );
218
+ return () => {
219
+ current = false;
220
+ };
221
+ }, [chain, resolveDataPath]);
222
+
223
+ // ...
224
+ }
225
+ ```
226
+
227
+ The host pushes config updates into the mounted module, so the chain can change or be cleared at any
228
+ time: reset the resolved value whenever it does, and ignore responses to an outdated chain.
229
+ `resolveDataPath` resolves to `null` when the chain can't be resolved on the current record and
230
+ rejects on a host error or timeout, so handle both.
231
+
232
+ #### Other `ui:options`
233
+
234
+ - `errorMessages` — per-keyword validation messages, e.g.
235
+ `{ 'ui:options': { errorMessages: { pattern: 'Use letters only.' } } }` (keys are JSON Schema
236
+ keywords: `pattern`, `minLength`, `maximum`, …).
237
+ - On an enum multi-select: `unsetSelectsAll: true` shows an unset value as every option selected,
238
+ for a key whose absence means "all" and should stay absent.
239
+ - On an array of objects: `identifierField` (item property that labels each row), `emptyText`,
240
+ `editLabel` (the editing panel's title), `listAriaLabel`, and `inlineItems: true` to render the
241
+ items inline instead of as a list with an editing panel.
242
+
144
243
  ### `bridge-frame` — talking to the host from the iframed document
145
244
 
146
245
  The host renders a document-delivered module in an iframe and speaks to it over `postMessage`. Wrap
@@ -279,6 +378,21 @@ resolve to `null` and `children` renders, so a stuck fallback is always a host/o
279
378
 
280
379
  Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`.
281
380
 
381
+ ### [HIGH] `DataSourceField` on a non-string property, or read as the resolved value
382
+
383
+ Declare a `DataSourceField` property as `z.string()`. On a non-string property the host stores a
384
+ structured data-source object instead of the chain string, and `resolveDataPath` only accepts a
385
+ string — it resolves to `null`. On a string property the stored value is the method chain the user
386
+ typed (e.g. `"current_account.id"`), not the account ID: pass `data.accountId` to `resolveDataPath`
387
+ rather than using it directly.
388
+
389
+ ### [MEDIUM] A `ui:field` name the host doesn't register
390
+
391
+ A `ui:field` name the host has no component for falls back to RJSF's default field for the
392
+ property's type — no build or validation error, just a plain input instead of the intended control.
393
+ Spell `'DataSourceField'` exactly; module-supplied field components are not possible (see
394
+ "Everything is JSON" above).
395
+
282
396
  ### [HIGH] `key` uses the wrong charset or collides with another module
283
397
 
284
398
  Wrong: