UNPKG

@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
//#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 };