@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.
82 lines (81 loc) • 3.82 kB
JavaScript
//#region src/config-form.ts
/** JSON pointer, in the nested schema, of the module's own config schema. */
const REMOTE_CONFIG_POINTER_PREFIX = "#/properties/config";
/** Keywords whose values are instance data, never schemas, so a `$ref` key inside them is data. */
const LITERAL_KEYWORDS = new Set([
"const",
"default",
"enum",
"examples"
]);
/** Keywords whose values map arbitrary names to schemas, so a name like `default` is a schema. */
const SCHEMA_MAP_KEYWORDS = new Set([
"$defs",
"definitions",
"dependentSchemas",
"patternProperties",
"properties"
]);
function rewriteSchemaMap(value, schemaId) {
if (value == null || typeof value !== "object" || Array.isArray(value)) return value;
return Object.fromEntries(Object.entries(value).map(([name, schema]) => [name, rewriteReferences(schema, schemaId)]));
}
function rewriteReferences(value, schemaId) {
if (Array.isArray(value)) return value.map((item) => rewriteReferences(item, schemaId));
if (value == null || typeof value !== "object") return value;
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
if (key === "$ref" && typeof child === "string") return [key, rewriteReference(child, schemaId)];
if (LITERAL_KEYWORDS.has(key)) return [key, child];
if (SCHEMA_MAP_KEYWORDS.has(key)) return [key, rewriteSchemaMap(child, schemaId)];
return [key, rewriteReferences(child, schemaId)];
}));
}
function rewriteReference(reference, schemaId) {
if (schemaId != null) {
if (reference === schemaId) return REMOTE_CONFIG_POINTER_PREFIX;
if (reference.startsWith(`${schemaId}#`)) return rewriteReference(reference.slice(schemaId.length));
}
if (reference === "#") return REMOTE_CONFIG_POINTER_PREFIX;
if (reference.startsWith("#/$defs/") || reference.startsWith("#/definitions/")) return reference;
if (reference.startsWith("#/")) return `${REMOTE_CONFIG_POINTER_PREFIX}${reference.slice(1)}`;
return reference;
}
/**
* Nest a manifest `configSchema` under a top-level `config` property, rewriting `$ref`s so they
* still resolve. Drops `$schema` and `$id`; keeps `$defs` / `definitions` at the root.
*/
function nestRemoteConfigSchema(configSchema) {
const { $defs, definitions, $id, ...rest } = configSchema;
const body = Object.fromEntries(Object.entries(rest).filter(([key]) => key !== "$schema"));
const schemaId = typeof $id === "string" ? $id : void 0;
return {
type: "object",
properties: { config: rewriteReferences(body, schemaId) },
...$defs != null && { $defs: rewriteSchemaMap($defs, schemaId) },
...definitions != null && { definitions: rewriteSchemaMap(definitions, schemaId) }
};
}
/**
* Nest a manifest `uiConfigSchema` to match {@link nestRemoteConfigSchema}, hiding the wrapper
* object's title so the module's fields render as if top level.
*/
function nestRemoteUiConfigSchema(uiConfigSchema) {
return { config: {
...uiConfigSchema,
"ui:title": ""
} };
}
/**
* A config holding each top-level `configSchema` property's `default`, overlaid with the selected
* layout's `defaultConfig`. Hosts pass `manifest.defaultConfig?.[layoutType]`; both sources are
* cloned so the result can be mutated freely.
*/
function remoteConfigDefaults(configSchema, layoutDefaults) {
const properties = configSchema?.properties;
return {
...properties == null || typeof properties !== "object" || Array.isArray(properties) ? {} : Object.fromEntries(Object.entries(properties).filter(([, propertySchema]) => typeof propertySchema === "object" && propertySchema != null && !Array.isArray(propertySchema) && "default" in propertySchema).map(([key, propertySchema]) => [key, structuredClone(propertySchema.default)])),
...structuredClone(layoutDefaults ?? {})
};
}
//#endregion
export { REMOTE_CONFIG_POINTER_PREFIX, nestRemoteConfigSchema, nestRemoteUiConfigSchema, remoteConfigDefaults };