@persona/layout-editor-remote-module
Version:
Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.
103 lines (99 loc) • 3.95 kB
JavaScript
import { z } from "zod";
//#region src/define.ts
/**
* Identity function at runtime — it exists to give authors a typed,
* discoverable way to build a `personaLayoutModules({ modules: [...] })`
* entry, nothing more. All real work (path resolution, build-graph
* injection, schema conversion, manifest emission) happens in the Vite
* plugin, not here.
*/
function defineLayoutModule(input) {
return input;
}
//#endregion
//#region src/manifest-path.ts
/**
* Default path (site-root-relative) the manifest document is served from and
* written to, when `PersonaLayoutModulesOptions.manifestPath` isn't
* overridden. A standalone module (rather than living in vite-plugin.ts, the
* one place that actually uses it) so it can also be re-exported from the
* runtime-safe `.` entry — a consumer wiring up something like CORS for the
* manifest route in its own server, outside of vite.config.ts, needs this
* default without pulling in Vite.
*/
const DEFAULT_MANIFEST_PATH = "/_layout_modules_manifest.json";
//#endregion
//#region src/schema.ts
/**
* Mirrors the host's layout grid column count; the enum schemas below must
* match the host's values.
*/
const MAX_GRID_COLUMNS = 12;
const layoutTypeSchema = z.enum([
"account",
"case",
"transaction"
]);
const moduleCategorySchema = z.enum([
"build_your_own",
"components",
"management",
"related_records"
]);
const moduleBadgeSchema = z.enum([
"beta",
"deprecated",
"improved",
"new"
]);
/**
* A manifest `key` is embedded verbatim into a persisted module identity
* string (`${sourceId}/${key}`) and used in DOM ids and analytics — restrict it
* to a safe, stable charset up front rather than discovering the hard way that
* some producer emitted spaces or slashes.
*/
const moduleKeySchema = z.string().min(1).regex(/^[a-z0-9_]+$/, "key must be lowercase alphanumeric with underscores only");
const gridSizeSchema = z.object({
height: z.number().int().min(1),
width: z.number().int().min(1).max(MAX_GRID_COLUMNS)
});
/**
* The module's main view is a page URL that the host iframes.
*/
const remoteDeliverySchema = z.object({ document: z.string().min(1) });
const remoteEntrySchema = z.object({ render: remoteDeliverySchema });
/**
* Loose check shared by `configSchema` and `uiConfigSchema` — this validates
* the manifest envelope, not the arbitrary object either one carries. Both
* are trusted structurally (an object) and used for their stated purpose
* (JSON Schema validation, RJSF ui hints) by the consumer, not re-validated
* here.
*/
const looseObjectSchema = z.record(z.string(), z.unknown());
const remoteModuleRegistrationManifestV1Schema = z.object({
allowAddingToLayout: z.boolean(),
allowDynamicHeight: z.boolean().optional(),
badge: moduleBadgeSchema.optional(),
category: moduleCategorySchema.optional(),
categoryCustom: z.string().trim().min(1).max(80).optional(),
configSchema: looseObjectSchema.optional(),
defaultConfig: z.partialRecord(layoutTypeSchema, looseObjectSchema).optional(),
defaultSize: z.partialRecord(layoutTypeSchema, gridSizeSchema),
descriptionShort: z.string().min(1),
displayName: z.string().min(1),
icon: z.string().min(1).optional(),
internalUsersOnly: z.boolean().optional(),
key: moduleKeySchema,
layouts: z.array(layoutTypeSchema).default([]),
manifestVersion: z.literal("v1"),
remote: remoteEntrySchema,
uiConfigSchema: looseObjectSchema.optional()
}).refine((manifest) => manifest.layouts.every((layout) => manifest.defaultSize[layout]), {
message: "defaultSize must have an entry for every layout in `layouts`",
path: ["defaultSize"]
}).refine((manifest) => manifest.layouts.length === 0 || Object.keys(manifest.defaultConfig ?? {}).every((layout) => manifest.layouts.includes(layout)), {
message: "defaultConfig has an entry for a layout not in `layouts`",
path: ["defaultConfig"]
});
//#endregion
export { DEFAULT_MANIFEST_PATH as n, defineLayoutModule as r, remoteModuleRegistrationManifestV1Schema as t };