48 lines
5.8 KiB
TypeScript
48 lines
5.8 KiB
TypeScript
/**
|
|
* 把可移植 renderer 描述与受信 React 组件装配为 React Flow 类型表。
|
|
* 验证版本/结构及 rendererId 冲突,并通过弱缓存稳定 memo 包装身份;不实例化 editor 或元素模型。
|
|
*/
|
|
import {memo} from 'react';
|
|
import type { NodeTypes, EdgeTypes } from '@xyflow/react';
|
|
import type { ComponentDefinition, Registry } from '../canvas-core/types.ts';
|
|
import { fault } from '../canvas-core/values.ts';
|
|
import { createRendererCatalog, type RendererDescriptor, type RendererCatalog } from './renderers.ts';
|
|
/** 节点/连线分别提供对应组件,可另提供渐进预览组件;模型解释协议仍由 RendererDescriptor 决定。 */
|
|
export type ReactRendererDefinition=RendererDescriptor&({readonly structure:'node';readonly render:NodeTypes[string];readonly preview?:NodeTypes[string]}|{readonly structure:'connection';readonly render:EdgeTypes[string];readonly preview?:EdgeTypes[string]});
|
|
/** 冻结的装配结果,普通与 preview 类型表分开交给各显示路径。 */
|
|
export type ReactRenderers={readonly catalog:RendererCatalog;readonly definitions:readonly ReactRendererDefinition[];readonly nodeTypes:NodeTypes;readonly edgeTypes:EdgeTypes;readonly previewNodeTypes:NodeTypes;readonly previewEdgeTypes:EdgeTypes};
|
|
/** 应用扩展可独立安装模型定义与显示实现;二者各自校验,不把 React 函数保存进元素数据。 */
|
|
export type CanvasRuntimeExtension={readonly components?:readonly ComponentDefinition[];readonly renderers?:readonly ReactRendererDefinition[]};
|
|
// 同一组件引用只创建一次 memo 包装,弱键不永久保留不再使用的插件组件。
|
|
const memoizedRenderers=new WeakMap<object,NodeTypes[string]|EdgeTypes[string]>();
|
|
/** $$typeof 为 react.memo 则原样返回;其它受支持组件按原引用弱缓存 React.memo 包装。
|
|
* 不调用 render、不解析 lazy 内容,也不缓存组件状态;包装身份稳定不保证所有 props 变化都免渲染。 */
|
|
function compiledRenderer(render:NodeTypes[string]|EdgeTypes[string]):NodeTypes[string]|EdgeTypes[string]{
|
|
if((render as {$$typeof?:symbol}).$$typeof===Symbol.for('react.memo'))return render;
|
|
const key=render as object,previous=memoizedRenderers.get(key);if(previous)return previous;
|
|
const compiled=memo(render as NodeTypes[string]);memoizedRenderers.set(key,compiled);return compiled;
|
|
}
|
|
/** 先验证纯描述,再验证 React 组件形状与可选模型 registry。
|
|
* 同一 structure 内 rendererId 可别名共用,但普通/预览组件引用必须一致;node/edge 类型表彼此独立。
|
|
* 标记对象只做结构识别,不证明组件安全、纯净或必能成功渲染;安装来源仍须受信。
|
|
* 每次调用新建目录/表/外壳,只有组件 memo 包装共享;冻结注册表不冻结函数闭包或 React 内部状态。 */
|
|
export function createReactRenderers(definitions:readonly ReactRendererDefinition[],registry?:Registry):ReactRenderers{
|
|
// 类型表无对象原型,保留名也已被描述目录拒绝,防止类型键继承/原型污染歧义。
|
|
const catalog=createRendererCatalog(definitions),nodes:NodeTypes=Object.create(null),edges:EdgeTypes=Object.create(null),previewNodes:NodeTypes=Object.create(null),previewEdges:EdgeTypes=Object.create(null),captured:ReactRendererDefinition[]=[];
|
|
for(let index=0;index<definitions.length;index++){
|
|
const definition=definitions[index],descriptor=catalog.definitions[index],render=definition.render,preview=definition.preview,tag=render&&typeof render==='object'?(render as {$$typeof?:symbol}).$$typeof:undefined;
|
|
// 接受函数及 React memo/forward_ref/lazy 标记对象;不是组件的值在装配时拒绝。
|
|
if(typeof render!=='function'&&![Symbol.for('react.memo'),Symbol.for('react.forward_ref'),Symbol.for('react.lazy')].includes(tag as symbol))throw fault('invalid_react_renderer');
|
|
const previewTag=preview&&typeof preview==='object'?(preview as {$$typeof?:symbol}).$$typeof:undefined;if(preview!==undefined&&typeof preview!=='function'&&![Symbol.for('react.memo'),Symbol.for('react.forward_ref'),Symbol.for('react.lazy')].includes(previewTag as symbol))throw fault('invalid_react_preview');
|
|
// 可选 registry 只核 componentId/dataVersion/structure 精确存在,不执行某个元素的数据 schema/迁移。
|
|
// 未提供 registry 也可装配纯显示目录,真正投影元素时仍需模型解释与 fallback 判断。
|
|
if(registry){const model=registry.components.get(descriptor.componentId);if(!model||model.dataVersion!==descriptor.dataVersion||model.structure!==descriptor.structure)throw fault('renderer_model_mismatch',[descriptor.componentId]);}
|
|
const table=descriptor.structure==='node'?nodes:edges,previews=descriptor.structure==='node'?previewNodes:previewEdges,compiled=compiledRenderer(render),compiledPreview=preview?compiledRenderer(preview):undefined;
|
|
if(Object.hasOwn(table,descriptor.rendererId)&&(table[descriptor.rendererId]!==compiled||previews[descriptor.rendererId]!==compiledPreview))throw fault('renderer_id_conflict',[descriptor.rendererId]);
|
|
// 定义保留原 render/preview 引用,类型表存编译后的 memo 引用;不混淆这两层身份。
|
|
// 缺 preview 则不创建对应 previewTypes 项,工厂不自动用正式组件充当预览;宿主决定降级路径。
|
|
Object.defineProperty(table,descriptor.rendererId,{value:compiled,enumerable:true,configurable:true});if(compiledPreview)Object.defineProperty(previews,descriptor.rendererId,{value:compiledPreview,enumerable:true,configurable:true});captured.push(Object.freeze({...descriptor,render,...(preview?{preview}:{})}) as ReactRendererDefinition);
|
|
}
|
|
return Object.freeze({catalog,definitions:Object.freeze(captured),nodeTypes:Object.freeze(nodes),edgeTypes:Object.freeze(edges),previewNodeTypes:Object.freeze(previewNodes),previewEdgeTypes:Object.freeze(previewEdges)});
|
|
}
|