@persona/layout-editor-remote-module 0.5.0 → 0.5.2
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 +24 -7
- package/dist/bridge-frame.d.mts +4 -1
- package/dist/bridge-frame.mjs +2 -2
- package/dist/bridge-host.mjs +1 -1
- package/dist/{define-Dt8sbrVO.d.mts → define-C-3P4CFz.d.mts} +11 -24
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{protocol-DMsysRFd.mjs → protocol-Dz3ZX59L.mjs} +3 -7
- package/dist/{schema-BeI8GdXS.mjs → schema-Bc3ZwB8y.mjs} +2 -4
- package/dist/vite-plugin.d.mts +2 -2
- package/dist/vite-plugin.mjs +19 -19
- package/package.json +3 -3
- package/skills/getting-started/SKILL.md +162 -45
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @persona/layout-editor-remote-module
|
|
2
2
|
|
|
3
3
|
Shared manifest format and Vite plugin for remotely-defined dashboard layout modules — used by
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Persona dashboard apps to declare a layout module directly in a typical app's `vite.config.ts`,
|
|
5
|
+
without editing the host.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -81,6 +81,23 @@ import { RemoteModuleFrame } from '@persona/layout-editor-remote-module/bridge-h
|
|
|
81
81
|
|
|
82
82
|
The required `sandbox` value is chosen by the host.
|
|
83
83
|
|
|
84
|
+
By default, the bridge accepts host origins matching `*.withpersona.com`,
|
|
85
|
+
`*.withpersona-staging.com`, `localhost`, and `127.0.0.1`. Add development or preview origins with
|
|
86
|
+
`allowedHostOrigins`, optionally composing the exported `personaDashboardHostOrigins` helper:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import {
|
|
90
|
+
LayoutModuleBridge,
|
|
91
|
+
personaDashboardHostOrigins,
|
|
92
|
+
} from '@persona/layout-editor-remote-module/bridge-frame';
|
|
93
|
+
|
|
94
|
+
<LayoutModuleBridge
|
|
95
|
+
allowedHostOrigins={(origin) => personaDashboardHostOrigins(origin) || myDevHost(origin)}
|
|
96
|
+
>
|
|
97
|
+
{({ data }) => <RiskSummary data={data} />}
|
|
98
|
+
</LayoutModuleBridge>;
|
|
99
|
+
```
|
|
100
|
+
|
|
84
101
|
The document entry uses `LayoutModuleBridge` as a render-prop component:
|
|
85
102
|
|
|
86
103
|
```tsx
|
|
@@ -108,11 +125,11 @@ cannot derive itself:
|
|
|
108
125
|
iframe it arrives through `ready()`, `getContext()`, and the `context` render prop (all
|
|
109
126
|
`RemoteModuleContext | null` until init).
|
|
110
127
|
|
|
111
|
-
The host still appends `access_token`, `environment_id`, and `locale` as query parameters. The
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
The host still appends `access_token`, `environment_id`, and `locale` as query parameters. The app
|
|
129
|
+
hosting the module consumes and strips those parameters itself, so by the time `LayoutModuleBridge`
|
|
130
|
+
runs they have already been handled; the bridge neither reads nor strips them. Keep the host
|
|
131
|
+
iframe's `referrerpolicy` at its default `no-referrer` (or use `strict-origin`) so the URL
|
|
132
|
+
parameters cannot leak through subsequent requests.
|
|
116
133
|
|
|
117
134
|
The bridge's security model is intentionally narrow:
|
|
118
135
|
|
package/dist/bridge-frame.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { n as Json, t as RemoteModuleContext } from "./protocol-edoRdXpF.mjs";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
//#region src/bridge/origin.d.ts
|
|
5
|
+
declare function personaDashboardHostOrigins(origin: string): boolean;
|
|
6
|
+
//#endregion
|
|
4
7
|
//#region src/bridge/iframe/createModuleBridge.d.ts
|
|
5
8
|
type ModuleBridgeInitPayload = {
|
|
6
9
|
data: Json;
|
|
@@ -42,4 +45,4 @@ declare function LayoutModuleBridge({
|
|
|
42
45
|
children
|
|
43
46
|
}: LayoutModuleBridgeProps): ReactNode;
|
|
44
47
|
//#endregion
|
|
45
|
-
export { type Json, LayoutModuleBridge, type LayoutModuleBridgeProps, type LayoutModuleBridgeRenderProps, type RemoteModuleContext };
|
|
48
|
+
export { type Json, LayoutModuleBridge, type LayoutModuleBridgeProps, type LayoutModuleBridgeRenderProps, type RemoteModuleContext, personaDashboardHostOrigins };
|
package/dist/bridge-frame.mjs
CHANGED
|
@@ -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-
|
|
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";
|
|
2
2
|
import { Component, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -347,4 +347,4 @@ function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeigh
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
//#endregion
|
|
350
|
-
export { LayoutModuleBridge };
|
|
350
|
+
export { LayoutModuleBridge, personaDashboardHostOrigins };
|
package/dist/bridge-host.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-
|
|
1
|
+
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-Dz3ZX59L.mjs";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -79,31 +79,20 @@ type RemoteModuleRegistrationManifestV1Schema = typeof remoteModuleRegistrationM
|
|
|
79
79
|
type Json = boolean | JsonObject | JsonValue[] | null | number | string;
|
|
80
80
|
/**
|
|
81
81
|
* Loose JSON Schema object. Deliberately not a full JSON Schema type system —
|
|
82
|
-
* this package only needs to carry what `z.toJSONSchema()` emits
|
|
83
|
-
* to
|
|
84
|
-
* it on the other.
|
|
82
|
+
* this package only needs to carry what `z.toJSONSchema()` emits for consumers
|
|
83
|
+
* to validate or render.
|
|
85
84
|
*/
|
|
86
85
|
type JsonSchemaObject = Record<string, unknown>;
|
|
87
|
-
/**
|
|
88
|
-
* Mirrors persona-web's `LayoutType`
|
|
89
|
-
* (front-end/dashboard/modules/lib/types.ts).
|
|
90
|
-
*/
|
|
86
|
+
/** Mirrors the host's supported layout values. */
|
|
91
87
|
type LayoutType = z.infer<typeof layoutTypeSchema>;
|
|
92
|
-
/**
|
|
93
|
-
* Mirrors persona-web's `ModuleBadge`
|
|
94
|
-
* (front-end/dashboard/modules/lib/config-constants.tsx).
|
|
95
|
-
*/
|
|
88
|
+
/** Mirrors the host's supported module badges. */
|
|
96
89
|
type ModuleBadge = z.infer<typeof moduleBadgeSchema>;
|
|
97
|
-
/**
|
|
98
|
-
* Mirrors persona-web's `ModuleCategory` values
|
|
99
|
-
* (front-end/dashboard/modules/lib/module-library-config.ts).
|
|
100
|
-
*/
|
|
90
|
+
/** Mirrors the host's module-library categories. */
|
|
101
91
|
type ModuleCategory = z.infer<typeof moduleCategorySchema>;
|
|
102
92
|
/**
|
|
103
|
-
* Options passed to a remote module's `defaultConfig` export.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* scaffolding a remote module has no business branching on.
|
|
93
|
+
* Options passed to a remote module's `defaultConfig` export. Narrower than the
|
|
94
|
+
* host's default-config options; host-internal flags are not exposed to remote
|
|
95
|
+
* modules.
|
|
107
96
|
*/
|
|
108
97
|
interface RemoteModuleDefaultConfigOptions {
|
|
109
98
|
layoutType: LayoutType;
|
|
@@ -119,7 +108,7 @@ type RemoteModuleDelivery = z.infer<typeof remoteDeliverySchema>;
|
|
|
119
108
|
* optional and feature-detected by name — see the "one URL, conventional
|
|
120
109
|
* named exports" design decision. Rendering/sandboxing `render` is out of
|
|
121
110
|
* scope for this package; only `defaultConfig` has a concrete consumer here
|
|
122
|
-
* (
|
|
111
|
+
* (the host's default-config loader). Not applicable to a `document`
|
|
123
112
|
* delivery — nothing is ever imported for those, so none of these exports are
|
|
124
113
|
* reachable.
|
|
125
114
|
*/
|
|
@@ -133,10 +122,8 @@ interface RemoteModuleExports {
|
|
|
133
122
|
* Wire format for a remotely-defined layout module. Must stay
|
|
134
123
|
* JSON-serializable: no functions, no ReactNode, no zod.
|
|
135
124
|
*
|
|
136
|
-
* Producers: `defineLayoutModule()`
|
|
137
|
-
*
|
|
138
|
-
* Consumers: persona-web's `manifestToRegistration()`
|
|
139
|
-
* (front-end/dashboard/modules/lib/remote/adapt.ts).
|
|
125
|
+
* Producers: `defineLayoutModule()` and this package's Vite plugin.
|
|
126
|
+
* Consumers: the host application.
|
|
140
127
|
*/
|
|
141
128
|
type RemoteModuleRegistrationManifestV1 = z.infer<typeof remoteModuleRegistrationManifestV1Schema>;
|
|
142
129
|
type JsonObject = {
|
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-
|
|
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";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/manifest-path.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as DEFAULT_MANIFEST_PATH, r as defineLayoutModule, t as remoteModuleRegistrationManifestV1Schema } from "./schema-
|
|
1
|
+
import { n as DEFAULT_MANIFEST_PATH, r as defineLayoutModule, t as remoteModuleRegistrationManifestV1Schema } from "./schema-Bc3ZwB8y.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/parse.ts
|
|
@@ -2,11 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
//#region src/bridge/origin.ts
|
|
4
4
|
const dashboardHostOriginPattern = /^https:\/\/([a-z0-9-]+\.)*withpersona(-staging)?\.com$/;
|
|
5
|
-
const pagesHostOriginPattern = /^https:\/\/[a-z0-9-]+\.persona-web\.pages\.dev$/;
|
|
6
|
-
const coderHostOriginPattern = /^https:\/\/[a-z0-9-]+\.coder\.development\.personerds\.com$/;
|
|
7
5
|
const localHostOriginPattern = /^http:\/\/(?:localhost|127\.0\.0\.1):\d+$/;
|
|
8
6
|
function personaDashboardHostOrigins(origin) {
|
|
9
|
-
return dashboardHostOriginPattern.test(origin) ||
|
|
7
|
+
return dashboardHostOriginPattern.test(origin) || localHostOriginPattern.test(origin);
|
|
10
8
|
}
|
|
11
9
|
function resolveAllowedHostOrigins(allowedHostOrigins) {
|
|
12
10
|
if (allowedHostOrigins === void 0) return personaDashboardHostOrigins;
|
|
@@ -32,10 +30,8 @@ function assertNotSelfOrigin(moduleOrigin) {
|
|
|
32
30
|
//#endregion
|
|
33
31
|
//#region src/schema.ts
|
|
34
32
|
/**
|
|
35
|
-
* Mirrors
|
|
36
|
-
*
|
|
37
|
-
* mirror persona-web's corresponding enum values and are kept in sync by hand;
|
|
38
|
-
* this package has no dependency on persona-web.
|
|
33
|
+
* Mirrors the host's layout grid column count; the enum schemas below must
|
|
34
|
+
* match the host's values.
|
|
39
35
|
*/
|
|
40
36
|
const MAX_GRID_COLUMNS = 12;
|
|
41
37
|
const layoutTypeSchema = z.enum([
|
|
@@ -28,10 +28,8 @@ const DEFAULT_MANIFEST_PATH = "/_layout_modules_manifest.json";
|
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/schema.ts
|
|
30
30
|
/**
|
|
31
|
-
* Mirrors
|
|
32
|
-
*
|
|
33
|
-
* mirror persona-web's corresponding enum values and are kept in sync by hand;
|
|
34
|
-
* this package has no dependency on persona-web.
|
|
31
|
+
* Mirrors the host's layout grid column count; the enum schemas below must
|
|
32
|
+
* match the host's values.
|
|
35
33
|
*/
|
|
36
34
|
const MAX_GRID_COLUMNS = 12;
|
|
37
35
|
const layoutTypeSchema = z.enum([
|
package/dist/vite-plugin.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as LayoutModuleEntry, r as defineLayoutModule, t as LayoutModuleDefinitionInput } from "./define-
|
|
1
|
+
import { n as LayoutModuleEntry, r as defineLayoutModule, t as LayoutModuleDefinitionInput } from "./define-C-3P4CFz.mjs";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
3
|
|
|
4
4
|
//#region src/vite-plugin.d.ts
|
|
@@ -33,7 +33,7 @@ interface PersonaLayoutModulesOptions {
|
|
|
33
33
|
* wires every `code`-delivered entry into the build as an addressable ESM
|
|
34
34
|
* chunk, ensures every `document`-delivered (`.html`) entry is actually
|
|
35
35
|
* built/served, and emits a `RemoteModuleRegistrationManifestV1[]` document
|
|
36
|
-
*
|
|
36
|
+
* the host can consume.
|
|
37
37
|
*
|
|
38
38
|
* Modules are configured directly here, not discovered by globbing the
|
|
39
39
|
* source tree — a module IS whatever's in `options.modules`, full stop.
|
package/dist/vite-plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as DEFAULT_MANIFEST_PATH, r as defineLayoutModule, t as remoteModuleRegistrationManifestV1Schema } from "./schema-
|
|
1
|
+
import { n as DEFAULT_MANIFEST_PATH, r as defineLayoutModule, t as remoteModuleRegistrationManifestV1Schema } from "./schema-Bc3ZwB8y.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { build } from "esbuild";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -123,7 +123,7 @@ function ensureRemoteShimFiles() {
|
|
|
123
123
|
* wires every `code`-delivered entry into the build as an addressable ESM
|
|
124
124
|
* chunk, ensures every `document`-delivered (`.html`) entry is actually
|
|
125
125
|
* built/served, and emits a `RemoteModuleRegistrationManifestV1[]` document
|
|
126
|
-
*
|
|
126
|
+
* the host can consume.
|
|
127
127
|
*
|
|
128
128
|
* Modules are configured directly here, not discovered by globbing the
|
|
129
129
|
* source tree — a module IS whatever's in `options.modules`, full stop.
|
|
@@ -135,15 +135,17 @@ function personaLayoutModules(options) {
|
|
|
135
135
|
let definitions = [];
|
|
136
136
|
return {
|
|
137
137
|
async closeBundle() {
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
@@ -366,8 +367,7 @@ function matchDevBundleRequest(url, definitions) {
|
|
|
366
367
|
* array) into an array of `{ find, replacement }`. esbuild's own `alias`
|
|
367
368
|
* option only accepts exact string keys (no RegExp, no partial-path
|
|
368
369
|
* matching) — a real limitation this doesn't try to hide, just the common
|
|
369
|
-
* case: a plain string-to-string alias map, which
|
|
370
|
-
* subapp's `vite.config.ts` actually uses.
|
|
370
|
+
* case: a plain string-to-string alias map, which many apps use.
|
|
371
371
|
*/
|
|
372
372
|
function resolveAliasForEsbuild(resolvedConfig) {
|
|
373
373
|
const alias = {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.mts",
|
|
@@ -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": "^
|
|
44
|
+
"vite": "^6 || ^7 || ^8",
|
|
45
45
|
"zod": "^4.3"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
@@ -1,37 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: getting-started
|
|
3
3
|
description: >
|
|
4
|
-
Wire up @persona/layout-editor-remote-module in a
|
|
5
|
-
personaLayoutModules in vite.config.ts, author modules with defineLayoutModule
|
|
6
|
-
(.html) render delivery, add a zod configSchema and an RJSF uiConfigSchema
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module
|
|
11
|
-
|
|
12
|
-
data from the
|
|
4
|
+
Wire up @persona/layout-editor-remote-module in a Persona dashboard app: register
|
|
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
|
|
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'
|
|
16
|
-
library_version: '0.4.0'
|
|
17
|
-
sources:
|
|
18
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/vite-plugin.ts'
|
|
19
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/define.ts'
|
|
20
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/schema.ts'
|
|
21
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/manifest-path.ts'
|
|
22
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/bridge/iframe/index.ts'
|
|
23
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/bridge/iframe/LayoutModuleBridge.tsx'
|
|
24
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/bridge/iframe/createModuleBridge.ts'
|
|
25
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/src/bridge/protocol.ts'
|
|
26
|
-
- 'persona-id/persona-layout-editor:packages/remote-module/README.md'
|
|
27
17
|
---
|
|
28
18
|
|
|
29
19
|
# Getting started with @persona/layout-editor-remote-module
|
|
30
20
|
|
|
31
|
-
Shared manifest format and Vite plugin for remotely-defined dashboard layout modules — lets
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`{ modules: RemoteModuleRegistrationManifestV1[] }` JSON document
|
|
21
|
+
Shared manifest format and Vite plugin for remotely-defined dashboard layout modules — lets Persona
|
|
22
|
+
dashboard apps declare a layout module directly in a typical app's `vite.config.ts`, without editing
|
|
23
|
+
the host. The plugin builds each module and emits a
|
|
24
|
+
`{ modules: RemoteModuleRegistrationManifestV1[] }` JSON document the host fetches and validates.
|
|
35
25
|
|
|
36
26
|
This package is publicly available from the npm registry.
|
|
37
27
|
|
|
@@ -92,11 +82,11 @@ not relative to `vite.config.ts`'s own directory when `root` is overridden elsew
|
|
|
92
82
|
|
|
93
83
|
### Document delivery — iframed page
|
|
94
84
|
|
|
95
|
-
A module's `render.entry` must end in `.html`:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
A module's `render.entry` must end in `.html`: the host iframes it rather than importing anything.
|
|
86
|
+
Any other extension (`.ts`/`.tsx`/`.js`) builds as a `code` delivery, which the plugin and manifest
|
|
87
|
+
still accept but the host does not render yet — the module shows up in the library and renders
|
|
88
|
+
nothing. Use `path` for a client-routed SPA whose `index.html` already renders the whole app — it
|
|
89
|
+
names the logical route the SPA's own router should land on, not the file's build output path:
|
|
100
90
|
|
|
101
91
|
```ts
|
|
102
92
|
defineLayoutModule({
|
|
@@ -115,7 +105,7 @@ plugin detects that and doesn't inject it again.
|
|
|
115
105
|
|
|
116
106
|
### `configSchema` — validated module configuration
|
|
117
107
|
|
|
118
|
-
Author config as zod; the plugin converts it to JSON Schema (`z.toJSONSchema()`) for
|
|
108
|
+
Author config as zod; the plugin converts it to JSON Schema (`z.toJSONSchema()`) for the host's
|
|
119
109
|
config panel:
|
|
120
110
|
|
|
121
111
|
```ts
|
|
@@ -134,9 +124,9 @@ as static JSON Schema, not re-executed as zod on the consuming side.
|
|
|
134
124
|
|
|
135
125
|
### `uiConfigSchema` — config panel layout
|
|
136
126
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
|
140
130
|
|
|
141
131
|
```ts
|
|
142
132
|
defineLayoutModule({
|
|
@@ -152,11 +142,109 @@ defineLayoutModule({
|
|
|
152
142
|
});
|
|
153
143
|
```
|
|
154
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
|
+
|
|
155
243
|
### `bridge-frame` — talking to the host from the iframed document
|
|
156
244
|
|
|
157
|
-
The host
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
The host renders a document-delivered module in an iframe and speaks to it over `postMessage`. Wrap
|
|
246
|
+
the module's React root in `LayoutModuleBridge` from the `bridge-frame` entry; it owns the whole
|
|
247
|
+
handshake and hands the module a render prop:
|
|
160
248
|
|
|
161
249
|
```tsx
|
|
162
250
|
import { LayoutModuleBridge } from '@persona/layout-editor-remote-module/bridge-frame';
|
|
@@ -179,7 +267,7 @@ export function App() {
|
|
|
179
267
|
- `context` (`RemoteModuleContext`) is where the module is rendered — host-owned facts the module
|
|
180
268
|
cannot derive itself. It arrives with the initial data and re-renders whenever it changes (see
|
|
181
269
|
below).
|
|
182
|
-
- `resolveDataPath(dataPath, variableMap?)` returns `Promise<Json>`; the host
|
|
270
|
+
- `resolveDataPath(dataPath, variableMap?)` returns `Promise<Json>`; the host owns the
|
|
183
271
|
implementation. It rejects if the host reports an error or does not answer within
|
|
184
272
|
`resolveTimeoutMs` (default 30s). Outside an iframe it rejects immediately, so a module opened
|
|
185
273
|
directly in a tab still renders.
|
|
@@ -189,9 +277,23 @@ export function App() {
|
|
|
189
277
|
- Mount exactly one `LayoutModuleBridge` per document.
|
|
190
278
|
|
|
191
279
|
Props: `fallback?: ReactNode`, `observeHeight?: boolean` (default `true`),
|
|
192
|
-
`resolveTimeoutMs?: number`, `allowedHostOrigins?` (
|
|
193
|
-
|
|
194
|
-
`
|
|
280
|
+
`resolveTimeoutMs?: number`, `allowedHostOrigins?` (to add host origins outside the built-in
|
|
281
|
+
allow-list). Built-in origins include `*.withpersona.com`, `*.withpersona-staging.com`, `localhost`,
|
|
282
|
+
and `127.0.0.1`. Dev and preview dashboards must be added explicitly; compose the built-in predicate
|
|
283
|
+
with an app-specific check:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import {
|
|
287
|
+
LayoutModuleBridge,
|
|
288
|
+
personaDashboardHostOrigins,
|
|
289
|
+
} from '@persona/layout-editor-remote-module/bridge-frame';
|
|
290
|
+
|
|
291
|
+
<LayoutModuleBridge
|
|
292
|
+
allowedHostOrigins={(origin) => personaDashboardHostOrigins(origin) || myDevHost(origin)}
|
|
293
|
+
>
|
|
294
|
+
{({ data }) => <RiskSummary data={data} />}
|
|
295
|
+
</LayoutModuleBridge>;
|
|
296
|
+
```
|
|
195
297
|
|
|
196
298
|
#### Host context
|
|
197
299
|
|
|
@@ -223,11 +325,11 @@ function RiskSummary({ context }: { context: RemoteModuleContext | null }) {
|
|
|
223
325
|
|
|
224
326
|
#### Access token and environment id
|
|
225
327
|
|
|
226
|
-
The host still appends `access_token`, `environment_id`, and `locale` to the iframe `src`. The
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
328
|
+
The host still appends `access_token`, `environment_id`, and `locale` to the iframe `src`. The app
|
|
329
|
+
hosting the module consumes and strips those parameters itself, so by the time `LayoutModuleBridge`
|
|
330
|
+
runs they have already been handled; the bridge neither reads nor strips them. Keep the host
|
|
331
|
+
iframe's `referrerpolicy` at its default `no-referrer` (or use `strict-origin`) so the URL
|
|
332
|
+
parameters cannot leak through subsequent requests.
|
|
231
333
|
|
|
232
334
|
## Common Mistakes
|
|
233
335
|
|
|
@@ -276,6 +378,21 @@ resolve to `null` and `children` renders, so a stuck fallback is always a host/o
|
|
|
276
378
|
|
|
277
379
|
Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`.
|
|
278
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
|
+
|
|
279
396
|
### [HIGH] `key` uses the wrong charset or collides with another module
|
|
280
397
|
|
|
281
398
|
Wrong:
|