diff --git a/example/dsl-hook/typings/comp-temp.d.ts b/comp-temp.d.ts similarity index 97% rename from example/dsl-hook/typings/comp-temp.d.ts rename to comp-temp.d.ts index 6deea86..c8fa5e8 100644 --- a/example/dsl-hook/typings/comp-temp.d.ts +++ b/comp-temp.d.ts @@ -6,7 +6,7 @@ declare global { type PseudoType = 'HOVER' | 'ACTIVE' | 'FOCUS' | 'DISABLED'; /** * 工具类样式,如:.flex-1 { flex: 1 } - * 通常为写死的样式,不会根据数据动态生成。当value与DesignToCode.StyleNode.value相同时,可以直接使用StyleRule.className作为DesignToCode.StyleNode.className + * 通常为写死的样式,不会根据数据动态生成。当value与DSLToCode.StyleNode.value相同时,可以直接使用StyleRule.className作为DSLToCode.StyleNode.className */ type UtilityStyle = { type: 'UTILITY', @@ -197,9 +197,8 @@ declare global { name: string, importName: string, path: string, + type: 'script' | 'style', }[], - extendImport?: [string, string][], - stylePath?: string[], framework: 'VUE2' | 'VUE3' | 'REACT', components: ComponentItem[], icons?: Icons, diff --git a/dsl.d.ts b/dsl.d.ts index 9f94e4c..2cb3025 100644 --- a/dsl.d.ts +++ b/dsl.d.ts @@ -16,7 +16,7 @@ declare global { readonly fileMap: Record root: MGLayerNode settings: DSLSettings - entry: MGDSLFile + entry: FileId /** * 预览代码时所引入esm模块,需要讲配置模型上的依赖模块引入 */ @@ -57,15 +57,15 @@ declare global { */ type TokenItem = TokenCommonItem | TokenTextItem type TokenCommonItem = { - id: string, + id: string type: | 'color' | 'padding' | 'border-radius' | 'border-width' | 'gap' - name: TokenName, - value: TokenValue, + name: TokenName + value: TokenValue /** * 是否是多段 */ @@ -80,16 +80,16 @@ declare global { | 'letterspacing' type TokenTextItem = { - id: string, + id: string type: 'text' - name: TokenName, + name: TokenName textItems: Record }; type TokenTextSubItem = { type: TokenTextSubItemType - name: TokenName, - value: TokenValue | TokenName, + name: TokenName + value: TokenValue } /** @@ -246,7 +246,7 @@ declare global { * 自动布局 */ type AutoLayout = { - direction: 'COLUMN' | 'ROW', + direction: 'COLUMN' | 'ROW' layoutWrap: 'NO_WRAP' | 'WRAP' // 轴距 itemSpacing: Dimension | 'AUTO' @@ -285,7 +285,7 @@ declare global { right?: Dimension top?: Dimension bottom?: Dimension - }, + } /** * 包含外描边和阴影,实际渲染的bound */ @@ -453,15 +453,17 @@ declare global { * 条件判断 */ interface IfStatement { + id: OperationId type: 'OPERATION' operationType: 'If_STATEMENT' // 表达式 + condition: string consequent: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } alternate: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } } @@ -470,17 +472,21 @@ declare global { * 迭代器 */ interface Iteration { + id: OperationId type: 'OPERATION' operationType: 'ITERATOR' - // 迭代的变量 + // 迭代器的变量名 variable: string body: MGNode + // 作为key的变量名,属于迭代变量中元素上的字段 + key?: string } /** * 原始字符串 */ interface Raw { + id: OperationId type: 'OPERATION' operationType: 'RAW' body: string @@ -489,14 +495,16 @@ declare global { * 三目运算 */ interface TernaryExpression { + id: OperationId type: 'OPERATION' operationType: 'TERNARY_EXPRESSION' + condition: string trueExpression: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } falseExpression: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } } @@ -518,7 +526,7 @@ declare global { entryLayerId: LayerId chunks: FileId[] data: Record - props: Record + props: Record methods: Record computed: Record // 导入 @@ -532,8 +540,8 @@ declare global { 'NUMBER': number 'BOOLEAN': boolean 'FUNCTION': string - 'OBJECT': Record - 'ARRAY': any[] + 'OBJECT': Record> + 'ARRAY': ComponentPropValue[] 'SLOT': string | MGLayerNode[] } @@ -574,7 +582,53 @@ declare global { returnValue?: string } - type Data = ComponentPropString | ComponentPropNumber | ComponentPropBoolean | ComponentPropFunction | ComponentPropObject | ComponentPropArray + type DSLNumber = { + type: 'NUMBER' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: number | undefined + name: string + } + + type DSLBoolean = { + type: 'BOOLEAN' + defaultValue?: boolean | undefined + name: string + } + + type DSLString = { + type: 'STRING' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: string | undefined + name: string + } + + type DSLFunction = { + type: 'FUNCTION' + defaultValue?: string | undefined + name: string + } + + type DSLArray = { + type: 'ARRAY' + defaultValue?: Array | undefined + name: string + } + + type DSLObject = { + type: 'OBJECT' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: {[key: string]: any} + name: string + } + + type Data = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject + type Prop = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject interface Computed { name: string diff --git a/example/dsl-hook/App.css b/example/dsl-hook/App.css new file mode 100644 index 0000000..41b3c5b --- /dev/null +++ b/example/dsl-hook/App.css @@ -0,0 +1 @@ +.hello{color:red} \ No newline at end of file diff --git a/example/dsl-hook/App.tsx b/example/dsl-hook/App.tsx new file mode 100644 index 0000000..2b430b4 --- /dev/null +++ b/example/dsl-hook/App.tsx @@ -0,0 +1,186 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react' +import './App.css' +import { sendMsgToPlugin, UIMessage, PluginMessage } from '@messages/sender' + +function App() { + const [mg] = useState('MasterGo'); + const dsl = useRef({} as MGDSL.JSDSLData); + + useEffect(() => { + window.onmessage = (event: MessageEvent) => { + const { data: res } = event + const { type, data }: { data: MGDSL.JSDSLData, type: PluginMessage } = res + if (type === PluginMessage.DSLGENERATED) { + dsl.current = data + // hook dsl + handleDsl(); + } + } + }, []) + const handleDsl = async () => { + // something requested from the remote server + const res = await requestFromRemoteServer(); + const { framework } = dsl.current + if (framework === "VUE3") { + modifyVUEDSL(res) + } else if (framework === 'REACT') { + modifyReactDSL() + } else if (framework === "ANDROID") { + + } + console.log('修改后的数据', dsl.current); + // send it back to plugin after being processed + sendMsgToPlugin({ + type: UIMessage.PREPROCESS, + data: dsl.current + }) + } + + + + const modifyReactDSL = () => { + const { entry, root, fileMap } = dsl.current + const entryFile = fileMap[entry] + // modify the global variants of entry file + const { props, data, methods } = entryFile + props['show'] = { + type: 'BOOLEAN', + defaultValue: true, + name: 'show' + } + data['count'] = { + type: 'NUMBER', + defaultValue: 1, + name: 'count', + } + methods['countdown'] = { + name: 'countdown', + args: ['timeout'], + content: ` + setTimeout(() => { + setCount(() => { + const newCount = Math.random() * 100 + console.log('newCount is ', newCount); + return newCount + }) + }, timeout);` + }; + const cssStyle = (root as MGDSL.MGLayerNode).style as MGDSL.CssNodeStyle; + cssStyle.dynamicInlineStyles = { + visibility: 'show' + } + } + + const modifyVUEDSL = (res: any) => { + const { entry, root, fileMap } = dsl.current + const entryFile = fileMap[entry] + // modify the global variants of entry file + const { props, data, methods } = entryFile + props['show'] = { + type: 'BOOLEAN', + defaultValue: true, + name: 'show' + } + data['count'] = { + type: 'NUMBER', + defaultValue: 1, + name: 'count', + } + methods['countdown'] = { + name: 'countdown', + args: ['timeout'], + content: ` + setTimeout(() => { + count.value = Math.random() * 100 + console.log('newCount is ', count.value); + }, timeout);` + }; + const cssStyle = (root as MGDSL.MGLayerNode).style as MGDSL.CssNodeStyle; + const dynamicClass = cssStyle.attributes['style'] + // see if their is the attribute of style + if (!dynamicClass) { + cssStyle.attributes['style'] = { + name: 'style', + type: "DYNAMIC", + value: `{visibility: show? 'visible': 'hidden'}`, + valueType: "OBJECT" + } + } + // option 2 + // const vIfOrVShow = cssStyle.attributes['v-if'] || cssStyle.attributes['v-show'] + // if (!vIfOrVShow) { + // cssStyle.attributes['v-if'] = { + // name: 'v-if', + // type: "STATIC", + // value: 'show', + // valueType: "STRING" + // } + // } + addOperationToDSL(res); + } + + // addOperation + const addOperationToDSL = (data: Array<{ id: string, description: string }>) => { + const { root, nodeMap, fileMap } = dsl.current + // add operation to childNode + const childId = root.children[0] + if (nodeMap[childId]) { + const node = nodeMap[childId] + // generate operationNode + const iteration = generateIterator(node, 'list'); + // replace layerNode with iteration + root.children[0] = iteration.id + // store it + nodeMap[iteration.id] = iteration + // root has been serialized so we should update the nodeMap after we change it + nodeMap[root.id] = root; + // insert text to node + const raw = generateRaw(`{{item.description}}`); + (node as MGDSL.MGLayerNode).children.push(raw.id); + nodeMap[raw.id] = raw + + // changeFileData + const file = fileMap[(node as MGDSL.MGLayerNode).relatedFile] + file.data['list'] = { + name: 'list', + type: 'ARRAY', + defaultValue: data, + } + } + } + + // generate iteration operation + const generateIterator = (node: MGDSL.MGNode, name: string): MGDSL.Iteration => { + return { + id: `iteration-${node.id}`, + type: 'OPERATION', + operationType: "ITERATOR", + variable: name, + // key: 'id', + body: node + } + } + + // generate raw + const generateRaw = (rawString: string): MGDSL.Raw => ({ + id: 'raw_1', + type: 'OPERATION', + operationType: 'RAW', + body: rawString + }) + + const requestFromRemoteServer = async () => { + return Promise.resolve([{ + id: 'item_id_1', + description: 'item1' + }, { + id: 'item_id_2', + description: 'item2' + }]) + } + + return ( +
Hello {mg}
+ ) +} +export default App diff --git a/example/dsl-hook/dsl.d.ts b/example/dsl-hook/dsl.d.ts new file mode 100644 index 0000000..3686fd7 --- /dev/null +++ b/example/dsl-hook/dsl.d.ts @@ -0,0 +1,643 @@ +import type { Properties, PropertiesHyphen } from 'csstype'; + +declare global { + namespace MGDSL { + interface MGDSLData { + /** + * 版本信息,遵循SemVer规则 + */ + readonly version: string + /** + * 框架类型 + */ + framework: Framework + readonly nodeMap: Record + readonly localStyleMap: StyleMap + readonly fileMap: Record + root: MGLayerNode + settings: DSLSettings + entry: FileId + /** + * 预览代码时所引入esm模块,需要讲配置模型上的依赖模块引入 + */ + esmImportMap: Record + } + + // js dsl模板 + interface JSDSLData extends MGDSLData{ + /** + * 全局样式表 + */ + globalStyleMap: { + [classId: string]: ClassStyle + } + } + interface ReactDSLData extends JSDSLData { + readonly framework: 'REACT' + } + interface Vue3DSLData extends JSDSLData { + readonly framework: 'VUE3' + } + interface Vue2DSLData extends JSDSLData { + readonly framework: 'VUE2' + } + interface AndroidDSLData extends MGDSLData { + readonly framework: 'ANDROID' + } + interface IOSDSLData extends MGDSLData { + readonly framework: 'IOS' + } + + type Framework = 'REACT' | 'VUE2' | 'VUE3' | 'ANDROID' | 'IOS' + + type TokenName = string + type TokenValue = MGDSL.StyleSet[keyof MGDSL.StyleSet] + /** + * 自定义样式 + */ + type TokenItem = TokenCommonItem | TokenTextItem + type TokenCommonItem = { + id: string, + type: + | 'color' + | 'padding' + | 'border-radius' + | 'border-width' + | 'gap' + name: TokenName, + value: TokenValue, + /** + * 是否是多段 + */ + isMultiple?: boolean + } + type TokenTextSubItemType = + | 'fontfamily' + | 'fontstyle' + | 'fontsize' + | 'lineheight' + | 'decoration' + | 'letterspacing' + + type TokenTextItem = { + id: string, + type: 'text' + name: TokenName, + textItems: Record + }; + + type TokenTextSubItem = { + type: TokenTextSubItemType + name: TokenName, + value: TokenValue | TokenName, + } + + /** + * TODO: 安卓自定义样式 + */ + type AndroidStyleItem = any + /** + * 自定义样式map + */ + type StyleMap = { + [styleId: string]: TokenItem | AndroidStyleItem + }; + + /** + * 配置 + */ + interface DSLSettings { + /** + * 是否使用自定义样式,默认为true + */ + useToken: boolean + } + + interface ClassStyle { + id: ClassId + name: string + scoped: boolean + value: StyleSet + // 伪类 + pseudo?: 'HOVER' | 'ACTIVE' | 'FOCUS' | 'DISABLED' + /** + * css类型 + * */ + type: 'group' | 'class' | 'pseudo' | 'id' | 'attribute' | 'combinators' + } + + type NodeId = LayerId | OperationId + type LayerId = string + type OperationId = string + + type MGNode = MGLayerNode | MGOperationNode + + interface MGLayerNode { + type: 'LAYER' + id: LayerId + children: NodeId[] + name: string + /** + * 图层在代码中的组件名 + */ + componentName: string + /** + * 图层实际类型名称 RECTANGLE TEXT FRAME... + */ + layerType: NodeType + // 是否是蒙版 + isMask: boolean + // 是否隐藏 + isVisible: boolean + layout: NodeLayout + style: CssNodeStyle | IOSNodeStyle | AndroidNodeStyle + /** + * 是否是根元素 + */ + isRoot: boolean + parent?: NodeId + /** + * 关联的文件 + */ + relatedFile: FileId + /** + * 是否拆分成新文件,默认false + */ + isNewFile?: boolean + } + + interface MGComponentNode extends MGLayerNode { + layerType: 'COMPONENT' + alias: string + /** + * 是组件集子组件的话则存在 + */ + componentSetId?: string + } + + interface MGInstanceNode extends MGLayerNode { + layerType: 'INSTANCE' + /** + * 实例的主组件Layer, 这里不开通用模型就不解析 + */ + mainComponent?: LayerId + } + + interface MGCustomNode extends MGLayerNode { + layerType: 'CUSTOM' + tagName?: string + props?: ComponentProp[] + imports: ImportItem[] + } + + interface MGTextNode extends MGLayerNode { + characters: string + } + + type NodeType = + | 'GROUP' + | 'FRAME' + | 'RECTANGLE' + | 'TEXT' + | 'LINE' + | 'ELLIPSE' + | 'POLYGON' + | 'STAR' + | 'PEN' + | 'COMPONENT' + | 'COMPONENTSET' + | 'INSTANCE' + | 'BOOLEANOPERATION' + | 'SLICE' + | 'CONNECTOR' + | 'SECTION' + | 'CUSTOM' + + /** + * 图层的布局信息 + */ + interface NodeLayout { + /** + * 相对矩阵 + */ + matrix?: Matrix + width?: Dimension + height?: Dimension + /** + * 实际在画布渲染的宽 包括阴影外描边 + */ + renderWidth?: Dimension + /** + * 实际在画布渲染的高 包括阴影外描边 + */ + renderHeight?: Dimension + /** + * 自动布局 + */ + autoLayout?: AutoLayout + overflow?: 'HIDDEN' | 'VISIBLE' + /** + * 相对于父元素的布局 + */ + relatedLayout?: AbsoluteLayout | RelatedAutoLayout + } + + /** + * 自动布局 + */ + type AutoLayout = { + direction: 'COLUMN' | 'ROW', + layoutWrap: 'NO_WRAP' | 'WRAP' + // 轴距 + itemSpacing: Dimension | 'AUTO' + // 交叉轴距, 只有在 layoutWrap = 'WRAP'时生效 + crossAxisSpacing: Dimension | 'AUTO' | null + paddingTop: Dimension + paddingRight: Dimension + paddingBottom: Dimension + paddingLeft: Dimension + /** + * 主轴对齐方式 + */ + mainAxisAlignItems: AlignTypes + /** + * 交叉轴对齐方式 + */ + crossAxisAlignItems: Exclude + /** + * 仅当换行的时候,交叉轴的多行对齐方式 + */ + crossAxisAlignContent: 'AUTO' | 'SPACE_BETWEEN' + /** + * 描边是否包含在布局内 + */ + strokesIncludedInLayout: boolean + itemReverseZIndex: boolean + } + + /** + * 绝对定位 + */ + type AbsoluteLayout = { + type: 'ABSOLUTE' + bound: { + left?: Dimension + right?: Dimension + top?: Dimension + bottom?: Dimension + }, + /** + * 包含外描边和阴影,实际渲染的bound + */ + renderBound: { + left?: Dimension + right?: Dimension + top?: Dimension + bottom?: Dimension + } + } + + /** + * 相对于父元素的自动布局 + */ + type RelatedAutoLayout = { + type: 'AUTO' + alignSelf: 'STRETCH' | 'INHERIT' | 'AUTO' + flexGrow: number + } + + type Matrix = [[number, number, number], [number, number, number]] + /** + * flex布局主轴 + */ + type AlignTypes = 'START' | 'END' | 'CENTER' | 'SPACE_BETWEEN' + /** + * 尺寸单位 + */ + type Dimension = { + type: 'PIXEL' | 'PERCENT' | 'CALC' + value: number | string + } + + interface NodeStyle { + /** + * 和localClassMap的key保持一致 + */ + id: `style-${NodeId}` + type: NodeStyleType + disable?: boolean + } + + /** + * 分段样式 + */ + type TextSegStyle = { + start: number + end: number + textStyleId: string + textStyle: StyleSet + } + + interface CssNodeStyle extends NodeStyle { + /** + * css类名 + */ + name: string + /** + * UI样式 + */ + value: StyleSet + /** + * 布局样式 + */ + layoutStyles: StyleSet + /** + * key为属性名称 + */ + attributes: Record + /** + * 行内样式 + */ + inlineStyles?: StyleSet + /** + * 动态行内样式 + */ + dynamicInlineStyles?: { + [key in keyof StyleSet]: string + } + /** + * 文字分段样式 + */ + textStyles?: TextSegStyle[] + /** + * 样式名数组 + */ + classList?: ClassId[] + /** + * 标签名称 + */ + tag?: 'IMG' | 'DIV' | 'TEXT' | 'BUTTON' | 'INPUT' | 'SLOT' | 'SVG' | 'OPTION' + /** + * 子选择器 + */ + subSelectors?: Array + } + + /** + * STATIC 静态属性 + * DYNAMIC 动态属性 + * METHOD 方法 + * UNBIND 只定义,但没有使用的属性 + */ + type Attribute = 'STATIC' | 'DYNAMIC' | 'METHOD' | 'UNBIND' + + interface AttributeItem { + type: Attribute + /** + * 属性名 + */ + name: string + /** + * 属性值或者是对应的函数名称 + */ + value: string + /** + * 参数类型 + */ + valueType: keyof ComponentPropType + /** + * 属性值的来源 + */ + valueSource?: 'PROPS' | 'METHODS' | 'DATA' + /** + * 函数的表达式 + */ + expression?: string + /** + * 默认值 + */ + defaultValue?: string | number | boolean + /** + * 方法的传参 + */ + arguments?: string[] + } + + type IOSNodeStyle = NodeStyle + + type AndroidNodeStyle = NodeStyle + + interface StyleSet + extends Properties, + PropertiesHyphen { + } + + // style-class-xx + type ClassId = string + + type NodeStyleType = + | 'VIEW' + | 'SVG' + | 'IMAGE' + | 'TEXT' + | 'INPUT' + | 'BUTTON' + | 'SCROLLVIEW' + + /** + * 运算符 + */ + type MGOperationNode = IfStatement | Iteration | Raw | TernaryExpression + + /** + * 条件判断 + */ + interface IfStatement { + id: OperationId + type: 'OPERATION' + operationType: 'If_STATEMENT' + // 表达式 + condition: string + consequent: { + type: 'MGNode' | 'EXPRESSION' + body: MGNode | string + } + alternate: { + type: 'MGNode' | 'EXPRESSION' + body: MGNode | string + } + } + + /** + * 迭代器 + */ + interface Iteration { + id: OperationId + type: 'OPERATION' + operationType: 'ITERATOR' + // 迭代器的变量名 + variable: string + body: MGNode + // 作为key的变量名,属于迭代变量中元素上的字段 + key?: string + } + + /** + * 原始字符串 + */ + interface Raw { + id: OperationId + type: 'OPERATION' + operationType: 'RAW' + body: string + } + /** + * 三目运算 + */ + interface TernaryExpression { + id: OperationId + type: 'OPERATION' + operationType: 'TERNARY_EXPRESSION' + condition: string + trueExpression: { + type: 'MGNode' | 'EXPRESSION' + body: MGNode | string + } + falseExpression: { + type: 'MGNode' | 'EXPRESSION' + body: MGNode | string + } + } + + type ImportItem = { + name: string + path: string + /** + * DEFAULT: import name from 'path' + * ALL: import * as name from 'path' + * NAMED: import { name } from 'path' + */ + type: 'DEFAULT' | 'ALL'// | 'NAMED' + } + + interface MGDSLFile { + id: FileId + name: string + entryLayerId: LayerId + chunks: FileId[] + data: Record + props: Record + methods: Record + computed: Record + // 导入 + imports: ImportItem[] + } + + type FileId = string + + type ComponentPropType = { + 'STRING': string + 'NUMBER': number + 'BOOLEAN': boolean + 'FUNCTION': string + 'OBJECT': Record> + 'ARRAY': ComponentPropValue[] + 'SLOT': string | MGLayerNode[] + } + + type ComponentPropValue = { + type: T + value: ComponentPropType[T] + } + + type ComponentPropItem = { + /** + * 类型 + */ + type: T + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: ComponentPropType[T] + /** + * 属性名,根据aliasName和originalName经过处理后的名称,用于最终的展示 + */ + name: string + } + + type ComponentPropString = ComponentPropItem<'STRING'> + type ComponentPropNumber = ComponentPropItem<'NUMBER'> + type ComponentPropBoolean = ComponentPropItem<'BOOLEAN'> + type ComponentPropFunction = ComponentPropItem<'FUNCTION'> + type ComponentPropObject = ComponentPropItem<'OBJECT'> + type ComponentPropArray = ComponentPropItem<'ARRAY'> + type ComponentPropSlot = ComponentPropItem<'SLOT'> + + type ComponentProp = ComponentPropString | ComponentPropNumber | ComponentPropBoolean | ComponentPropFunction | ComponentPropObject | ComponentPropArray | ComponentPropSlot + + interface Method { + name: string + args: Array + content: string + returnValue?: string + } + + type DSLNumber = { + type: 'NUMBER' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: number | undefined + name: string + } + + type DSLBoolean = { + type: 'BOOLEAN' + defaultValue?: boolean | undefined + name: string + } + + type DSLString = { + type: 'STRING' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: string | undefined + name: string + } + + type DSLFunction = { + type: 'FUNCTION' + defaultValue?: string | undefined + name: string + } + + type DSLArray = { + type: 'ARRAY' + defaultValue?: Array | undefined + name: string + } + + type DSLObject = { + type: 'OBJECT' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: {[key: string]: any} + name: string + } + + type Data = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject + type Prop = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject + + interface Computed { + name: string + args: Array + content: string + returnValue?: string + dependencies?: Array + } + } +} + +export { }; diff --git a/example/dsl-hook/index.d.ts b/example/dsl-hook/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/example/dsl-hook/index.tsx b/example/dsl-hook/index.tsx new file mode 100644 index 0000000..256bd43 --- /dev/null +++ b/example/dsl-hook/index.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import './App.css' +import App from './App' + +ReactDOM.render( + + + , + document.getElementById('root') +) diff --git a/example/dsl-hook/lib/main.ts b/example/dsl-hook/lib/main.ts index c60f331..6cea0f4 100644 --- a/example/dsl-hook/lib/main.ts +++ b/example/dsl-hook/lib/main.ts @@ -2,35 +2,20 @@ import { UIMessage, sendMsgToUI, PluginMessage } from '@messages/sender' mg.showUI(__html__) -const handler = ({ data, callback }: { data: MGDSL.MGDSLData, callback(dslData: MGDSL.MGDSLData): void }) => { +let sendDSLToMG = (processedDSLData: MGDSL.MGDSLData) => {} +//@ts-ignore +mg.codegen.on("generateDSL", ({ data, callback }) => { sendDSLToMG = callback // send to ui to process it properly sendMsgToUI({ type: PluginMessage.DSLGENERATED, data, }) -} - -let sendDSLToMG = (processedDSLData: MGDSL.MGDSLData) => {} - -/** - * monitor when dslData has been - */ -//@ts-ignore -mg.codegen.on("generateDSL", handler) - -/** - * plugin closed - */ -mg.once('close', () => { - //@ts-ignore - mg.codegen.off('generateDSL', handler) -}) +}); function restore() { sendDSLToMG = (processedDSLData: MGDSL.MGDSLData) => {} } - mg.ui.onmessage = (msg: { type: UIMessage, data: MGDSL.MGDSLData }) => { const { type, data } = msg if (type === UIMessage.PREPROCESS) { diff --git a/example/dsl-hook/main.ts b/example/dsl-hook/main.ts new file mode 100644 index 0000000..6cea0f4 --- /dev/null +++ b/example/dsl-hook/main.ts @@ -0,0 +1,26 @@ +import { UIMessage, sendMsgToUI, PluginMessage } from '@messages/sender' + +mg.showUI(__html__) + +let sendDSLToMG = (processedDSLData: MGDSL.MGDSLData) => {} +//@ts-ignore +mg.codegen.on("generateDSL", ({ data, callback }) => { + sendDSLToMG = callback + // send to ui to process it properly + sendMsgToUI({ + type: PluginMessage.DSLGENERATED, + data, + }) +}); + +function restore() { + sendDSLToMG = (processedDSLData: MGDSL.MGDSLData) => {} +} +mg.ui.onmessage = (msg: { type: UIMessage, data: MGDSL.MGDSLData }) => { + const { type, data } = msg + if (type === UIMessage.PREPROCESS) { + // get the dsl processed + sendDSLToMG(data) + restore(); + } +} \ No newline at end of file diff --git a/example/dsl-hook/manifest.json b/example/dsl-hook/manifest.json index 731aaea..ad2d885 100644 --- a/example/dsl-hook/manifest.json +++ b/example/dsl-hook/manifest.json @@ -2,7 +2,12 @@ "name": "dsl-demo", "main": "dist/main.js", "ui": "dist/index.html", - "permissions": ["currentuser"], + "permissions": [ + "currentuser" + ], "editor_type": "devMode", - "capabilities": ["codegen"] + "capabilities": [ + "codegen" + ], + "id": 119690857571362 } \ No newline at end of file diff --git a/example/dsl-hook/typings/dsl.d.ts b/example/dsl-hook/typings/dsl.d.ts index 3e03894..3686fd7 100644 --- a/example/dsl-hook/typings/dsl.d.ts +++ b/example/dsl-hook/typings/dsl.d.ts @@ -16,7 +16,7 @@ declare global { readonly fileMap: Record root: MGLayerNode settings: DSLSettings - entry: MGDSLFile + entry: FileId /** * 预览代码时所引入esm模块,需要讲配置模型上的依赖模块引入 */ @@ -453,15 +453,17 @@ declare global { * 条件判断 */ interface IfStatement { + id: OperationId type: 'OPERATION' operationType: 'If_STATEMENT' // 表达式 + condition: string consequent: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } alternate: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } } @@ -470,17 +472,21 @@ declare global { * 迭代器 */ interface Iteration { + id: OperationId type: 'OPERATION' operationType: 'ITERATOR' - // 迭代的变量 + // 迭代器的变量名 variable: string body: MGNode + // 作为key的变量名,属于迭代变量中元素上的字段 + key?: string } /** * 原始字符串 */ interface Raw { + id: OperationId type: 'OPERATION' operationType: 'RAW' body: string @@ -489,14 +495,16 @@ declare global { * 三目运算 */ interface TernaryExpression { + id: OperationId type: 'OPERATION' operationType: 'TERNARY_EXPRESSION' + condition: string trueExpression: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } falseExpression: { - type: 'MGNode' | 'IDENTIFIER' + type: 'MGNode' | 'EXPRESSION' body: MGNode | string } } @@ -518,7 +526,7 @@ declare global { entryLayerId: LayerId chunks: FileId[] data: Record - props: Record + props: Record methods: Record computed: Record // 导入 @@ -574,7 +582,53 @@ declare global { returnValue?: string } - type Data = ComponentPropString | ComponentPropNumber | ComponentPropBoolean | ComponentPropFunction | ComponentPropObject | ComponentPropArray + type DSLNumber = { + type: 'NUMBER' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: number | undefined + name: string + } + + type DSLBoolean = { + type: 'BOOLEAN' + defaultValue?: boolean | undefined + name: string + } + + type DSLString = { + type: 'STRING' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: string | undefined + name: string + } + + type DSLFunction = { + type: 'FUNCTION' + defaultValue?: string | undefined + name: string + } + + type DSLArray = { + type: 'ARRAY' + defaultValue?: Array | undefined + name: string + } + + type DSLObject = { + type: 'OBJECT' + /** + * 默认值,也可作为初始值使用 + */ + defaultValue?: {[key: string]: any} + name: string + } + + type Data = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject + type Prop = DSLString | DSLNumber | DSLBoolean | DSLFunction | DSLArray | DSLObject interface Computed { name: string diff --git a/example/dsl-hook/ui/App.tsx b/example/dsl-hook/ui/App.tsx index fdefd00..2b430b4 100644 --- a/example/dsl-hook/ui/App.tsx +++ b/example/dsl-hook/ui/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react' +import React, { useState, useEffect, useRef, useMemo } from 'react' import './App.css' import { sendMsgToPlugin, UIMessage, PluginMessage } from '@messages/sender' @@ -22,12 +22,13 @@ function App() { const res = await requestFromRemoteServer(); const { framework } = dsl.current if (framework === "VUE3") { - modifyVUEDSL() + modifyVUEDSL(res) } else if (framework === 'REACT') { modifyReactDSL() } else if (framework === "ANDROID") { } + console.log('修改后的数据', dsl.current); // send it back to plugin after being processed sendMsgToPlugin({ type: UIMessage.PREPROCESS, @@ -36,10 +37,12 @@ function App() { } + const modifyReactDSL = () => { - const { entry, root } = dsl.current + const { entry, root, fileMap } = dsl.current + const entryFile = fileMap[entry] // modify the global variants of entry file - const { props, data, methods } = entry + const { props, data, methods } = entryFile props['show'] = { type: 'BOOLEAN', defaultValue: true, @@ -68,10 +71,11 @@ function App() { } } - const modifyVUEDSL = () => { - const { entry, root } = dsl.current + const modifyVUEDSL = (res: any) => { + const { entry, root, fileMap } = dsl.current + const entryFile = fileMap[entry] // modify the global variants of entry file - const { props, data, methods } = entry + const { props, data, methods } = entryFile props['show'] = { type: 'BOOLEAN', defaultValue: true, @@ -98,14 +102,9 @@ function App() { cssStyle.attributes['style'] = { name: 'style', type: "DYNAMIC", - value: JSON.stringify({visibility: 'show'}), + value: `{visibility: show? 'visible': 'hidden'}`, valueType: "OBJECT" } - } else { - // modify directly - const prevValue = JSON.parse(dynamicClass['value']) - Object.assign(prevValue, { visibility: 'show' }); - dynamicClass.value = JSON.stringify(prevValue); } // option 2 // const vIfOrVShow = cssStyle.attributes['v-if'] || cssStyle.attributes['v-show'] @@ -117,10 +116,67 @@ function App() { // valueType: "STRING" // } // } + addOperationToDSL(res); + } + + // addOperation + const addOperationToDSL = (data: Array<{ id: string, description: string }>) => { + const { root, nodeMap, fileMap } = dsl.current + // add operation to childNode + const childId = root.children[0] + if (nodeMap[childId]) { + const node = nodeMap[childId] + // generate operationNode + const iteration = generateIterator(node, 'list'); + // replace layerNode with iteration + root.children[0] = iteration.id + // store it + nodeMap[iteration.id] = iteration + // root has been serialized so we should update the nodeMap after we change it + nodeMap[root.id] = root; + // insert text to node + const raw = generateRaw(`{{item.description}}`); + (node as MGDSL.MGLayerNode).children.push(raw.id); + nodeMap[raw.id] = raw + + // changeFileData + const file = fileMap[(node as MGDSL.MGLayerNode).relatedFile] + file.data['list'] = { + name: 'list', + type: 'ARRAY', + defaultValue: data, + } + } } + // generate iteration operation + const generateIterator = (node: MGDSL.MGNode, name: string): MGDSL.Iteration => { + return { + id: `iteration-${node.id}`, + type: 'OPERATION', + operationType: "ITERATOR", + variable: name, + // key: 'id', + body: node + } + } + + // generate raw + const generateRaw = (rawString: string): MGDSL.Raw => ({ + id: 'raw_1', + type: 'OPERATION', + operationType: 'RAW', + body: rawString + }) + const requestFromRemoteServer = async () => { - return Promise.resolve({}) + return Promise.resolve([{ + id: 'item_id_1', + description: 'item1' + }, { + id: 'item_id_2', + description: 'item2' + }]) } return ( diff --git a/tsconfig.json b/tsconfig.json index eee0b71..d178a13 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ ], }, "include": [ - "dsl.d.ts", + "*.d.ts", ] } \ No newline at end of file