UNPKG

@omnicajs/vue-remote

Version:

Proxy renderer for Vue.js based on @remote-ui

788 lines (787 loc) 25 kB
import { createRenderer, h, defineComponent } from "vue"; import "@remote-ui/rpc"; import { K as KIND_COMPONENT, e as KIND_TEXT, p as KIND_FRAGMENT, b as addMethod, q as ACTION_INVOKE, c as KIND_ROOT, l as ACTION_INSERT_CHILD, r as capture, m as ACTION_REMOVE_CHILD, f as KIND_COMMENT, o as ACTION_UPDATE_TEXT, v as visitArray, s as isObject, t as visitObject, j as isFunction, k as keysOf, n as ACTION_UPDATE_PROPERTIES, R as ROOT_ID, u as arraify, A as ACTION_MOUNT, H as HTMLTagList, M as MathMLTagList, S as SVGTagList, g as REMOTE_SLOT } from "./dom-Duh2pvkf.js"; function isRemoteComponent(value) { return value != null && value.kind === KIND_COMPONENT; } function isRemoteFragment(value) { return value != null && value.kind === KIND_FRAGMENT; } function isRemoteText(value) { return value != null && value.kind === KIND_TEXT; } const normalizeChild = (child, root) => typeof child === "string" ? root.createText(child) : child; const normalizeChildren = (children, root) => children.map((child) => normalizeChild(child, root)); const traverse = (element, each) => { const _traverse = (element2) => { if ("children" in element2) { for (const child of element2.children) { each(child); _traverse(child); } } }; _traverse(element); }; function attach(context, parent, node) { const { progenitors, parents } = context; const progenitor = parent.kind === KIND_ROOT ? parent : parent.progenitor; if (progenitor) { progenitors.set(node, progenitor); } parents.set(node, parent); attachFragments(context, node); traverse(node, (descendant) => { if (progenitor) { progenitors.set(descendant, progenitor); } attachFragments(context, descendant); }); } function attachFragments(context, node) { if (node.kind === KIND_COMPONENT) { Object.values(node.properties).forEach((prop) => { if (isRemoteFragment(prop)) { attach(context, node, prop); } }); } } function detach(context, node) { const { progenitors, parents } = context; progenitors.delete(node); parents.delete(node); traverse(node, (descendant) => { progenitors.delete(descendant); detachFragments(context, descendant); }); detachFragments(context, node); } function detachFragments(context, node) { if (node.kind !== KIND_COMPONENT) { return; } const properties = node.properties; for (const key of Object.keys(properties)) { const p = properties[key]; if (isRemoteFragment(p)) { detach(context, p); } } } const update = (context, node, remote, local) => { var _a; if (context.mounted && (node.kind === KIND_ROOT || ((_a = node.progenitor) == null ? void 0 : _a.kind) === KIND_ROOT)) { remote(context.channel); } local(); }; function dataOf(context, parent) { switch (parent == null ? void 0 : parent.kind) { case KIND_COMPONENT: return context.components.get(parent); case KIND_FRAGMENT: return context.fragments.get(parent); case KIND_ROOT: return context; } } const insertToArray = (target, el, before) => { if (before == null) { target.push(el); } else { target.splice(target.indexOf(before), 0, el); } return target; }; const remoteFromArray = (target, index) => { const result = [...target]; result.splice(index, 1); return result; }; const insert = (context, parent, child, before = null) => { const currentParent = child.parent; const currentIndex = (currentParent == null ? void 0 : currentParent.children.indexOf(child)) ?? -1; attach(context, parent, child); let newChildren; const parentData = dataOf(context, parent); if (currentParent) { const currentParentData = dataOf(context, currentParent); const currentChildren = remoteFromArray(currentParentData.children, currentIndex); if (currentParent === parent) { newChildren = currentChildren; } else { currentParentData.children = capture(currentChildren, context.strict); newChildren = [...parentData.children]; } } else { newChildren = [...parentData.children]; } parentData.children = capture(insertToArray(newChildren, child, before), context.strict); }; const appendChild = (context, parent, child) => { if (!context.nodes.has(child)) { throw new Error("Cannot append a node that was not created by this remote root"); } const currentParent = child.parent; const currentIndex = (currentParent == null ? void 0 : currentParent.children.indexOf(child)) ?? -1; return update(context, parent, (channel) => { channel( ACTION_INSERT_CHILD, parent.id, currentIndex < 0 ? parent.children.length : parent.children.length - 1, child.serialize(), currentParent ? currentParent.id : false ); }, () => insert(context, parent, child)); }; const insertBefore = (context, parent, child, before) => { if (!context.nodes.has(child)) { throw new Error("Cannot insert a node that was not created by this remote root"); } if (before && before.id === child.id) return; if (before && !parent.children.includes(before)) { throw new DOMException( "Cannot add a child before an element that is not a child of the target parent.", "HierarchyRequestError" ); } const oldIndex = parent.children.indexOf(child) ?? -1; const oldParent = child.parent; const beforeIndex = before ? parent.children.indexOf(before) : -1; return update(context, parent, (channel) => channel( ACTION_INSERT_CHILD, parent.id, beforeIndex < 0 ? parent.children.length : oldIndex < 0 || oldIndex > beforeIndex ? beforeIndex : beforeIndex - 1, child.serialize(), oldParent ? oldParent.id : false ), () => insert(context, parent, child, before)); }; const removeChild = (context, parent, child) => { const data = dataOf(context, parent); return update(context, parent, (channel) => channel( ACTION_REMOVE_CHILD, parent.id, parent.children.indexOf(child) ), () => { detach(context, child); data.children = capture(remoteFromArray( data.children, data.children.indexOf(child) ), context.strict); }); }; const addAttachMethod = (context) => addMethod(context, "attach", (parent, node) => attach(context, parent, node)); const addDetachMethod = (context) => addMethod(context, "detach", (node) => detach(context, node)); const addAppendMethod$1 = (context) => addMethod(context, "append", (parent, children) => { for (const child of children) { appendChild(context, parent, child); } }); const addUpdateMethod = (context) => addMethod(context, "update", (node, remote, local) => update(context, node, remote, local)); const addReplaceMethod$1 = (context) => addMethod(context, "replace", (parent, children) => { for (const child of parent.children) { removeChild(context, parent, child); } for (const child of children) { appendChild(context, parent, child); } }); const addCollectMethod = (context) => addMethod(context, "collect", (node) => { if (context.nodes.has(node)) { return; } context.nodes.add(node); Object.defineProperty(node, "parent", { get: () => context.parents.get(node) ?? null, configurable: true, enumerable: true }); Object.defineProperty(node, "progenitor", { get: () => context.progenitors.get(node) ?? null, configurable: true, enumerable: true }); }); const addInvokeMethod = (context) => addMethod(context, "invoke", (node, method, payload) => { if (!context.nodes.has(node)) { throw new Error("Cannot invoke method for a node that was not created by this remote root"); } return new Promise((resolve, reject) => { context.channel( ACTION_INVOKE, node.id, method, payload, resolve, reject ); }); }); const createRemoteRootData = (channel, { components, strict = true } = {}) => { if (strict) { Object.freeze(components); } return { strict, mounted: false, channel, children: [], nodes: /* @__PURE__ */ new WeakSet(), parents: /* @__PURE__ */ new WeakMap(), progenitors: /* @__PURE__ */ new WeakMap(), components: /* @__PURE__ */ new WeakMap(), fragments: /* @__PURE__ */ new WeakMap() }; }; const createTreeContext = (channel, options = {}) => { const context = createRemoteRootData(channel, options); let lastId = 0; addMethod(context, "nextId", () => `${++lastId}`); addCollectMethod(context); addAttachMethod(context); addDetachMethod(context); addAppendMethod$1(context); addUpdateMethod(context); addMethod(context, "insert", (parent, child, before) => insertBefore(context, parent, child, before)); addMethod(context, "removeChild", (parent, child) => removeChild(context, parent, child)); addReplaceMethod$1(context); addInvokeMethod(context); return context; }; const createRemoteComment = (content, root, context) => { const id = context.nextId(); const data = { text: content }; const node = { kind: KIND_COMMENT, get id() { return id; }, get root() { return root; }, get text() { return data.text; }, update: (text) => context.update( node, (channel) => channel(ACTION_UPDATE_TEXT, node.id, text), () => data.text = text ), serialize: () => ({ id, kind: KIND_COMMENT, text: data.text }), remove: () => { var _a; return (_a = node.parent) == null ? void 0 : _a.removeChild(node); }, print: () => `Comment(${data.text})` }; context.collect(node); return node; }; const isProxy = (value) => { return value instanceof Function && "__current" in value; }; const toProxy = (value) => { const _proxy = (...args) => { return _proxy.__current ? _proxy.__current(...args) : void 0; }; Object.defineProperty(_proxy, "__current", { value, configurable: false, enumerable: false, writable: true }); return _proxy; }; function proxyFunctionsIn(value, visited = /* @__PURE__ */ new Map()) { if (visited.has(value)) { return visited.get(value); } if (isRemoteFragment(value)) { visited.set(value, value); return value; } if (Array.isArray(value)) { return visitArray(value, visited, proxyFunctionsIn); } if (isObject(value)) { return visitObject(value, visited, proxyFunctionsIn); } if (isFunction(value)) { const proxy = toProxy(value); visited.set(value, proxy); return proxy; } visited.set(value, value); return value; } const collectProxies = (value, visited = /* @__PURE__ */ new Set()) => { if (visited.has(value)) { return void 0; } visited.add(value); if (Array.isArray(value)) { return value.reduce((all, element) => { const nested = collectProxies(element, visited); return nested ? [...all, ...nested] : all; }, []); } if (isObject(value)) { return keysOf(value).reduce((all, key) => { const nested = collectProxies(value[key], visited); return nested ? [...all, ...nested] : all; }, []); } return isProxy(value) ? [value] : void 0; }; const prepareProxiesUnset = (value) => { var _a; return ((_a = collectProxies(value)) == null ? void 0 : _a.map((p) => [p, void 0])) ?? []; }; function prepareProxies(oldValue, newValue, visited = /* @__PURE__ */ new Set()) { if (visited.has(oldValue)) { return [oldValue, [], true]; } visited.add(oldValue); if (isProxy(oldValue)) { return isFunction(newValue) ? [oldValue, [[oldValue, newValue]], true] : [proxyFunctionsIn(newValue), [], false]; } if (Array.isArray(oldValue)) { return !Array.isArray(newValue) ? [proxyFunctionsIn(newValue), prepareProxiesUnset(oldValue), false] : prepareProxiesInArray(oldValue, newValue, visited); } if (isObject(oldValue) && !isRemoteFragment(oldValue)) { return !isObject(newValue) ? [proxyFunctionsIn(newValue), prepareProxiesUnset(oldValue), false] : prepareProxiesInObject(oldValue, newValue, visited); } return [newValue, [], oldValue === newValue]; } function prepareProxiesInObject(oldValue, newValue, visited) { const normalized = {}; const records = []; let changed = false; for (const key of keysOf(oldValue)) { const oldEl = oldValue[key]; if (!(key in newValue)) { records.push(...prepareProxiesUnset(oldEl)); changed = true; } const newEl = newValue[key]; const [updated, record, skip] = prepareProxies(oldEl, newEl, visited); records.push(...record); if (!skip) { normalized[key] = updated; changed = true; } } for (const key of keysOf(newValue)) { if (!(key in normalized)) { normalized[key] = proxyFunctionsIn(newValue[key]); changed = true; } } return [normalized, records, !changed]; } function prepareProxiesInArray(oldValue, newValue, visited) { const normalized = []; const records = []; let changed = false; for (let i = 0; i < Math.max(oldValue.length, newValue.length); i++) { const oldEl = oldValue[i]; const newEl = newValue[i]; if (i >= newValue.length) { records.push(...prepareProxiesUnset(oldEl)); changed = true; continue; } if (i >= oldValue.length) { normalized[i] = proxyFunctionsIn(newEl); changed = true; continue; } const [updated, record, skip] = prepareProxies(oldEl, newEl, visited); records.push(...record); normalized[i] = skip ? oldEl : updated; changed = !skip || changed; } return [normalized, records, !changed]; } const updateProxies = (records) => { for (const [fn, current] of records) { if (fn.__current !== current) { fn.__current = current; } } }; function createRemoteComponent(type, properties, children, root, context) { const id = context.nextId(); const descriptor = typeof type === "object" && "type" in type ? type : null; const data = createRemoteComponentData(properties, children, root, context); const node = { kind: KIND_COMPONENT, get id() { return id; }, get type() { return descriptor ? descriptor.type : type; }, get root() { return root; }, get children() { return data.children; }, get properties() { return data.properties.original; }, append: (...children2) => context.append( node, normalizeChildren(children2, root) ), insertBefore: (child, before) => context.insert( node, normalizeChild(child, root), before ), updateProperties: (properties2) => updateProperties( context, node, properties2 ), replace: (...children2) => context.replace( node, normalizeChildren(children2, root) ), removeChild: (child) => context.removeChild(node, child), remove: () => node.parent ? context.removeChild(node.parent, node) : null, invoke: (method, ...payload) => !descriptor || (descriptor == null ? void 0 : descriptor.hasMethod(method)) ? context.invoke(node, method, payload) : Promise.reject(`Method ${method} is not supported`), serialize: () => ({ id, kind: KIND_COMPONENT, type, properties: data.properties.serializable, children: data.children.map((c) => c.serialize()) }), print: () => _print(id, type, data.properties.original, data.children) }; context.collect(node); context.components.set(node, data); data.children.forEach((c) => context.attach(node, c)); return node; } const RESERVED = ["children"]; const notReserved = (name) => !RESERVED.includes(name); function createRemoteComponentData(properties, children, root, context) { const original = properties ?? {}; const serializable = {}; for (const key of keysOf(original).filter(notReserved)) { serializable[key] = proxyFunctionsIn(serializeProperty(original[key])); } return { properties: { original: capture(original, context.strict), serializable }, children: capture(normalizeChildren(children, root), context.strict) }; } function updateProperties(context, component, properties) { const componentData = context.components.get(component); const normalized = {}; const records = []; let changed = false; for (const key of keysOf(properties).filter(notReserved)) { const oldOriginal = componentData.properties.original[key]; const newOriginal = properties[key]; const oldSerializable = componentData.properties.serializable[key]; const newSerializable = serializeProperty(newOriginal); if (oldSerializable === newSerializable && (newSerializable == null || typeof newSerializable !== "object")) { continue; } const [value, record, skip] = prepareProxies(oldSerializable, newSerializable); records.push(...record); if (!skip) { normalized[key] = value; changed = true; if (isRemoteFragment(oldOriginal)) { context.detach(oldOriginal); } if (isRemoteFragment(newOriginal)) { context.attach(component, newOriginal); } } } return context.update(component, (channel) => { if (changed) { channel(ACTION_UPDATE_PROPERTIES, component.id, normalized); } }, () => { componentData.properties.original = capture({ ...componentData.properties.original, ...properties }, context.strict); componentData.properties.serializable = { ...componentData.properties.serializable, ...normalized }; updateProxies(records); }); } function serializeProperty(property) { return isRemoteFragment(property) ? property.serialize() : property; } function _print(id, type, _properties, children) { const _head = `${typeof type === "string" ? type : type.type}:${id}`; const _children = children.map((c) => typeof c === "string" ? c : c.print()); const _body = _children.length > 0 ? ` ${_indent(_children.join(",\n"))} ` : ""; return `${_head}[${_body}]`; } function _indent(text) { return text.split("\n").map((line) => ` ${line}`).join("\n"); } const createRemoteFragment = (root, context) => { const id = context.nextId(); const data = { children: capture([], context.strict) }; const fragment = { kind: KIND_FRAGMENT, get id() { return id; }, get root() { return root; }, get children() { return data.children; }, append: (...children) => context.append(fragment, normalizeChildren(children, root)), insertBefore: (child, before) => context.insert( fragment, normalizeChild(child, root), before ), replace: (...children) => context.replace(fragment, normalizeChildren(children, root)), removeChild: (child) => context.removeChild(fragment, child), serialize: () => ({ id, kind: KIND_FRAGMENT, children: data.children.map((c) => c.serialize()) }) }; context.collect(fragment); context.fragments.set(fragment, data); return fragment; }; const createRemoteText = (content, root, context) => { const id = context.nextId(); const data = { text: content }; const node = { kind: KIND_TEXT, get id() { return id; }, get root() { return root; }, get text() { return data.text; }, update: (text) => context.update( node, (channel) => channel(ACTION_UPDATE_TEXT, node.id, text), () => data.text = text ), serialize: () => ({ id, kind: KIND_TEXT, text: data.text }), remove: () => { var _a; return (_a = node.parent) == null ? void 0 : _a.removeChild(node); }, print: () => `Text(${data.text})` }; context.collect(node); return node; }; function createRemoteRoot$1(channel, { components, strict = true } = {}) { const context = createTreeContext(channel, { components, strict }); const root = { kind: KIND_ROOT, options: capture({ strict, components }, strict), get id() { return ROOT_ID; }, get children() { return context.children; }, removeChild: (child) => context.removeChild(root, child) }; addCreateCommentMethod(root, context); addCreateComponentMethod(root, context); addCreateFragmentMethod(root, context); addCreateTextMethod(root, context); addMountMethod(root, context); addAppendMethod(root, context); addInsertMethod(root, context); addReplaceMethod(root, context); return root; } function addCreateCommentMethod(root, context) { addMethod(root, "createComment", (text = "") => { return createRemoteComment(text, root, context); }); } function addCreateComponentMethod(root, context) { addMethod(root, "createComponent", (type, ...rest) => { const components = root.options.components; if (components && !components.some((c) => c === type || c.type === type)) { throw new Error(`Unsupported component: ${type}`); } const _type = (components == null ? void 0 : components.find((c) => c === type || c.type === type)) ?? type; const [properties, children, ...restChildren] = rest; return createRemoteComponent(_type, properties, [ ...arraify(children ?? []), ...restChildren ], root, context); }); } function addCreateFragmentMethod(root, context) { addMethod(root, "createFragment", () => createRemoteFragment(root, context)); } function addCreateTextMethod(root, context) { addMethod(root, "createText", (text = "") => { return createRemoteText(text, root, context); }); } function addMountMethod(root, context) { addMethod(root, "mount", () => { if (context.mounted) { return Promise.resolve(); } context.mounted = true; return Promise.resolve(context.channel( ACTION_MOUNT, context.children.map((c) => c.serialize()) )); }); } function addAppendMethod(root, context) { addMethod(root, "append", (...children) => { context.append(root, normalizeChildren(children, root)); }); } function addInsertMethod(root, context) { addMethod(root, "insertBefore", (child, before) => { context.insert(root, normalizeChild(child, root), before); }); } function addReplaceMethod(root, context) { addMethod(root, "replace", (...children) => { context.replace(root, normalizeChildren(children, root)); }); } const nextSibling = (node) => { const { parent } = node; if (parent == null) { return null; } const { children } = parent; return children[children.indexOf(node) + 1] ?? null; }; const setElementText = (element, text) => { const [node] = element.children; if (node && isRemoteText(node)) { node.update(text); } else { element.replace(text); } }; const setText = (node, text) => { if (isRemoteText(node)) { node.update(text); } else { setElementText(node, text); } }; const createRemoteRenderer = (root) => createRenderer({ patchProp(element, key, _, next) { if (!isRemoteComponent(element)) { throw new Error("Unexpected: Attempt to patch props on a root node"); } element.updateProperties({ [key]: next }); }, insert: (child, parent, anchor) => parent.insertBefore(child, anchor), remove: (node) => { var _a; return (_a = node.parent) == null ? void 0 : _a.removeChild(node); }, createElement: (type) => root.createComponent(type), createText: (text) => root.createText(text), createComment: (text) => root.createComment(text), parentNode: (node) => node.parent, nextSibling, setText, setElementText }); const createRemoteRoot = (channel, options = {}) => createRemoteRoot$1(channel, { ...options, components: [ ...HTMLTagList, ...MathMLTagList, ...SVGTagList, REMOTE_SLOT, ...options.components ?? [] ] }); const toRemoteSlots = (named, slots) => { const actual = named.filter((slotName) => slotName in slots); if (actual.length === 0) { return slots; } return { default: () => { var _a; return [ ..."default" in slots ? [(_a = slots.default) == null ? void 0 : _a.call(slots)] : [], ...actual.map((slotName) => h(REMOTE_SLOT, { name: slotName }, { default: slots[slotName] })) ]; } }; }; const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1); const fallthroughEvents = (emits, emit) => { if (emits === void 0) { return {}; } const events = Array.isArray(emits) ? emits : Object.keys(emits); return events.reduce((processed, event) => { processed["on" + capitalize(event)] = (...args) => emit(event, ...args); return processed; }, {}); }; const defineRemoteComponent = (type, emits = void 0, slots = []) => defineComponent({ name: type, inheritAttrs: false, ...emits ? { emits } : {}, setup(_, { attrs, emit, slots: internalSlots }) { return () => h(type, { ...attrs, ...fallthroughEvents(emits, emit) }, toRemoteSlots(slots, internalSlots)); } }); export { ACTION_INSERT_CHILD, ACTION_MOUNT, ACTION_REMOVE_CHILD, ACTION_UPDATE_PROPERTIES, ACTION_UPDATE_TEXT, KIND_COMMENT, KIND_COMPONENT, KIND_FRAGMENT, KIND_ROOT, KIND_TEXT, ROOT_ID, createRemoteRenderer, createRemoteRoot, defineRemoteComponent };